Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ on:
pull_request:
workflow_dispatch:

permissions:
contents: read

jobs:
check:
strategy:
Expand Down Expand Up @@ -121,3 +124,46 @@ jobs:
env:
CODSPEED_SKIP_UPLOAD: true
CODSPEED_DEBUG: true

# Bazel's rules_js runs node by absolute path, so the runner's `node` wrapper
# on PATH never injects the V8 flags and the plugin must restart itself.
restart-without-node-wrapper:
runs-on: "ubuntu-latest"
name: "restart without node wrapper (${{ matrix.mode }})"
strategy:
matrix:
mode: [simulation, walltime, memory]
fail-fast: false
steps:
- uses: "actions/checkout@v4"
with:
fetch-depth: 0
submodules: true
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
with:
cache: pnpm
node-version: 24
- name: Restore turbo cache
uses: ./.github/actions/turbo-cache
with:
key-suffix: node24
- run: pnpm install --frozen-lockfile --prefer-offline
- run: pnpm turbo run build
# Resolved before the action prefixes PATH with its wrapper.
- run: echo "REAL_NODE=$(command -v node)" >> "$GITHUB_ENV"
- uses: CodSpeedHQ/action@main
with:
mode: ${{ matrix.mode }}
working-directory: examples/with-javascript-esm
run: |
set -o pipefail
"$REAL_NODE" tinybench.js 2>&1 | tee tinybench.log
"$REAL_NODE" benchmark-js.js 2>&1 | tee benchmark-js.log
env:
CODSPEED_SKIP_UPLOAD: true
CODSPEED_DEBUG: true
- name: Check the benchmarks ran with the V8 flags
working-directory: examples/with-javascript-esm
run: |
! grep -E "missing required flags|failed to restart node" tinybench.log benchmark-js.log
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
3 changes: 3 additions & 0 deletions packages/benchmark.js-plugin/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
ensureV8Flags,
getCallingFile,
InstrumentHooks,
mongoMeasurement,
Expand All @@ -19,6 +20,8 @@ import { CodSpeedBenchmark } from "./types";
declare const __VERSION__: string;

tryIntrospect();
// Must run before any benchmark state exists: it may replace this process.
ensureV8Flags();

interface WithCodSpeedBenchmark extends Omit<
Benchmark,
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export type {
SetupInstrumentsRequestBody,
SetupInstrumentsResponse,
} from "./generated/openapi";
export { getV8Flags, tryIntrospect } from "./introspection";
export { ensureV8Flags, getV8Flags, tryIntrospect } from "./introspection";
export {
getUnsupportedNodeVersionWarning,
SUPPORTED_NODE_MAJORS,
Expand Down
97 changes: 95 additions & 2 deletions packages/core/src/introspection.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { writeFileSync } from "fs";
import { readlinkSync, writeFileSync } from "fs";
import path from "path";
import { isMainThread } from "worker_threads";

import { getInstrumentMode } from "./runnerMode";
import native_core from "./native_core";
import { getCodspeedRunnerMode, getInstrumentMode } from "./runnerMode";

const CUSTOM_INTROSPECTION_EXIT_CODE = 0;

const V8_LOG_FILENAME_PATTERN = "codspeed-v8-%p.log";
const RESTARTED_PID_ENV = "__CODSPEED_NODE_CORE_RESTARTED_PID__";

// Fixed seeds so Math.random() and hash table layout match on every run
const getSeedFlags = (nodeVersionMajor: number) => {
Expand Down Expand Up @@ -91,6 +94,96 @@ export const tryIntrospect = () => {
}
};

const EVAL_FLAGS = ["-e", "--eval", "-p", "--print"];

// `node -` or `node` without a script reads the script from stdin (or opens the
// REPL). The input is consumed by then, so the restarted process would get none.
const readsScriptFromStdin = () => {
const scriptPath = process.argv[1];
if (scriptPath === "-") {
return true;
}
if (scriptPath !== undefined) {
return false;
}
return !process.execArgv.some((arg) =>
EVAL_FLAGS.some((flag) => arg === flag || arg.startsWith(`${flag}=`)),
);
};

/**
* Restarts the process with the V8 flags the runner mode requires when the
* launcher did not pass them (e.g. Bazel executes node by absolute path, so the
* runner's `node` wrapper never runs). Must be called before instrument-hooks
* is used: the restart replaces this process.
*
* Cases that cannot restart safely, including a failed exec, run without the
* flags; `checkV8Flags` warns once instrument-hooks is set up.
*/
export const ensureV8Flags = () => {
if (getCodspeedRunnerMode() === "disabled" || !native_core.isBound) {
return;
}

const missingFlags = getV8Flags().filter(
(flag) => !process.execArgv.includes(flag),
);
if (missingFlags.length === 0) {
return;
}

// Worker threads and IPC children cannot replace their process, and an IPC
// channel would be lost.
if (!isMainThread || process.send !== undefined) {
return;
}

if (readsScriptFromStdin()) {
return;
}

// The restart did not help (its launcher stripped the flags again): do not
// restart in a loop.
if (process.env[RESTARTED_PID_ENV] === String(process.pid)) {
return;
}

const getExecutable = () => {
switch (process.platform) {
case "linux":
// rules_js replaces process.execPath with its shell shim.
return readlinkSync("/proc/self/exe");
case "darwin":
return process.execPath;
default:
return undefined;
}
};

try {
const executable = getExecutable();
if (executable === undefined) {
return;
}

process.env[RESTARTED_PID_ENV] = String(process.pid);
// The runner's wrapper passes its flags before the user's, so user flags win.
native_core.restart(executable, [
executable,
...missingFlags,
...process.execArgv,
...process.argv.slice(1),
]);
Comment thread
not-matthias marked this conversation as resolved.
} catch (error) {
// No /proc, or exec failed: the current image is still usable, so measure
// without the flags and let checkV8Flags warn.
delete process.env[RESTARTED_PID_ENV];
console.warn(
`[CodSpeed] failed to restart node with the required V8 flags: ${String(error)}`,
);
}
};

export const checkV8Flags = () => {
const requiredFlags = getV8Flags();
const actualFlags = process.execArgv;
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/native_core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { LinuxPerf } from "./linux_perf/linux_perf";
interface NativeCore {
InstrumentHooks: InstrumentHooks;
LinuxPerf: typeof LinuxPerf;
restart(executable: string, args: string[]): never;
}

interface NativeCoreWithBindingStatus extends NativeCore {
Expand All @@ -26,6 +27,11 @@ try {
logDebug("Failed to bind native core, instruments will not work.");
logDebug(e);
native_core = {
restart: () => {
throw new Error(
`[CodSpeed] Cannot restart Node.js because the native core module is not bound: ${String(e)}`,
);
},
LinuxPerf: class LinuxPerf {
start() {
return false;
Expand Down
33 changes: 17 additions & 16 deletions packages/core/src/native_core/instruments/hooks_wrapper.cc
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,35 @@ namespace codspeed_native {
namespace instruments {
namespace hooks_wrapper {

static InstrumentHooks *hooks = nullptr;
namespace {

void InitializeGlobal() {
if (!hooks) {
hooks = instrument_hooks_init();
}
// Created on first use: init opens the runner FIFO, so importing the module must
// not trigger it.
InstrumentHooks *GetHooks() {
static InstrumentHooks *hooks = instrument_hooks_init();
return hooks;
}

} // namespace

Napi::Boolean IsInstrumented(const Napi::CallbackInfo &info) {
Napi::Env env = info.Env();

bool instrumented = instrument_hooks_is_instrumented(hooks);
bool instrumented = instrument_hooks_is_instrumented(GetHooks());
return Napi::Boolean::New(env, instrumented);
}

Napi::Number StartBenchmark(const Napi::CallbackInfo &info) {
Napi::Env env = info.Env();

uint8_t result = instrument_hooks_start_benchmark(hooks);
uint8_t result = instrument_hooks_start_benchmark(GetHooks());
return Napi::Number::New(env, result);
}

Napi::Number StopBenchmark(const Napi::CallbackInfo &info) {
Napi::Env env = info.Env();

uint8_t result = instrument_hooks_stop_benchmark(hooks);
uint8_t result = instrument_hooks_stop_benchmark(GetHooks());
return Napi::Number::New(env, result);
}

Expand All @@ -54,7 +57,7 @@ Napi::Number SetExecutedBenchmark(const Napi::CallbackInfo &info) {
std::string uri = info[1].As<Napi::String>().Utf8Value();

uint8_t result =
instrument_hooks_set_executed_benchmark(hooks, pid, uri.c_str());
instrument_hooks_set_executed_benchmark(GetHooks(), pid, uri.c_str());
return Napi::Number::New(env, result);
}

Expand All @@ -76,8 +79,8 @@ Napi::Number SetIntegration(const Napi::CallbackInfo &info) {
std::string name = info[0].As<Napi::String>().Utf8Value();
std::string version = info[1].As<Napi::String>().Utf8Value();

uint8_t result =
instrument_hooks_set_integration(hooks, name.c_str(), version.c_str());
uint8_t result = instrument_hooks_set_integration(
GetHooks(), name.c_str(), version.c_str());
return Napi::Number::New(env, result);
}

Expand All @@ -104,7 +107,7 @@ Napi::Number SetEnvironment(const Napi::CallbackInfo &info) {
std::string value = info[2].As<Napi::String>().Utf8Value();

uint8_t result = instrument_hooks_set_environment(
hooks, section_name.c_str(), key.c_str(), value.c_str());
GetHooks(), section_name.c_str(), key.c_str(), value.c_str());
return Napi::Number::New(env, result);
}

Expand All @@ -125,7 +128,7 @@ Napi::Number WriteEnvironment(const Napi::CallbackInfo &info) {

int32_t pid = info[0].As<Napi::Number>().Int32Value();

uint8_t result = instrument_hooks_write_environment(hooks, pid);
uint8_t result = instrument_hooks_write_environment(GetHooks(), pid);
return Napi::Number::New(env, result);
}

Expand Down Expand Up @@ -193,7 +196,7 @@ Napi::Number AddMarker(const Napi::CallbackInfo &info) {
}

uint8_t result =
instrument_hooks_add_marker(hooks, pid, marker_type, timestamp);
instrument_hooks_add_marker(GetHooks(), pid, marker_type, timestamp);
return Napi::Number::New(env, result);
}

Expand Down Expand Up @@ -221,8 +224,6 @@ Napi::Value __attribute__ ((noinline)) __codspeed_root_frame__(const Napi::Callb
Napi::Object Initialize(Napi::Env env, Napi::Object exports) {
Napi::Object instrumentHooksObj = Napi::Object::New(env);

InitializeGlobal();

instrumentHooksObj.Set(Napi::String::New(env, "isInstrumented"),
Napi::Function::New(env, IsInstrumented));
instrumentHooksObj.Set(Napi::String::New(env, "startBenchmark"),
Expand Down
5 changes: 0 additions & 5 deletions packages/core/src/native_core/instruments/hooks_wrapper.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,6 @@ namespace codspeed_native {
namespace instruments {
namespace hooks_wrapper {

// Global initialization
// WARNING: InitializeGlobal() must be called before using any other functions
// All API functions assume the global instance is already initialized
void InitializeGlobal();

Napi::Boolean IsInstrumented(const Napi::CallbackInfo &info);
Napi::Number StartBenchmark(const Napi::CallbackInfo &info);
Napi::Number StopBenchmark(const Napi::CallbackInfo &info);
Expand Down
Loading
Loading