perf: group the timeline's lanes in one pass

Stacking by game rebuilt a lane's whole array for every row it added, which is
quadratic in the lane's length. That is cheap at three events and not at a
reader with fourteen games switched on and the future plotted — and it is not
paid once, because the board re-renders on every clock tick.

Appending into the array instead is the same output: `Map` preserves insertion
order, so lanes still arrive in the order their first row did and the rows
inside one keep the order they were given, which is what the existing tests
pin.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Lucas Winther
2026-08-20 06:13:24 +02:00
co-authored by Claude Opus 5
parent 4fe563dcbb
commit 1e7a7abc64
+7 -1
View File
@@ -97,9 +97,15 @@ export function timelineLanes<T extends Row>(
return [{ id: "all", game: null, rows: [...rows].sort(order) }]; return [{ id: "all", game: null, rows: [...rows].sort(order) }];
} }
// Appended into rather than rebuilt per row: the copy-and-reset form this
// replaced was quadratic in a lane's length, and it runs on every render of a
// board that redraws each second. `Map` keeps insertion order, so the lanes
// still arrive in the order their first row did.
const byGame = new Map<LaneId, T[]>(); const byGame = new Map<LaneId, T[]>();
for (const row of rows) { for (const row of rows) {
byGame.set(row.event.game, [...(byGame.get(row.event.game) ?? []), row]); const lane = byGame.get(row.event.game);
if (lane === undefined) byGame.set(row.event.game, [row]);
else lane.push(row);
} }
let lanes = [...byGame]; let lanes = [...byGame];