Skip to content

Commit c20bf72

Browse files
--wip-- [skip ci]
1 parent 92f0d3c commit c20bf72

File tree

9 files changed

+472
-16
lines changed

9 files changed

+472
-16
lines changed

examples/with-typescript-esm/bench/tinybench/index.bench.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ import { withCodSpeed } from "@codspeed/tinybench-plugin";
22
import { Bench } from "tinybench";
33
import { registerFiboBenchmarks } from "./fibo.bench";
44
import { registerFoobarbazBenchmarks } from "./foobarbaz.bench";
5+
import { registerTimingBenchmarks } from "./timing.bench";
56

67
export const bench = withCodSpeed(new Bench());
78

89
(async () => {
910
registerFiboBenchmarks(bench);
1011
registerFoobarbazBenchmarks(bench);
12+
registerTimingBenchmarks(bench);
1113

1214
await bench.run();
1315
console.table(bench.table());
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { Bench } from "tinybench";
2+
3+
const sleep = (ms: number): Promise<void> => {
4+
return new Promise((resolve) => setTimeout(resolve, ms));
5+
};
6+
7+
export function registerTimingBenchmarks(bench: Bench) {
8+
bench.add("wait 1ms", async () => {
9+
await sleep(1);
10+
});
11+
12+
bench.add("wait 500ms", async () => {
13+
await sleep(500);
14+
});
15+
16+
bench.add("wait 1sec", async () => {
17+
await sleep(1000);
18+
});
19+
}

packages/core/src/walltime.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import fs from "fs";
2+
import path from "path";
3+
4+
declare const __VERSION__: string;
5+
6+
export interface BenchmarkStats {
7+
min_ns: number;
8+
max_ns: number;
9+
mean_ns: number;
10+
stdev_ns: number;
11+
q1_ns: number;
12+
median_ns: number;
13+
q3_ns: number;
14+
rounds: number;
15+
total_time: number;
16+
iqr_outlier_rounds: number;
17+
stdev_outlier_rounds: number;
18+
iter_per_round: number;
19+
warmup_iters: number;
20+
}
21+
22+
export interface BenchmarkConfig {
23+
warmup_time_ns: number;
24+
min_round_time_ns: number;
25+
max_time_ns: number;
26+
max_rounds: number | null;
27+
}
28+
29+
export interface Benchmark {
30+
name: string;
31+
uri: string;
32+
config: BenchmarkConfig;
33+
stats: BenchmarkStats;
34+
}
35+
36+
export interface InstrumentInfo {
37+
type: string;
38+
clock_info: {
39+
implementation: string;
40+
monotonic: boolean;
41+
adjustable: boolean;
42+
resolution: number;
43+
};
44+
}
45+
46+
export interface ResultData {
47+
creator: {
48+
name: string;
49+
version: string;
50+
pid: number;
51+
};
52+
instrument: InstrumentInfo;
53+
benchmarks: Benchmark[];
54+
}
55+
56+
export function getProfileFolder(): string | null {
57+
return process.env.CODSPEED_PROFILE_FOLDER || null;
58+
}
59+
60+
export function getCreatorMetadata() {
61+
return {
62+
creator: {
63+
name: "@codspeed/core",
64+
version: __VERSION__,
65+
pid: process.pid,
66+
},
67+
};
68+
}
69+
70+
export function getWalltimeInstrumentInfo(): InstrumentInfo {
71+
return {
72+
type: "walltime",
73+
clock_info: {
74+
implementation: "perf_counter",
75+
monotonic: true,
76+
adjustable: false,
77+
resolution: 1e-9, // nanosecond resolution
78+
},
79+
};
80+
}
81+
82+
export function writeWalltimeResults(
83+
benchmarks: Benchmark[],
84+
integrationName: string
85+
) {
86+
const profileFolder = getProfileFolder();
87+
let resultPath: string;
88+
89+
if (profileFolder) {
90+
const resultsDir = path.join(profileFolder, "results");
91+
fs.mkdirSync(resultsDir, { recursive: true });
92+
resultPath = path.join(resultsDir, `${process.pid}.json`);
93+
} else {
94+
// Fallback: write to .codspeed in current working directory
95+
const codspeedDir = path.join(process.cwd(), ".codspeed");
96+
fs.mkdirSync(codspeedDir, { recursive: true });
97+
resultPath = path.join(codspeedDir, `results_${Date.now()}.json`);
98+
}
99+
100+
const data: ResultData = {
101+
creator: {
102+
name: integrationName,
103+
version: __VERSION__,
104+
pid: process.pid,
105+
},
106+
instrument: getWalltimeInstrumentInfo(),
107+
benchmarks: benchmarks,
108+
};
109+
110+
fs.writeFileSync(resultPath, JSON.stringify(data, null, 2));
111+
console.log(`[CodSpeed] Results written to ${resultPath}`);
112+
}
113+
114+
export function createDefaultBenchmarkConfig(): BenchmarkConfig {
115+
return {
116+
warmup_time_ns: 1_000_000_000, // 1 second default
117+
min_round_time_ns: 1_000_000, // 1ms default
118+
max_time_ns: 3_000_000_000, // 3 seconds default
119+
max_rounds: null,
120+
};
121+
}

packages/tinybench-plugin/src/index.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import {
22
getGitDir,
3-
getMeasurementMode,
43
Measurement,
5-
MeasurementMode,
64
mongoMeasurement,
75
SetupInstrumentsRequestBody,
86
SetupInstrumentsResponse,
@@ -21,7 +19,12 @@ tryIntrospect();
2119
export const taskUriMap = new WeakMap<Bench, Map<string, string>>();
2220

2321
export function withCodSpeed(bench: Bench): Bench {
24-
const measurementMode = getMeasurementMode();
22+
// Check if CODSPEED_ENV is defined
23+
if (!process.env.CODSPEED_ENV) {
24+
// If CODSPEED_ENV is not defined, return bench unchanged (run as normal tinybench)
25+
return bench;
26+
}
27+
2528
const rootCallingFile = getCallingFile();
2629

2730
// Initialize URI mapping for this bench instance
@@ -40,20 +43,18 @@ export function withCodSpeed(bench: Bench): Bench {
4043
return rawAdd.bind(bench)(name, fn, opts);
4144
};
4245

43-
// Apply the appropriate measurement strategy based on mode and instrumentation
44-
if (
45-
measurementMode === MeasurementMode.Instrumentation &&
46-
Measurement.isInstrumented()
47-
) {
46+
// Apply the appropriate measurement strategy based on CODSPEED_RUNNER_MODE
47+
const runnerMode = process.env.CODSPEED_RUNNER_MODE;
48+
49+
if (runnerMode === "instrumentation") {
4850
runInstrumentedBench(bench, rootCallingFile);
49-
} else if (measurementMode === MeasurementMode.WallTime) {
51+
} else if (runnerMode === "walltime") {
5052
runWalltimeBench(bench, rootCallingFile);
5153
} else {
52-
// Fallback: instrumentation requested but not available
5354
const rawRun = bench.run;
5455
bench.run = async () => {
5556
console.warn(
56-
`[CodSpeed] ${bench.tasks.length} benches detected but no instrumentation found, falling back to tinybench`
57+
`[CodSpeed] CODSPEED_ENV is defined but CODSPEED_RUNNER_MODE="${runnerMode}" is not recognized, falling back to tinybench`
5758
);
5859
return await rawRun.bind(bench)();
5960
};
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { Bench } from "tinybench";
2+
import { beforeEach, describe, expect, it, vi } from "vitest";
3+
import { withCodSpeed } from ".";
4+
5+
const mockInstrumented = vi.hoisted(() => ({
6+
runInstrumentedBench: vi.fn(),
7+
}));
8+
9+
vi.mock("./instrumented", () => ({
10+
...mockInstrumented,
11+
}));
12+
13+
const mockWalltime = vi.hoisted(() => ({
14+
runWalltimeBench: vi.fn(),
15+
}));
16+
17+
vi.mock("./walltime", () => ({
18+
...mockWalltime,
19+
}));
20+
21+
describe("withCodSpeed behavior without different codspeed modes", () => {
22+
beforeEach(() => {
23+
vi.clearAllMocks();
24+
// Ensure no CODSPEED env vars are set
25+
delete process.env.CODSPEED_ENV;
26+
delete process.env.CODSPEED_RUNNER_MODE;
27+
});
28+
29+
it("should return the same bench instance unchanged when no CODSPEED_ENV", async () => {
30+
const originalBench = new Bench({ iterations: 10, time: 10 });
31+
const wrappedBench = withCodSpeed(originalBench);
32+
const shouldBeCalled = vi.fn();
33+
wrappedBench.add("test task", shouldBeCalled);
34+
await wrappedBench.run();
35+
36+
// Should return the exact same instance
37+
expect(wrappedBench).toBe(originalBench);
38+
expect(shouldBeCalled.mock.calls.length).toBeGreaterThan(1000);
39+
});
40+
41+
it("should run in instrumented mode when CODSPEED_RUNNER_MODE=instrumentation", async () => {
42+
process.env.CODSPEED_ENV = "true";
43+
process.env.CODSPEED_RUNNER_MODE = "instrumentation";
44+
45+
withCodSpeed(new Bench());
46+
47+
expect(mockInstrumented.runInstrumentedBench).toHaveBeenCalled();
48+
expect(mockWalltime.runWalltimeBench).not.toHaveBeenCalled();
49+
});
50+
51+
it("should run in walltime mode when CODSPEED_RUNNER_MODE=walltime", async () => {
52+
process.env.CODSPEED_ENV = "true";
53+
process.env.CODSPEED_RUNNER_MODE = "walltime";
54+
55+
withCodSpeed(new Bench());
56+
57+
expect(mockInstrumented.runInstrumentedBench).not.toHaveBeenCalled();
58+
expect(mockWalltime.runWalltimeBench).toHaveBeenCalled();
59+
});
60+
});
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import {
2+
Measurement,
3+
mongoMeasurement,
4+
optimizeFunction,
5+
setupCore,
6+
teardownCore,
7+
} from "@codspeed/core";
8+
import { Bench } from "tinybench";
9+
import { taskUriMap } from "./index";
10+
11+
declare const __VERSION__: string;
12+
13+
function getTaskUri(
14+
bench: Bench,
15+
taskName: string,
16+
rootCallingFile: string
17+
): string {
18+
const uriMap = taskUriMap.get(bench);
19+
return uriMap?.get(taskName) || `${rootCallingFile}::${taskName}`;
20+
}
21+
22+
export function runInstrumentedBench(
23+
bench: Bench,
24+
rootCallingFile: string
25+
): void {
26+
bench.run = async () => {
27+
console.log(
28+
`[CodSpeed] running with @codspeed/tinybench v${__VERSION__} (instrumented mode)`
29+
);
30+
setupCore();
31+
32+
for (const task of bench.tasks) {
33+
const uri = getTaskUri(bench, task.name, rootCallingFile);
34+
35+
// Access private fnOpts to get hooks
36+
const fnOpts = (task as any).fnOpts;
37+
38+
// Call beforeAll hook if it exists
39+
await fnOpts?.beforeAll?.call(task, "run");
40+
41+
// run optimizations
42+
await optimizeFunction(async () => {
43+
await fnOpts?.beforeEach?.call(task, "run");
44+
await (task as any).fn(); // Access private fn property
45+
await fnOpts?.afterEach?.call(task, "run");
46+
});
47+
48+
// run instrumented benchmark
49+
await fnOpts?.beforeEach?.call(task, "run");
50+
51+
await mongoMeasurement.start(uri);
52+
global.gc?.();
53+
await (async function __codspeed_root_frame__() {
54+
Measurement.startInstrumentation();
55+
await (task as any).fn(); // Access private fn property
56+
Measurement.stopInstrumentation(uri);
57+
})();
58+
await mongoMeasurement.stop(uri);
59+
60+
await fnOpts?.afterEach?.call(task, "run");
61+
62+
await fnOpts?.afterAll?.call(task, "run");
63+
64+
// print results
65+
console.log(
66+
` ✔ ${Measurement.isInstrumented() ? "Measured" : "Checked"} ${uri}`
67+
);
68+
}
69+
70+
teardownCore();
71+
console.log(`[CodSpeed] Done running ${bench.tasks.length} benches.`);
72+
return bench.tasks;
73+
};
74+
}

0 commit comments

Comments
 (0)