Skip to content
Merged
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
27 changes: 27 additions & 0 deletions .github/actions/test-native-abi/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Test Native ABI
description: >
Set up the given Node.js major and run the core package's integ tests
in-band, without rebuilding, to catch native addon ABI mismatches and
V8-version-specific natives-syntax behavior. The flags mirror what the
runner passes in a real benchmark process; they cannot go through
NODE_OPTIONS (--allow-natives-syntax is rejected there), so jest runs
in-band under a flagged node instead of forking workers.

inputs:
node-version:
description: Node.js major version to test under
required: true

runs:
using: composite
steps:
- uses: actions/setup-node@v6
with:
node-version: ${{ inputs.node-version }}
- shell: bash
working-directory: packages/core
run: >
node --interpreted-frames-native-stack --allow-natives-syntax
"$(node -p 'require.resolve("jest/bin/jest")')"
-c jest.config.integ.js --runInBand
tests/index.integ.test.ts tests/optimization.integ.test.ts
24 changes: 9 additions & 15 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,6 @@ jobs:
native-abi:
runs-on: "ubuntu-latest"
name: Native addon ABI compatibility
env:
# The flags the runner passes in a real benchmark process. They cannot go
# through NODE_OPTIONS (--allow-natives-syntax is rejected there), so jest
# runs in-band under a flagged node instead of forking workers.
NODE_OPTS: "--interpreted-frames-native-stack --allow-natives-syntax"
steps:
- uses: "actions/checkout@v4"
with:
Expand All @@ -51,21 +46,20 @@ jobs:
key-suffix: native-abi
- run: pnpm install --frozen-lockfile --prefer-offline
# Build one prebuild set, then load it without rebuilding under each
# runtime via the jest integ test. The other jobs compile the addon
# with the Node version that loads it, so they cannot detect an ABI
# mismatch.
# runtime via the composite action below. The other jobs compile the
# addon with the Node version that loads it, so they cannot detect an
# ABI mismatch or a V8-version-specific natives-syntax failure.
- run: pnpm turbo run build --filter=@codspeed/core

- uses: actions/setup-node@v6
- uses: ./.github/actions/test-native-abi
with:
node-version: "22"
- run: node ${{ env.NODE_OPTS }} "$(node -p 'require.resolve("jest/bin/jest")')" -c jest.config.integ.js --runInBand tests/index.integ.test.ts
working-directory: packages/core
- uses: actions/setup-node@v6
- uses: ./.github/actions/test-native-abi
with:
node-version: "24"
- run: node ${{ env.NODE_OPTS }} "$(node -p 'require.resolve("jest/bin/jest")')" -c jest.config.integ.js --runInBand tests/index.integ.test.ts
working-directory: packages/core
- uses: ./.github/actions/test-native-abi
with:
node-version: "26"

list-examples:
runs-on: "ubuntu-latest"
Expand All @@ -89,7 +83,7 @@ jobs:
needs: list-examples
strategy:
matrix:
node-version: ["22", "24"]
node-version: ["22", "24", "26"]
example: ${{ fromJson(needs.list-examples.outputs.examples) }}
fail-fast: false
steps:
Expand Down
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
24.19.0
26.8.1
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-typescript": "^11.1.5",
"@types/jest": "^29.5.0",
"@types/node": "^24.10.1",
"@types/node": "^26.3.0",
"@typescript-eslint/eslint-plugin": "^8.57.0",
"@typescript-eslint/parser": "^8.57.0",
"esbuild": "^0.17.16",
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/nodeVersion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* 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];
export const SUPPORTED_NODE_MAJORS = [22, 24, 26];

export function getUnsupportedNodeVersionWarning(
version: string,
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/optimization.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
// %OptimizeFunctionOnNextCall aborts the process on V8 >= 14.6 unless the function
// was marked with %PrepareFunctionForOptimization first.
//
// Both calls must stay inline: `fn` is only referenced inside an eval string, so a
// helper taking it as a parameter looks unused and bundlers drop the argument.

export const optimizeFunction = async (fn: CallableFunction) => {
// Source: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#optimization-killers
// a total of 7 calls seems to be the sweet spot
eval("%PrepareFunctionForOptimization(fn)");
await fn();
await fn();
await fn();
Expand All @@ -14,6 +21,7 @@ export const optimizeFunction = async (fn: CallableFunction) => {
export const optimizeFunctionSync = (fn: CallableFunction) => {
// Source: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#optimization-killers
// a total of 7 calls seems to be the sweet spot
eval("%PrepareFunctionForOptimization(fn)");
fn();
fn();
fn();
Expand Down
13 changes: 8 additions & 5 deletions packages/core/tests/nodeVersion.integ.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,18 @@ const { getUnsupportedNodeVersionWarning, warnCi } = require("..") as {
};

describe("getUnsupportedNodeVersionWarning", () => {
it.each(["22.22.2", "24.19.0"])("should not warn on Node %s", (version) => {
expect(getUnsupportedNodeVersionWarning(version)).toBeNull();
});
it.each(["22.22.2", "24.19.0", "26.8.1"])(
"should not warn on Node %s",
(version) => {
expect(getUnsupportedNodeVersionWarning(version)).toBeNull();
},
);

it.each(["20.5.1", "23.11.0", "26.0.0"])(
it.each(["20.5.1", "23.11.0", "27.0.0"])(
"should warn on Node %s",
(version) => {
expect(getUnsupportedNodeVersionWarning(version)).toBe(
`[CodSpeed] Node.js v${version} is not supported: CodSpeed is tested on Node.js 22, 24. Support for other versions is experimental and measurements may be unstable.`,
`[CodSpeed] Node.js v${version} is not supported: CodSpeed is tested on Node.js 22, 24, 26. Support for other versions is experimental and measurements may be unstable.`,
);
},
);
Expand Down
43 changes: 43 additions & 0 deletions packages/core/tests/optimization.integ.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { execFileSync } from "child_process";
import path from "path";

// A function optimized without being marked for manual optimization first aborts
// the process on V8 >= 14.6, which no in-process assertion can observe. The helpers
// are exercised through the built package so the bundled natives calls are covered.
const runWithNatives = (snippet: string): string =>
execFileSync(process.execPath, ["--allow-natives-syntax", "-e", snippet], {
encoding: "utf8",
});

const corePath = JSON.stringify(path.join(__dirname, ".."));

// %GetOptimizationStatus's bitmask layout shifts between V8 versions, so a
// specific bit isn't safe to assert on. Comparing the status before and after
// the optimize call instead proves TurboFan recompiled the function, without
// depending on what any bit means.
describe("optimization helpers", () => {
it("should optimize a sync function without aborting", () => {
const stdout = runWithNatives(
`const target = () => 1;
const before = %GetOptimizationStatus(target);
require(${corePath}).optimizeFunctionSync(target);
console.log("done", before, %GetOptimizationStatus(target));`,
);
const [, before, after] = stdout.match(/done (\d+) (\d+)/) ?? [];
expect(stdout).toContain("done");
expect(after).not.toEqual(before);
});

it("should optimize an async function without aborting", () => {
const stdout = runWithNatives(
`const target = async () => 1;
const before = %GetOptimizationStatus(target);
require(${corePath})
.optimizeFunction(target)
.then(() => console.log("done", before, %GetOptimizationStatus(target)));`,
);
const [, before, after] = stdout.match(/done (\d+) (\d+)/) ?? [];
expect(stdout).toContain("done");
expect(after).not.toEqual(before);
});
});
Comment thread
not-matthias marked this conversation as resolved.
2 changes: 1 addition & 1 deletion packages/vitest-plugin/src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { fromPartial } from "@total-typescript/shoehorn";
import { getV8Flags } from "@codspeed/core";
import { fromPartial } from "@total-typescript/shoehorn";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import codspeedPlugin from "../index";

Expand Down
Loading
Loading