@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 CHANGED
@@ -43,7 +43,7 @@ export default () => {
43
43
 
44
44
  ```sh
45
45
  $ bencher benchmark.js
46
- runner: 1058.6 ops/s4.5, p=0.05, n=98)
46
+ runner: 1’037.8 ops/sec18.8, p=0.001, n=98)
47
47
  ```
48
48
 
49
49
  ## LICENSE
@@ -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 = 1;
36
- const ITALIC = 3;
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: 10,
67
+ default: 5,
64
68
  describe: 'width of iteration sweep',
65
69
  })
66
- .option('warm-up-iterations', {
70
+ .option('warm-up-duration', {
67
71
  type: 'number',
68
- default: 100,
69
- describe: 'number of warm up iterations',
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'], warmUpIterations = argv['warmUpIterations'], } = m.options ?? {};
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 (warmUpIterations <= 0) {
89
- throw new Error(`${file}: options.warmUpIterations must be positive`);
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
- warmUpIterations,
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 = style(m.name, BOLD) + ':' + ' '.repeat(maxNameLength - m.name.length);
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, usedSamples } = run(m, {
123
+ const { ops, maxError, outliers, severeOutliers } = run(m, {
114
124
  onTick,
115
125
  });
116
- const stats = style(`(±${nice(maxError)}, p=${P_VALUE}, n=${usedSamples})`, ITALIC);
117
- (0, fs_1.writeSync)(process.stdout.fd, `${PREV_LINE}${paddedName} ${nice(ops)} ops/sec ${stats}\n`);
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
- usedSamples,
163
+ outliers,
164
+ severeOutliers,
146
165
  };
147
166
  }
148
167
  function warmUp(m) {
149
168
  // Initial warm-up
150
- for (let i = 0; i < m.options.warmUpIterations; i++) {
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
- // Within each iteration bin get rid of the outliers.
194
- const withoutOutliers = new Array();
195
- for (const [iterations, durations] of bins) {
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 filtered = durations.filter((d) => d >= outlierLow && d <= outlierHigh);
204
- for (const duration of filtered) {
205
- withoutOutliers.push({ iterations, duration });
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 (withoutOutliers.length < 2) {
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 withoutOutliers) {
241
+ for (const { duration, iterations } of samples) {
214
242
  meanDuration += duration;
215
243
  meanIterations += iterations;
216
244
  }
217
- meanDuration /= withoutOutliers.length;
218
- meanIterations /= withoutOutliers.length;
245
+ meanDuration /= samples.length;
246
+ meanIterations /= samples.length;
219
247
  let betaNum = 0;
220
248
  let betaDenom = 0;
221
- for (const { duration, iterations } of withoutOutliers) {
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 withoutOutliers) {
258
+ for (const { duration, iterations } of samples) {
231
259
  stdError += (duration - alpha - beta * iterations) ** 2;
232
260
  }
233
- stdError /= withoutOutliers.length - 2;
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: samples.length - withoutOutliers.length,
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) + "'" + result.slice(i);
275
+ result = result.slice(0, i) + '’' + result.slice(i);
250
276
  }
251
277
  return result;
252
278
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indutny/bencher",
3
- "version": "1.1.4",
3
+ "version": "1.2.0",
4
4
  "description": "Simple benchmarking tool",
5
5
  "bin": {
6
6
  "bencher": "./dist/bin/bencher.js"