@indutny/bencher 1.1.5 → 2.0.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 +2 -5
- package/dist/bin/bencher.js +74 -44
- package/package.json +9 -9
package/README.md
CHANGED
|
@@ -24,11 +24,8 @@ npm install -g @indutny/bencher
|
|
|
24
24
|
## Usage
|
|
25
25
|
|
|
26
26
|
```js
|
|
27
|
-
// benchmark.js
|
|
28
|
-
export const name = 'runner';
|
|
29
|
-
|
|
30
27
|
// Function to benchmark
|
|
31
|
-
export
|
|
28
|
+
export function benchmarkName() => {
|
|
32
29
|
let sum = 0;
|
|
33
30
|
for (let i = 0; i < 1e6; i++) {
|
|
34
31
|
sum += i;
|
|
@@ -43,7 +40,7 @@ export default () => {
|
|
|
43
40
|
|
|
44
41
|
```sh
|
|
45
42
|
$ bencher benchmark.js
|
|
46
|
-
|
|
43
|
+
benchmark.js/benchmarkName: 1’037.8 ops/sec (±18.8, p=0.001, n=98)
|
|
47
44
|
```
|
|
48
45
|
|
|
49
46
|
## LICENSE
|
package/dist/bin/bencher.js
CHANGED
|
@@ -32,8 +32,11 @@ 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';
|
|
39
42
|
// From https://github.com/mayuki/Kurukuru
|
|
@@ -61,19 +64,24 @@ async function main() {
|
|
|
61
64
|
.option('w', {
|
|
62
65
|
alias: 'sweep-width',
|
|
63
66
|
type: 'number',
|
|
64
|
-
default:
|
|
67
|
+
default: 5,
|
|
65
68
|
describe: 'width of iteration sweep',
|
|
66
69
|
})
|
|
67
|
-
.option('warm-up-
|
|
70
|
+
.option('warm-up-duration', {
|
|
68
71
|
type: 'number',
|
|
69
|
-
default:
|
|
70
|
-
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",
|
|
71
79
|
}).argv;
|
|
72
|
-
const modules = await Promise.all(argv._.map(async (file) => {
|
|
80
|
+
const modules = (await Promise.all(argv._.map(async (file) => {
|
|
73
81
|
var _a;
|
|
74
82
|
const path = await (0, promises_1.realpath)(String(file));
|
|
75
83
|
const m = await (_a = path, Promise.resolve().then(() => __importStar(require(_a))));
|
|
76
|
-
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 ?? {};
|
|
77
85
|
if (duration <= 0) {
|
|
78
86
|
throw new Error(`${file}: options.duration must be positive`);
|
|
79
87
|
}
|
|
@@ -86,23 +94,30 @@ async function main() {
|
|
|
86
94
|
if (samples / sweepWidth < 2) {
|
|
87
95
|
throw new Error(`${file}: options.samples must be greater than 2 * sweepWidth`);
|
|
88
96
|
}
|
|
89
|
-
if (
|
|
90
|
-
throw new Error(`${file}: options.
|
|
97
|
+
if (warmUpDuration <= 0) {
|
|
98
|
+
throw new Error(`${file}: options.warmUpDuration must be positive`);
|
|
99
|
+
}
|
|
100
|
+
const functions = new Array();
|
|
101
|
+
for (const key of Object.keys(m)) {
|
|
102
|
+
if (typeof m[key] === 'function') {
|
|
103
|
+
functions.push(m[key]);
|
|
104
|
+
}
|
|
91
105
|
}
|
|
92
|
-
return {
|
|
93
|
-
name:
|
|
106
|
+
return functions.map((fn) => ({
|
|
107
|
+
name: `${file}/${fn.name}`,
|
|
94
108
|
options: {
|
|
95
109
|
duration,
|
|
96
110
|
sweepWidth,
|
|
97
111
|
samples,
|
|
98
|
-
|
|
112
|
+
warmUpDuration,
|
|
113
|
+
ignoreOutliers,
|
|
99
114
|
},
|
|
100
|
-
|
|
101
|
-
};
|
|
102
|
-
}));
|
|
115
|
+
fn,
|
|
116
|
+
}));
|
|
117
|
+
}))).flat();
|
|
103
118
|
const maxNameLength = modules.reduce((len, { name }) => Math.max(len, name.length), 0);
|
|
104
119
|
for (const m of modules) {
|
|
105
|
-
const paddedName =
|
|
120
|
+
const paddedName = BOLD + m.name + RESET + ':' + ' '.repeat(maxNameLength - m.name.length);
|
|
106
121
|
// Just to reserve the line
|
|
107
122
|
(0, fs_1.writeSync)(process.stdout.fd, '\n');
|
|
108
123
|
let ticks = 0;
|
|
@@ -111,12 +126,20 @@ async function main() {
|
|
|
111
126
|
ticks = (ticks + 1) % SPINNER.length;
|
|
112
127
|
};
|
|
113
128
|
onTick();
|
|
114
|
-
const { ops, maxError,
|
|
129
|
+
const { ops, maxError, outliers, severeOutliers } = run(m, {
|
|
115
130
|
onTick,
|
|
116
131
|
});
|
|
117
|
-
const stats =
|
|
118
|
-
|
|
119
|
-
|
|
132
|
+
const stats = [
|
|
133
|
+
`±${nice(maxError)}`,
|
|
134
|
+
`p=${P_VALUE}`,
|
|
135
|
+
`o=${outliers + severeOutliers}/${m.options.samples}`,
|
|
136
|
+
];
|
|
137
|
+
let warning = '';
|
|
138
|
+
if (!m.options.ignoreOutliers && severeOutliers !== 0) {
|
|
139
|
+
warning = `${RESET}${RED} severe outliers=${severeOutliers}`;
|
|
140
|
+
}
|
|
141
|
+
(0, fs_1.writeSync)(process.stdout.fd, `${PREV_LINE}${paddedName} ${nice(ops)} ops/sec ` +
|
|
142
|
+
`${GREY + ITALIC}(${stats.join(', ')})${warning}${RESET}\n`);
|
|
120
143
|
}
|
|
121
144
|
}
|
|
122
145
|
function run(m, { onTick }) {
|
|
@@ -135,22 +158,24 @@ function run(m, { onTick }) {
|
|
|
135
158
|
nextTick += tickEvery;
|
|
136
159
|
}
|
|
137
160
|
}
|
|
138
|
-
const { beta, confidence, outliers } = regress(m, samples);
|
|
161
|
+
const { beta, confidence, outliers, severeOutliers } = regress(m, samples);
|
|
139
162
|
const ops = 1 / beta;
|
|
140
163
|
const lowOps = 1 / (beta + confidence);
|
|
141
164
|
const highOps = 1 / (beta - confidence);
|
|
142
165
|
const maxError = Math.max(highOps - ops, ops - lowOps);
|
|
143
|
-
const usedSamples = samples.length - outliers;
|
|
144
166
|
return {
|
|
145
167
|
ops,
|
|
146
168
|
maxError,
|
|
147
|
-
|
|
169
|
+
outliers,
|
|
170
|
+
severeOutliers,
|
|
148
171
|
};
|
|
149
172
|
}
|
|
150
173
|
function warmUp(m) {
|
|
151
174
|
// Initial warm-up
|
|
152
|
-
|
|
153
|
-
|
|
175
|
+
const coldDuration = measure(m, 1);
|
|
176
|
+
const warmUpIterations = m.options.warmUpDuration / coldDuration;
|
|
177
|
+
for (let i = 0; i < warmUpIterations; i++) {
|
|
178
|
+
m.fn();
|
|
154
179
|
}
|
|
155
180
|
// Compute max duration per base sample
|
|
156
181
|
let sampleMultiplier = 0;
|
|
@@ -173,7 +198,7 @@ function measure(m, iterations) {
|
|
|
173
198
|
let sum = 0;
|
|
174
199
|
const start = process.hrtime.bigint();
|
|
175
200
|
for (let i = 0; i < iterations; i++) {
|
|
176
|
-
sum += m.
|
|
201
|
+
sum += m.fn();
|
|
177
202
|
}
|
|
178
203
|
const duration = Number(process.hrtime.bigint() - start) / 1e9;
|
|
179
204
|
if (isNaN(sum)) {
|
|
@@ -192,35 +217,42 @@ function regress(m, samples) {
|
|
|
192
217
|
}
|
|
193
218
|
bin.push(duration);
|
|
194
219
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
220
|
+
let outliers = 0;
|
|
221
|
+
let severeOutliers = 0;
|
|
222
|
+
// Within each iteration bin identify the outliers for reporting purposes.
|
|
223
|
+
for (const [, durations] of bins) {
|
|
198
224
|
durations.sort();
|
|
199
225
|
const p25 = durations[Math.floor(durations.length * 0.25)] ?? -Infinity;
|
|
200
226
|
const p75 = durations[Math.ceil(durations.length * 0.75)] ?? +Infinity;
|
|
201
227
|
const iqr = p75 - p25;
|
|
202
228
|
const outlierLow = p25 - iqr * 1.5;
|
|
203
229
|
const outlierHigh = p75 + iqr * 1.5;
|
|
230
|
+
const badOutlierLow = p25 - iqr * 3;
|
|
231
|
+
const badOutlierHigh = p75 + iqr * 3;
|
|
204
232
|
// Tukey's method
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
233
|
+
for (const d of durations) {
|
|
234
|
+
if (d < badOutlierLow || d > badOutlierHigh) {
|
|
235
|
+
severeOutliers++;
|
|
236
|
+
}
|
|
237
|
+
else if (d < outlierLow || d > outlierHigh) {
|
|
238
|
+
outliers++;
|
|
239
|
+
}
|
|
208
240
|
}
|
|
209
241
|
}
|
|
210
|
-
if (
|
|
242
|
+
if (samples.length < 2) {
|
|
211
243
|
throw new Error(`${m.name}: low sample count`);
|
|
212
244
|
}
|
|
213
245
|
let meanDuration = 0;
|
|
214
246
|
let meanIterations = 0;
|
|
215
|
-
for (const { duration, iterations } of
|
|
247
|
+
for (const { duration, iterations } of samples) {
|
|
216
248
|
meanDuration += duration;
|
|
217
249
|
meanIterations += iterations;
|
|
218
250
|
}
|
|
219
|
-
meanDuration /=
|
|
220
|
-
meanIterations /=
|
|
251
|
+
meanDuration /= samples.length;
|
|
252
|
+
meanIterations /= samples.length;
|
|
221
253
|
let betaNum = 0;
|
|
222
254
|
let betaDenom = 0;
|
|
223
|
-
for (const { duration, iterations } of
|
|
255
|
+
for (const { duration, iterations } of samples) {
|
|
224
256
|
betaNum += (duration - meanDuration) * (iterations - meanIterations);
|
|
225
257
|
betaDenom += (iterations - meanIterations) ** 2;
|
|
226
258
|
}
|
|
@@ -229,22 +261,20 @@ function regress(m, samples) {
|
|
|
229
261
|
// Intercept
|
|
230
262
|
const alpha = meanDuration - beta * meanIterations;
|
|
231
263
|
let stdError = 0;
|
|
232
|
-
for (const { duration, iterations } of
|
|
264
|
+
for (const { duration, iterations } of samples) {
|
|
233
265
|
stdError += (duration - alpha - beta * iterations) ** 2;
|
|
234
266
|
}
|
|
235
|
-
stdError /=
|
|
267
|
+
stdError /= samples.length - 2;
|
|
236
268
|
stdError /= betaDenom;
|
|
237
269
|
stdError = Math.sqrt(stdError);
|
|
238
270
|
return {
|
|
239
271
|
alpha,
|
|
240
272
|
beta,
|
|
241
273
|
confidence: STUDENT_T * stdError,
|
|
242
|
-
outliers
|
|
274
|
+
outliers,
|
|
275
|
+
severeOutliers,
|
|
243
276
|
};
|
|
244
277
|
}
|
|
245
|
-
function style(text, code) {
|
|
246
|
-
return `\x1b[${code}m${text}\x1b[m`;
|
|
247
|
-
}
|
|
248
278
|
function nice(n) {
|
|
249
279
|
let result = n.toFixed(1);
|
|
250
280
|
for (let i = result.length - 5; i > 0; i -= 3) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indutny/bencher",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Simple benchmarking tool",
|
|
5
5
|
"bin": {
|
|
6
6
|
"bencher": "./dist/bin/bencher.js"
|
|
@@ -9,13 +9,6 @@
|
|
|
9
9
|
"dist/bin/*.js",
|
|
10
10
|
"README.md"
|
|
11
11
|
],
|
|
12
|
-
"scripts": {
|
|
13
|
-
"build": "tsc",
|
|
14
|
-
"lint": "eslint --cache .",
|
|
15
|
-
"format": "prettier --cache --write .",
|
|
16
|
-
"check:format": "prettier --cache --check .",
|
|
17
|
-
"prepublish": "rm -rf dist && npm run build"
|
|
18
|
-
},
|
|
19
12
|
"keywords": [
|
|
20
13
|
"benchmark",
|
|
21
14
|
"sweep",
|
|
@@ -42,5 +35,12 @@
|
|
|
42
35
|
},
|
|
43
36
|
"dependencies": {
|
|
44
37
|
"yargs": "^17.6.2"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsc",
|
|
41
|
+
"lint": "eslint --cache .",
|
|
42
|
+
"format": "prettier --cache --write .",
|
|
43
|
+
"check:format": "prettier --cache --check .",
|
|
44
|
+
"prepublish": "rm -rf dist && npm run build"
|
|
45
45
|
}
|
|
46
|
-
}
|
|
46
|
+
}
|