Skip to content

Perf: External store with selectors for session stats (kill the context cascade) #3

@DFearing

Description

@DFearing

Goal

Stop O(N·M·C) rebuilds on every session tick. Each panel re-renders only when the slice it actually consumes has changed.

Today

  • web/components/agent-visualizer/session-stats-provider.tsx:31setSessionStats allocates a fresh outer Map on every publish, re-rendering every consumer of useSessionStatsData.
  • web/components/agent-visualizer/session-canvas-panel.tsx:118-122 — each session publishes once a second regardless of whether anything changed at the outer-Map level.
  • web/components/agent-visualizer/index.tsx:337-370feedConversations, feedAgents, agentToSession rebuild by iterating all sessions × all agents × all conversations on any stats change. The TODO at :368 flags this.

Plan

  1. Add web/lib/session-stats-store.ts — a tiny store backed by useSyncExternalStore, keyed by sessionId. Subscribers register (state) => slice selectors with shallow equality.
  2. setSessionStats writes only the changed session entry; subscribers whose selector result didn't change (shallow Map reference equality on agents/toolCalls/conversations) don't re-render.
  3. Convert MessageFeedPanel, CostSummaryPanel, TopBar to selector-based reads. Drop the denormalized maps in index.tsx — consumers iterate the store directly, or maintain incrementally per session.
  4. Switch publish from interval-based to event-driven now that fan-out is cheap. STATS_PUBLISH_INTERVAL_MS becomes the minimum gap, not the cadence.
  5. Sub-task: hoist <ControlBar> reads (session-canvas-panel.tsx:301) onto a useSyncExternalStore view of frameRef.currentTime so the chrome doesn't re-render at 4 Hz with the simulation state.

Acceptance

  • React DevTools profiler: with 5 sessions running, MessageFeedPanel re-renders only when its visible session messages change, not on every other session's tick.
  • CostSummaryPanel re-renders only when total cost shifts.
  • Per-frame React commit count flat as session count grows (visible in profiler).

Parallelism

Fully independent — no file conflicts with #2, #4, #5, or #6. Wave A.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions