Skip to content

Commit 40f0a9b

Browse files
committed
feat(plugins): use correct URI when defining bench in separate files
1 parent 9ff7304 commit 40f0a9b

File tree

14 files changed

+368
-36
lines changed

14 files changed

+368
-36
lines changed

packages/benchmark.js-plugin/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,15 @@
2222
"@babel/preset-env": "^7.22.5",
2323
"@types/benchmark": "^2.1.2",
2424
"@types/find-up": "^4.0.0",
25+
"@types/lodash": "^4.14.195",
2526
"@types/stack-trace": "^0.0.30",
2627
"benchmark": "^2.1.4",
2728
"jest-mock-extended": "^3.0.4"
2829
},
2930
"dependencies": {
3031
"@codspeed/core": "workspace:^1.1.0",
3132
"find-up": "^6.3.0",
33+
"lodash": "^4.17.10",
3234
"stack-trace": "1.0.0-pre2"
3335
},
3436
"peerDependencies": {
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { Suite } from "benchmark";
2+
import buildSuiteAdd from "../buildSuiteAdd";
3+
import { CodSpeedBenchmark } from "../types";
4+
5+
describe("buildSuiteAdd", () => {
6+
let emptyBench: () => void;
7+
let suite: Suite;
8+
9+
beforeEach(() => {
10+
emptyBench = () => {
11+
return;
12+
};
13+
suite = new Suite();
14+
});
15+
16+
it("should register benchmark name when using (options: Options)", () => {
17+
suite.add = buildSuiteAdd(suite);
18+
suite.add({ name: "test", fn: emptyBench });
19+
suite.forEach((bench: CodSpeedBenchmark) =>
20+
expect(bench.uri).toBe(
21+
"packages/benchmark.js-plugin/src/__tests__/buildSuiteAdd.test.ts::test"
22+
)
23+
);
24+
});
25+
26+
it("should register benchmark name when using (fn: Function, options?: Options)", () => {
27+
suite.add = buildSuiteAdd(suite);
28+
suite.add(emptyBench, { name: "test" });
29+
suite.forEach((bench: CodSpeedBenchmark) =>
30+
expect(bench.uri).toBe(
31+
"packages/benchmark.js-plugin/src/__tests__/buildSuiteAdd.test.ts::test"
32+
)
33+
);
34+
});
35+
36+
it("should register benchmark name when using (name: string, options?: Options)", () => {
37+
suite.add = buildSuiteAdd(suite);
38+
suite.add("test", { fn: emptyBench });
39+
suite.forEach((bench: CodSpeedBenchmark) =>
40+
expect(bench.uri).toBe(
41+
"packages/benchmark.js-plugin/src/__tests__/buildSuiteAdd.test.ts::test"
42+
)
43+
);
44+
});
45+
46+
it("should register benchmark name when using (name: string, fn: Function, options?: Options)", () => {
47+
suite.add = buildSuiteAdd(suite);
48+
suite.add("test", emptyBench);
49+
suite.forEach((bench: CodSpeedBenchmark) =>
50+
expect(bench.uri).toBe(
51+
"packages/benchmark.js-plugin/src/__tests__/buildSuiteAdd.test.ts::test"
52+
)
53+
);
54+
});
55+
56+
it("should register benchmark name when suite name is defined", () => {
57+
suite.name = "suite";
58+
suite.add = buildSuiteAdd(suite);
59+
suite.add("test", emptyBench);
60+
suite.forEach((bench: CodSpeedBenchmark) =>
61+
expect(bench.uri).toBe(
62+
"packages/benchmark.js-plugin/src/__tests__/buildSuiteAdd.test.ts::suite::test"
63+
)
64+
);
65+
});
66+
67+
it("should call rawAdd with options object", () => {
68+
const rawAdd = jest.fn();
69+
suite.add = rawAdd;
70+
suite.add = buildSuiteAdd(suite);
71+
const options = { name: "test", delay: 100 };
72+
suite.add(options);
73+
expect(rawAdd).toHaveBeenCalledWith(options);
74+
});
75+
76+
it("should call rawAdd with function and options object", () => {
77+
const rawAdd = jest.fn();
78+
suite.add = rawAdd;
79+
suite.add = buildSuiteAdd(suite);
80+
const fn = emptyBench;
81+
const options = { name: "test", delay: 100 };
82+
suite.add("test", fn, options);
83+
expect(rawAdd).toHaveBeenCalledWith("test", fn, {
84+
...options,
85+
uri: "packages/benchmark.js-plugin/src/__tests__/buildSuiteAdd.test.ts::test",
86+
});
87+
});
88+
89+
it("should call rawAdd with name and options object", () => {
90+
const rawAdd = jest.fn();
91+
suite.add = rawAdd;
92+
suite.add = buildSuiteAdd(suite);
93+
const options = { name: "test", delay: 100 };
94+
suite.add("test", options);
95+
expect(rawAdd).toHaveBeenCalledWith("test", options);
96+
});
97+
98+
it("should call rawAdd with function and undefined options", () => {
99+
const rawAdd = jest.fn();
100+
suite.add = rawAdd;
101+
suite.add = buildSuiteAdd(suite);
102+
const fn = emptyBench;
103+
suite.add("test", fn);
104+
expect(rawAdd).toHaveBeenCalledWith("test", fn, {
105+
uri: "packages/benchmark.js-plugin/src/__tests__/buildSuiteAdd.test.ts::test",
106+
});
107+
});
108+
});
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { Options, Suite } from "benchmark";
2+
import { isFunction, isPlainObject } from "lodash";
3+
import getCallingFile from "./getCallingFile";
4+
5+
function isOptions(options: unknown): options is Options {
6+
return isPlainObject(options);
7+
}
8+
9+
export default function buildSuiteAdd(suite: Suite) {
10+
const rawAdd = suite.add;
11+
const suiteName = suite.name;
12+
13+
function registerBenchmarkName(name: string) {
14+
const callingFile = getCallingFile(2); // [here, suite.add, actual caller]
15+
let uri = callingFile;
16+
if (suiteName !== undefined) {
17+
uri += `::${suiteName}`;
18+
}
19+
uri += `::${name}`;
20+
21+
return uri;
22+
}
23+
24+
function add(options: Options): Suite;
25+
// eslint-disable-next-line @typescript-eslint/ban-types
26+
function add(fn: Function, options?: Options): Suite;
27+
function add(name: string, options?: Options): Suite;
28+
// eslint-disable-next-line @typescript-eslint/ban-types
29+
function add(name: string, fn: Function, options?: Options): Suite;
30+
function add(name: unknown, fn?: unknown, opts?: unknown) {
31+
// 1 argument: (options: Options)
32+
if (isOptions(name)) {
33+
if (name.name !== undefined) {
34+
const rawFn = name.fn;
35+
if (typeof rawFn === "function") {
36+
const uri = registerBenchmarkName(name.name);
37+
const options = Object.assign({}, name, { uri });
38+
return rawAdd.bind(suite)(options);
39+
}
40+
}
41+
return rawAdd.bind(suite)(name);
42+
}
43+
44+
// 2 arguments: (fn: Function, options?: Options)
45+
if (isFunction(name) && (isOptions(fn) || fn === undefined)) {
46+
if (fn !== undefined) {
47+
if (fn.name !== undefined) {
48+
const uri = registerBenchmarkName(fn.name);
49+
const options = Object.assign({}, fn, { uri });
50+
return rawAdd.bind(suite)(name, options);
51+
}
52+
}
53+
return rawAdd.bind(suite)(name, fn);
54+
}
55+
56+
// 2 arguments: (name: string, options?: Options)
57+
if (typeof name === "string" && (isOptions(fn) || fn === undefined)) {
58+
if (fn !== undefined && typeof fn.fn === "function") {
59+
const uri = registerBenchmarkName(name);
60+
const options = Object.assign({}, fn, { uri });
61+
return rawAdd.bind(suite)(name, options);
62+
}
63+
return rawAdd.bind(suite)(name, fn);
64+
}
65+
66+
// 3 arguments: (name: string, fn: Function, options?: Options)
67+
if (
68+
typeof name === "string" &&
69+
isFunction(fn) &&
70+
(isOptions(opts) || opts === undefined)
71+
) {
72+
const uri = registerBenchmarkName(name);
73+
const options = Object.assign({}, opts ?? {}, { uri });
74+
return rawAdd.bind(suite)(name, fn, options);
75+
}
76+
}
77+
78+
return add;
79+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { findUpSync, Options as FindupOptions } from "find-up";
2+
import path, { dirname } from "path";
3+
import { get as getStackTrace } from "stack-trace";
4+
import { fileURLToPath } from "url";
5+
6+
function getGitDir(path: string): string | undefined {
7+
const dotGitPath = findUpSync(".git", {
8+
cwd: path,
9+
type: "directory",
10+
} as FindupOptions);
11+
return dotGitPath ? dirname(dotGitPath) : undefined;
12+
}
13+
14+
export default function getCallingFile(depth: number): string {
15+
const stack = getStackTrace();
16+
let callingFile = stack[depth + 1].getFileName();
17+
const gitDir = getGitDir(callingFile);
18+
if (gitDir === undefined) {
19+
throw new Error("Could not find a git repository");
20+
}
21+
if (callingFile.startsWith("file://")) {
22+
callingFile = fileURLToPath(callingFile);
23+
}
24+
return path.relative(gitDir, callingFile);
25+
}

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

Lines changed: 14 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,9 @@ import {
55
optimizeFunctionSync,
66
} from "@codspeed/core";
77
import Benchmark from "benchmark";
8-
import { findUpSync, Options as FindupOptions } from "find-up";
9-
import path, { dirname } from "path";
10-
import { get as getStackTrace } from "stack-trace";
11-
import { fileURLToPath } from "url";
8+
import buildSuiteAdd from "./buildSuiteAdd";
9+
import getCallingFile from "./getCallingFile";
10+
import { CodSpeedBenchmark } from "./types";
1211

1312
declare const __VERSION__: string;
1413

@@ -32,7 +31,7 @@ interface WithCodSpeedBenchmark
3231
run(options?: Benchmark.Options): Benchmark | Promise<Benchmark>;
3332
}
3433

35-
interface WithCodSpeedSuite
34+
export interface WithCodSpeedSuite
3635
extends Omit<
3736
Benchmark.Suite,
3837
| "run"
@@ -94,12 +93,16 @@ function withCodSpeedBenchmark(bench: Benchmark): WithCodSpeedBenchmark {
9493
};
9594
return bench;
9695
}
97-
const callingFile = getCallingFile();
96+
const callingFile = getCallingFile(2); // [here, withCodSpeed, actual caller]
97+
const codspeedBench = bench as BenchmarkWithOptions;
98+
if (codspeedBench.name !== undefined) {
99+
codspeedBench.uri = `${callingFile}::${bench.name}`;
100+
}
98101
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/ban-ts-comment
99102
// @ts-ignore
100103
bench.run = async function (options?: Benchmark.Options): Promise<Benchmark> {
101104
await runBenchmarks({
102-
benches: [bench as unknown as BenchmarkWithOptions],
105+
benches: [codspeedBench],
103106
baseUri: callingFile,
104107
benchmarkCompletedListeners: bench.listeners("complete"),
105108
options,
@@ -120,7 +123,8 @@ function withCodSpeedSuite(suite: Benchmark.Suite): WithCodSpeedSuite {
120123
};
121124
return suite as WithCodSpeedSuite;
122125
}
123-
const callingFile = getCallingFile();
126+
suite.add = buildSuiteAdd(suite);
127+
const callingFile = getCallingFile(2); // [here, withCodSpeed, actual caller]
124128
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/ban-ts-comment
125129
// @ts-ignore
126130
suite.run = async function (
@@ -143,7 +147,7 @@ function withCodSpeedSuite(suite: Benchmark.Suite): WithCodSpeedSuite {
143147
return suite as WithCodSpeedSuite;
144148
}
145149

146-
type BenchmarkWithOptions = Benchmark & { options: Benchmark.Options };
150+
type BenchmarkWithOptions = CodSpeedBenchmark & { options: Benchmark.Options };
147151

148152
interface RunBenchmarksOptions {
149153
benches: BenchmarkWithOptions[];
@@ -156,13 +160,12 @@ async function runBenchmarks({
156160
benches,
157161
baseUri,
158162
benchmarkCompletedListeners,
159-
options,
160163
}: RunBenchmarksOptions): Promise<void> {
161164
console.log(`[CodSpeed] running with @codspeed/benchmark.js v${__VERSION__}`);
162165
initCore();
163166
for (let i = 0; i < benches.length; i++) {
164167
const bench = benches[i];
165-
const uri = baseUri + "::" + (bench.name ?? `unknown_${i}`);
168+
const uri = bench.uri ?? `${baseUri}::unknown_${i}`;
166169
const isAsync = bench.options.async || bench.options.defer;
167170
let benchPayload;
168171
if (bench.options.defer) {
@@ -199,24 +202,3 @@ async function runBenchmarks({
199202
}
200203
console.log(`[CodSpeed] Done running ${benches.length} benches.`);
201204
}
202-
203-
function getCallingFile(): string {
204-
const stack = getStackTrace();
205-
let callingFile = stack[3].getFileName(); // [here, withCodSpeed, withCodSpeedX, actual caller]
206-
const gitDir = getGitDir(callingFile);
207-
if (gitDir === undefined) {
208-
throw new Error("Could not find a git repository");
209-
}
210-
if (callingFile.startsWith("file://")) {
211-
callingFile = fileURLToPath(callingFile);
212-
}
213-
return path.relative(gitDir, callingFile);
214-
}
215-
216-
function getGitDir(path: string): string | undefined {
217-
const dotGitPath = findUpSync(".git", {
218-
cwd: path,
219-
type: "directory",
220-
} as FindupOptions);
221-
return dotGitPath ? dirname(dotGitPath) : undefined;
222-
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import Benchmark, { Options } from "benchmark";
2+
3+
export interface CodSpeedBenchOptions extends Options {
4+
uri: string;
5+
}
6+
7+
export interface CodSpeedBenchmark extends Benchmark {
8+
uri: string;
9+
}

packages/benchmark.js-plugin/tests/index.integ.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ const mockCore = mockDeep<Measurement>();
44
import type { Measurement } from "@codspeed/core";
55
import Benchmark from "benchmark";
66
import { withCodSpeed } from "..";
7+
import { registerBenchmarks } from "./registerBenchmarks";
8+
import { registerOtherBenchmarks } from "./registerOtherBenchmarks";
79

810
jest.mock("@codspeed/core", () => ({
911
...jest.requireActual("@codspeed/core"),
@@ -209,4 +211,32 @@ describe("Benchmark.Suite", () => {
209211
}
210212
}
211213
);
214+
it("check nested file path is in the uri when bench is registered in another file", async () => {
215+
mockCore.isInstrumented.mockReturnValue(true);
216+
const suite = withCodSpeed(new Benchmark.Suite("thesuite"));
217+
registerBenchmarks(suite);
218+
const onComplete = jest.fn();
219+
suite.on("complete", onComplete);
220+
await suite.run({ maxTime: 0.1, initCount: 1 });
221+
expect(mockCore.startInstrumentation).toHaveBeenCalled();
222+
expect(mockCore.stopInstrumentation).toHaveBeenCalledWith(
223+
"packages/benchmark.js-plugin/tests/registerBenchmarks.ts::thesuite::RegExp"
224+
);
225+
});
226+
it("check that benchmarks with same name have different URIs when registered in different files", async () => {
227+
mockCore.isInstrumented.mockReturnValue(true);
228+
const suite = withCodSpeed(new Benchmark.Suite("thesuite"));
229+
registerBenchmarks(suite);
230+
registerOtherBenchmarks(suite);
231+
const onComplete = jest.fn();
232+
suite.on("complete", onComplete);
233+
await suite.run({ maxTime: 0.1, initCount: 1 });
234+
expect(mockCore.startInstrumentation).toHaveBeenCalled();
235+
expect(mockCore.stopInstrumentation).toHaveBeenCalledWith(
236+
"packages/benchmark.js-plugin/tests/registerBenchmarks.ts::thesuite::RegExp"
237+
);
238+
expect(mockCore.stopInstrumentation).toHaveBeenCalledWith(
239+
"packages/benchmark.js-plugin/tests/registerOtherBenchmarks.ts::thesuite::RegExp"
240+
);
241+
});
212242
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import type { WithCodSpeedSuite } from "..";
2+
3+
export function registerBenchmarks(suite: WithCodSpeedSuite) {
4+
suite.add(
5+
"RegExp",
6+
function () {
7+
/o/.test("Hello World!");
8+
},
9+
{ maxTime: 0.1 }
10+
);
11+
}

0 commit comments

Comments
 (0)