5
5
const os = require ( 'os' ) ;
6
6
const fs = require ( 'fs' ) ;
7
7
const path = require ( 'path' ) ;
8
- const { Benchmark } = require ( 'benchmark ' ) ;
8
+ const assert = require ( 'assert ' ) ;
9
9
10
+ const { red, green, yellow, cyan, grey } = require ( './colors' ) ;
10
11
const {
11
12
exec,
12
13
copyFile,
@@ -16,8 +17,17 @@ const {
16
17
readdirRecursive,
17
18
} = require ( './utils' ) ;
18
19
20
+ const NS_PER_SEC = 1e9 ;
19
21
const LOCAL = 'local' ;
20
22
23
+ const minTime = 0.05 * NS_PER_SEC ;
24
+ // The maximum time a benchmark is allowed to run before finishing.
25
+ const maxTime = 5 * NS_PER_SEC ;
26
+ // The minimum sample size required to perform statistical analysis.
27
+ const minSamples = 15 ;
28
+ // The default number of times to execute a test on a benchmark's first cycle.
29
+ const initCount = 10 ;
30
+
21
31
function LOCAL_DIR ( ...paths ) {
22
32
return path . join ( __dirname , '..' , ...paths ) ;
23
33
}
@@ -93,43 +103,132 @@ function runBenchmark(benchmark, environments) {
93
103
const benches = environments . map ( environment => {
94
104
const module = require ( path . join ( environment . distPath , benchmark ) ) ;
95
105
benchmarkName = module . name ;
96
- return new Benchmark ( environment . revision , module . measure ) ;
106
+ return {
107
+ name : environment . revision ,
108
+ fn : module . measure ,
109
+ } ;
97
110
} ) ;
98
111
99
112
console . log ( '⏱️ ' + benchmarkName ) ;
113
+ const results = [ ] ;
100
114
for ( let i = 0 ; i < benches . length ; ++ i ) {
101
- benches [ i ] . run ( { async : false } ) ;
102
- process . stdout . write ( ' ' + cyan ( i + 1 ) + ' tests completed.\u000D' ) ;
115
+ const { name, fn } = benches [ i ] ;
116
+ try {
117
+ const samples = collectSamples ( fn ) ;
118
+ results . push ( { name, samples, ...computeStats ( samples ) } ) ;
119
+ process . stdout . write ( ' ' + cyan ( i + 1 ) + ' tests completed.\u000D' ) ;
120
+ } catch ( error ) {
121
+ console . log ( ' ' + name + ': ' + red ( String ( error ) ) ) ;
122
+ }
103
123
}
104
124
console . log ( '\n' ) ;
105
125
106
- beautifyBenchmark ( benches ) ;
126
+ beautifyBenchmark ( results ) ;
107
127
console . log ( '' ) ;
108
128
}
109
129
110
- function beautifyBenchmark ( results ) {
111
- const benches = results . map ( result => ( {
112
- name : result . name ,
113
- error : result . error ,
114
- ops : result . hz ,
115
- deviation : result . stats . rme ,
116
- numRuns : result . stats . sample . length ,
117
- } ) ) ;
130
+ function collectSamples ( fn ) {
131
+ clock ( initCount , fn ) ; // initial warm up
132
+
133
+ // Cycles a benchmark until a run `count` can be established.
134
+ // Resolve time span required to achieve a percent uncertainty of at most 1%.
135
+ // For more information see http://spiff.rit.edu/classes/phys273/uncert/uncert.html.
136
+ let count = initCount ;
137
+ let clocked = 0 ;
138
+ while ( ( clocked = clock ( count , fn ) ) < minTime ) {
139
+ // Calculate how many more iterations it will take to achieve the `minTime`.
140
+ count += Math . ceil ( ( ( minTime - clocked ) * count ) / clocked ) ;
141
+ }
118
142
119
- const nameMaxLen = maxBy ( benches , ( { name } ) => name . length ) ;
120
- const opsTop = maxBy ( benches , ( { ops } ) => ops ) ;
121
- const opsMaxLen = maxBy ( benches , ( { ops } ) => beautifyNumber ( ops ) . length ) ;
143
+ let elapsed = 0 ;
144
+ const samples = [ ] ;
122
145
123
- for ( const bench of benches ) {
124
- if ( bench . error ) {
125
- console . log ( ' ' + bench . name + ': ' + red ( String ( bench . error ) ) ) ;
126
- continue ;
127
- }
128
- printBench ( bench ) ;
146
+ // If time permits, increase sample size to reduce the margin of error.
147
+ while ( samples . length < minSamples || elapsed < maxTime ) {
148
+ clocked = clock ( count , fn ) ;
149
+ assert ( clocked > 0 ) ;
150
+
151
+ elapsed += clocked ;
152
+ // Compute the seconds per operation.
153
+ samples . push ( clocked / count ) ;
154
+ }
155
+
156
+ return samples ;
157
+ }
158
+
159
+ // Clocks the time taken to execute a test per cycle (secs).
160
+ function clock ( count , fn ) {
161
+ const start = process . hrtime . bigint ( ) ;
162
+ for ( let i = 0 ; i < count ; ++ i ) {
163
+ fn ( ) ;
164
+ }
165
+ return Number ( process . hrtime . bigint ( ) - start ) ;
166
+ }
167
+
168
+ // T-Distribution two-tailed critical values for 95% confidence.
169
+ // See http://www.itl.nist.gov/div898/handbook/eda/section3/eda3672.htm.
170
+ const tTable = /* prettier-ignore */ {
171
+ '1' : 12.706 , '2' : 4.303 , '3' : 3.182 , '4' : 2.776 , '5' : 2.571 , '6' : 2.447 ,
172
+ '7' : 2.365 , '8' : 2.306 , '9' : 2.262 , '10' : 2.228 , '11' : 2.201 , '12' : 2.179 ,
173
+ '13' : 2.16 , '14' : 2.145 , '15' : 2.131 , '16' : 2.12 , '17' : 2.11 , '18' : 2.101 ,
174
+ '19' : 2.093 , '20' : 2.086 , '21' : 2.08 , '22' : 2.074 , '23' : 2.069 , '24' : 2.064 ,
175
+ '25' : 2.06 , '26' : 2.056 , '27' : 2.052 , '28' : 2.048 , '29' : 2.045 , '30' : 2.042 ,
176
+ infinity : 1.96 ,
177
+ } ;
178
+
179
+ // Computes stats on benchmark results.
180
+ function computeStats ( samples ) {
181
+ assert ( samples . length > 1 ) ;
182
+
183
+ // Compute the sample mean (estimate of the population mean).
184
+ let mean = 0 ;
185
+ for ( const x of samples ) {
186
+ mean += x ;
187
+ }
188
+ mean /= samples . length ;
189
+
190
+ // Compute the sample variance (estimate of the population variance).
191
+ let variance = 0 ;
192
+ for ( const x of samples ) {
193
+ variance += Math . pow ( x - mean , 2 ) ;
194
+ }
195
+ variance /= samples . length - 1 ;
196
+
197
+ // Compute the sample standard deviation (estimate of the population standard deviation).
198
+ const sd = Math . sqrt ( variance ) ;
199
+
200
+ // Compute the standard error of the mean (a.k.a. the standard deviation of the sampling distribution of the sample mean).
201
+ const sem = sd / Math . sqrt ( samples . length ) ;
202
+
203
+ // Compute the degrees of freedom.
204
+ const df = samples . length - 1 ;
205
+
206
+ // Compute the critical value.
207
+ const critical = tTable [ df ] || tTable . infinity ;
208
+
209
+ // Compute the margin of error.
210
+ const moe = sem * critical ;
211
+
212
+ // The relative margin of error (expressed as a percentage of the mean).
213
+ const rme = ( moe / mean ) * 100 || 0 ;
214
+
215
+ return {
216
+ ops : NS_PER_SEC / mean ,
217
+ deviation : rme ,
218
+ } ;
219
+ }
220
+
221
+ function beautifyBenchmark ( results ) {
222
+ const nameMaxLen = maxBy ( results , ( { name } ) => name . length ) ;
223
+ const opsTop = maxBy ( results , ( { ops } ) => ops ) ;
224
+ const opsMaxLen = maxBy ( results , ( { ops } ) => beautifyNumber ( ops ) . length ) ;
225
+
226
+ for ( const result of results ) {
227
+ printBench ( result ) ;
129
228
}
130
229
131
230
function printBench ( bench ) {
132
- const { name, ops, deviation, numRuns } = bench ;
231
+ const { name, ops, deviation, samples } = bench ;
133
232
console . log (
134
233
' ' +
135
234
nameStr ( ) +
@@ -139,7 +238,7 @@ function beautifyBenchmark(results) {
139
238
grey ( '\xb1' ) +
140
239
deviationStr ( ) +
141
240
cyan ( '%' ) +
142
- grey ( ' (' + numRuns + ' runs sampled)' ) ,
241
+ grey ( ' (' + samples . length + ' runs sampled)' ) ,
143
242
) ;
144
243
145
244
function nameStr ( ) {
@@ -160,22 +259,6 @@ function beautifyBenchmark(results) {
160
259
}
161
260
}
162
261
163
- function red ( str ) {
164
- return '\u001b[31m' + str + '\u001b[0m' ;
165
- }
166
- function green ( str ) {
167
- return '\u001b[32m' + str + '\u001b[0m' ;
168
- }
169
- function yellow ( str ) {
170
- return '\u001b[33m' + str + '\u001b[0m' ;
171
- }
172
- function cyan ( str ) {
173
- return '\u001b[36m' + str + '\u001b[0m' ;
174
- }
175
- function grey ( str ) {
176
- return '\u001b[90m' + str + '\u001b[0m' ;
177
- }
178
-
179
262
function beautifyNumber ( num ) {
180
263
return Number ( num . toFixed ( num > 100 ? 0 : 2 ) ) . toLocaleString ( ) ;
181
264
}
0 commit comments