perf(cluster): batch the console lines instead of one render per line
Every line of cluster traffic went straight to state: a spread copy of a 2000-element array, a slice copy, and a React render, per line. An RBN feed sends hundreds a second, so that was tens of MB/s of garbage in the renderer and hundreds of re-renders - and it ran whether the console was open or not, so the cost was paid for a panel nobody was looking at. Reported on an old PC where the UI had stopped responding. Lines are staged in a ref and flushed on a 200 ms timer, the same shape the spot handler already uses. The staging buffer is bounded too, so a burst longer than the console can show is not carried in full just to be sliced away on commit.
This commit is contained in:
+32
-4
@@ -1413,14 +1413,42 @@ export default function App() {
|
||||
const [clusterLines, setClusterLines] = useState<ClusterLine[]>([]);
|
||||
const [clusterConsoleOpen, setClusterConsoleOpen] = useState(false);
|
||||
const clusterConsoleRef = useRef<HTMLDivElement | null>(null);
|
||||
// Console lines are STAGED and flushed on a timer, never applied one by one.
|
||||
//
|
||||
// Every line of cluster traffic reaches this handler — spots, MOTD, WHO, the
|
||||
// lot — and an RBN feed alone puts out hundreds a second. Committing each one
|
||||
// meant two copies of a 2000-element array (spread, then slice) plus a React
|
||||
// render PER LINE: tens of megabytes of garbage per second, and the renders
|
||||
// happened even with the console closed, so an operator paid for a panel they
|
||||
// were not looking at. On an older machine that is enough to make the whole UI
|
||||
// crawl. Batching turns hundreds of updates a second into five.
|
||||
const pendingLinesRef = useRef<ClusterLine[]>([]);
|
||||
const pendingLineTimer = useRef<number | undefined>(undefined);
|
||||
useEffect(() => {
|
||||
const off = EventsOn('cluster:line', (l: any) => {
|
||||
const flushLines = () => {
|
||||
pendingLineTimer.current = undefined;
|
||||
const batch = pendingLinesRef.current;
|
||||
if (batch.length === 0) return;
|
||||
pendingLinesRef.current = [];
|
||||
setClusterLines((prev) => {
|
||||
const next = [...prev, l as ClusterLine];
|
||||
return next.length > CONSOLE_CAP ? next.slice(next.length - CONSOLE_CAP) : next;
|
||||
const total = prev.length + batch.length;
|
||||
return total > CONSOLE_CAP ? prev.slice(total - CONSOLE_CAP).concat(batch) : prev.concat(batch);
|
||||
});
|
||||
};
|
||||
const off = EventsOn('cluster:line', (l: any) => {
|
||||
const buf = pendingLinesRef.current;
|
||||
buf.push(l as ClusterLine);
|
||||
// Bound the staging buffer too: a burst longer than the console can show
|
||||
// would otherwise be carried in full just to be sliced away on commit.
|
||||
if (buf.length > CONSOLE_CAP) buf.splice(0, buf.length - CONSOLE_CAP);
|
||||
if (pendingLineTimer.current === undefined) {
|
||||
pendingLineTimer.current = window.setTimeout(flushLines, 200);
|
||||
}
|
||||
});
|
||||
return () => { off?.(); };
|
||||
return () => {
|
||||
off?.();
|
||||
if (pendingLineTimer.current !== undefined) window.clearTimeout(pendingLineTimer.current);
|
||||
};
|
||||
}, []);
|
||||
// Follow the tail, but ONLY when already at the bottom — otherwise scrolling up
|
||||
// to read a SH/DX reply would yank you back down on the next spot.
|
||||
|
||||
Reference in New Issue
Block a user