@indutny/bencher 1.1.4 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/bin/bencher.js +62 -36
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/bin/bencher.js
CHANGED
|
@@ -32,10 +32,14 @@ const promises_1 = require("fs/promises");
|
|
|
32
32
|
const yargs_1 = __importDefault(require("yargs"));
|
|
33
33
|
const helpers_1 = require("yargs/helpers");
|
|
34
34
|
// ANSI colors
|
|
35
|
-
const BOLD =
|
|
36
|
-
const ITALIC =
|
|
35
|
+
const BOLD = '\x1b[1m';
|
|
36
|
+
const ITALIC = '\x1b[3m';
|
|
37
|
+
const RESET = '\x1b[m';
|
|
38
|
+
const GREY = '\x1b[90m';
|
|
39
|
+
const RED = '\x1b[31m';
|
|
37
40
|
// Go back to previous line, clear the line
|
|
38
41
|
const PREV_LINE = '\x1b[F\x1b[K';
|
|
42
|
+
// From https://github.com/mayuki/Kurukuru
|
|
39
43
|
const SPINNER = ['⠋', '⠙', '⠸', '⠴', '⠦', '⠇'];
|
|
40
44
|
const TICK_FREQUENCY = 1 / 4;
|
|
41
45
|
// t-distribution value for large sample count and p=0.001
|
|
@@ -60,19 +64,24 @@ async function main() {
|
|
|
60
64
|
.option('w', {
|
|
61
65
|
alias: 'sweep-width',
|
|
62
66
|
type: 'number',
|
|
63
|
-
default:
|
|
67
|
+
default: 5,
|
|
64
68
|
describe: 'width of iteration sweep',
|
|
65
69
|
})
|
|
66
|
-
.option('warm-up-
|
|
70
|
+
.option('warm-up-duration', {
|
|
67
71
|
type: 'number',
|
|
68
|
-
default:
|
|
69
|
-
describe: '
|
|
72
|
+
default: 0.5,
|
|
73
|
+
describe: 'duration of warm up',
|
|
74
|
+
})
|
|
75
|
+
.option('ignore-outliers', {
|
|
76
|
+
alias: 'q',
|
|
77
|
+
type: 'boolean',
|
|
78
|
+
describe: "don't report severe outliers",
|
|
70
79
|
}).argv;
|
|
71
80
|
const modules = await Promise.all(argv._.map(async (file) => {
|
|
72
81
|
var _a;
|
|
73
82
|
const path = await (0, promises_1.realpath)(String(file));
|
|
74
83
|
const m = await (_a = path, Promise.resolve().then(() => __importStar(require(_a))));
|
|
75
|
-
const { duration = argv['duration'], sweepWidth = argv['sweepWidth'], samples = argv['samples'],
|
|
84
|
+
const { duration = argv['duration'], sweepWidth = argv['sweepWidth'], samples = argv['samples'], warmUpDuration = argv['warmUpDuration'], ignoreOutliers = argv['ignoreOutliers'], } = m.options ?? {};
|
|
76
85
|
if (duration <= 0) {
|
|
77
86
|
throw new Error(`${file}: options.duration must be positive`);
|
|
78
87
|
}
|
|
@@ -85,8 +94,8 @@ async function main() {
|
|
|
85
94
|
if (samples / sweepWidth < 2) {
|
|
86
95
|
throw new Error(`${file}: options.samples must be greater than 2 * sweepWidth`);
|
|
87
96
|
}
|
|
88
|
-
if (
|
|
89
|
-
throw new Error(`${file}: options.
|
|
97
|
+
if (warmUpDuration <= 0) {
|
|
98
|
+
throw new Error(`${file}: options.warmUpDuration must be positive`);
|
|
90
99
|
}
|
|
91
100
|
return {
|
|
92
101
|
name: m.name ?? String(file),
|
|
@@ -94,14 +103,15 @@ async function main() {
|
|
|
94
103
|
duration,
|
|
95
104
|
sweepWidth,
|
|
96
105
|
samples,
|
|
97
|
-
|
|
106
|
+
warmUpDuration,
|
|
107
|
+
ignoreOutliers,
|
|
98
108
|
},
|
|
99
109
|
default: m.default,
|
|
100
110
|
};
|
|
101
111
|
}));
|
|
102
112
|
const maxNameLength = modules.reduce((len, { name }) => Math.max(len, name.length), 0);
|
|
103
113
|
for (const m of modules) {
|
|
104
|
-
const paddedName =
|
|
114
|
+
const paddedName = BOLD + m.name + RESET + ':' + ' '.repeat(maxNameLength - m.name.length);
|
|
105
115
|
// Just to reserve the line
|
|
106
116
|
(0, fs_1.writeSync)(process.stdout.fd, '\n');
|
|
107
117
|
let ticks = 0;
|
|
@@ -110,11 +120,20 @@ async function main() {
|
|
|
110
120
|
ticks = (ticks + 1) % SPINNER.length;
|
|
111
121
|
};
|
|
112
122
|
onTick();
|
|
113
|
-
const { ops, maxError,
|
|
123
|
+
const { ops, maxError, outliers, severeOutliers } = run(m, {
|
|
114
124
|
onTick,
|
|
115
125
|
});
|
|
116
|
-
const stats =
|
|
117
|
-
|
|
126
|
+
const stats = [
|
|
127
|
+
`±${nice(maxError)}`,
|
|
128
|
+
`p=${P_VALUE}`,
|
|
129
|
+
`o=${outliers + severeOutliers}/${m.options.samples}`,
|
|
130
|
+
];
|
|
131
|
+
let warning = '';
|
|
132
|
+
if (!m.options.ignoreOutliers && severeOutliers !== 0) {
|
|
133
|
+
warning = `${RESET}${RED} severe outliers=${severeOutliers}`;
|
|
134
|
+
}
|
|
135
|
+
(0, fs_1.writeSync)(process.stdout.fd, `${PREV_LINE}${paddedName} ${nice(ops)} ops/sec ` +
|
|
136
|
+
`${GREY + ITALIC}(${stats.join(', ')})${warning}${RESET}\n`);
|
|
118
137
|
}
|
|
119
138
|
}
|
|
120
139
|
function run(m, { onTick }) {
|
|
@@ -133,21 +152,23 @@ function run(m, { onTick }) {
|
|
|
133
152
|
nextTick += tickEvery;
|
|
134
153
|
}
|
|
135
154
|
}
|
|
136
|
-
const { beta, confidence, outliers } = regress(m, samples);
|
|
155
|
+
const { beta, confidence, outliers, severeOutliers } = regress(m, samples);
|
|
137
156
|
const ops = 1 / beta;
|
|
138
157
|
const lowOps = 1 / (beta + confidence);
|
|
139
158
|
const highOps = 1 / (beta - confidence);
|
|
140
159
|
const maxError = Math.max(highOps - ops, ops - lowOps);
|
|
141
|
-
const usedSamples = samples.length - outliers;
|
|
142
160
|
return {
|
|
143
161
|
ops,
|
|
144
162
|
maxError,
|
|
145
|
-
|
|
163
|
+
outliers,
|
|
164
|
+
severeOutliers,
|
|
146
165
|
};
|
|
147
166
|
}
|
|
148
167
|
function warmUp(m) {
|
|
149
168
|
// Initial warm-up
|
|
150
|
-
|
|
169
|
+
const coldDuration = measure(m, 1);
|
|
170
|
+
const warmUpIterations = m.options.warmUpDuration / coldDuration;
|
|
171
|
+
for (let i = 0; i < warmUpIterations; i++) {
|
|
151
172
|
m.default();
|
|
152
173
|
}
|
|
153
174
|
// Compute max duration per base sample
|
|
@@ -190,35 +211,42 @@ function regress(m, samples) {
|
|
|
190
211
|
}
|
|
191
212
|
bin.push(duration);
|
|
192
213
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
214
|
+
let outliers = 0;
|
|
215
|
+
let severeOutliers = 0;
|
|
216
|
+
// Within each iteration bin identify the outliers for reporting purposes.
|
|
217
|
+
for (const [, durations] of bins) {
|
|
196
218
|
durations.sort();
|
|
197
219
|
const p25 = durations[Math.floor(durations.length * 0.25)] ?? -Infinity;
|
|
198
220
|
const p75 = durations[Math.ceil(durations.length * 0.75)] ?? +Infinity;
|
|
199
221
|
const iqr = p75 - p25;
|
|
200
222
|
const outlierLow = p25 - iqr * 1.5;
|
|
201
223
|
const outlierHigh = p75 + iqr * 1.5;
|
|
224
|
+
const badOutlierLow = p25 - iqr * 3;
|
|
225
|
+
const badOutlierHigh = p75 + iqr * 3;
|
|
202
226
|
// Tukey's method
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
227
|
+
for (const d of durations) {
|
|
228
|
+
if (d < badOutlierLow || d > badOutlierHigh) {
|
|
229
|
+
severeOutliers++;
|
|
230
|
+
}
|
|
231
|
+
else if (d < outlierLow || d > outlierHigh) {
|
|
232
|
+
outliers++;
|
|
233
|
+
}
|
|
206
234
|
}
|
|
207
235
|
}
|
|
208
|
-
if (
|
|
236
|
+
if (samples.length < 2) {
|
|
209
237
|
throw new Error(`${m.name}: low sample count`);
|
|
210
238
|
}
|
|
211
239
|
let meanDuration = 0;
|
|
212
240
|
let meanIterations = 0;
|
|
213
|
-
for (const { duration, iterations } of
|
|
241
|
+
for (const { duration, iterations } of samples) {
|
|
214
242
|
meanDuration += duration;
|
|
215
243
|
meanIterations += iterations;
|
|
216
244
|
}
|
|
217
|
-
meanDuration /=
|
|
218
|
-
meanIterations /=
|
|
245
|
+
meanDuration /= samples.length;
|
|
246
|
+
meanIterations /= samples.length;
|
|
219
247
|
let betaNum = 0;
|
|
220
248
|
let betaDenom = 0;
|
|
221
|
-
for (const { duration, iterations } of
|
|
249
|
+
for (const { duration, iterations } of samples) {
|
|
222
250
|
betaNum += (duration - meanDuration) * (iterations - meanIterations);
|
|
223
251
|
betaDenom += (iterations - meanIterations) ** 2;
|
|
224
252
|
}
|
|
@@ -227,26 +255,24 @@ function regress(m, samples) {
|
|
|
227
255
|
// Intercept
|
|
228
256
|
const alpha = meanDuration - beta * meanIterations;
|
|
229
257
|
let stdError = 0;
|
|
230
|
-
for (const { duration, iterations } of
|
|
258
|
+
for (const { duration, iterations } of samples) {
|
|
231
259
|
stdError += (duration - alpha - beta * iterations) ** 2;
|
|
232
260
|
}
|
|
233
|
-
stdError /=
|
|
261
|
+
stdError /= samples.length - 2;
|
|
234
262
|
stdError /= betaDenom;
|
|
235
263
|
stdError = Math.sqrt(stdError);
|
|
236
264
|
return {
|
|
237
265
|
alpha,
|
|
238
266
|
beta,
|
|
239
267
|
confidence: STUDENT_T * stdError,
|
|
240
|
-
outliers
|
|
268
|
+
outliers,
|
|
269
|
+
severeOutliers,
|
|
241
270
|
};
|
|
242
271
|
}
|
|
243
|
-
function style(text, code) {
|
|
244
|
-
return `\x1b[${code}m${text}\x1b[m`;
|
|
245
|
-
}
|
|
246
272
|
function nice(n) {
|
|
247
273
|
let result = n.toFixed(1);
|
|
248
274
|
for (let i = result.length - 5; i > 0; i -= 3) {
|
|
249
|
-
result = result.slice(0, i) +
|
|
275
|
+
result = result.slice(0, i) + '’' + result.slice(i);
|
|
250
276
|
}
|
|
251
277
|
return result;
|
|
252
278
|
}
|