Stop rebuilding the hand log after every fold, call and raise #48

Merged
jheaps merged 1 commits from feature/casino-holdem-render-perf into master 2026-09-01 09:30:13 -06:00
Owner

Profiled a long night at a full table — 24 seats, 200 logged hands — because that's the state the tool is slowest in and the one a dealer reaches at 2am. Stacked on #47.

Where the time went

A full re-render cost 30ms, and it runs after every single action. Broken down:

ms
renderHandHistory 25.74
renderStandings 1.52
renderSeats 1.31
everything else combined < 0.3

25.7ms of every action was tearing down and rebuilding 200 rows of a panel that hadn't changed and that nobody is looking at mid-hand.

The fix

The log only changes when a hand is settled or the history is cleared — maybe once every thirty taps. It now carries a cheap signature (length, hand counter, newest row) and is skipped when that hasn't moved. Entries are immutable once written and only ever added at the front, so the signature catches a recorded hand, a cleared history, and an undo that puts a hand back.

renderAll, every action     30.07ms  ->  4.18ms     (7.2x)
renderHandHistory, skipped  25.74ms  ->  0ms
renderHandHistory, rebuild  25.74ms  ->  9.05ms     (2.8x)

The rebuild got faster too, for the times it does run — once per hand, where 25ms of jank lands exactly on the "next hand" tap. toLocaleTimeString was the single most expensive call in the loop and a logged hand's timestamp never changes, so the formatted string is kept; and rows are built into a DocumentFragment so the panel lays out once instead of two hundred times.

Making sure it can't go stale

A cache like this is only worth having if it can't go stale — a dealer reading last hand's payouts while settling this one is worse than a slow panel. 10 tests cover the invalidation directly: unchanged through a street of betting; changed by settling, by clearing, by an undo back to a shorter log, by two hands differing only in the pot, and by the counter moving on.

Plus Playwright against the real page: rendered rows match stored history through settling a hand in the wizard, undoing it, clearing the log, and recording again — DOM matched state at every step.

Validation

109 tests passing (10 new). Touch and layout guarantees from the earlier rounds re-checked and still holding: 0 of 87 controls on touch-action: auto, 0 tap-highlighted, every stack still painting inside its own puck. No console errors.

One measurement I'm not claiming as a win: I tried timing tap-to-painted and got 31ms, but that's dominated by waiting two animation frames (~33ms at 60fps), so it says nothing useful about the change. The per-call numbers above are the real evidence.

Noted, not changed

HoldemState.save() runs on every action and serialises the whole table — ~296KB at 200 hands, since each entry carries a 20-row ledger. It measured 1.9–5ms depending on run. That's the next thing worth looking at if per-tap cost matters more, but it's a persistence-model change rather than a render fix, so I left it out of this one.

Profiled a long night at a full table — **24 seats, 200 logged hands** — because that's the state the tool is slowest in and the one a dealer reaches at 2am. Stacked on #47. ## Where the time went A full re-render cost **30ms**, and it runs after every single action. Broken down: | | ms | | --- | --- | | **renderHandHistory** | **25.74** | | renderStandings | 1.52 | | renderSeats | 1.31 | | everything else combined | < 0.3 | 25.7ms of every action was tearing down and rebuilding 200 rows of a panel that hadn't changed and that nobody is looking at mid-hand. ## The fix The log only changes when a hand is settled or the history is cleared — maybe once every thirty taps. It now carries a cheap signature (length, hand counter, newest row) and is skipped when that hasn't moved. Entries are immutable once written and only ever added at the front, so the signature catches a recorded hand, a cleared history, and an undo that puts a hand back. ``` renderAll, every action 30.07ms -> 4.18ms (7.2x) renderHandHistory, skipped 25.74ms -> 0ms renderHandHistory, rebuild 25.74ms -> 9.05ms (2.8x) ``` The rebuild got faster too, for the times it does run — once per hand, where 25ms of jank lands exactly on the "next hand" tap. `toLocaleTimeString` was the single most expensive call in the loop and a logged hand's timestamp never changes, so the formatted string is kept; and rows are built into a `DocumentFragment` so the panel lays out once instead of two hundred times. ## Making sure it can't go stale A cache like this is only worth having if it can't go stale — a dealer reading *last* hand's payouts while settling this one is worse than a slow panel. **10 tests cover the invalidation directly**: unchanged through a street of betting; changed by settling, by clearing, by an undo back to a shorter log, by two hands differing only in the pot, and by the counter moving on. Plus Playwright against the real page: rendered rows match stored history through settling a hand in the wizard, undoing it, clearing the log, and recording again — DOM matched state at every step. ## Validation 109 tests passing (10 new). Touch and layout guarantees from the earlier rounds re-checked and still holding: 0 of 87 controls on `touch-action: auto`, 0 tap-highlighted, every stack still painting inside its own puck. No console errors. One measurement I'm **not** claiming as a win: I tried timing tap-to-painted and got 31ms, but that's dominated by waiting two animation frames (~33ms at 60fps), so it says nothing useful about the change. The per-call numbers above are the real evidence. ## Noted, not changed `HoldemState.save()` runs on every action and serialises the whole table — ~296KB at 200 hands, since each entry carries a 20-row ledger. It measured 1.9–5ms depending on run. That's the next thing worth looking at if per-tap cost matters more, but it's a persistence-model change rather than a render fix, so I left it out of this one.
jheaps changed target branch from feature/casino-holdem-keyboard-focus to master 2026-09-01 09:30:10 -06:00
jheaps added 1 commit 2026-09-01 09:30:10 -06:00
Profiled a long night at a full table — 24 seats, 200 logged hands — because
that's the state the tool is slowest in and the one a dealer reaches at 2am.

A full re-render cost 30ms, and it runs after every single action. Broken down,
25.7ms of that was renderHandHistory: tearing down and rebuilding 200 rows of a
panel that hadn't changed and that nobody is looking at mid-hand. Everything
else on the table put together came to about 4ms.

The log only changes when a hand is settled or the history is cleared — maybe
once every thirty taps. It now carries a cheap signature (length, hand counter,
and the newest row) and is skipped when that hasn't moved. Entries are immutable
once written and only ever added at the front, so the signature catches a
recorded hand, a cleared history, and an undo that puts a hand back.

    renderAll, every action     30.07ms -> 4.18ms
    renderHandHistory, skipped  25.74ms -> 0ms
    renderHandHistory, rebuild  25.74ms -> 9.05ms

The rebuild got faster too, for the times it does run — once per hand, where
25ms of jank lands exactly on the "next hand" tap. toLocaleTimeString was the
single most expensive call in the loop and a logged hand's timestamp never
changes, so the formatted string is kept; and the rows are built into a fragment
so the panel lays out once instead of two hundred times.

A cache like this is only worth having if it can't go stale — a dealer reading
last hand's payouts while settling this one is worse than a slow panel. 10 tests
cover the invalidation directly: unchanged through a street of betting, changed
by settling, by clearing, by an undo back to a shorter log, by two hands that
differ only in the pot, and by the counter moving on.

Validated with Playwright against the real page: the rendered rows match the
stored history through settling a hand in the wizard, undoing it, clearing the
log and recording again. Touch and layout guarantees from the earlier rounds
still hold — 0 of 87 controls on touch-action auto, 0 tap-highlighted, every
stack still inside its own puck. 109 tests passing.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
jheaps merged commit a14087fa7d into master 2026-09-01 09:30:13 -06:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: jheaps/JoshHeaps.Net#48