Skip to content

Commit ffeaa23

Browse files
committed
feat(vitest-plugin): add support for benchmark hooks
1 parent eea918d commit ffeaa23

File tree

7 files changed

+253
-91
lines changed

7 files changed

+253
-91
lines changed

examples/with-typescript-esm/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,6 @@
1616
"esbuild-register": "^3.4.2",
1717
"tinybench": "^2.5.0",
1818
"typescript": "^5.1.3",
19-
"vitest": "^1.0.3"
19+
"vitest": "^1.2.2"
2020
}
2121
}

packages/vitest-plugin/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ First, install the plugin [`@codspeed/vitest-plugin`](https://www.npmjs.com/pack
2020

2121
> [!NOTE]
2222
> The CodSpeed plugin is only compatible with
23-
> [vitest@1.0.0](https://www.npmjs.com/package/vitest/v/1.0.0)
23+
> [vitest@1.2.2](https://www.npmjs.com/package/vitest/v/1.2.2)
2424
> and above.
2525
2626
```sh
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import {
2+
afterAll,
3+
afterEach,
4+
beforeAll,
5+
beforeEach,
6+
bench,
7+
describe,
8+
expect,
9+
} from "vitest";
10+
11+
let count = -1;
12+
13+
beforeAll(() => {
14+
count += 1;
15+
});
16+
17+
beforeEach(() => {
18+
count += 1;
19+
});
20+
21+
// the count is multiplied by 2 because the bench function is called twice with codspeed (once for the optimization and once for the actual measurement)
22+
bench("one", () => {
23+
expect(count).toBe(1 * 2);
24+
});
25+
26+
describe("level1", () => {
27+
bench("two", () => {
28+
expect(count).toBe(2 * 2);
29+
});
30+
31+
bench("three", () => {
32+
expect(count).toBe(3 * 2);
33+
});
34+
35+
describe("level 2", () => {
36+
beforeEach(() => {
37+
count += 1;
38+
});
39+
40+
bench("five", () => {
41+
expect(count).toBe(5 * 2);
42+
});
43+
44+
describe("level 3", () => {
45+
bench("seven", () => {
46+
expect(count).toBe(7 * 2);
47+
});
48+
});
49+
});
50+
51+
describe("level 2 bench nested beforeAll", () => {
52+
beforeAll(() => {
53+
count = 0;
54+
});
55+
56+
bench("one", () => {
57+
expect(count).toBe(1 * 2);
58+
});
59+
});
60+
61+
bench("two", () => {
62+
expect(count).toBe(2 * 2);
63+
});
64+
});
65+
66+
describe("hooks cleanup", () => {
67+
let cleanUpCount = 0;
68+
describe("run", () => {
69+
beforeAll(() => {
70+
cleanUpCount += 10;
71+
});
72+
beforeEach(() => {
73+
cleanUpCount += 1;
74+
});
75+
afterEach(() => {
76+
cleanUpCount -= 1;
77+
});
78+
afterAll(() => {
79+
cleanUpCount -= 10;
80+
});
81+
82+
bench("one", () => {
83+
expect(cleanUpCount).toBe(11);
84+
});
85+
bench("two", () => {
86+
expect(cleanUpCount).toBe(11);
87+
});
88+
});
89+
bench("end", () => {
90+
expect(cleanUpCount).toBe(0);
91+
});
92+
});

packages/vitest-plugin/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,12 @@
3131
},
3232
"peerDependencies": {
3333
"vite": "^4.2.0 || ^5.0.0",
34-
"vitest": ">=1.0.0-beta.4 || >=1"
34+
"vitest": ">=1.2.2"
3535
},
3636
"devDependencies": {
3737
"@total-typescript/shoehorn": "^0.1.1",
3838
"execa": "^8.0.1",
3939
"vite": "^5.0.0",
40-
"vitest": "^1.0.3"
40+
"vitest": "^1.2.2"
4141
}
4242
}

packages/vitest-plugin/src/__tests__/runner.test.ts

Lines changed: 42 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { fromPartial } from "@total-typescript/shoehorn";
2-
import { describe, expect, it, vi } from "vitest";
2+
import { describe, expect, it, Suite, vi } from "vitest";
33
import { getBenchFn } from "vitest/suite";
44
import CodSpeedRunner from "../runner";
55

@@ -25,21 +25,31 @@ vi.mock("@codspeed/core", async (importOriginal) => {
2525

2626
console.log = vi.fn();
2727

28-
vi.mock("vitest/suite");
28+
vi.mock("vitest/suite", () => ({
29+
getBenchFn: vi.fn(),
30+
// wrapping the value in vi.fn(...) here will not work for some reason
31+
getHooks: () => ({
32+
beforeAll: [],
33+
beforeEach: [],
34+
afterEach: [],
35+
afterAll: [],
36+
}),
37+
}));
2938
const mockedGetBenchFn = vi.mocked(getBenchFn);
39+
3040
describe("CodSpeedRunner", () => {
3141
it("should run the bench functions only twice", async () => {
3242
const benchFn = vi.fn();
3343
mockedGetBenchFn.mockReturnValue(benchFn);
3444

3545
const runner = new CodSpeedRunner(fromPartial({}));
36-
await runner.runSuite(
37-
fromPartial({
38-
filepath: __filename,
39-
name: "test suite",
40-
tasks: [{ mode: "run", meta: { benchmark: true }, name: "test bench" }],
41-
})
42-
);
46+
const suite = fromPartial<Suite>({
47+
filepath: __filename,
48+
name: "test suite",
49+
tasks: [{ mode: "run", meta: { benchmark: true }, name: "test bench" }],
50+
});
51+
suite.tasks[0].suite = suite;
52+
await runner.runSuite(suite);
4353

4454
// setup
4555
expect(coreMocks.setupCore).toHaveBeenCalledTimes(1);
@@ -71,26 +81,29 @@ describe("CodSpeedRunner", () => {
7181
mockedGetBenchFn.mockReturnValue(benchFn);
7282

7383
const runner = new CodSpeedRunner(fromPartial({}));
74-
await runner.runSuite(
75-
fromPartial({
76-
filepath: __filename,
77-
name: "test suite",
78-
tasks: [
79-
{
80-
type: "suite",
81-
name: "nested suite",
82-
mode: "run",
83-
tasks: [
84-
{
85-
mode: "run",
86-
meta: { benchmark: true },
87-
name: "test bench",
88-
},
89-
],
90-
},
91-
],
92-
})
93-
);
84+
const rootSuite = fromPartial<Suite>({
85+
filepath: __filename,
86+
name: "test suite",
87+
tasks: [
88+
{
89+
type: "suite",
90+
name: "nested suite",
91+
mode: "run",
92+
tasks: [
93+
{
94+
mode: "run",
95+
meta: { benchmark: true },
96+
name: "test bench",
97+
},
98+
],
99+
},
100+
],
101+
});
102+
rootSuite.tasks[0].suite = rootSuite;
103+
// @ts-expect-error type is not narrow enough, but it is fine
104+
rootSuite.tasks[0].tasks[0].suite = rootSuite.tasks[0];
105+
106+
await runner.runSuite(rootSuite);
94107

95108
// setup
96109
expect(coreMocks.setupCore).toHaveBeenCalledTimes(1);

packages/vitest-plugin/src/runner.ts

Lines changed: 72 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,33 @@ import {
88
teardownCore,
99
} from "@codspeed/core";
1010
import path from "path";
11-
import { Benchmark, Suite } from "vitest";
11+
import { Benchmark, chai, Suite, Task } from "vitest";
1212
import { NodeBenchmarkRunner } from "vitest/runners";
13-
import { getBenchFn } from "vitest/suite";
13+
import { getBenchFn, getHooks } from "vitest/suite";
14+
15+
type SuiteHooks = ReturnType<typeof getHooks>;
16+
17+
function getSuiteHooks(suite: Suite, name: keyof SuiteHooks) {
18+
return getHooks(suite)[name];
19+
}
20+
21+
export async function callSuiteHook<T extends keyof SuiteHooks>(
22+
suite: Suite,
23+
currentTask: Task,
24+
name: T
25+
): Promise<void> {
26+
if (name === "beforeEach" && suite.suite) {
27+
await callSuiteHook(suite.suite, currentTask, name);
28+
}
29+
30+
const hooks = getSuiteHooks(suite, name);
31+
32+
await Promise.all(hooks.map((fn) => fn()));
33+
34+
if (name === "afterEach" && suite.suite) {
35+
await callSuiteHook(suite.suite, currentTask, name);
36+
}
37+
}
1438

1539
const currentFileName =
1640
typeof __filename === "string"
@@ -26,39 +50,62 @@ function logCodSpeed(message: string) {
2650
console.log(`[CodSpeed] ${message}`);
2751
}
2852

29-
async function runBenchmarkSuite(suite: Suite, parentSuiteName?: string) {
30-
const benchmarkGroup: Benchmark[] = [];
31-
const benchmarkSuiteGroup: Suite[] = [];
32-
for (const task of suite.tasks) {
33-
if (task.mode !== "run") continue;
53+
async function runBench(benchmark: Benchmark, currentSuiteName: string) {
54+
const uri = `${currentSuiteName}::${benchmark.name}`;
55+
const fn = getBenchFn(benchmark);
3456

35-
if (task.meta?.benchmark) benchmarkGroup.push(task as Benchmark);
36-
else if (task.type === "suite") benchmarkSuiteGroup.push(task);
57+
await callSuiteHook(benchmark.suite, benchmark, "beforeEach");
58+
try {
59+
await optimizeFunction(fn);
60+
} catch (e) {
61+
// if the error is not an assertion error, we want to fail the run
62+
// we allow assertion errors because we want to be able to use `expect` in the benchmark to allow for better authoring
63+
// assertions are allowed to fail in the optimization phase since it might be linked to stateful code
64+
if (!(e instanceof chai.AssertionError)) {
65+
throw e;
66+
}
3767
}
68+
await callSuiteHook(benchmark.suite, benchmark, "afterEach");
69+
70+
await callSuiteHook(benchmark.suite, benchmark, "beforeEach");
71+
await mongoMeasurement.start(uri);
72+
await (async function __codspeed_root_frame__() {
73+
Measurement.startInstrumentation();
74+
// @ts-expect-error we do not need to bind the function to an instance of tinybench's Bench
75+
await fn();
76+
Measurement.stopInstrumentation(uri);
77+
})();
78+
await mongoMeasurement.stop(uri);
79+
await callSuiteHook(benchmark.suite, benchmark, "afterEach");
80+
81+
logCodSpeed(`${uri} done`);
82+
}
3883

84+
async function runBenchmarkSuite(suite: Suite, parentSuiteName?: string) {
3985
const currentSuiteName = parentSuiteName
4086
? parentSuiteName + "::" + suite.name
4187
: suite.name;
4288

43-
for (const subSuite of benchmarkSuiteGroup) {
44-
await runBenchmarkSuite(subSuite, currentSuiteName);
89+
// do not call `beforeAll` if we are in the root suite, since it is already called by vitest
90+
// see https://github.com/vitest-dev/vitest/blob/1fee63f2598edc228017f18eca325f85ee54aee0/packages/runner/src/run.ts#L293
91+
if (parentSuiteName !== undefined) {
92+
await callSuiteHook(suite, suite, "beforeAll");
4593
}
4694

47-
for (const benchmark of benchmarkGroup) {
48-
const uri = `${currentSuiteName}::${benchmark.name}`;
49-
const fn = getBenchFn(benchmark);
95+
for (const task of suite.tasks) {
96+
if (task.mode !== "run") continue;
5097

51-
await optimizeFunction(fn);
52-
await mongoMeasurement.start(uri);
53-
await (async function __codspeed_root_frame__() {
54-
Measurement.startInstrumentation();
55-
// @ts-expect-error we do not need to bind the function to an instance of tinybench's Bench
56-
await fn();
57-
Measurement.stopInstrumentation(uri);
58-
})();
59-
await mongoMeasurement.stop(uri);
60-
61-
logCodSpeed(`${uri} done`);
98+
if (task.meta?.benchmark) {
99+
await runBench(task as Benchmark, currentSuiteName);
100+
} else if (task.type === "suite") {
101+
await runBenchmarkSuite(task, currentSuiteName);
102+
}
103+
}
104+
105+
// do not call `afterAll` if we are in the root suite, since it is already called by vitest
106+
// see https://github.com/vitest-dev/vitest/blob/1fee63f2598edc228017f18eca325f85ee54aee0/packages/runner/src/run.ts#L324
107+
if (parentSuiteName !== undefined) {
108+
await callSuiteHook(suite, suite, "afterAll");
62109
}
63110
}
64111

0 commit comments

Comments
 (0)