[PR] Fix client tab lockup from per-tick full map rebuild #14

Open
agent-claude wants to merge 0 commits from claude/13-client-render-lockup into main
Collaborator

Fixes #13

Problem

Production tabs lock up after a while (reported at ticks ~9,000 and ~294,000); refresh doesn't recover, a new tab does.

Once any crop is planted, ColonySim.stepTick() marks every growing tile dirty each tick, so every 10 Hz delta has changedTiles. The client then:

  • destroyed/recreated all 10,000 terrain sprites + all crop sprites + every item stack (new Text each) per delta, and
  • destroyed/recreated every pawn's Container/Graphics/Sprites/Text every animation frame.

Fix (client only — no sim-core/server/wire changes)

  • colony-renderer.ts: retained per-tile terrain/crop sprites patched only for tiles listed in changedTiles; zone/mine overlay redrawn only when zones are added or a mine flag flips; buildings/items redrawn only when touched. All delta-driven work is accumulated and flushed once per frame in the ticker. Pawn and item-stack display objects are pooled and reused.
  • main.ts: HUD/roster/inspector DOM rebuilds coalesced into one requestAnimationFrame callback (no work while the tab is backgrounded).
  • onTickDeltaApplied now takes the TickDelta instead of two booleans.

Verification (local, same colony with one 20×12 growing zone)

Main-thread blocked Worst stall JS heap
Before (origin/main) 5.2 s per 10 s 378 ms 1,097 MB ~14 s after load
After ~0 in steady state 172 ms (initial load) flat 23–25 MB over 40 s

Visually checked terrain, zones, crops, item stack counts, pawns, labels and HUD panels; no console errors. npm test (16 tests) and npm run typecheck pass.

Follow-up (not in this PR)

Every growing tile still goes over the wire every tick (~88 tile records per delta in this test), which scales with farm size. Worth a separate look at sending crop growth only on stage changes or at a lower rate.

🤖 Generated with Claude Code

Fixes #13 ## Problem Production tabs lock up after a while (reported at ticks ~9,000 and ~294,000); refresh doesn't recover, a new tab does. Once any crop is planted, `ColonySim.stepTick()` marks every growing tile dirty each tick, so every 10 Hz delta has `changedTiles`. The client then: - destroyed/recreated all **10,000 terrain sprites** + all crop sprites + every item stack (new `Text` each) per delta, and - destroyed/recreated every pawn's Container/Graphics/Sprites/`Text` **every animation frame**. ## Fix (client only — no sim-core/server/wire changes) - `colony-renderer.ts`: retained per-tile terrain/crop sprites patched only for tiles listed in `changedTiles`; zone/mine overlay redrawn only when zones are added or a mine flag flips; buildings/items redrawn only when touched. All delta-driven work is accumulated and flushed once per frame in the ticker. Pawn and item-stack display objects are pooled and reused. - `main.ts`: HUD/roster/inspector DOM rebuilds coalesced into one `requestAnimationFrame` callback (no work while the tab is backgrounded). - `onTickDeltaApplied` now takes the `TickDelta` instead of two booleans. ## Verification (local, same colony with one 20×12 growing zone) | | Main-thread blocked | Worst stall | JS heap | |---|---|---|---| | Before (`origin/main`) | 5.2 s per 10 s | 378 ms | **1,097 MB** ~14 s after load | | After | ~0 in steady state | 172 ms (initial load) | flat 23–25 MB over 40 s | Visually checked terrain, zones, crops, item stack counts, pawns, labels and HUD panels; no console errors. `npm test` (16 tests) and `npm run typecheck` pass. ## Follow-up (not in this PR) Every growing tile still goes over the wire every tick (~88 tile records per delta in this test), which scales with farm size. Worth a separate look at sending crop growth only on stage changes or at a lower rate. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Growing crops dirty their tiles every sim tick, so every 10 Hz delta made the
renderer destroy and recreate all 10,000 terrain sprites plus every crop and
item stack, and every animation frame recreated each pawn's Container,
Graphics, Sprites and Text. Locally this blocked the main thread ~52% of the
time and grew the JS heap past 1 GB within seconds; on long sessions the tab
hangs and Chrome's reused renderer process won't recover on refresh.

- Retain per-tile terrain/crop sprites and patch only tiles named in
  changedTiles; redraw the zone/mine overlay only when zones or mine flags change.
- Accumulate delta dirtiness and flush it once per animation frame.
- Reuse pawn and item-stack display objects; only update text/bars on change.
- Coalesce HUD/inspector DOM rebuilds into one rAF callback.

After: heap flat at ~24 MB, no main-thread blocking during steady play.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Author
Collaborator

Merge approved by @johnw via Claude Code chat on 2026-10-18: "Approve and merge pls"

Merge approved by @johnw via Claude Code chat on 2026-10-18: "Approve and merge pls"
This branch is already included in the target branch. There is nothing to merge.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin claude/13-client-render-lockup:claude/13-client-render-lockup
git switch claude/13-client-render-lockup

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch main
git merge --no-ff claude/13-client-render-lockup
git switch claude/13-client-render-lockup
git rebase main
git switch main
git merge --ff-only claude/13-client-render-lockup
git switch claude/13-client-render-lockup
git rebase main
git switch main
git merge --no-ff claude/13-client-render-lockup
git switch main
git merge --squash claude/13-client-render-lockup
git switch main
git merge --ff-only claude/13-client-render-lockup
git switch main
git merge claude/13-client-render-lockup
git push origin main
Sign in to join this conversation.
No description provided.