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" ;
3
12
import { NodeBenchmarkRunner } from "vitest/runners" ;
4
13
import { patchRootSuiteWithFullFilePath } from "./common" ;
5
14
15
+ declare const __VERSION__ : string ;
16
+
6
17
const currentFileName =
7
18
typeof __filename === "string"
8
19
? __filename
9
20
: new URL ( "walltime.mjs" , import . meta. url ) . pathname ;
10
21
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
+
11
43
/**
12
44
* 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
14
46
*/
15
47
export class WalltimeRunner extends NodeBenchmarkRunner {
16
48
async runSuite ( suite : Suite ) : Promise < void > {
@@ -19,12 +51,149 @@ export class WalltimeRunner extends NodeBenchmarkRunner {
19
51
20
52
patchRootSuiteWithFullFilePath ( suite ) ;
21
53
54
+ console . log (
55
+ `[CodSpeed] running with @codspeed/vitest-plugin v${ __VERSION__ } (walltime mode)`
56
+ ) ;
57
+
22
58
// Let Vitest's default benchmark runner handle execution
23
- // Results will be captured by CodSpeedReporter via onTaskUpdate
24
59
await super . runSuite ( suite ) ;
25
60
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
+
26
72
teardownCore ( ) ;
27
73
}
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
+ }
28
197
}
29
198
30
- export default WalltimeRunner ;
199
+ export default WalltimeRunner ;
0 commit comments