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
35 changes: 35 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,42 @@
- run: pnpm install --frozen-lockfile --prefer-offline
- run: pnpm turbo run lint typecheck test

native-abi:
runs-on: "ubuntu-latest"
name: Native addon ABI compatibility
env:
# The flags the runner passes in a real benchmark process.
NODE_OPTS: "--interpreted-frames-native-stack --allow-natives-syntax"
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-file: .nvmrc
- name: Restore turbo cache
uses: ./.github/actions/turbo-cache
with:
key-suffix: native-abi
- run: pnpm install --frozen-lockfile --prefer-offline
# One prebuild set, built once, then loaded by every supported Node
# major. The other jobs compile the addon with the same Node they run it
# under, so they cannot see an ABI mismatch.
- run: pnpm turbo run build --filter=@codspeed/core

- uses: actions/setup-node@v6
with:
node-version: "22"
- run: node ${{ env.NODE_OPTS }} scripts/assert-native-binding.cjs
- uses: actions/setup-node@v6
with:
node-version: "24"
- run: node ${{ env.NODE_OPTS }} scripts/assert-native-binding.cjs

list-examples:

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}
runs-on: "ubuntu-latest"
name: List examples
outputs:
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"gypfile": true,
"scripts": {
"build": "rollup -c",
"build-native-addon": "prebuildify --name node --strip --no-napi --target 22.0.0 --target 24.0.0",
"build-native-addon": "prebuildify --name node.napi --napi --strip --target 22.0.0",
"build-tracer-client": "openapi --client axios --input ./tracer.spec.json --name MongoTracer --output ./src/generated/openapi",
"test": "jest --passWithNoTests --silent",
"test/integ": "jest --passWithNoTests --silent -c jest.config.integ.js",
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@ export const setupCore = () => {
warnOnUnsupportedNodeVersion();

if (!native_core.isBound) {
const reason =
native_core.bindError instanceof Error
? native_core.bindError.message
: String(native_core.bindError);
throw new Error(
"Native core module is not bound, CodSpeed integration will not work properly",
`Native core module is not bound, CodSpeed integration will not work properly (Node ${process.version}, ABI ${process.versions.modules}, ${process.platform}-${process.arch}): ${reason}`,
);
}

Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/native_core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ interface NativeCore {

interface NativeCoreWithBindingStatus extends NativeCore {
isBound: boolean;
bindError?: unknown;
}

let native_core: NativeCoreWithBindingStatus;
Expand Down Expand Up @@ -76,6 +77,7 @@ try {
MARKER_TYPE_BENCHMARK_END: 3,
},
isBound: false,
bindError: e,
};
}

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/native_core/linux_perf/linux_perf.cc
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ LinuxPerf::LinuxPerf(const Napi::CallbackInfo &info)
Napi::Value LinuxPerf::Start(const Napi::CallbackInfo &info) {
if (handler == nullptr) {
v8::Isolate *isolate = v8::Isolate::GetCurrent();
handler = new LinuxPerfHandler(isolate);
handler = new LinuxPerfHandler(isolate, info.Env());
handler->Enable();
return Napi::Boolean::New(info.Env(), true);
}
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/native_core/linux_perf/linux_perf.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace codspeed_native {

class LinuxPerfHandler : public v8::CodeEventHandler {
public:
explicit LinuxPerfHandler(v8::Isolate *isolate);
LinuxPerfHandler(v8::Isolate *isolate, napi_env env);
~LinuxPerfHandler() override;

void Handle(v8::CodeEvent *code_event) override;
Expand All @@ -19,6 +19,7 @@ class LinuxPerfHandler : public v8::CodeEventHandler {
std::ofstream mapFile;
std::string FormatName(v8::CodeEvent *code_event);
v8::Isolate *isolate_;
napi_env env_;
};

class LinuxPerf : public Napi::ObjectWrap<LinuxPerf> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@

namespace codspeed_native {

LinuxPerfHandler::LinuxPerfHandler(v8::Isolate *isolate)
LinuxPerfHandler::LinuxPerfHandler(v8::Isolate *isolate, napi_env env)
: v8::CodeEventHandler(isolate) {
isolate_ = isolate;
env_ = env;
int pid = static_cast<int>(uv_os_getpid());
mapFile.open("/tmp/perf-" + std::to_string(pid) + ".map");
}
Expand All @@ -17,7 +18,7 @@ LinuxPerfHandler::~LinuxPerfHandler() { mapFile.close(); }
std::string LinuxPerfHandler::FormatName(v8::CodeEvent *code_event) {
std::string name = std::string(code_event->GetComment());
if (name.empty()) {
name = v8LocalStringToString(code_event->GetFunctionName());
name = v8LocalStringToString(env_, code_event->GetFunctionName());
}
return name;
}
Expand All @@ -27,7 +28,8 @@ void LinuxPerfHandler::Handle(v8::CodeEvent *code_event) {
<< code_event->GetCodeSize() << " ";
mapFile << v8::CodeEvent::GetCodeEventTypeName(code_event->GetCodeType())
<< ":" << FormatName(code_event) << " "
<< v8LocalStringToString(code_event->GetScriptName()) << std::dec
<< v8LocalStringToString(env_, code_event->GetScriptName())
<< std::dec
<< ":" << code_event->GetScriptLine() << ":"
<< code_event->GetScriptColumn() << std::endl;
}
Expand Down
32 changes: 26 additions & 6 deletions packages/core/src/native_core/linux_perf/utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,34 @@
#define LINUX_PERF_UTILS_H

#include "v8-profiler.h"
#include <js_native_api.h>
#include <string>

// The string conversions are the only part of the V8 C++ API used here that is
// not ABI-stable: Utf8Value's constructor gained a defaulted argument in Node
// 24, and WriteUtf8 gave way to WriteUtf8V2 in Node 26, so none of them
// resolves on every major. Node-API is versioned and does not move, and
// napi_value is layout-compatible with v8::Local<v8::Value> by construction.
static inline std::string
v8LocalStringToString(v8::Local<v8::String> v8String) {
// Utf8Value NUL-terminates, so the c-string constructor stops at the first
// embedded NUL, as callers expect for symbol names. It yields nullptr when
// the conversion throws.
v8::String::Utf8Value value(v8::Isolate::GetCurrent(), v8String);
return *value ? std::string(*value) : std::string();
v8LocalStringToString(napi_env env, v8::Local<v8::String> v8String) {
if (v8String.IsEmpty()) {
return std::string();
}

napi_value value = reinterpret_cast<napi_value>(*v8String);
size_t length = 0;
if (napi_get_value_string_utf8(env, value, nullptr, 0, &length) != napi_ok) {
return std::string();
}

std::string result(length + 1, '\0');
if (napi_get_value_string_utf8(env, value, result.data(), result.size(),
&length) != napi_ok) {
return std::string();
}

result.resize(length);
return result;
}

#endif // LINUX_PERF_UTILS_H
7 changes: 3 additions & 4 deletions packages/core/src/nodeVersion.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
/**
* Majors the native addon ships prebuilds for, as listed in the
* `build-native-addon` targets in package.json. Prebuilds are matched on the
* exact ABI version, so on any other major the addon only loads when it has
* been compiled from source locally.
* Majors CodSpeed is tested against. The native addon itself is built as a
* single Node-API binary and loads on any major, so this only gates the
* warning about measurement stability.
*/
export const SUPPORTED_NODE_MAJORS = [22, 24];

Expand Down
29 changes: 29 additions & 0 deletions scripts/assert-native-binding.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env node
// Loads the prebuilt native addon through the same resolution a consumer gets
// and runs the perf-map handler. Symbols in the addon are bound lazily, so a
// prebuild that is incompatible with the running ABI loads without complaint
// and only dies once a V8 entry point is actually called.
const fs = require("fs");
const path = require("path");

const core = require(
path.join(__dirname, "..", "packages", "core", "dist", "index.cjs.js"),
);

const runtime = `Node ${process.version} (ABI ${process.versions.modules}, ${process.platform}-${process.arch})`;

core.setupCore();
core.teardownCore();

const perfMap = path.join("/tmp", `perf-${process.pid}.map`);
const entries = fs
.readFileSync(perfMap, "utf8")
.split("\n")
.filter((line) => line.length > 0);
fs.unlinkSync(perfMap);

if (entries.length === 0) {
throw new Error(`${runtime}: the perf map handler produced no entries`);
}

console.log(`${runtime}: bound, ${entries.length} perf map entries`);
Loading