@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 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 default () => {
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
- runner: 1058.6 ops/s4.5, p=0.05, n=98)
43
+ benchmark.js/benchmarkName: 1’037.8 ops/sec18.8, p=0.001, n=98)
47
44
  ```
48
45
 
49
46
  ## 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
- 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'], 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,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 (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`);
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: m.name ?? String(file),
106
+ return functions.map((fn) => ({
107
+ name: `${file}/${fn.name}`,
94
108
  options: {
95
109
  duration,
96
110
  sweepWidth,
97
111
  samples,
98
- warmUpIterations,
112
+ warmUpDuration,
113
+ ignoreOutliers,
99
114
  },
100
- default: m.default,
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 = style(m.name, BOLD) + ':' + ' '.repeat(maxNameLength - m.name.length);
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, usedSamples } = run(m, {
129
+ const { ops, maxError, outliers, severeOutliers } = run(m, {
115
130
  onTick,
116
131
  });
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`);
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
- usedSamples,
169
+ outliers,
170
+ severeOutliers,
148
171
  };
149
172
  }
150
173
  function warmUp(m) {
151
174
  // Initial warm-up
152
- for (let i = 0; i < m.options.warmUpIterations; i++) {
153
- m.default();
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.default();
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
- // Within each iteration bin get rid of the outliers.
196
- const withoutOutliers = new Array();
197
- for (const [iterations, durations] of bins) {
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 filtered = durations.filter((d) => d >= outlierLow && d <= outlierHigh);
206
- for (const duration of filtered) {
207
- withoutOutliers.push({ iterations, duration });
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 (withoutOutliers.length < 2) {
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 withoutOutliers) {
247
+ for (const { duration, iterations } of samples) {
216
248
  meanDuration += duration;
217
249
  meanIterations += iterations;
218
250
  }
219
- meanDuration /= withoutOutliers.length;
220
- meanIterations /= withoutOutliers.length;
251
+ meanDuration /= samples.length;
252
+ meanIterations /= samples.length;
221
253
  let betaNum = 0;
222
254
  let betaDenom = 0;
223
- for (const { duration, iterations } of withoutOutliers) {
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 withoutOutliers) {
264
+ for (const { duration, iterations } of samples) {
233
265
  stdError += (duration - alpha - beta * iterations) ** 2;
234
266
  }
235
- stdError /= withoutOutliers.length - 2;
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: samples.length - withoutOutliers.length,
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": "1.1.5",
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
+ }