PERF: stop rebuilding and transmitting the full chat/task roster on every state push #126

Open
opened 2026-09-09 14:11:45 +00:00 by nsaspy · 0 comments
Owner

Finding

helpers/state_snapshot.py::build_snapshot_from_request() always materializes complete contexts and tasks arrays, even when the dirty event is only a new log token/progress update for the currently selected chat. helpers/state_monitor.py may emit state pushes every 25 ms per connected sid while activity continues, and every push rebuilds that full snapshot.

The frontend then calls chatsStore.applyContexts(snapshot.contexts) and tasksStore.applyTasks(snapshot.tasks) on each snapshot even when roster metadata did not change.

This makes active streaming cost proportional to total number of loaded chats/tasks, not to the changed context. It also repeats per browser tab.

Current amplification

  • StateMonitor default debounce: 0.025 seconds.
  • Each sid has its own projection and independently calls build_snapshot_from_request().
  • Every snapshot serializes all live non-background contexts and task metadata, sorts them, JSON-encodes them, sends them over the socket, parses them in the browser, and re-enters Alpine roster reactivity.
  • The source comment says the throttle caps at <= 1 push / 100 ms / sid, but the actual default is 25 ms. The contract and implementation disagree.

Direction

Split high-frequency active-context state from low-frequency/global roster state.

Possible protocol shape:

  • active-context stream: log delta, progress, pause/run state, notification delta
  • roster snapshot/delta: chat/task metadata only when its own version changes
  • roster version/hash included in handshake/push envelope
  • frontend applies a roster only when version changes
  • optional explicit roster mutation events for create/delete/rename/project/profile/run-state transitions

Do not require a polling-style full global snapshot to preserve push compatibility.

Acceptance

  • Log streaming cost does not scale linearly with total chat/task count.
  • A log-only dirty wave does not rebuild or transmit unchanged contexts/tasks.
  • Chat/task roster has an explicit monotonic version or equivalent invalidation contract.
  • Frontend does not reapply unchanged roster data.
  • Create/delete/rename/project/profile/running-state changes still propagate promptly to every relevant tab.
  • Multiple tabs do not independently force identical expensive global roster reconstruction for every log chunk.
  • Resolve the 25 ms implementation vs 100 ms source-contract mismatch; choose and benchmark an intentional cadence.
  • Benchmarks cover active streaming with 10, 100, 1,000+ chats and 1/3/10 connected tabs.
  • Capture server snapshot CPU, serialized bytes/sec, browser JSON/roster apply time, and push latency.
## Finding `helpers/state_snapshot.py::build_snapshot_from_request()` always materializes complete `contexts` and `tasks` arrays, even when the dirty event is only a new log token/progress update for the currently selected chat. `helpers/state_monitor.py` may emit state pushes every 25 ms per connected sid while activity continues, and every push rebuilds that full snapshot. The frontend then calls `chatsStore.applyContexts(snapshot.contexts)` and `tasksStore.applyTasks(snapshot.tasks)` on each snapshot even when roster metadata did not change. This makes active streaming cost proportional to **total number of loaded chats/tasks**, not to the changed context. It also repeats per browser tab. ## Current amplification - StateMonitor default debounce: `0.025` seconds. - Each sid has its own projection and independently calls `build_snapshot_from_request()`. - Every snapshot serializes all live non-background contexts and task metadata, sorts them, JSON-encodes them, sends them over the socket, parses them in the browser, and re-enters Alpine roster reactivity. - The source comment says the throttle caps at <= 1 push / 100 ms / sid, but the actual default is 25 ms. The contract and implementation disagree. ## Direction Split high-frequency active-context state from low-frequency/global roster state. Possible protocol shape: - active-context stream: log delta, progress, pause/run state, notification delta - roster snapshot/delta: chat/task metadata only when its own version changes - roster version/hash included in handshake/push envelope - frontend applies a roster only when version changes - optional explicit roster mutation events for create/delete/rename/project/profile/run-state transitions Do not require a polling-style full global snapshot to preserve push compatibility. ## Acceptance - [ ] Log streaming cost does not scale linearly with total chat/task count. - [ ] A log-only dirty wave does not rebuild or transmit unchanged `contexts`/`tasks`. - [ ] Chat/task roster has an explicit monotonic version or equivalent invalidation contract. - [ ] Frontend does not reapply unchanged roster data. - [ ] Create/delete/rename/project/profile/running-state changes still propagate promptly to every relevant tab. - [ ] Multiple tabs do not independently force identical expensive global roster reconstruction for every log chunk. - [ ] Resolve the 25 ms implementation vs 100 ms source-contract mismatch; choose and benchmark an intentional cadence. - [ ] Benchmarks cover active streaming with 10, 100, 1,000+ chats and 1/3/10 connected tabs. - [ ] Capture server snapshot CPU, serialized bytes/sec, browser JSON/roster apply time, and push latency.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
nsaspy/a0-symbolics#126
No description provided.