From e962378d9c408c49a978db5b18b3b8c8d26fde8e Mon Sep 17 00:00:00 2001 From: not-matthias Date: Fri, 6 Nov 2026 19:43:09 +0100 Subject: [PATCH 1/2] fix(tinybench): warm up the root frame before measuring Analysis runs called the `__codspeed_root_frame__` wrapper for the first time after `startBenchmark()`, so V8 compiled it, and the perf map recorded it, inside the measurement. Only the first benchmark of a process paid for it: `switch 1` of examples/with-typescript-simple-esm measured 156,638 Ir under callgrind, against 3,152 Ir for `switch 2`. The warmup now calls the benchmark through the same wrapper, which brings `switch 1` down to 6,964 Ir. --- packages/tinybench-plugin/src/analysis.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/tinybench-plugin/src/analysis.ts b/packages/tinybench-plugin/src/analysis.ts index 65bd6183..bf0184f2 100644 --- a/packages/tinybench-plugin/src/analysis.ts +++ b/packages/tinybench-plugin/src/analysis.ts @@ -32,16 +32,18 @@ class AnalysisBenchRunner extends BaseBenchRunner { const { fnOpts, fn } = this.getTaskData(task); await fnOpts?.beforeAll?.call(task, "run"); + // Warm up through the root frame: V8 compiles it on its first call, which + // must not land in the measurement. + const rootFrame = wrapWithRootFrame(fn); await optimizeFunction(async () => { await fnOpts?.beforeEach?.call(task, "run"); - await fn(); + await rootFrame(); await fnOpts?.afterEach?.call(task, "run"); }); await fnOpts?.beforeEach?.call(task, "run"); await mongoMeasurement.start(uri); global.gc?.(); - const rootFrame = wrapWithRootFrame(fn); InstrumentHooks.startBenchmark(); try { await rootFrame(); @@ -62,16 +64,16 @@ class AnalysisBenchRunner extends BaseBenchRunner { fnOpts?.beforeAll?.call(task, "run"); + const rootFrame = wrapWithRootFrameSync(fn); optimizeFunctionSync(async () => { fnOpts?.beforeEach?.call(task, "run"); - await fn(); + await rootFrame(); fnOpts?.afterEach?.call(task, "run"); }); fnOpts?.beforeEach?.call(task, "run"); global.gc?.(); - const rootFrame = wrapWithRootFrameSync(fn); InstrumentHooks.startBenchmark(); try { rootFrame(); From 329b067a7c5a76d58700928ec2fbfa920aa20997 Mon Sep 17 00:00:00 2001 From: not-matthias Date: Fri, 6 Nov 2026 19:43:10 +0100 Subject: [PATCH 2/2] fix(core): bind the native addon's symbols at load With lazy binding, the first `stopBenchmark()` of a process resolved `instrument_hooks_stop_benchmark` through the dynamic linker while the benchmark was still measured. Link the addon with `-z now` so symbols resolve when it loads. Under callgrind this removes 2,339 Ir from the first benchmark of examples/with-vitest-v5, and brings `switch 1` of examples/with-typescript-simple-esm to 4,625 Ir. --- packages/core/binding.gyp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/core/binding.gyp b/packages/core/binding.gyp index 448466e3..dea28a94 100644 --- a/packages/core/binding.gyp +++ b/packages/core/binding.gyp @@ -36,6 +36,12 @@ "-Wno-unused-but-set-variable", "-Wno-type-limits" ], + # Resolve symbols at load: lazy binding would resolve + # instrument_hooks_stop_benchmark on its first call, inside the + # first measured benchmark. + "ldflags": [ + "-Wl,-z,now" + ], "sources": [ "src/native_core/linux_perf/linux_perf.cc", "src/native_core/linux_perf/linux_perf_listener.cc",