Repository navigation
fix: make simulation GC deterministic after async setup and fix bench function calls - #124
not-matthias wants to merge 4 commits into
Conversation
--predictable and --predictable-gc-schedule fix the heap growing schedule but V8 still posts incremental marking steps as tasks on the event loop. Async work before a benchmark (Vitest's ESM loading, an awaited readFile loop) runs a varying number of those tasks, so when marking finishes, how many GCs ran and where objects end up differ between identical runs. Under Valgrind this shows as steps in both instruction counts (lazy sweeping inside the measured call) and cache misses. --no-incremental-marking-task makes marking progress only on allocation, which is deterministic. The flag exists on every supported Node major (22, 24, 26).
In analysis mode, runSync passed an async lambda to optimizeFunctionSync. Each warmup call returned at its first await, so its afterEach hook ran after the measured call instead of right after the warmup call, and the measured call ran with every warmup's state still live. The lambda is now synchronous, like the function runSync accepts.
The Vitest 5 provider only awaited functions declared async. A function that returns a promise without being async was run on the sync path, so its asynchronous work ran after the measured window closed and after afterEach. The provider now calls a non-async function once between its hooks, like tinybench does, and takes the async path when it returns a thenable. beforeAll is awaited up front for both paths.
tinybench calls a function that isn't declared async once in bench.add to tell whether it returns a promise. The walltime root-frame wrapper is sync, so every async benchmark was probed this way, and with forks every process re-registered and probed every benchmark in the file. Benchmarks with side effects ran in forks that never measure them.
A process now registers an async no-op for benchmarks it doesn't run. For the benchmarks it runs, an async function is registered with { async: true } so tinybench 6 skips the probe; the wrapper stays sync because an async wrapper adds an await to every iteration.
[Medium impact] The PR appears safe to merge; no new actionable defects were found. SummaryMakes simulation GC scheduling more repeatable and fixes benchmark call ordering.
Acknowledged by Reviews (1) · Last reviewed commit: "fix(tinybench): only call a benchmark in..." · Reviewed by Greptile |
Merging this PR will degrade performance by 10.78%4 warnings, 1 note
Warning Please fix the performance issues or acknowledge them on CodSpeed. Performance Changes
Tip Investigate this regression by commenting Comparing Footnotes
|
TLDR: Simulation results of benchmarks behind async setup (Vitest module loading, awaited I/O) change between identical runs, because V8 still runs incremental-marking steps as event-loop tasks under
--predictable. This PR turns those tasks off and fixes three bugs in how the tinybench and Vitest 5 integrations call benchmark functions.GC determinism (
core)--no-incremental-marking-taskto the analysis V8 flags. Marking then progresses only on allocation.traverse), 0.143% → 0.003% (alloc), 0.050% → 0.000% (lodash).readFileloop: GCs before the measured call were 51 or 52, now 48 in 40/40 runs.Benchmark function calls (each has a regression test that fails on
main)runSync, simulation: the warmup lambda wasasync, so each warmup'safterEachran after the measured call.asyncran on the sync path, so its async work ran outside the measured window.bench.add. A fork now calls only its own benchmark.Review notes
traverse−0.9%: marking work no longer lands in the measured call.allochas a 1.2% step in 2/16 runs with this PR (0.065% spread before). It is the same without the flag and--no-flush-bytecodedoesn't remove it. Natively, the GC count and young-generation usage at the measured call are identical in 24/24 runs, so the step is Valgrind-only and not attributed yet.--no-flush-bytecodewas evaluated and isn't added.packages/tinybench-plugin/src/analysis.ts(samerunSynclambda); the resolution keeps both changes.Closes COD-4179