vibescrobble! every agent, live on stage. vibescrobble.com

perf(web): draw thousands of agents in a fraction of the frame time #813

targeting main from claude/perf-board
Merged opened by and assigned to quaternion-karabela.test.pds.did.bot
targeting main from claude/perf-board

The board does 2–3× less work per frame from 2,000 agents up, with no intended visual change. There are four parts:

  • Feed and loop: pointer moves hit-test from a grid, a burst of live lines writes one stream row per frame, and a still board stops redrawing for its clock.
  • Drawing: label widths are cached and label collision uses a 64 px cell index, off-screen work-group backgrounds are skipped, the background grid is a cached layer, and collapse membership is computed when something changes rather than walked per agent per frame.
  • Layout: work nodes and agents live in spatial grids, so a tick stays near-linear. At 10,000 agents that is about 13,000 pair checks a frame instead of 20 million. settle() stops after a 4 s budget, so a large board is placed in about 4 s instead of minutes, and keeps gathering in later frames.
  • Rank: each agent's rank and resting distance are cached alongside collapse membership. Only operator, provisioned and deleted events, and an agent's first scrobble, mark them dirty, which covers every input collapses() and isHost read.

Frame time (median) from web/dev/bench/board.mjs in headless Chromium with software GL, so absolute numbers are pessimistic:

agents whole board one household close-up
2,030 44.1 → 20.1 ms 84.1 → 32.1 ms 51.6 → 31.4 ms
5,030 136.2 → 44.7 ms 148.2 → 84.4 ms 151.5 → 59.2 ms
10,030 247.4 → 78.4 ms 333.7 → 125.7 ms 333.7 → 137.2 ms

At 10,000 agents what remains is mostly layout.js:tick and step, then draw.js:collapses close in. A live board never rests, because births and deaths keep it warm. Letting settled agents sleep was tried and dropped, because the motion test requires a still board and a moving one to end in the same place.

All web tests pass apart from the two known failures, the shells test and the placeholder guard. web/dev/test/feed.test.mjs pins hover to a bounded number of agents at 5,000, draw-cull.test.mjs pins a zoomed-in frame to what is on screen, and layout.test.mjs pins pair checks per tick near-linear at 5,000. Node benchmarks for each part are in web/dev/bench/. The screenshots compare main and this branch at one household's zoom. The mock places nodes a little differently on each run, and the drawing is the same.

before: one household on main after: the same view on this branch

🤖 Generated with Claude Code

Commits 13

perf(web): fix the layout bench and report rank lookups
perf(web): cache each agent's rank and rest off the tick
perf(web): bench the layout and pin its pair checks
perf(web): index the layout's groups and agents so a tick stays linear
perf(web): read collapse membership once a frame
docs(web): say how to benchmark the drawing
test(web): pin a frame to the agents on screen
perf(web): measure only the work groups that can be seen
perf(web): cache label widths and index the collision boxes
test(web): measure the feed, hover and frame loop at thousands
perf(web): write one stream row per frame burst, not per event
perf(web): stop redrawing a sleeping board for its clock
perf(web): answer a pointer move from a grid of the board