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
27 changes: 0 additions & 27 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,30 +87,3 @@ jobs:
env:
CODSPEED_SKIP_UPLOAD: true
CODSPEED_DEBUG: true

walltime-macos-test:
runs-on: macos-latest
steps:
- uses: "actions/checkout@v4"
with:
fetch-depth: 0
submodules: true
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v3
with:
cache: pnpm
node-version-file: .nvmrc
- name: Restore turbo cache
uses: ./.github/actions/turbo-cache
- run: pnpm install --frozen-lockfile --prefer-offline
- run: pnpm turbo run build

- name: Run benchmarks
uses: CodSpeedHQ/action@main
env:
CODSPEED_SKIP_UPLOAD: "true"
# Samply fails to profile pnpm targets for now
CODSPEED_PROFILER_ENABLED: "false"
with:
run: pnpm turbo run bench --filter=@codspeed/vitest-plugin
mode: walltime
28 changes: 28 additions & 0 deletions .github/workflows/codspeed.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,34 @@ jobs:
pnpm --workspace-concurrency 1 -r bench-tinybench
pnpm --workspace-concurrency 1 -r bench-vitest

codspeed-walltime-macos:
runs-on: macos-latest
steps:
- uses: "actions/checkout@v4"
with:
fetch-depth: 0
submodules: true
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v3
with:
cache: pnpm
node-version-file: .nvmrc
- name: Restore turbo cache
uses: ./.github/actions/turbo-cache
- run: pnpm install --frozen-lockfile --prefer-offline
- run: pnpm turbo run build

- name: Run macOS-only benchmarks
uses: CodSpeedHQ/action@main
with:
working-directory: packages/vitest-plugin
# Only run the macOS-only bench file: the rest of the suite already
# runs on the linux walltime job, and uploading the same benchmark
# twice for one commit is not supported.
run: pnpm turbo run bench --env-mode=loose --filter=@codspeed/vitest-plugin -- macos
mode: walltime
runner-version: latest

electron-e2e:
name: Run electron inbox e2e
runs-on: codspeed-macro
Expand Down
6 changes: 4 additions & 2 deletions packages/benchmark.js-plugin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
SetupInstrumentsResponse,
teardownCore,
tryIntrospect,
wrapWithRootFrame,
wrapWithRootFrameSync,
} from "@codspeed/core";
import Benchmark from "benchmark";
import buildSuiteAdd from "./buildSuiteAdd";
Expand Down Expand Up @@ -195,7 +197,7 @@ async function runBenchmarks({
await optimizeFunction(benchPayload);
await mongoMeasurement.start(uri);
global.gc?.();
await (async function __codspeed_root_frame__() {
await wrapWithRootFrame(async () => {
InstrumentHooks.startBenchmark();
await benchPayload();
InstrumentHooks.stopBenchmark();
Expand All @@ -205,7 +207,7 @@ async function runBenchmarks({
} else {
optimizeFunctionSync(benchPayload);
await mongoMeasurement.start(uri);
(function __codspeed_root_frame__() {
wrapWithRootFrameSync(() => {
InstrumentHooks.startBenchmark();
benchPayload();
InstrumentHooks.stopBenchmark();
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export type {
} from "./generated/openapi";
export { getV8Flags, tryIntrospect } from "./introspection";
export { optimizeFunction, optimizeFunctionSync } from "./optimization";
export { wrapWithRootFrame, wrapWithRootFrameSync } from "./rootFrame";
export * from "./utils";
export * from "./walltime";
export type { InstrumentMode };
Expand Down
19 changes: 19 additions & 0 deletions packages/core/src/rootFrame.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Wrap a benchmark function so it executes under a frame named
* `__codspeed_root_frame__`. CodSpeed uses this frame to locate the
* benchmark root in collected call stacks; samples without it cannot be
* attributed to a benchmark.
*/
export function wrapWithRootFrame<T>(
fn: () => T | Promise<T>,
): () => Promise<T> {
return async function __codspeed_root_frame__() {
return await fn();
};
}

export function wrapWithRootFrameSync<T>(fn: () => T): () => T {
return function __codspeed_root_frame__() {
return fn();
};
}
21 changes: 4 additions & 17 deletions packages/tinybench-plugin/src/analysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import {
InstrumentHooks,
mongoMeasurement,
optimizeFunction,
wrapWithRootFrame,
wrapWithRootFrameSync,
} from "@codspeed/core";
import { Bench, Fn, FnOptions, Task } from "tinybench";
import { BaseBenchRunner } from "./shared";
Expand All @@ -25,18 +27,6 @@ class AnalysisBenchRunner extends BaseBenchRunner {
return InstrumentHooks.isInstrumented() ? "Measured" : "Checked";
}

private wrapFunctionWithFrame(fn: Fn, isAsync: boolean): Fn {
if (isAsync) {
return async function __codspeed_root_frame__() {
await fn();
};
} else {
return function __codspeed_root_frame__() {
fn();
};
}
}

protected async runTaskAsync(task: Task, uri: string): Promise<void> {
const { fnOpts, fn } = task as unknown as { fnOpts?: FnOptions; fn: Fn };

Expand All @@ -50,10 +40,7 @@ class AnalysisBenchRunner extends BaseBenchRunner {
await mongoMeasurement.start(uri);

global.gc?.();
await this.wrapWithInstrumentHooksAsync(
this.wrapFunctionWithFrame(fn, true),
uri,
);
await this.wrapWithInstrumentHooksAsync(wrapWithRootFrame(fn), uri);

await mongoMeasurement.stop(uri);
await fnOpts?.afterEach?.call(task, "run");
Expand All @@ -68,7 +55,7 @@ class AnalysisBenchRunner extends BaseBenchRunner {
fnOpts?.beforeAll?.call(task, "run");
fnOpts?.beforeEach?.call(task, "run");

this.wrapWithInstrumentHooks(this.wrapFunctionWithFrame(fn, false), uri);
this.wrapWithInstrumentHooks(wrapWithRootFrameSync(fn), uri);

fnOpts?.afterEach?.call(task, "run");
fnOpts?.afterAll?.call(task, "run");
Expand Down
27 changes: 26 additions & 1 deletion packages/tinybench-plugin/src/shared.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { InstrumentHooks, setupCore, teardownCore } from "@codspeed/core";
import {
getInstrumentMode,
InstrumentHooks,
MARKER_TYPE_BENCHMARK_END,
MARKER_TYPE_BENCHMARK_START,
setupCore,
teardownCore,
} from "@codspeed/core";
import { Bench, Fn, Task } from "tinybench";
import { getTaskUri } from "./uri";

Expand Down Expand Up @@ -63,25 +70,43 @@ export abstract class BaseBenchRunner {
protected abstract finalizeAsyncRun(): Task[];
protected abstract finalizeSyncRun(): Task[];

private sendRunMarkers(runStart: bigint, runEnd: bigint): void {
if (getInstrumentMode() !== "walltime") {
return;
}
InstrumentHooks.addMarker(
process.pid,
MARKER_TYPE_BENCHMARK_START,
runStart,
);
InstrumentHooks.addMarker(process.pid, MARKER_TYPE_BENCHMARK_END, runEnd);
}

public setupBenchMethods(): void {
this.bench.run = async () => {
this.setupBenchRun();

const runStart = InstrumentHooks.currentTimestamp();
for (const task of this.bench.tasks) {
const uri = this.getTaskUri(task);
await this.runTaskAsync(task, uri);
}
const runEnd = InstrumentHooks.currentTimestamp();
this.sendRunMarkers(runStart, runEnd);

return this.finalizeAsyncRun();
};

this.bench.runSync = () => {
this.setupBenchRun();

const runStart = InstrumentHooks.currentTimestamp();
for (const task of this.bench.tasks) {
const uri = this.getTaskUri(task);
this.runTaskSync(task, uri);
}
const runEnd = InstrumentHooks.currentTimestamp();
this.sendRunMarkers(runStart, runEnd);

return this.finalizeSyncRun();
};
Expand Down
21 changes: 6 additions & 15 deletions packages/tinybench-plugin/src/walltime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import {
mongoMeasurement,
msToNs,
msToS,
wrapWithRootFrame,
wrapWithRootFrameSync,
writeWalltimeResults,
type BenchmarkStats,
type Benchmark as CodspeedBenchmark,
Expand Down Expand Up @@ -64,21 +66,10 @@ class WalltimeBenchRunner extends BaseBenchRunner {

private wrapTaskFunction(task: Task, isAsync: boolean): void {
const { fn } = task as unknown as { fn: Fn };
if (isAsync) {
// eslint-disable-next-line no-inner-declarations
async function __codspeed_root_frame__() {
await fn();
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(task as any).fn = __codspeed_root_frame__;
} else {
// eslint-disable-next-line no-inner-declarations
function __codspeed_root_frame__() {
fn();
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(task as any).fn = __codspeed_root_frame__;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(task as any).fn = isAsync
? wrapWithRootFrame(fn)
: wrapWithRootFrameSync(fn);
}

private registerCodspeedBenchmarkFromTask(task: Task): void {
Expand Down
2 changes: 2 additions & 0 deletions packages/tinybench-plugin/tests/index.integ.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ const mockCore = vi.hoisted(() => {
startBenchmark: vi.fn(),
stopBenchmark: vi.fn(),
setExecutedBenchmark: vi.fn(),
currentTimestamp: vi.fn().mockReturnValue(0n),
addMarker: vi.fn(),
},
optimizeFunction: vi
.fn()
Expand Down
16 changes: 16 additions & 0 deletions packages/vitest-plugin/benches/macos.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { bench, describe } from "vitest";

const isMacOS = process.platform === "darwin";

function fibo(n: number): number {
if (n < 2) return 1;
return fibo(n - 1) + fibo(n - 2);
}

// macOS-only benchmark: skipped on every other platform, so it only runs on
// the `codspeed-walltime-macos` CI job (see .github/workflows/codspeed.yml).
describe.skipIf(!isMacOS)("macos only", () => {
bench("fibo darwin", () => {
fibo(30);
});
});
3 changes: 2 additions & 1 deletion packages/vitest-plugin/src/analysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
optimizeFunction,
setupCore,
teardownCore,
wrapWithRootFrame,
} from "@codspeed/core";
import { Benchmark, type RunnerTestSuite } from "vitest";
import { NodeBenchmarkRunner } from "vitest/runners";
Expand Down Expand Up @@ -47,7 +48,7 @@ async function runAnalysisBench(
await callSuiteHook(suite, benchmark, "beforeEach");
await mongoMeasurement.start(uri);
global.gc?.();
await (async function __codspeed_root_frame__() {
await wrapWithRootFrame(async () => {
InstrumentHooks.startBenchmark();
// @ts-expect-error we do not need to bind the function to an instance of tinybench's Bench
await fn();
Expand Down
28 changes: 20 additions & 8 deletions packages/vitest-plugin/src/walltime/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import {
InstrumentHooks,
MARKER_TYPE_BENCHMARK_END,
MARKER_TYPE_BENCHMARK_START,
setupCore,
wrapWithRootFrame,
writeWalltimeResults,
} from "@codspeed/core";
import { Fn } from "tinybench";
import {
RunnerTaskEventPack,
RunnerTaskResultPack,
Expand Down Expand Up @@ -66,6 +68,7 @@ export class WalltimeRunner extends NodeBenchmarkRunner {
this.isTinybenchHookedWithCodspeed = true;

const originalRun = tinybench.Task.prototype.run;
const pid = process.pid;

const getSuiteUri = (): string => {
if (this.currentSuiteId === null) {
Expand All @@ -75,21 +78,30 @@ export class WalltimeRunner extends NodeBenchmarkRunner {
};

tinybench.Task.prototype.run = async function () {
const { fn } = this as { fn: Fn };
const suiteUri = getSuiteUri();

function __codspeed_root_frame__() {
return fn();
}
(this as { fn: Fn }).fn = __codspeed_root_frame__;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const task = this as any;
const originalFn = task.fn;
task.fn = wrapWithRootFrame(() => originalFn.call(task));

InstrumentHooks.startBenchmark();
await originalRun.call(this);
const runStart = InstrumentHooks.currentTimestamp();
try {
await originalRun.call(this);
} finally {
task.fn = originalFn;
}
const runEnd = InstrumentHooks.currentTimestamp();
InstrumentHooks.stopBenchmark();

// Emit a single marker pair covering the whole measurement run
InstrumentHooks.addMarker(pid, MARKER_TYPE_BENCHMARK_START, runStart);
InstrumentHooks.addMarker(pid, MARKER_TYPE_BENCHMARK_END, runEnd);

// Look up the URI by task name
const uri = `${suiteUri}::${this.name}`;
InstrumentHooks.setExecutedBenchmark(process.pid, uri);
InstrumentHooks.setExecutedBenchmark(pid, uri);

return this;
Comment on lines 88 to 106

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 stopBenchmark and marker emissions skipped on benchmark error

The finally block only restores task.fn; if originalRun.call(this) throws, execution jumps past lines 95–104. InstrumentHooks.startBenchmark() on line 88 has already been called, so the profiler is left in an unbalanced started-but-never-stopped state. Any subsequent benchmark measured in the same process will have its instrumentation corrupted. The newly added addMarker / setExecutedBenchmark calls are also silently dropped.

stopBenchmark() (and the marker pair) should be moved inside the finally block, alongside the task.fn restoration, so they run regardless of whether originalRun throws.

};
Expand Down
Loading