Skip to content

Commit 731e693

Browse files
committed
chore(vitest,tinybench): emit benchmark markers
Emit benchmark start/end markers for the tinybench plugin and the vitest walltime runner, wrapping the measured function in a root frame.
1 parent 079e488 commit 731e693

9 files changed

Lines changed: 84 additions & 44 deletions

File tree

packages/benchmark.js-plugin/src/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {
99
SetupInstrumentsResponse,
1010
teardownCore,
1111
tryIntrospect,
12+
wrapWithRootFrame,
13+
wrapWithRootFrameSync,
1214
} from "@codspeed/core";
1315
import Benchmark from "benchmark";
1416
import buildSuiteAdd from "./buildSuiteAdd";
@@ -195,7 +197,7 @@ async function runBenchmarks({
195197
await optimizeFunction(benchPayload);
196198
await mongoMeasurement.start(uri);
197199
global.gc?.();
198-
await (async function __codspeed_root_frame__() {
200+
await wrapWithRootFrame(async () => {
199201
InstrumentHooks.startBenchmark();
200202
await benchPayload();
201203
InstrumentHooks.stopBenchmark();
@@ -205,7 +207,7 @@ async function runBenchmarks({
205207
} else {
206208
optimizeFunctionSync(benchPayload);
207209
await mongoMeasurement.start(uri);
208-
(function __codspeed_root_frame__() {
210+
wrapWithRootFrameSync(() => {
209211
InstrumentHooks.startBenchmark();
210212
benchPayload();
211213
InstrumentHooks.stopBenchmark();

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ export type {
7777
} from "./generated/openapi";
7878
export { getV8Flags, tryIntrospect } from "./introspection";
7979
export { optimizeFunction, optimizeFunctionSync } from "./optimization";
80+
export { wrapWithRootFrame, wrapWithRootFrameSync } from "./rootFrame";
8081
export * from "./utils";
8182
export * from "./walltime";
8283
export type { InstrumentMode };

packages/core/src/rootFrame.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* Wrap a benchmark function so it executes under a frame named
3+
* `__codspeed_root_frame__`. CodSpeed uses this frame to locate the
4+
* benchmark root in collected call stacks; samples without it cannot be
5+
* attributed to a benchmark.
6+
*/
7+
export function wrapWithRootFrame<T>(
8+
fn: () => T | Promise<T>,
9+
): () => Promise<T> {
10+
return async function __codspeed_root_frame__() {
11+
return await fn();
12+
};
13+
}
14+
15+
export function wrapWithRootFrameSync<T>(fn: () => T): () => T {
16+
return function __codspeed_root_frame__() {
17+
return fn();
18+
};
19+
}

packages/tinybench-plugin/src/analysis.ts

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import {
33
InstrumentHooks,
44
mongoMeasurement,
55
optimizeFunction,
6+
wrapWithRootFrame,
7+
wrapWithRootFrameSync,
68
} from "@codspeed/core";
79
import { Bench, Fn, FnOptions, Task } from "tinybench";
810
import { BaseBenchRunner } from "./shared";
@@ -25,18 +27,6 @@ class AnalysisBenchRunner extends BaseBenchRunner {
2527
return InstrumentHooks.isInstrumented() ? "Measured" : "Checked";
2628
}
2729

28-
private wrapFunctionWithFrame(fn: Fn, isAsync: boolean): Fn {
29-
if (isAsync) {
30-
return async function __codspeed_root_frame__() {
31-
await fn();
32-
};
33-
} else {
34-
return function __codspeed_root_frame__() {
35-
fn();
36-
};
37-
}
38-
}
39-
4030
protected async runTaskAsync(task: Task, uri: string): Promise<void> {
4131
const { fnOpts, fn } = task as unknown as { fnOpts?: FnOptions; fn: Fn };
4232

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

5242
global.gc?.();
53-
await this.wrapWithInstrumentHooksAsync(
54-
this.wrapFunctionWithFrame(fn, true),
55-
uri,
56-
);
43+
await this.wrapWithInstrumentHooksAsync(wrapWithRootFrame(fn), uri);
5744

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

71-
this.wrapWithInstrumentHooks(this.wrapFunctionWithFrame(fn, false), uri);
58+
this.wrapWithInstrumentHooks(wrapWithRootFrameSync(fn), uri);
7259

7360
fnOpts?.afterEach?.call(task, "run");
7461
fnOpts?.afterAll?.call(task, "run");

packages/tinybench-plugin/src/shared.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { InstrumentHooks, setupCore, teardownCore } from "@codspeed/core";
1+
import {
2+
getInstrumentMode,
3+
InstrumentHooks,
4+
MARKER_TYPE_BENCHMARK_END,
5+
MARKER_TYPE_BENCHMARK_START,
6+
setupCore,
7+
teardownCore,
8+
} from "@codspeed/core";
29
import { Bench, Fn, Task } from "tinybench";
310
import { getTaskUri } from "./uri";
411

@@ -63,25 +70,43 @@ export abstract class BaseBenchRunner {
6370
protected abstract finalizeAsyncRun(): Task[];
6471
protected abstract finalizeSyncRun(): Task[];
6572

73+
private sendRunMarkers(runStart: bigint, runEnd: bigint): void {
74+
if (getInstrumentMode() !== "walltime") {
75+
return;
76+
}
77+
InstrumentHooks.addMarker(
78+
process.pid,
79+
MARKER_TYPE_BENCHMARK_START,
80+
runStart,
81+
);
82+
InstrumentHooks.addMarker(process.pid, MARKER_TYPE_BENCHMARK_END, runEnd);
83+
}
84+
6685
public setupBenchMethods(): void {
6786
this.bench.run = async () => {
6887
this.setupBenchRun();
6988

89+
const runStart = InstrumentHooks.currentTimestamp();
7090
for (const task of this.bench.tasks) {
7191
const uri = this.getTaskUri(task);
7292
await this.runTaskAsync(task, uri);
7393
}
94+
const runEnd = InstrumentHooks.currentTimestamp();
95+
this.sendRunMarkers(runStart, runEnd);
7496

7597
return this.finalizeAsyncRun();
7698
};
7799

78100
this.bench.runSync = () => {
79101
this.setupBenchRun();
80102

103+
const runStart = InstrumentHooks.currentTimestamp();
81104
for (const task of this.bench.tasks) {
82105
const uri = this.getTaskUri(task);
83106
this.runTaskSync(task, uri);
84107
}
108+
const runEnd = InstrumentHooks.currentTimestamp();
109+
this.sendRunMarkers(runStart, runEnd);
85110

86111
return this.finalizeSyncRun();
87112
};

packages/tinybench-plugin/src/walltime.ts

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import {
33
mongoMeasurement,
44
msToNs,
55
msToS,
6+
wrapWithRootFrame,
7+
wrapWithRootFrameSync,
68
writeWalltimeResults,
79
type BenchmarkStats,
810
type Benchmark as CodspeedBenchmark,
@@ -64,21 +66,10 @@ class WalltimeBenchRunner extends BaseBenchRunner {
6466

6567
private wrapTaskFunction(task: Task, isAsync: boolean): void {
6668
const { fn } = task as unknown as { fn: Fn };
67-
if (isAsync) {
68-
// eslint-disable-next-line no-inner-declarations
69-
async function __codspeed_root_frame__() {
70-
await fn();
71-
}
72-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
73-
(task as any).fn = __codspeed_root_frame__;
74-
} else {
75-
// eslint-disable-next-line no-inner-declarations
76-
function __codspeed_root_frame__() {
77-
fn();
78-
}
79-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
80-
(task as any).fn = __codspeed_root_frame__;
81-
}
69+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
70+
(task as any).fn = isAsync
71+
? wrapWithRootFrame(fn)
72+
: wrapWithRootFrameSync(fn);
8273
}
8374

8475
private registerCodspeedBenchmarkFromTask(task: Task): void {

packages/tinybench-plugin/tests/index.integ.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ const mockCore = vi.hoisted(() => {
1616
startBenchmark: vi.fn(),
1717
stopBenchmark: vi.fn(),
1818
setExecutedBenchmark: vi.fn(),
19+
currentTimestamp: vi.fn().mockReturnValue(0n),
20+
addMarker: vi.fn(),
1921
},
2022
optimizeFunction: vi
2123
.fn()

packages/vitest-plugin/src/analysis.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
optimizeFunction,
66
setupCore,
77
teardownCore,
8+
wrapWithRootFrame,
89
} from "@codspeed/core";
910
import { Benchmark, type RunnerTestSuite } from "vitest";
1011
import { NodeBenchmarkRunner } from "vitest/runners";
@@ -47,7 +48,7 @@ async function runAnalysisBench(
4748
await callSuiteHook(suite, benchmark, "beforeEach");
4849
await mongoMeasurement.start(uri);
4950
global.gc?.();
50-
await (async function __codspeed_root_frame__() {
51+
await wrapWithRootFrame(async () => {
5152
InstrumentHooks.startBenchmark();
5253
// @ts-expect-error we do not need to bind the function to an instance of tinybench's Bench
5354
await fn();

packages/vitest-plugin/src/walltime/index.ts

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import {
22
InstrumentHooks,
3+
MARKER_TYPE_BENCHMARK_END,
4+
MARKER_TYPE_BENCHMARK_START,
35
setupCore,
6+
wrapWithRootFrame,
47
writeWalltimeResults,
58
} from "@codspeed/core";
6-
import { Fn } from "tinybench";
79
import {
810
RunnerTaskEventPack,
911
RunnerTaskResultPack,
@@ -66,6 +68,7 @@ export class WalltimeRunner extends NodeBenchmarkRunner {
6668
this.isTinybenchHookedWithCodspeed = true;
6769

6870
const originalRun = tinybench.Task.prototype.run;
71+
const pid = process.pid;
6972

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

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

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

8688
InstrumentHooks.startBenchmark();
87-
await originalRun.call(this);
89+
const runStart = InstrumentHooks.currentTimestamp();
90+
try {
91+
await originalRun.call(this);
92+
} finally {
93+
task.fn = originalFn;
94+
}
95+
const runEnd = InstrumentHooks.currentTimestamp();
8896
InstrumentHooks.stopBenchmark();
8997

98+
// Emit a single marker pair covering the whole measurement run
99+
InstrumentHooks.addMarker(pid, MARKER_TYPE_BENCHMARK_START, runStart);
100+
InstrumentHooks.addMarker(pid, MARKER_TYPE_BENCHMARK_END, runEnd);
101+
90102
// Look up the URI by task name
91103
const uri = `${suiteUri}::${this.name}`;
92-
InstrumentHooks.setExecutedBenchmark(process.pid, uri);
104+
InstrumentHooks.setExecutedBenchmark(pid, uri);
93105

94106
return this;
95107
};

0 commit comments

Comments
 (0)