From a6af4e39b29c66b3575dac40254de1c4507b09ae Mon Sep 17 00:00:00 2001 From: not-matthias Date: Thu, 22 Oct 2026 16:25:23 +0200 Subject: [PATCH] fix(core): exclude the running node executable from callgrind The runner only passes `--obj-skip` for the `node` it resolves on PATH. When benchmarks run another binary, such as one provided by a Bazel toolchain, callgrind records the whole V8 runtime between the JS frames: thousands of native functions that drown the flamegraph and make it expensive to process. Register `process.execPath` on callgrind's obj-skip list in `setupCore`, through the instrument-hooks client request, as pytest-codspeed does for the Python executable. It runs before any benchmark starts the instrumentation, and is a no-op outside Valgrind. --- packages/core/src/index.ts | 4 ++++ packages/core/src/native_core/index.ts | 4 ++++ .../core/src/native_core/instruments/hooks.ts | 9 ++++++++ .../native_core/instruments/hooks_wrapper.cc | 23 +++++++++++++++++++ .../native_core/instruments/hooks_wrapper.h | 1 + 5 files changed, 41 insertions(+) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 63ac394f..4f9f4b2f 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -26,6 +26,10 @@ export const setupCore = () => { } native_core.InstrumentHooks.setIntegration("codspeed-node", __VERSION__); + // The runner only obj-skips the `node` it finds on PATH. Skip the executable + // that actually runs, since a toolchain such as Bazel's brings its own: when + // left in, callgrind records the whole V8 runtime between the JS frames. + native_core.InstrumentHooks.callgrindAddObjSkip(process.execPath); // In walltime, we use node's native profiling options rather than our own, cf `getV8Flags` if (getCodspeedRunnerMode() !== "walltime") { linuxPerf.start(); diff --git a/packages/core/src/native_core/index.ts b/packages/core/src/native_core/index.ts index d0ad3587..4e64f654 100644 --- a/packages/core/src/native_core/index.ts +++ b/packages/core/src/native_core/index.ts @@ -67,6 +67,10 @@ try { addMarker: (_pid: number, _markerType: number, _timestamp: bigint) => { return 0; }, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + callgrindAddObjSkip: (_path: string) => { + return 0; + }, __codspeed_root_frame__: (callback: () => T): T => { return callback(); }, diff --git a/packages/core/src/native_core/instruments/hooks.ts b/packages/core/src/native_core/instruments/hooks.ts index b5ddebea..b9172129 100644 --- a/packages/core/src/native_core/instruments/hooks.ts +++ b/packages/core/src/native_core/instruments/hooks.ts @@ -63,6 +63,15 @@ export interface InstrumentHooks { */ addMarker(pid: number, markerType: number, timestamp: bigint): number; + /** + * Add an object file to callgrind's obj-skip list, as `--obj-skip` does. + * Only affects code callgrind has not instrumented yet. No-op outside + * Valgrind. + * @param path Path of the object file, resolved to its real path + * @returns 0 on success, non-zero on error + */ + callgrindAddObjSkip(path: string): number; + /** * Execute a callback function with __codspeed_root_frame__ in its stack trace * @param callback Function to execute diff --git a/packages/core/src/native_core/instruments/hooks_wrapper.cc b/packages/core/src/native_core/instruments/hooks_wrapper.cc index e2099626..c616e61a 100644 --- a/packages/core/src/native_core/instruments/hooks_wrapper.cc +++ b/packages/core/src/native_core/instruments/hooks_wrapper.cc @@ -135,6 +135,27 @@ Napi::BigInt CurrentTimestamp(const Napi::CallbackInfo &info) { return Napi::BigInt::New(env, ts); } +Napi::Number CallgrindAddObjSkip(const Napi::CallbackInfo &info) { + Napi::Env env = info.Env(); + + if (info.Length() != 1) { + Napi::TypeError::New(env, "Expected 1 argument: path") + .ThrowAsJavaScriptException(); + return Napi::Number::New(env, 1); + } + + if (!info[0].IsString()) { + Napi::TypeError::New(env, "Expected string (path)") + .ThrowAsJavaScriptException(); + return Napi::Number::New(env, 1); + } + + std::string path = info[0].As().Utf8Value(); + + uint8_t result = instrument_hooks_callgrind_add_obj_skip(path.c_str()); + return Napi::Number::New(env, result); +} + Napi::Number AddMarker(const Napi::CallbackInfo &info) { Napi::Env env = info.Env(); @@ -220,6 +241,8 @@ Napi::Object Initialize(Napi::Env env, Napi::Object exports) { Napi::Function::New(env, CurrentTimestamp)); instrumentHooksObj.Set(Napi::String::New(env, "addMarker"), Napi::Function::New(env, AddMarker)); + instrumentHooksObj.Set(Napi::String::New(env, "callgrindAddObjSkip"), + Napi::Function::New(env, CallgrindAddObjSkip)); instrumentHooksObj.Set(Napi::String::New(env, "__codspeed_root_frame__"), Napi::Function::New(env, __codspeed_root_frame__)); diff --git a/packages/core/src/native_core/instruments/hooks_wrapper.h b/packages/core/src/native_core/instruments/hooks_wrapper.h index 793103bc..d5c92daf 100644 --- a/packages/core/src/native_core/instruments/hooks_wrapper.h +++ b/packages/core/src/native_core/instruments/hooks_wrapper.h @@ -21,6 +21,7 @@ Napi::Number SetEnvironment(const Napi::CallbackInfo &info); Napi::Number WriteEnvironment(const Napi::CallbackInfo &info); Napi::BigInt CurrentTimestamp(const Napi::CallbackInfo &info); Napi::Number AddMarker(const Napi::CallbackInfo &info); +Napi::Number CallgrindAddObjSkip(const Napi::CallbackInfo &info); Napi::Object Initialize(Napi::Env env, Napi::Object exports); } // namespace hooks_wrapper