mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
205 lines
5.0 KiB
Markdown
205 lines
5.0 KiB
Markdown
# Issue 004: Sensor Telemetry Render Frequency
|
|
|
|
## Summary
|
|
|
|
The `/` page receives frequent `sensorFrame` messages and renders multiple telemetry/HUD
|
|
consumers from those frames. On throttled mobile CPU, this shows up as repeated SVG path
|
|
attribute mutations and significant live DOM churn.
|
|
|
|
## Severity
|
|
|
|
Medium-high.
|
|
|
|
This is likely the biggest non-input-manager source of steady rendering work.
|
|
|
|
## Affected Files
|
|
|
|
- `webui/src/context/TelemetryContext.jsx`
|
|
- `webui/src/components/TopDownMap/TopDownMapContent.jsx`
|
|
- `webui/src/components/TopDownMap/visuals.jsx`
|
|
- `webui/src/components/DriverVideo/index.jsx`
|
|
- `webui/src/components/HudOverlays/*`
|
|
- `webui/src/components/TelemetryPanel/index.jsx`
|
|
- `webui/src/components/PiHostStatsCard/index.jsx`
|
|
|
|
## Evidence
|
|
|
|
Artifact:
|
|
|
|
```sh
|
|
perf/results/2026-06-11T03-22-44-714Z-root-runtime/root-runtime-report.json
|
|
```
|
|
|
|
In an 18s `/` audit at mobile viewport with 6x CPU throttle:
|
|
|
|
```txt
|
|
sensorFrame count: 277
|
|
sensorFrame bytes: 520,169
|
|
```
|
|
|
|
That is about 15.4 sensor frames/sec.
|
|
|
|
The same audit saw SVG attribute mutations:
|
|
|
|
```txt
|
|
path d mutations: 621
|
|
path fill mutations: 619
|
|
```
|
|
|
|
These map strongly to `TopDownMapContent.jsx` and `visuals.jsx`, where sensor values are
|
|
converted into SVG arcs, cones, wheels, brush visuals, and colors.
|
|
|
|
Artifact:
|
|
|
|
```sh
|
|
perf/results/2026-06-11T03-21-51-263Z-root-runtime/root-runtime-report.json
|
|
```
|
|
|
|
In a 30s audit:
|
|
|
|
```txt
|
|
sensorFrame count among latest sampled 1000 messages: 306
|
|
sensorFrame bytes: 574,616
|
|
```
|
|
|
|
## Current Code Path
|
|
|
|
`TelemetryContext.jsx` updates every incoming frame:
|
|
|
|
```jsx
|
|
function handleSensorFrame({ roverId, sensors = {}, frame = {} }) {
|
|
if (!roverId) return;
|
|
const previous = framesRef.current[roverId] ?? {};
|
|
framesRef.current = {
|
|
...framesRef.current,
|
|
[roverId]: {
|
|
...previous,
|
|
roverId,
|
|
sensors,
|
|
raw: frame?.data || null,
|
|
receivedAt: Date.now(),
|
|
},
|
|
};
|
|
notifyRover(roverId);
|
|
}
|
|
```
|
|
|
|
Consumers subscribe by rover:
|
|
|
|
```jsx
|
|
useTelemetryFrame(roverId)
|
|
```
|
|
|
|
Every `notifyRover(roverId)` wakes all consumers for that rover.
|
|
|
|
## Why This Matters
|
|
|
|
Telemetry itself may need to remain high-frequency for control safety or debugging, but the
|
|
visual UI usually does not need to render at full sensor frequency.
|
|
|
|
The expensive part is not receiving the socket message. It is causing React/SVG/HUD updates
|
|
and DOM attribute changes at the same cadence.
|
|
|
|
## Fix Strategies
|
|
|
|
### Option A: Throttle Render Notifications
|
|
|
|
Keep the latest frame in `framesRef` immediately, but notify subscribers at a lower rate.
|
|
|
|
Example:
|
|
|
|
```jsx
|
|
const pendingRoversRef = useRef(new Set());
|
|
const notifyScheduledRef = useRef(false);
|
|
|
|
function scheduleNotify(roverId) {
|
|
pendingRoversRef.current.add(roverId);
|
|
if (notifyScheduledRef.current) return;
|
|
notifyScheduledRef.current = true;
|
|
setTimeout(() => {
|
|
notifyScheduledRef.current = false;
|
|
const rovers = [...pendingRoversRef.current];
|
|
pendingRoversRef.current.clear();
|
|
rovers.forEach((id) => notifyRover(id));
|
|
}, 100); // 10Hz visual updates
|
|
}
|
|
```
|
|
|
|
Then `handleSensorFrame` writes latest data immediately but calls `scheduleNotify(roverId)`.
|
|
|
|
### Option B: Separate Raw Telemetry From Visual Telemetry
|
|
|
|
Expose:
|
|
|
|
```jsx
|
|
useTelemetryFrameRaw(roverId) // high-frequency, only for critical logic
|
|
useTelemetryFrameVisual(roverId) // throttled for UI rendering
|
|
```
|
|
|
|
Use throttled visual telemetry in:
|
|
|
|
- TopDownMap
|
|
- HUD overlays
|
|
- telemetry panels
|
|
- battery bars
|
|
- host stats display
|
|
|
|
Keep raw telemetry for safety mechanisms if needed.
|
|
|
|
### Option C: Memoize/Split Sensor Consumers by Field
|
|
|
|
Do not re-render the entire map or all overlays when only one sensor field changed.
|
|
|
|
Examples:
|
|
|
|
- Battery components subscribe only to battery fields.
|
|
- Light bump bars subscribe only to light bump fields.
|
|
- Overcurrent overlay subscribes only to overcurrent fields.
|
|
|
|
This is more architectural but can reduce unnecessary work.
|
|
|
|
### Option D: Render TopDownMap Less Often
|
|
|
|
At the component level:
|
|
|
|
```jsx
|
|
const visualSensors = useThrottledValue(sensors, 100);
|
|
```
|
|
|
|
This is easier than changing `TelemetryContext`, but less central.
|
|
|
|
## Recommended Path
|
|
|
|
Start with a throttled visual notification path in `TelemetryContext`. It gives the largest
|
|
blast-radius reduction without changing every consumer immediately.
|
|
|
|
A reasonable target:
|
|
|
|
- Desktop: 10-15Hz visual updates
|
|
- Mobile: 5-10Hz visual updates
|
|
|
|
## Validation
|
|
|
|
Run:
|
|
|
|
```sh
|
|
CPU_THROTTLE=6 VIEWPORT=390x844 MOBILE=1 SAMPLE_MS=18000 node perf/live-root-runtime-audit.mjs https://rover.otter.land/ perf/results
|
|
```
|
|
|
|
Expected improvements:
|
|
|
|
- `path d` and `path fill` mutation counts should drop.
|
|
- `LayoutCount` and `RecalcStyleCount` may drop.
|
|
- `ScriptDuration` should drop.
|
|
- Frame p95 should improve.
|
|
|
|
Socket `sensorFrame` count may remain the same if only rendering is throttled.
|
|
|
|
## Risks
|
|
|
|
- Do not throttle logic that protects the rover or sends control commands unless you are sure
|
|
it is only display logic.
|
|
- Operators may expect smooth sensor visuals, but 10Hz is usually enough for dashboards.
|
|
- Make sure low battery/overcurrent warnings still feel responsive.
|
|
|