Files
MultiRoombaRover/perf/issues/004-sensor-telemetry-render-frequency.md
T
2026-06-12 13:28:41 -04:00

5.0 KiB

FIXED!

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:

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:

sensorFrame count: 277
sensorFrame bytes: 520,169

That is about 15.4 sensor frames/sec.

The same audit saw SVG attribute mutations:

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:

perf/results/2026-06-11T03-21-51-263Z-root-runtime/root-runtime-report.json

In a 30s audit:

sensorFrame count among latest sampled 1000 messages: 306
sensorFrame bytes: 574,616

Current Code Path

TelemetryContext.jsx updates every incoming frame:

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:

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:

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:

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:

const visualSensors = useThrottledValue(sensors, 100);

This is easier than changing TelemetryContext, but less central.

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:

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.