UI lag during token streaming / many background subagents: measured root cause (per-token fan-out, no stream batching) #5014
Replies: 2 comments
|
Post-restart verification: both fixes are live and the lag is gone. Applied as local patches to the installed packages (dsh-host-apiproxy + dsh-client-runtime, both 0.1.1-rc.2), then restarted the web process (patched files loaded, confirmed by file mtime vs process start). Server side (V8 CDP profile, 25s while tokens streamed, 15,682 samples):
Browser side (renderer main-thread, active GLM 5.3 streaming):
What changed:
Measurement notes: WS msg/s is deliberately NOT the success metric here; downstream consumers re-flatten batches to one frame per WS message by design, so msg/s stays the same while per-message work drops. The meaningful metrics are server CPU share and renderer main-thread availability, both shown above. Playwright MCP CDP tracing returned no events in this environment, so renderer health was measured with a rAF frame-rate probe instead. Happy to prepare this as a proper upstream PR if the maintainers want it. |
|
Measured on 0.1.2-alpha.4 (commit Independent measurements confirming the fan-out cost described in this discussion, plus one twist that makes it worse in multi-agent setups. Setup. One background agent streaming continuously (a subagent generating a long LLM answer; compressed journal growth ~550–830 B/s ≈ 15–25 events/s). A separate Chrome tab connected to the web UI, sitting on the empty workspace screen (no session open, DevTools closed). Renderer CPU measured process-level via Measurements
Mechanism. Every delta of every session — including sessions the user is not viewing — is fanned out to all connected clients, and the client's coarse notification channel (the one feeding the session-list/manager projection) triggers a re-render per event at frame rate. We first enabled alpha.4's built-in rAF batching for the hot stream paths ( Client-side mitigation we shipped locally (measured). Throttled the coarse channel's publish interval to 333 ms (3 updates/s), keeping the visible session's own channel at full frame rate. Result with the same background stream running: renderer CPU 0.5% avg (max 8.6%); visible-stream smoothness unchanged (median DOM update interval 100 ms, p90 104 ms; max plateau 4 s = the model's hidden reasoning phase). Suggestion. Server-side batching as proposed here fixes the fan-out cost for all clients at once — we would rather delete our local patch. An additional client-side safeguard worth considering: throttle or de-prioritize notifications for sessions that are not currently rendered, so UI cost stays independent of how many agents stream in parallel. |
Uh oh!
There was an error while loading. Please reload this page.
Summary
The dsh Web GUI (
127.0.0.1:3080) becomes visibly laggy while tokens stream and/or many background subagents run, even though the machine looks idle. Live profiling on both sides shows the same root cause: per-token event fan-out with no server-side stream batching, plus O(surface) client work per message. Fix direction: server-side stream coalescing, client-side snapshot memoization, and cheaper token metering.Environment
danger-full-access(all sessions)deepseek-v4-flashvia local router (stated for completeness; irrelevant to streaming smoothness)Reproduction conditions
Server-side evidence
Live V8 CPU profile via CDP over the existing
--inspectport (no restart needed):FrameQueue.iteratedsh-host-apiproxy/lib/index.js:1108(host-to-client event mux drain loop)Mechanism:
events.mux(dsh-host-apiproxy/lib/index.js:3525) creates one queue per connected browser, subscribes to every session, and pushes one frame per session/event (index.js:3556+)dsh-session/lib/index.js:1444-1484) through all session-projection units, the token meter (dsh-token-meter/lib/index.js:500-522;structuredClone+deepFreezeof the full surface onmeasure()), persistence write-behind (structuredClone, 200ms batched zstd + fsync,dsh-session-persistence/lib/index.js:324-333,436), and one WebSocket frame to the browserAbstractApiClient.onEnvelope,dsh-host-apiproxy/lib/index.js:5210-5253) covers only diagnostics listeners, not the conversation rendererBrowser-side evidence
Chrome DevTools performance trace (38.9s window, 564k events):
publishFunctionCallbuildListSnapshot5,305 ms self (dsh-client-runtime/client.js:8552);SessionManager.notifier4,532 ms (client.js:7843); plus walk / flattenLineage / projectList / indexSubagentDescendants (37.5% of all sampled JS in dsh-client-runtime)Verdict: two-sided, same root cause. (a) The server pushes ~339 frames/s with no batching; (b) the renderer does heavy per-message snapshot/projection JS. "Too much layout/paint per frame" is not supported (1.7%).
What was ruled out
workspace-writesessions causing total HTTP blackout): this is constant lag ondanger-full-accesssessions, no blackoutProposed fix direction
dsh-host-apiproxy: flush the conversation stream at most ~every 50-100ms instead of one frame per token. Cuts ws msg/s and publish count ~10-50x; biggest leverbuildListSnapshot/ projections indsh-client-runtimeNote
A local workaround (server batching + client debounce) is in progress; will report back with before/after measurements once applied and verified.
All reactions