Skip to content

feat: run each benchmark in its own process - #101

Draft
not-matthias wants to merge 6 commits into
mainfrom
cod-3960-implement-process-isolation-in-codspeed-node
Draft

not-matthias wants to merge 6 commits into
mainfrom
cod-3960-implement-process-isolation-in-codspeed-node

Conversation

@not-matthias

Copy link
Copy Markdown
Member

TLDR: Benchmarks in one process share allocator and JIT state, so one benchmark shifts the next; each instrumented benchmark now runs in a fresh process (one full Vitest run per benchmark for the Vitest plugin).

  • Env: CODSPEED_PROCESS_ISOLATION (unset/1/true on by default, 0/false off, other values error), CODSPEED_ISOLATED_BENCHMARK=<uri> set on each child
  • The parent runs benchmarks one after the other, each in a child; the child runs only its URI and prints no banner or summary
  • Re-exec uses process.execPath + process.execArgv + process.argv.slice(1), so the V8 flags from the runner survive
  • Only instrumented runs (simulation/memory, hooks active) are affected; walltime and non-CodSpeed runs are untouched
  • The child filter overrides user filters
  • Child failure exits the parent with the same code (128 + N for signal N)
  • Tests: core (env, exit codes, execArgv), benchmark.js, tinybench (async and sync), Vitest 5, each checking PID counts on/off and a failing benchmark

Review notes

  • Vitest records CODSPEED_ISOLATION_COMMAND from the main process; each worker re-runs the whole Vitest command per benchmark, and the child's Vitest reporter output is not silenced
  • In-process tests against a mocked core must set CODSPEED_PROCESS_ISOLATION=false
  • The legacy Vitest 3/4 runner was not exercised with isolation on locally
  • The parent still runs setupCore, so it writes a perf map and environment file for a PID that measures nothing

Mirrors the codspeed-rust implementation (CodSpeedHQ/codspeed-rust, process isolation)

Closes COD-3960

Benchmarks of one process share allocator, JIT and thread state, so what one
benchmark leaves behind (raised mmap threshold, fragmented heap, optimized
code, retained caches) changes the measurement of every benchmark after it.
Adding or changing one benchmark can then shift unrelated results.

Add `Isolation` for the integrations. It is on by default and turned off with
`CODSPEED_PROCESS_ISOLATION=false`. `Isolation.run` re-executes the current
command with `CODSPEED_ISOLATED_BENCHMARK` set to one benchmark URI, so each
benchmark gets a fresh process. The child only runs that benchmark, which
overrides user filters.

- `CODSPEED_PROCESS_ISOLATION`: unset, `1` or `true` is on, `0` or `false` is
  off, anything else is an error.
- `CODSPEED_ISOLATED_BENCHMARK`: set by the parent on each child.
- `CODSPEED_ISOLATION_COMMAND`: JSON command to re-run, for runners that
  measure in worker processes whose argv is not the benchmark command.

The child is started with `process.execPath` and `process.execArgv`, so the V8
flags set by the runner survive. A failing child ends the parent with the same
exit code (128 + N when signal N killed it). Children run sequentially with
`spawnSync`, which keeps the output in order. Re-exec is used instead of fork
because fork only copies the calling thread.
Route every instrumented benchmark through `Isolation.run`. A child process
only runs its own benchmark and skips the banner, the summary line and the
completion listeners that the parent already handles. A run call that owns no
benchmark of the child does not touch the instruments.
The analysis runner measures every task through `Isolation.run` (async and
sync). Walltime runs and runs without active instrument hooks are not
isolated. A child only measures its own task and stays quiet about the banner
and the summary line.
Both the Vitest 5 provider and the legacy analysis runner measure through
`Isolation.run`. An isolated child still reports every benchmark to Vitest, but
only measures its own.

Vitest measures in forked workers, and the argv of a worker is not the command
that starts the benchmarks. The plugin records the command of the main Vitest
process in `CODSPEED_ISOLATION_COMMAND` from its `config()` hook, before the
workers are forked, and each worker re-runs that command once per benchmark.
This means one full Vitest run per benchmark.
Run fixtures in simulation mode with the instrument hooks stubbed (they only
exist under the CodSpeed runner) and check, for core, benchmark.js, tinybench
and Vitest 5, that:
- every benchmark runs in a fresh process by default;
- `CODSPEED_PROCESS_ISOLATION=false` runs them all in one process;
- a failing benchmark fails the run;
- core forwards the child exit code (128 + N for signals) and the execArgv.

Tests that drive a plugin in-process against a mocked core now set
`CODSPEED_PROCESS_ISOLATION=false`, because isolation would start the test
runner again for every benchmark.
Document the environment contract, the parent and child roles, and the Vitest
and test caveats.
@not-matthias
not-matthias force-pushed the cod-3960-implement-process-isolation-in-codspeed-node branch from a33e439 to 4bd0611 Compare October 21, 2026 10:14
@codspeed

codspeed Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Merging this PR will degrade performance by 79.42%

3 warnings, 1 note

⚠️ Walltime results may be unreliable

Some benchmarks used the Walltime instrument outside CodSpeed Macro Runners, which can make the results inconsistent.

Learn more about Macro Runners

See affected benchmarks

⚠️ 30 benchmarks are dominated by system calls

System calls cannot be simulated consistently and are left out of the measure. The reported value understates the real cost. Switch to the Walltime instrument to measure them.

Learn more about system calls

See affected benchmarks

⚠️ Base and head ran on different runner types

Some benchmarks with significant performance changes were compared across different runner types, so the changes might not come from the code. Run base and head on the same runner type, or use CodSpeed Macro Runners for more stable results.

Learn more about Macro Runners

Open the report in CodSpeed to investigate

ℹ️ 28 benchmarks have no flamegraph

The flamegraph wasn't generated: most of the time was spent in runtime internals, which would leave nothing useful to analyze.

See affected benchmarks

⚡ 6 improved benchmarks
❌ 73 regressed benchmarks
✅ 123 untouched benchmarks
🆕 3 new benchmarks
⏩ 118 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
❌ Memory short body 656 B 30,112 B -97.82%
❌ Memory test_iterative_fibo_100 656 B 30,112 B -97.82%
❌ Memory test_iterative_fibo_10 656 B 30,112 B -97.82%
❌ Memory test_recursive_cached_fibo_20 656 B 30,112 B -97.82%
❌ Memory test_recursive_fibo_20 656 B 30,112 B -97.82%
❌ Memory test_recursive_cached_fibo_20 656 B 30,112 B -97.82%
❌ Memory test sync baz 10 656 B 30,112 B -97.82%
❌ Memory switch 2 656 B 30,112 B -97.82%
❌ Memory test_recursive_cached_fibo_30 656 B 30,112 B -97.82%
❌ Memory test_recursive_fibo_20 656 B 30,112 B -97.82%
❌ Memory test_iterative_fibo_100 656 B 30,112 B -97.82%
❌ Memory test_iterative_fibo_10 656 B 30,112 B -97.82%
❌ Memory test sync baz 100 656 B 30,112 B -97.82%
❌ Memory test_recursive_cached_fibo_30 656 B 30,112 B -97.82%
❌ Memory switch 2 656 B 30,112 B -97.82%
❌ Memory test sync baz 100 656 B 30,112 B -97.82%
❌ Memory recursive fibo 15 with hooks 656 B 30,112 B -97.82%
❌ Memory recursive fibo 15 with hooks 656 B 30,112 B -97.82%
❌ Memory test_recursive_cached_fibo_10 784 B 30,112 B -97.4%
❌ Memory test sync baz 10 784 B 30,112 B -97.4%
... ... ... ... ... ...

ℹ️ Only the first 20 benchmarks are displayed. Go to the app to view all benchmarks.

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing cod-3960-implement-process-isolation-in-codspeed-node (4bd0611) with main (d15b97d)

Open in CodSpeed

Footnotes

  1. 118 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

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.

1 participant