Skip to content

Commit fea06fe

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 74046c9 commit fea06fe

9 files changed

Lines changed: 80 additions & 62 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: 16 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import {
33
MARKER_TYPE_BENCHMARK_END,
44
MARKER_TYPE_BENCHMARK_START,
55
setupCore,
6+
wrapWithRootFrame,
67
writeWalltimeResults,
78
} from "@codspeed/core";
8-
import { Fn } from "tinybench";
99
import {
1010
RunnerTaskEventPack,
1111
RunnerTaskResultPack,
@@ -78,37 +78,27 @@ export class WalltimeRunner extends NodeBenchmarkRunner {
7878
};
7979

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

84-
const finishRound = (roundStart: bigint): void => {
85-
const roundEnd = InstrumentHooks.currentTimestamp();
86-
InstrumentHooks.addMarker(pid, MARKER_TYPE_BENCHMARK_START, roundStart);
87-
InstrumentHooks.addMarker(pid, MARKER_TYPE_BENCHMARK_END, roundEnd);
88-
};
89-
90-
function __codspeed_root_frame__(): unknown {
91-
const roundStart = InstrumentHooks.currentTimestamp();
92-
const result = fn();
93-
if (
94-
result !== null &&
95-
typeof result === "object" &&
96-
typeof (result as PromiseLike<unknown>).then === "function"
97-
) {
98-
return (result as PromiseLike<unknown>).then((value) => {
99-
finishRound(roundStart);
100-
return value;
101-
});
102-
}
103-
finishRound(roundStart);
104-
return result;
105-
}
106-
(this as { fn: Fn }).fn = __codspeed_root_frame__ as Fn;
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));
10787

10888
InstrumentHooks.startBenchmark();
109-
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();
11096
InstrumentHooks.stopBenchmark();
11197

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+
112102
// Look up the URI by task name
113103
const uri = `${suiteUri}::${this.name}`;
114104
InstrumentHooks.setExecutedBenchmark(pid, uri);

0 commit comments

Comments
 (0)