Cap the forecast sent over the wire to avoid exceeding WebSocket max_size
A fine enough dt over a multi-hour brew can produce a single Forecast message several MB in size - large enough to exceed the websockets library's default 1 MiB max_size and get the connection closed outright (code 1009) right after Load. Confirmed by reproducing with dt=0.1: the 276KB message at dt=1 was safe, but the equivalent ~2.7MB at dt=0.1 reliably disconnected the client. SudTask._send_forecast() now thins T/Theta to at most MAX_FORECAST_POINTS (1000) via simple decimation before sending - self.forecast_t/ forecast_theta themselves stay at full simulated resolution, since _continue_forecast_after_confirm()'s bisect-based truncation needs exact t-value matches, and SudLogTask's logs/forecast_*.json wants full fidelity. Only the copy actually sent to clients is thinned, which the GUI's few-hundred-pixel-wide plot can't show the difference from anyway. Verified against a live server+client over the real WebSocket connection with the dt=0.1 config that originally triggered the disconnect: 4/4 clean runs, forecast arrives capped under 1000 points, and the rendered plot is visually identical to the un-downsampled version. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DkkuG48uHFCGKe6dPSERFk
This commit is contained in:
+31
-2
@@ -7,6 +7,34 @@ from components import APid, AStirrer
|
||||
from components.plant import APlant
|
||||
from components.sud import Sud, SudState
|
||||
|
||||
# Upper bound on how many (t, theta) points the forecast is thinned to
|
||||
# before going out over the wire (see _send_forecast()) - a fine enough
|
||||
# dt over a multi-hour brew can otherwise produce a single JSON message
|
||||
# of several MB, large enough to exceed the websockets library's default
|
||||
# 1 MiB max_size and get the connection closed outright (code 1009). The
|
||||
# GUI's forecast plot is a few hundred pixels wide, so this many points
|
||||
# is already far more resolution than it can show; self.forecast_t/
|
||||
# forecast_theta themselves stay at full simulated resolution - this
|
||||
# only thins the copy actually sent to clients.
|
||||
MAX_FORECAST_POINTS = 1000
|
||||
|
||||
|
||||
def _downsample(t, theta, max_points=MAX_FORECAST_POINTS):
|
||||
"""Returns (t, theta) thinned to at most max_points entries by simple
|
||||
decimation, always keeping the first and last point - losing a few
|
||||
intermediate samples doesn't matter for a plot this size, but losing
|
||||
the endpoints would visibly truncate the curve or its final value."""
|
||||
n = len(t)
|
||||
if n <= max_points:
|
||||
return t, theta
|
||||
step = -(-n // max_points) # ceil(n / max_points)
|
||||
t_ds = t[::step]
|
||||
theta_ds = theta[::step]
|
||||
if t_ds[-1] != t[-1]:
|
||||
t_ds = t_ds + [t[-1]]
|
||||
theta_ds = theta_ds + [theta[-1]]
|
||||
return t_ds, theta_ds
|
||||
|
||||
|
||||
class SudTask(ATask):
|
||||
def __init__(self, sud: Sud, tc: APid, stirrer: AStirrer, pot: APlant, dt, interval, msg_handler: MsgIo,
|
||||
@@ -166,9 +194,10 @@ class SudTask(ATask):
|
||||
await self._send_forecast()
|
||||
|
||||
async def _send_forecast(self):
|
||||
t, theta = _downsample(self.forecast_t, self.forecast_theta)
|
||||
await self.send({'Forecast': {
|
||||
'T': self.forecast_t,
|
||||
'Theta': self.forecast_theta,
|
||||
'T': t,
|
||||
'Theta': theta,
|
||||
'Finished': self.forecast_finished,
|
||||
}})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user