Skip to content

Commit b10c641

Browse files
--wip-- [skip ci]
1 parent acf0046 commit b10c641

File tree

2 files changed

+174
-11
lines changed

2 files changed

+174
-11
lines changed

packages/vitest-plugin/src/index.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,6 @@ export default function codspeedPlugin(): Plugin {
6565
// Only set custom runner when CODSPEED_ENV is set
6666
if (runnerFile) {
6767
config.test!.runner = runnerFile;
68-
69-
// Add CodSpeedReporter when in walltime mode
70-
const runnerMode = process.env.CODSPEED_RUNNER_MODE;
71-
if (runnerMode !== "instrumentation") {
72-
config.test!.reporters = [getCodSpeedFileFromName("reporter")];
73-
}
7468
}
7569

7670
return config;
Lines changed: 174 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,48 @@
1-
import { logDebug, setupCore, teardownCore } from "@codspeed/core";
2-
import { Suite } from "vitest";
1+
import {
2+
calculateQuantiles,
3+
logDebug,
4+
mongoMeasurement,
5+
setupCore,
6+
teardownCore,
7+
writeWalltimeResults,
8+
type Benchmark as CoreBenchmark,
9+
type BenchmarkStats,
10+
} from "@codspeed/core";
11+
import { Suite, Task } from "vitest";
312
import { NodeBenchmarkRunner } from "vitest/runners";
413
import { patchRootSuiteWithFullFilePath } from "./common";
514

15+
declare const __VERSION__: string;
16+
617
const currentFileName =
718
typeof __filename === "string"
819
? __filename
920
: new URL("walltime.mjs", import.meta.url).pathname;
1021

22+
interface VitestBenchmarkResult {
23+
name: string;
24+
options?: {
25+
iterations?: number;
26+
time?: number;
27+
warmupIterations?: number;
28+
warmupTime?: number;
29+
};
30+
result?: {
31+
error?: unknown;
32+
duration?: number;
33+
mean?: number;
34+
min?: number;
35+
max?: number;
36+
stdDev?: number;
37+
samples?: number[];
38+
variance?: number;
39+
rme?: number;
40+
};
41+
}
42+
1143
/**
1244
* WalltimeRunner uses Vitest's default benchmark execution
13-
* and relies on the CodSpeedReporter to capture and process results
45+
* and extracts results from the suite after completion
1446
*/
1547
export class WalltimeRunner extends NodeBenchmarkRunner {
1648
async runSuite(suite: Suite): Promise<void> {
@@ -19,12 +51,149 @@ export class WalltimeRunner extends NodeBenchmarkRunner {
1951

2052
patchRootSuiteWithFullFilePath(suite);
2153

54+
console.log(
55+
`[CodSpeed] running with @codspeed/vitest-plugin v${__VERSION__} (walltime mode)`
56+
);
57+
2258
// Let Vitest's default benchmark runner handle execution
23-
// Results will be captured by CodSpeedReporter via onTaskUpdate
2459
await super.runSuite(suite);
2560

61+
// Extract benchmark results from the completed suite
62+
const benchmarks = await this.extractBenchmarkResults(suite);
63+
64+
// Write results to JSON file using core function
65+
if (benchmarks.length > 0) {
66+
writeWalltimeResults(benchmarks);
67+
console.log(
68+
`[CodSpeed] Done collecting walltime data for ${benchmarks.length} benches.`
69+
);
70+
}
71+
2672
teardownCore();
2773
}
74+
75+
private async extractBenchmarkResults(
76+
suite: Suite,
77+
parentPath = ""
78+
): Promise<CoreBenchmark[]> {
79+
const benchmarks: CoreBenchmark[] = [];
80+
const currentPath = parentPath
81+
? `${parentPath}::${suite.name}`
82+
: suite.name;
83+
84+
for (const task of suite.tasks) {
85+
if (task.result?.state === "pass") {
86+
const benchmark = await this.processBenchmarkTask(task, currentPath);
87+
if (benchmark) {
88+
benchmarks.push(benchmark);
89+
}
90+
} else if (task.type === "suite") {
91+
const nestedBenchmarks = await this.extractBenchmarkResults(
92+
task,
93+
currentPath
94+
);
95+
benchmarks.push(...nestedBenchmarks);
96+
}
97+
}
98+
99+
return benchmarks;
100+
}
101+
102+
private async processBenchmarkTask(
103+
task: Task,
104+
suitePath: string
105+
): Promise<CoreBenchmark | null> {
106+
const uri = `${suitePath}::${task.name}`;
107+
108+
// Notify mongoMeasurement about the benchmark
109+
await mongoMeasurement.start(uri);
110+
await mongoMeasurement.stop(uri);
111+
112+
const benchmarkResult = task.meta?.benchmark as
113+
| VitestBenchmarkResult
114+
| undefined;
115+
116+
if (benchmarkResult?.result) {
117+
const stats = this.convertVitestResultToBenchmarkStats(
118+
benchmarkResult.result
119+
);
120+
121+
const coreBenchmark: CoreBenchmark = {
122+
name: task.name,
123+
uri,
124+
config: {
125+
warmup_time_ns: benchmarkResult.options?.warmupTime
126+
? benchmarkResult.options.warmupTime * 1_000_000
127+
: null,
128+
min_round_time_ns: benchmarkResult.options?.time
129+
? benchmarkResult.options.time * 1_000_000
130+
: null,
131+
},
132+
stats,
133+
};
134+
135+
console.log(` ✔ Collected walltime data for ${uri}`);
136+
return coreBenchmark;
137+
} else {
138+
console.warn(` ⚠ No result data available for ${uri}`);
139+
return null;
140+
}
141+
}
142+
143+
private convertVitestResultToBenchmarkStats(
144+
result: NonNullable<VitestBenchmarkResult["result"]>
145+
): BenchmarkStats {
146+
// Convert milliseconds to nanoseconds
147+
const ms_to_ns = (ms: number) => ms * 1_000_000;
148+
149+
// Use samples if available, otherwise create from basic stats
150+
let sortedTimesNs: number[];
151+
let meanNs: number;
152+
let stdevNs: number;
153+
154+
if (result.samples && result.samples.length > 0) {
155+
sortedTimesNs = result.samples.map(ms_to_ns).sort((a, b) => a - b);
156+
meanNs = result.mean
157+
? ms_to_ns(result.mean)
158+
: sortedTimesNs.reduce((a, b) => a + b, 0) / sortedTimesNs.length;
159+
stdevNs = result.stdDev
160+
? ms_to_ns(result.stdDev)
161+
: ms_to_ns(Math.sqrt(result.variance || 0));
162+
} else {
163+
// Fallback to basic stats if no samples
164+
meanNs = result.mean
165+
? ms_to_ns(result.mean)
166+
: ms_to_ns(result.duration || 0);
167+
stdevNs = result.stdDev ? ms_to_ns(result.stdDev) : 0;
168+
sortedTimesNs = [meanNs]; // Single sample
169+
}
170+
171+
const {
172+
q1_ns,
173+
q3_ns,
174+
median_ns,
175+
iqr_outlier_rounds,
176+
stdev_outlier_rounds,
177+
} = calculateQuantiles({ meanNs, stdevNs, sortedTimesNs });
178+
179+
return {
180+
min_ns: result.min ? ms_to_ns(result.min) : sortedTimesNs[0] || meanNs,
181+
max_ns: result.max
182+
? ms_to_ns(result.max)
183+
: sortedTimesNs[sortedTimesNs.length - 1] || meanNs,
184+
mean_ns: meanNs,
185+
stdev_ns: stdevNs,
186+
q1_ns,
187+
median_ns,
188+
q3_ns,
189+
total_time: (result.duration || meanNs / 1_000_000) / 1000, // convert to seconds
190+
iter_per_round: sortedTimesNs.length,
191+
rounds: 1, // Vitest typically runs one round
192+
iqr_outlier_rounds,
193+
stdev_outlier_rounds,
194+
warmup_iters: 0, // Will be filled from options if available
195+
};
196+
}
28197
}
29198

30-
export default WalltimeRunner;
199+
export default WalltimeRunner;

0 commit comments

Comments
 (0)