Skip to content

feat(bench): measure main-thread blocking on the interaction path - #461

Merged
blove merged 1 commit into
mainfrom
blove/issue-458-interaction-longtasks
Aug 17, 2026
Merged

feat(bench): measure main-thread blocking on the interaction path#461
blove merged 1 commit into
mainfrom
blove/issue-458-interaction-longtasks

Conversation

@blove

@blove blove commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Closes #458. Step 1 of the #452/#457 sequence — instrument before tuning, so the next change (the slice-budget lever) can prove it didn't regress main-thread blocking rather than assert it.

What

measureRowSetChange now runs a longtask PerformanceObserver and emits post_interaction_long_tasks_count / _ms. Until now the interaction path collected no blocking metric at all — the scroll and updates paths already did — so a synchronous engine blocking for its whole sort and a cooperative engine that never blocks were indistinguishable in every published interaction table.

Three deliberate choices:

  • Attached at the trigger, not at function entry. A long task during the pre-trigger quiet wait is mount tail; charging it to the interaction poisons the metric. The trigger is the interaction — a blocking sort runs inside it.
  • Push-based, so unlike the height-error walk removed in fix(bench): measure row-height error on recorded frames only #456 this adds no per-frame DOM work to the window being measured.
  • Absent, not zero, when the host can't observe long tasks — same honesty rule as the row-height error.

First measurement

S2 sort, 3 repeats each, quiet machine:

latency long tasks
pretable @ 3k ~58ms 0
tanstack @ 3k ~33ms 0 (its ~15ms block is under the 50ms threshold)
tanstack @ 50k ~33ms 1 × 62ms, 3/3 runs, sd <1ms

So the honest comparative statement is finally expressible: at target scale TanStack reports a 33ms latency while blocking the main thread for 62ms per sort. (pretable at 50k is #457 — it never blocks but currently can't complete inside the window.)

Verification

The test stubs PerformanceObserver, plays synthetic entries at three moments — during the run's own quiet wait (via the frame stub's onFrame, not merely before the call — that distinction is what catches an observer attached at entry), inside the trigger, and after completion — and asserts count, total, and teardown.

Mutation-checked: attach-at-entry, never-disconnect, and metrics-dropped each redden the test. 156 bench tests, 16 bench-runner tests, typecheck, lint, format.

🤖 Generated with Claude Code

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>
@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 3:26am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

Vercel preview ready

Preview: https://pretable-87d86o85q-cacheplane.vercel.app
Commit: 284245b7fd22b43705ad0a92d595cdf2b51400b1

Updated automatically by the deploy-preview job.

@blove
blove merged commit 8efa72d into main Aug 17, 2026
20 checks passed
@blove
blove deleted the blove/issue-458-interaction-longtasks branch August 17, 2026 03:52
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 interaction path measures no main-thread blocking, so the advantage pretable pays for is invisible

1 participant