# Dashboard request — cap + evict for the KNN scatter, plus snapshot-replace semantics **Audience:** dashboard session (owns `training/dashboard/`). **Producer side:** `training/producers/knn.py stream` (currently running on the Pi). **Status:** request, not implementation. The producer side has reduced its cadence as a band-aid, but the underlying fix lives in `training/dashboard/static/dashboard.js` scene-9 (KNN scatter) handler. ## Problem The current scene-9 handler is: ```js on('embedding', m => { if (typeof m.x !== 'number' || typeof m.y !== 'number') return; const pt = { x: m.x, y: m.y, z: ..., phase: m.phase, predicted: m.predicted, cluster: ... }; points.push(pt); addStat(pt); rebuildLegend(); }); ``` Every `embedding` event pushes onto the `points` array with no cap. The producer republishes its (deterministic, stable) point set on a cycle so reconnecting browsers eventually see the scatter populate; each cycle therefore pushes the same N points onto `points` again, and over time the in-memory point count grows without bound. After ~10 minutes the browser starts slowing down. The producer side has band-aided this by reducing cycle cadence (200 points every 30 s, was 600 every 5 s). That's 18× slower accumulation, but still a leak. ## Two complementary fixes the dashboard could land ### A. Cap the points array (cheapest) Add a FIFO eviction: ```js const MAX_POINTS = 4000; // tune to taste on('embedding', m => { // ...validate + build pt as before... points.push(pt); if (points.length > MAX_POINTS) { points.shift(); // or splice(0, points.length - MAX_POINTS) } addStat(pt); rebuildLegend(); }); ``` This bounds memory regardless of how often the producer publishes. Existing visual quality stays the same once the cap is reached (the most-recent N points are kept). ### B. Snapshot-replace via a new event type For a cleaner architecture: the producer sends one `embedding_batch` event per cycle containing the full set of points; the handler *replaces* the contents of `points` rather than appending. Eliminates duplicate-publish leakage entirely and naturally supports the "refresh shows something" use case via state replay (see the companion request `dashboard-request-embedding-persistence.md`). Producer would emit: ```json { "type": "embedding_batch", "points": [ {x, y, z, phase, predicted, cluster}, ... ], "replace": true } ``` Handler: ```js on('embedding_batch', m => { if (m.replace) { points.length = 0; resetStats(); } for (const pt of m.points) { points.push(pt); addStat(pt); } rebuildLegend(); }); ``` If you want this, the producer side will switch to it; just confirm the event name and payload shape. ## Suggested order A first (1-line change, fixes the leak today). B second when you have time — pairs naturally with the snapshot/sticky-cache request for refresh-time hydration.