fix(bench): exclude sub-timer-resolution cases from the comparison - #203
Merged
Merged
Conversation
PR #201's benchmark comment reported implausible improvements (GameplayRecorder +2733%, createBeatResolver +1500%, 205 more cases at +90% or higher) for a PR that only changes how scripts are invoked, not any benchmarked function. Root cause (issue #202): several exports-benchmark cases (closure- allocating ones especially) run in tens to a few hundred nanoseconds per call under CI's timing budget (--time 50 --warmup-time 25) — below what tinybench's timer can resolve (p50Ms == 0). A percent computed from two independent runs of code this cheap is measurement noise: the same unmodified code, benchmarked twice back to back, can swing 50%+ with zero changes (verified: chart.eventToBeat went 60.5M -> 24.4M ops/s across two back-to-back runs of the identical build). CI's base/head split (separate worktrees/processes) is exactly the setup where that noise reads as a "regression" or "improvement". Added a reliability floor: compareSnapshots now excludes any case whose base or head median per-call latency (p50Ms) is at or below 0.001ms from the regression/improvement/unchanged classification entirely (not folded into "unchanged" -- a percent computed from noise isn't a real "no change" either). Excluded cases get their own count (ComparisonSummary.unreliableCaseCount) and a labeled section in the markdown report so they stay visible instead of silently vanishing. countUnreliableCases is exported separately from compareSnapshots so existing callers of compareSnapshots (and its existing tests) keep returning ComparedRow[] unchanged.
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
be-music-player-demo | 8915c39 | Commit Preview URL Branch Preview URL |
Sep 06 2026, 07:41 AM |
Exports Benchmark
Summary
Top Regressions
Top ImprovementsNo improvement over threshold. Excluded (sub-timer-resolution)Per-call latency at or below 0.001ms on at least one side — the reported time is measurement noise, not the case's real cost, so no percent change is shown.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #202.
PR #201's benchmark comment reported implausible improvements —
player-web.GameplayRecorder+2733%,chart.createBeatResolver+1500%, 205 more cases at +90% or higher — for a PR that only changes how scripts are invoked (tsx→node), not any benchmarked function's implementation.Root cause
Several exports-benchmark cases (closure-allocating ones especially) run in tens to a few hundred nanoseconds per call under CI's timing budget (
--time 50 --warmup-time 25) — below what tinybench's timer can resolve (p50Ms == 0). A percent computed from two independent runs of code this cheap is measurement noise, not signal.Verified this directly: benchmarking the identical built code twice back-to-back (no code change at all) swings a case like
chart.eventToBeatby ~58% (60.5M → 24.4M ops/s) purely from run-to-run noise. CI's base/head split (separate git worktrees / processes, see.github/workflows/ci.ymlbenchmark-prjob) is exactly the setup where that noise reads as a "regression" or "improvement" against the other run.(I initially thought this meant PR #201 changed nothing real — see the correction comment on #202. It turns out tsx's esbuild-transpiled output and Node's type-stripped source do differ in how V8 optimizes closure-allocating call sites under a short timing budget, so #201 is a real, reproducible speedup for those cases. The percentages are directionally real but the magnitude reported for sub-tick-resolution cases isn't trustworthy either way, which is the actual bug this PR fixes.)
Fix
Added a reliability floor to
scripts/bench/compare-results.ts:compareSnapshotsnow excludes any case whose base or head median per-call latency (p50Ms) is at or below 0.001ms from the regression/improvement/unchanged classification entirely — not folded into "unchanged", since a percent computed from noise isn't a real "no change" either. Excluded cases get their own count (ComparisonSummary.unreliableCaseCount) and a labeled "Excluded (sub-timer-resolution)" section in the markdown report, so they stay visible instead of silently vanishing.countUnreliableCasesis exported separately fromcompareSnapshotsso existing callers ofcompareSnapshots(and its existing tests) keep returningComparedRow[]unchanged — no signature break.Testing
countUnreliableCasescounting only comparable (both-sides-present) keys.build/lint/typecheck/testgreen (1975 tests).chart/utilscases were correctly excluded as sub-timer-resolution noise, and the two real regressions/one real improvement that remained are outside the floor.🤖 Generated with Claude Code