Skip to content

fix(bench): measure row-height error on recorded frames only - #456

Merged
blove merged 1 commit into
mainfrom
blove/issue-455-settle-tax
Aug 17, 2026
Merged

fix(bench): measure row-height error on recorded frames only#456
blove merged 1 commit into
mainfrom
blove/issue-455-settle-tax

Conversation

@blove

@blove blove commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Closes #455.

The bug

sampleVisibleRows measured each visible row's height error eagerly, on every polled frame — and that measurement resolves getComputedStyle on every cell of the row (canCellOverflowVertically). The pre-trigger quiet wait and the trigger frame paid it too, then discarded the result. Per poll:

rows cells/row forced style resolutions
pretable 12 6 72
tanstack 16 40 640
mui 23 41 943

All inside the window the harness is timing — so interaction_latency_ms taxed adapters in proportion to their rendered DOM. Measured on S2/sort: 8.8% of pretable's window was the harness; 34.6% of TanStack's.

The fix

The sample carries the row element and its rendered height; recordRowHeightErrors measures lazily, on exactly the frames it records. Recording happens in the same task as the sample, so the element is still attached and the DOM state read is the frame's own — the measurement's semantics are unchanged, only its placement.

No flag to get backwards: the eager/lazy split I sketched on the issue had a boolean threaded through a shared sample() closure, where a wrong value in one direction silently starves the p95. This shape can't express that mistake.

Effect, measured

Same protocol as the finding (source-map-classified samples in the interaction window):

harness before harness after window
pretable 3.25ms (8.8%) 1.58ms (4.3%) 36.3 → 36.1ms
tanstack 5.44ms (34.6%) 2.83ms (20.3%) 15.2 → 13.6ms

The remainder is the settle signature itself (rowId@top per row), which is the detector's actual job. And the number the metric exists to expose — the adapter-work ratio — is unchanged at ~3.2×, which is the point: this moves the observer tax, not the comparison. #452's gap stands.

Verification

The new test counts cell style resolutions on either side of the trigger and asserts three things at once: the quiet wait resolves none, the measured window still resolves some, and measurable_rows still reports — so deleting the measurement outright cannot pass.

Mutation-checked both ways:

mutation goes red
restore the eager measurement (the original bug) the counting arm
starve the measurement (heightError = null) that test plus two pre-existing metric tests

155 bench tests, typecheck, lint, format.

Not done here

The interaction baselines in 2026-08-16-s2-mount-and-interaction.json were measured with the old tax and are not re-run — their tick distributions carry an adapter-proportional observer cost, worst for MUI. Worth a re-measure in the next quiet window; the file's own findings already lean on the adapter-work ratio, which this PR shows is stable.

🤖 Generated with Claude Code

Closes #455.

`sampleVisibleRows` measured each visible row's height error eagerly, and that
measurement resolves `getComputedStyle` on EVERY cell of the row (the
`canCellOverflowVertically` filter). Every polled frame paid it: the pre-trigger
quiet wait and the trigger frame included, whose errors are then discarded. Per
poll that is 72 forced style resolutions for pretable, 640 for tanstack, 943
for mui — inside the very window the harness is timing, so
`interaction_latency_ms` taxed adapters in proportion to their rendered DOM.

Measured on S2/sort: the harness consumed 8.8% of pretable's interaction window
and 34.6% of TanStack's. The samples now carry the row element and its rendered
height, and `recordRowHeightErrors` measures lazily — the cost lands on exactly
the frames whose errors are recorded, in the same task as the sample, so the
DOM state read is the frame's own. After the fix the harness share is 4.3% and
20.3% respectively (the remainder is the settle signature itself), TanStack's
measured window shrinks 15.2 -> 13.6ms, and the adapter-work ratio the metric
exists to expose is unchanged at ~3.2x — which is the point: the fix moves the
observer tax, not the comparison.

The new test counts cell style resolutions on either side of the trigger and
asserts three things at once: the quiet wait resolves none, the measured window
still resolves some, and `measurable_rows` still reports — so deleting the
measurement outright cannot pass. Mutation-checked both ways: restoring the
eager measurement reddens the counting arm; starving the measurement reddens
that test plus two pre-existing metric tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
pretable Ignored Ignored Aug 17, 2026 1:57am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

Vercel preview ready

Preview: https://pretable-dari2ijyq-cacheplane.vercel.app
Commit: cc34c6b4ecdb777fcad7f1d36542eab98316fb25

Updated automatically by the deploy-preview job.

@blove
blove merged commit 4f798a7 into main Aug 17, 2026
19 checks passed
@blove
blove deleted the blove/issue-455-settle-tax branch August 17, 2026 02:11
blove added a commit that referenced this pull request Aug 17, 2026
Closes #458.

interaction_latency_ms latches the first changed rAF frame, and under that
definition a synchronous engine that blocks the main thread for its whole sort
and a cooperative one that never blocks were indistinguishable — the
interaction path collected no long-task metric at all (the scroll and updates
paths already did). Every comparative interaction table published so far was
structurally blind to the one property pretable pays its latency premium for.

The observer attaches AT the trigger, not at function entry: a long task during
the pre-trigger quiet wait is mount tail, and charging it to the interaction
would poison the metric. It is a push-based PerformanceObserver, so unlike the
height-error walk removed in #456 it adds no per-frame DOM work. Emitted as
post_interaction_long_tasks_count / _ms, absent (not zero) when the host cannot
observe long tasks — the same absent-vs-zero rule as the row-height error.

First measurement, S2 sort, 3 repeats each:

  3,000 rows:  pretable 0 long tasks | tanstack 0 (its ~15ms block is under
               the 50ms longtask threshold)
  50,000 rows: tanstack reports ~33ms latency while blocking the main thread
               for 62ms (1 task, 3/3 runs, sd <1ms)

The test plays synthetic entries into a stubbed observer at three moments —
during the run's own quiet wait, inside the trigger, and asserts teardown.
Mutation-checked: attaching at function entry, never disconnecting, and
dropping the metrics each redden it.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The settle detector runs inside the window it measures, and its cost scales with the grid's DOM

1 participant