Skip to content

Commit 2194b10

Browse files
GuillaumeLagrangeart049
authored andcommitted
refactor(vitest-plugin): make the walltimerunner class less cluttered
1 parent 4173870 commit 2194b10

File tree

4 files changed

+168
-187
lines changed

4 files changed

+168
-187
lines changed

packages/vitest-plugin/rollup.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export default defineConfig([
2727
external: ["@codspeed/core", /^vitest/],
2828
},
2929
{
30-
input: "src/walltime.ts",
30+
input: "src/walltime/index.ts",
3131
output: { file: "dist/walltime.mjs", format: "es" },
3232
plugins: jsPlugins(pkg.version),
3333
external: ["@codspeed/core", /^vitest/],

packages/vitest-plugin/src/walltime.ts

Lines changed: 0 additions & 186 deletions
This file was deleted.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { setupCore, writeWalltimeResults } from "@codspeed/core";
2+
import { type RunnerTestSuite } from "vitest";
3+
import { NodeBenchmarkRunner } from "vitest/runners";
4+
import { patchRootSuiteWithFullFilePath } from "../common";
5+
import { extractBenchmarkResults } from "./utils";
6+
7+
/**
8+
* WalltimeRunner uses Vitest's default benchmark execution
9+
* and extracts results from the suite after completion
10+
*/
11+
export class WalltimeRunner extends NodeBenchmarkRunner {
12+
private isTinybenchHookedWithCodspeed = false;
13+
private benchmarkUris = new Map<string, string>();
14+
15+
async runSuite(suite: RunnerTestSuite): Promise<void> {
16+
patchRootSuiteWithFullFilePath(suite);
17+
18+
setupCore();
19+
20+
await super.runSuite(suite);
21+
22+
const benchmarks = await extractBenchmarkResults(suite);
23+
24+
if (benchmarks.length > 0) {
25+
writeWalltimeResults(benchmarks);
26+
console.log(
27+
`[CodSpeed] Done collecting walltime data for ${benchmarks.length} benches.`
28+
);
29+
} else {
30+
console.warn(
31+
`[CodSpeed] No benchmark results found after suite execution`
32+
);
33+
}
34+
}
35+
}
36+
37+
export default WalltimeRunner;
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import {
2+
calculateQuantiles,
3+
msToNs,
4+
msToS,
5+
type Benchmark,
6+
type BenchmarkStats,
7+
} from "@codspeed/core";
8+
import {
9+
type Benchmark as VitestBenchmark,
10+
type RunnerTaskResult,
11+
type RunnerTestSuite,
12+
} from "vitest";
13+
import { getBenchOptions } from "vitest/suite";
14+
import { isVitestTaskBenchmark } from "../common";
15+
16+
export async function extractBenchmarkResults(
17+
suite: RunnerTestSuite,
18+
parentPath = ""
19+
): Promise<Benchmark[]> {
20+
const benchmarks: Benchmark[] = [];
21+
const currentPath = parentPath ? `${parentPath}::${suite.name}` : suite.name;
22+
23+
for (const task of suite.tasks) {
24+
if (isVitestTaskBenchmark(task) && task.result?.state === "pass") {
25+
const benchmark = await processBenchmarkTask(task, currentPath);
26+
if (benchmark) {
27+
benchmarks.push(benchmark);
28+
}
29+
} else if (task.type === "suite") {
30+
const nestedBenchmarks = await extractBenchmarkResults(task, currentPath);
31+
benchmarks.push(...nestedBenchmarks);
32+
}
33+
}
34+
35+
return benchmarks;
36+
}
37+
38+
async function processBenchmarkTask(
39+
task: VitestBenchmark,
40+
suitePath: string
41+
): Promise<Benchmark | null> {
42+
const uri = `${suitePath}::${task.name}`;
43+
44+
const result = task.result;
45+
if (!result) {
46+
console.warn(` ⚠ No result data available for ${uri}`);
47+
return null;
48+
}
49+
50+
try {
51+
// Get tinybench configuration options from vitest
52+
const benchOptions = getBenchOptions(task);
53+
54+
const stats = convertVitestResultToBenchmarkStats(result, benchOptions);
55+
56+
if (stats === null) {
57+
console.log(` ✔ No walltime data to collect for ${uri}`);
58+
return null;
59+
}
60+
61+
const coreBenchmark: Benchmark = {
62+
name: task.name,
63+
uri,
64+
config: {
65+
max_rounds: benchOptions.iterations ?? null,
66+
max_time_ns: benchOptions.time ? msToNs(benchOptions.time) : null,
67+
min_round_time_ns: null, // tinybench does not have an option for this
68+
warmup_time_ns:
69+
benchOptions.warmupIterations !== 0 && benchOptions.warmupTime
70+
? msToNs(benchOptions.warmupTime)
71+
: null,
72+
},
73+
stats,
74+
};
75+
76+
console.log(` ✔ Collected walltime data for ${uri}`);
77+
return coreBenchmark;
78+
} catch (error) {
79+
console.warn(` ⚠ Failed to process benchmark result for ${uri}:`, error);
80+
return null;
81+
}
82+
}
83+
84+
function convertVitestResultToBenchmarkStats(
85+
result: RunnerTaskResult,
86+
benchOptions: {
87+
time?: number;
88+
warmupTime?: number;
89+
warmupIterations?: number;
90+
iterations?: number;
91+
}
92+
): BenchmarkStats | null {
93+
const benchmark = result.benchmark;
94+
95+
if (!benchmark) {
96+
throw new Error("No benchmark data available in result");
97+
}
98+
99+
const { totalTime, min, max, mean, sd, samples } = benchmark;
100+
101+
// Get individual sample times in nanoseconds and sort them
102+
const sortedTimesNs = samples.map(msToNs).sort((a, b) => a - b);
103+
const meanNs = msToNs(mean);
104+
const stdevNs = msToNs(sd);
105+
106+
if (sortedTimesNs.length == 0) {
107+
// Sometimes the benchmarks can be completely optimized out and not even run, but its beforeEach and afterEach hooks are still executed, and the task is still considered a success.
108+
// This is the case for the hooks.bench.ts example in this package
109+
return null;
110+
}
111+
112+
const { q1_ns, q3_ns, median_ns, iqr_outlier_rounds, stdev_outlier_rounds } =
113+
calculateQuantiles({ meanNs, stdevNs, sortedTimesNs });
114+
115+
return {
116+
min_ns: msToNs(min),
117+
max_ns: msToNs(max),
118+
mean_ns: meanNs,
119+
stdev_ns: stdevNs,
120+
q1_ns,
121+
median_ns,
122+
q3_ns,
123+
total_time: msToS(totalTime),
124+
iter_per_round: 1, // as there is only one round in tinybench, we define that there were n rounds of 1 iteration
125+
rounds: sortedTimesNs.length,
126+
iqr_outlier_rounds,
127+
stdev_outlier_rounds,
128+
warmup_iters: benchOptions.warmupIterations ?? 0,
129+
};
130+
}

0 commit comments

Comments
 (0)