# Issue 001: Telemetry Fan-Out And Always-On HUD Maps ## Summary `/spectate` renders multiple rover cards at once, and every rover card includes a live HUD with an always-on top-down SVG map. During the 30s throttled audit, the page received 1,354 `sensorFrame` events and produced more than 5,300 SVG `path` attribute mutations. This is the strongest measured `/spectate` CPU problem. ## Severity High. This is high-frequency, always-on work. It scales with the number of visible rovers and hits exactly the devices we care about: mobile browsers and weaker computers. ## Upstream Likelihood Medium. The `/` telemetry issue will likely help if it introduces a central throttled visual telemetry path. However, `/spectate` has a unique multiplier: it renders several rover HUDs and several `TopDownMap` instances at the same time. Even after upstream telemetry throttling, `/spectate` probably still needs a policy for how many maps update and how often. Related upstream issue: ```txt perf/issues/004-sensor-telemetry-render-frequency.md ``` ## Affected Files - `webui/src/components/SpectateVideo/index.jsx` - `webui/src/components/HudOverlays/HudOverlay/index.jsx` - `webui/src/components/HudOverlays/HudOverlay/HudMapOverlay.jsx` - `webui/src/components/TopDownMap/TopDownMapContent.jsx` - `webui/src/components/TopDownMap/visuals.jsx` - `webui/src/context/TelemetryContext.jsx` - `webui/src/components/RoverMediaPlayer/index.jsx` - `webui/src/components/HudOverlays/OvercurrentOverlay/index.jsx` - `webui/src/components/HudOverlays/LowBatteryOverlay/index.jsx` - `webui/src/components/HudOverlays/VerticalBatteryOverlay/index.jsx` ## Evidence Artifact: ```sh perf/results/2026-06-12T04-55-16-888Z-root-runtime/root-runtime-report.json ``` 30s sample, mobile viewport, 6x CPU throttle: ```txt sensorFrame events: 1,354 sensorFrame bytes: 2,531,090 path d mutations: 2,688 path fill mutations: 2,682 Long tasks: 81 Worst long task: 375ms Average frame gap: 113.9ms p95 frame gap: 233.3ms ``` The path mutation pattern is the important clue. Normal text/status updates do not mutate thousands of SVG path `d` and `fill` attributes. `TopDownMap` does. ## Current Code Path `SpectateVideo` mounts a full media and overlay stack for each rover: ```txt webui/src/components/SpectateVideo/index.jsx:17 RoverMediaPlayer webui/src/components/SpectateVideo/index.jsx:26 HudOverlay variant="spectator" webui/src/components/SpectateVideo/index.jsx:33 OvercurrentOverlay webui/src/components/SpectateVideo/index.jsx:35 VerticalBatteryOverlay ``` `HudOverlay` subscribes to telemetry: ```txt webui/src/components/HudOverlays/HudOverlay/index.jsx:27 const frame = useTelemetryFrame(effectiveRoverId); ``` For spectator mode, it forces the top-down map on: ```txt webui/src/components/HudOverlays/HudOverlay/index.jsx:50 variant === 'spectator' ? true : ... ``` Then `HudMapOverlay` renders: ```txt webui/src/components/HudOverlays/HudOverlay/HudMapOverlay.jsx:35 ``` There is also duplicated telemetry subscription pressure. `RoverMediaPlayer` subscribes to telemetry for audio ducking and media state, while `HudOverlay`, battery overlays, and warning overlays can subscribe for the same rover. ## Why This Matters The raw socket message rate is not automatically a problem. The expensive part is waking multiple React subscribers, recomputing visual sensor geometry, and mutating SVG attributes for every visual update. On `/`, there is generally one primary driver view. On `/spectate`, the same sensor stream can be multiplied across several rover cards. ## Fix Strategies ### Option A: Add A Throttled Visual Telemetry Channel This is the most reusable upstream-friendly fix. Keep `TelemetryContext` storing the latest frame immediately, but notify visual consumers at a lower cadence: ```txt useTelemetryFrameRaw(roverId) high-frequency, only where truly needed useTelemetryFrameVisual(roverId) throttled display data ``` Use visual telemetry for: - `TopDownMap` - `HudOverlay` - `SpectatorTelemetryOverlay` - battery overlays - warning overlays where 100-200ms delay is acceptable Suggested rates: ```txt desktop visual telemetry: 10-15Hz mobile visual telemetry: 5-10Hz ``` ### Option B: Pass One Telemetry Frame Through `SpectateVideo` Right now, each overlay can subscribe independently. Instead, let `SpectateVideo` subscribe once per rover and pass `sensors` down: ```jsx const frame = useTelemetryFrameVisual(roverId); const sensors = frame?.sensors ?? null; ``` This may require small prop additions to the warning/battery overlays. ### Option C: Make Spectator HUD Maps Adaptive Spectator maps are currently always on. For low-end/mobile: - hide maps by default on portrait mobile - update maps at a lower cadence than text warnings - only animate the focused/first rover's map - show static/minimal maps for secondary rovers - make map visibility a spectator setting ### Option D: Memoize TopDownMap Geometry This is useful after throttling. Avoid recalculating path data when the sensor fields that drive a particular path did not change. ## Recommended Path Do this after the `/` telemetry issue if that work touches `TelemetryContext`. Best first implementation: 1. Add `useTelemetryFrameVisual(roverId)` with a 100-150ms visual notification cadence. 2. Switch `HudOverlay` and `TopDownMap` consumers to visual telemetry. 3. In `SpectateVideo`, subscribe once and pass sensors down where practical. 4. Add a spectator/mobile map policy so only the most important maps update live. ## Validation Run: ```sh CPU_THROTTLE=6 VIEWPORT=390x844 MOBILE=1 SAMPLE_MS=30000 \ node perf/live-root-runtime-audit.mjs https://rover.otter.land/spectate perf/results ``` Expected improvements: - `path d` and `path fill` mutations should drop sharply. - `ScriptDuration` should drop. - Long task count should drop. - Frame p95/p99 should improve. - `sensorFrame` socket count may stay the same if only rendering is throttled. ## Risks - Do not throttle control/safety logic that needs immediate data. - Keep battery/overcurrent warnings responsive enough to be trusted. - If map updates become too slow, operators may perceive the HUD as stale. A 5-10Hz map is usually enough for spectators, but verify by watching the live page.