@indutny/bencher 1.1.5 → 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,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 = 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';
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: 10,
67
+ default: 5,
65
68
  describe: 'width of iteration sweep',
66
69
  })
67
- .option('warm-up-iterations', {
70
+ .option('warm-up-duration', {
68
71
  type: 'number',
69
- default: 100,
70
- 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",
71
79
  }).argv;
72
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'], warmUpIterations = argv['warmUpIterations'], } = m.options ?? {};
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,8 +94,8 @@ 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 (warmUpIterations <= 0) {
90
- throw new Error(`${file}: options.warmUpIterations must be positive`);
97
+ if (warmUpDuration <= 0) {
98
+ throw new Error(`${file}: options.warmUpDuration must be positive`);
91
99
  }
92
100
  return {
93
101
  name: m.name ?? String(file),
@@ -95,14 +103,15 @@ async function main() {
95
103
  duration,
96
104
  sweepWidth,
97
105
  samples,
98
- warmUpIterations,
106
+ warmUpDuration,
107
+ ignoreOutliers,
99
108
  },
100
109
  default: m.default,
101
110
  };
102
111
  }));
103
112
  const maxNameLength = modules.reduce((len, { name }) => Math.max(len, name.length), 0);
104
113
  for (const m of modules) {
105
- const paddedName = style(m.name, BOLD) + ':' + ' '.repeat(maxNameLength - m.name.length);
114
+ const paddedName = BOLD + m.name + RESET + ':' + ' '.repeat(maxNameLength - m.name.length);
106
115
  // Just to reserve the line
107
116
  (0, fs_1.writeSync)(process.stdout.fd, '\n');
108
117
  let ticks = 0;
@@ -111,12 +120,20 @@ async function main() {
111
120
  ticks = (ticks + 1) % SPINNER.length;
112
121
  };
113
122
  onTick();
114
- const { ops, maxError, usedSamples } = run(m, {
123
+ const { ops, maxError, outliers, severeOutliers } = run(m, {
115
124
  onTick,
116
125
  });
117
- const stats = '\x1b[90m' +
118
- style(`(±${nice(maxError)}, p=${P_VALUE}, n=${usedSamples})`, ITALIC);
119
- (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`);
120
137
  }
121
138
  }
122
139
  function run(m, { onTick }) {
@@ -135,21 +152,23 @@ function run(m, { onTick }) {
135
152
  nextTick += tickEvery;
136
153
  }
137
154
  }
138
- const { beta, confidence, outliers } = regress(m, samples);
155
+ const { beta, confidence, outliers, severeOutliers } = regress(m, samples);
139
156
  const ops = 1 / beta;
140
157
  const lowOps = 1 / (beta + confidence);
141
158
  const highOps = 1 / (beta - confidence);
142
159
  const maxError = Math.max(highOps - ops, ops - lowOps);
143
- const usedSamples = samples.length - outliers;
144
160
  return {
145
161
  ops,
146
162
  maxError,
147
- usedSamples,
163
+ outliers,
164
+ severeOutliers,
148
165
  };
149
166
  }
150
167
  function warmUp(m) {
151
168
  // Initial warm-up
152
- 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++) {
153
172
  m.default();
154
173
  }
155
174
  // Compute max duration per base sample
@@ -192,35 +211,42 @@ function regress(m, samples) {
192
211
  }
193
212
  bin.push(duration);
194
213
  }
195
- // Within each iteration bin get rid of the outliers.
196
- const withoutOutliers = new Array();
197
- 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) {
198
218
  durations.sort();
199
219
  const p25 = durations[Math.floor(durations.length * 0.25)] ?? -Infinity;
200
220
  const p75 = durations[Math.ceil(durations.length * 0.75)] ?? +Infinity;
201
221
  const iqr = p75 - p25;
202
222
  const outlierLow = p25 - iqr * 1.5;
203
223
  const outlierHigh = p75 + iqr * 1.5;
224
+ const badOutlierLow = p25 - iqr * 3;
225
+ const badOutlierHigh = p75 + iqr * 3;
204
226
  // Tukey's method
205
- const filtered = durations.filter((d) => d >= outlierLow && d <= outlierHigh);
206
- for (const duration of filtered) {
207
- 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
+ }
208
234
  }
209
235
  }
210
- if (withoutOutliers.length < 2) {
236
+ if (samples.length < 2) {
211
237
  throw new Error(`${m.name}: low sample count`);
212
238
  }
213
239
  let meanDuration = 0;
214
240
  let meanIterations = 0;
215
- for (const { duration, iterations } of withoutOutliers) {
241
+ for (const { duration, iterations } of samples) {
216
242
  meanDuration += duration;
217
243
  meanIterations += iterations;
218
244
  }
219
- meanDuration /= withoutOutliers.length;
220
- meanIterations /= withoutOutliers.length;
245
+ meanDuration /= samples.length;
246
+ meanIterations /= samples.length;
221
247
  let betaNum = 0;
222
248
  let betaDenom = 0;
223
- for (const { duration, iterations } of withoutOutliers) {
249
+ for (const { duration, iterations } of samples) {
224
250
  betaNum += (duration - meanDuration) * (iterations - meanIterations);
225
251
  betaDenom += (iterations - meanIterations) ** 2;
226
252
  }
@@ -229,22 +255,20 @@ function regress(m, samples) {
229
255
  // Intercept
230
256
  const alpha = meanDuration - beta * meanIterations;
231
257
  let stdError = 0;
232
- for (const { duration, iterations } of withoutOutliers) {
258
+ for (const { duration, iterations } of samples) {
233
259
  stdError += (duration - alpha - beta * iterations) ** 2;
234
260
  }
235
- stdError /= withoutOutliers.length - 2;
261
+ stdError /= samples.length - 2;
236
262
  stdError /= betaDenom;
237
263
  stdError = Math.sqrt(stdError);
238
264
  return {
239
265
  alpha,
240
266
  beta,
241
267
  confidence: STUDENT_T * stdError,
242
- outliers: samples.length - withoutOutliers.length,
268
+ outliers,
269
+ severeOutliers,
243
270
  };
244
271
  }
245
- function style(text, code) {
246
- return `\x1b[${code}m${text}\x1b[m`;
247
- }
248
272
  function nice(n) {
249
273
  let result = n.toFixed(1);
250
274
  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": "1.1.5",
3
+ "version": "1.2.0",
4
4
  "description": "Simple benchmarking tool",
5
5
  "bin": {
6
6
  "bencher": "./dist/bin/bencher.js"