5.3 KiB
FIXED!
Issue 002: ControlContext Broad Invalidation
Summary
ControlContext exposes one large changing context value containing all control state,
pipeline objects, limiter state, and action callbacks. This likely causes many consumers
to re-render and effects to re-run when unrelated control state changes.
This is probably the multiplier behind the global input listener churn and some repeated
DOM commits on /.
Severity
High, but fix after issue 001 unless doing both together.
Issue 001 is the symptom with the clearest measurement. Issue 002 is the architectural cause that may also affect other parts of the page.
Affected Files
webui/src/controls/ControlContext.jsxwebui/src/controls/inputs/KeyboardInputManager.jsxwebui/src/controls/inputs/GamepadInputManager.jsx- Many consumers of
useControlSystem()
Evidence
The contextValue in ControlContext.jsx is:
const contextValue = useMemo(
() => ({
state,
dispatch,
pipeline,
overcurrentLimiter,
actions: {
setMode,
setDriveVector,
setAuxMotors,
...
},
}),
[
state,
pipeline,
overcurrentLimiter,
setMode,
setDriveVector,
...
],
);
Any state change changes the whole context value. Any consumer using:
const { state, actions } = useControlSystem();
is subscribed to the broad value, even if it only needs one action or one small state field.
This connects to measured symptoms:
- Input listener effects rerun because their action/callback dependencies change.
/runtime audit saw repeated attribute commits in UI that should not need to change every telemetry/log tick.- CPU profiles show large minified React frames (
C9,ad,Fl, etc.) alongside listener churn.
Why This Matters
React context invalidation is coarse. When provider value identity changes, every consumer below it can be scheduled to render. Even memoized children are not enough if they consume the changing context directly.
This project has several high-frequency update sources:
- telemetry frames
- logs
- session syncs
- control state changes
- settings state
- timers
If ControlContext changes broadly, it makes it harder to isolate these updates.
Likely Cause Pattern
Several action callbacks depend on state or pipeline, which changes their identity:
const setDriveVector = useCallback(..., [pipeline, recordControlIntent, state.manualDockAssist?.active]);
const setServoAngle = useCallback(..., [pipeline, recordControlIntent, state.manualDockAssist?.active]);
const runMacro = useCallback(..., [driveMacroBackoffEnabled, pipeline, ..., state.macros, ...]);
const startHorn = useCallback(..., [dispatch, normalizedHornSettings, pipeline, ..., state.horn?.active, ...]);
Those callbacks are then included in contextValue.
Consumers that include those callbacks in effects rerun when the callback identity changes.
Fix Strategy
Option A: Split State and Actions Contexts
Create separate contexts:
const ControlStateContext = createContext(null);
const ControlActionsContext = createContext(null);
Then:
<ControlStateContext.Provider value={stateValue}>
<ControlActionsContext.Provider value={actionsValue}>
{children}
</ControlActionsContext.Provider>
</ControlStateContext.Provider>
Action-only consumers should not rerender when state changes.
Option B: Add Selectors
Create a small external-store style control store, similar to SessionContext or
TelemetryContext, so consumers can select only the state slice they need:
useControlSelector((state) => state.camera.angle)
useControlActions()
This is more work but scales better.
Option C: Stabilize Actions With Refs
Keep action function identities stable and read latest state/pipeline from refs:
const latestRef = useRef(null);
latestRef.current = { state, pipeline, overcurrentLimiter, ... };
const setDriveVector = useCallback((vector, meta = {}) => {
const { state, pipeline } = latestRef.current;
...
}, []);
This pairs well with issue 001 because stable actions make stable input effects easier.
Recommended Path
For incremental work:
- Fix issue 001 directly using refs in input managers.
- Split
ControlActionsContextfromControlStateContext. - Convert heavy/control-critical consumers first:
KeyboardInputManagerGamepadInputManager- mobile controls
- right pane controls
- Later add selector hooks for state-heavy consumers.
Validation
After issue 001 + partial issue 002:
CPU_THROTTLE=6 VIEWPORT=390x844 MOBILE=1 SAMPLE_MS=30000 node perf/live-root-runtime-audit.mjs https://rover.otter.land/ perf/results
Look for:
- listener add/remove counts near 1
- fewer repeated input attribute mutations
- fewer long tasks
- lower
ScriptDurationandTaskDuration
For development builds, React DevTools Profiler would be ideal to verify fewer renders. The deployed production page does not currently publish source maps, so local profiling may be more useful after implementing.
Risks
- Ref-based stable actions can hide stale state bugs if refs are not updated reliably.
- Splitting context touches many files. Prefer a small staged migration.
- Control commands are safety-sensitive; verify stop/horn/mic/docking behavior carefully.