nano-benchmark 1.0.14 → 1.0.16

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
@@ -93,6 +93,37 @@ npx nano-watch bench-strings-concat.js backticks
93
93
 
94
94
  See [wiki](https://github.com/uhop/nano-bench/wiki) for more details.
95
95
 
96
+ ## User Timing API integration
97
+
98
+ Pass `-o` / `--observe` to `nano-bench` to emit
99
+ [User Timing](https://developer.mozilla.org/en-US/docs/Web/API/Performance_API/User_timing)
100
+ marks at calibration and sampling phase boundaries. Marks are written to the
101
+ standard performance timeline and are observable via `PerformanceObserver` or
102
+ visible in DevTools / `node --inspect` traces — useful for correlating
103
+ benchmark variability with GC pauses, V8 optimization events, etc.
104
+
105
+ Mark / measure names follow `nano-bench/<function-name>/<phase>`, where phase is
106
+ `find-level` (calibration) or `series` / `series-par` (sample collection).
107
+
108
+ ```js
109
+ import {PerformanceObserver} from 'node:perf_hooks';
110
+
111
+ const obs = new PerformanceObserver(list => {
112
+ for (const e of list.getEntries()) {
113
+ console.log(e.name, e.duration.toFixed(2), 'ms');
114
+ }
115
+ });
116
+ obs.observe({entryTypes: ['measure']});
117
+ ```
118
+
119
+ Marks have a small fixed cost per phase (no per-sample overhead), so leaving
120
+ `--observe` on does not affect measurement accuracy. Default is off.
121
+
122
+ Library users can opt in directly: `findLevel` / `benchmarkSeries` /
123
+ `benchmarkSeriesPar` / `measure` / `measurePar` all accept an `observe` option
124
+ (`boolean | string`) &mdash; `false` / unset for no marks, `true` for the default
125
+ label, or a string for a custom label.
126
+
96
127
  ## AI agents and contributing
97
128
 
98
129
  AI agents and AI-assisted developers: read [AGENTS.md](./AGENTS.md) first for project rules
@@ -111,6 +142,8 @@ BSD 3-Clause License
111
142
 
112
143
  ## Release history
113
144
 
145
+ - 1.0.16: _Added User Timing API integration: `--observe` flag._
146
+ - 1.0.15: _Updated dependencies._
114
147
  - 1.0.14: _Fixed Kruskal-Wallis post-hoc (Conover-Iman) pairwise comparison bug: corrected rank variance computation and critical value distribution. Added regression test._
115
148
  - 1.0.13: _Improved CLI help texts and documentation for brevity and clarity._
116
149
  - 1.0.12: _Added AI coding skills for writing benchmark files (write-bench, write-watch), shipped via npm. Added findLevel() tests. Expanded test suite._
@@ -126,3 +159,5 @@ BSD 3-Clause License
126
159
  - 1.0.2: _Added the `--self` option._
127
160
  - 1.0.1: _Added "self" argument to utilities so it can be used with Deno, Bun, etc._
128
161
  - 1.0.0: _Initial release._
162
+
163
+ The full release notes are in the wiki: [Release notes](https://github.com/uhop/nano-bench/wiki/Release-notes).
package/bin/nano-bench.js CHANGED
@@ -62,6 +62,10 @@ program
62
62
  .option('-s, --samples <samples>', 'number of samples', toInt, 100)
63
63
  .option('-p, --parallel', 'collect samples in parallel')
64
64
  .option('-b, --bootstrap <bootstrap>', 'number of bootstrap samples', toInt, 1000)
65
+ .option(
66
+ '-o, --observe',
67
+ 'emit User Timing marks at phase boundaries (PerformanceObserver/DevTools)'
68
+ )
65
69
  .option('--self', 'print the file name to stdout and exit')
66
70
  .showHelpAfterError('(add --help to see available options)');
67
71
 
@@ -246,7 +250,11 @@ while (iterations.length < names.length) {
246
250
 
247
251
  const batchSize = await findLevel(
248
252
  fn,
249
- {threshold: options.ms, startFrom: options.minIterations},
253
+ {
254
+ threshold: options.ms,
255
+ startFrom: options.minIterations,
256
+ observe: options.observe ? names[index] : undefined
257
+ },
250
258
  async (name, data) => {
251
259
  if (name === 'finding-level-next') {
252
260
  iterations[index] = data.n;
@@ -264,7 +272,10 @@ while (iterations.length < names.length) {
264
272
 
265
273
  for (let i = 0; i < iterations.length; ++i) {
266
274
  const batchSize = iterations[i],
267
- samples = await benchSeries(fns[names[i]], batchSize, {nSeries: options.samples});
275
+ samples = await benchSeries(fns[names[i]], batchSize, {
276
+ nSeries: options.samples,
277
+ observe: options.observe ? names[i] : undefined
278
+ });
268
279
  normalizeSamples(samples, batchSize);
269
280
  results.push(samples);
270
281
  stats.push(getStats(samples));
package/llms-full.txt CHANGED
@@ -38,6 +38,7 @@ nano-bench [options] <file>
38
38
  - `-b, --bootstrap <bootstrap>` — number of bootstrap resamples for CI estimation (default: 1000).
39
39
  - `-a, --alpha <alpha>` — significance level for confidence interval and tests (default: 0.05 = 95% CI).
40
40
  - `-p, --parallel` — collect samples in parallel (useful for async benchmarks).
41
+ - `-o, --observe` — emit User Timing marks at calibration and sampling phase boundaries, observable via `PerformanceObserver` or DevTools / `node --inspect` traces. Mark names follow `nano-bench/<function-name>/<phase>`. Default: off.
41
42
  - `-e, --export <name>` — name of the export to use from the file (default: `"default"`).
42
43
  - `--self` — print the script's file path to stdout and exit (for Deno/Bun usage).
43
44
 
@@ -206,3 +207,58 @@ Used by nano-watch for indefinite monitoring with constant memory:
206
207
 
207
208
  - **StatCounter** — Welford's online algorithm for streaming mean, variance (M2), skewness (M3), and kurtosis (M4). Numerically stable single-pass computation.
208
209
  - **MedianCounter** — approximate streaming median using a hierarchical median-of-three structure. Provides O(1) memory approximate median without storing all values.
210
+
211
+ ---
212
+
213
+ ## User Timing API integration
214
+
215
+ The `nano-bench` CLI accepts `-o` / `--observe`. When set, calibration and sampling phases emit `performance.mark` and `performance.measure` entries to the standard performance timeline. Observers can subscribe via `PerformanceObserver`; the entries are also visible in DevTools / `node --inspect` traces.
216
+
217
+ Mark and measure names follow the convention `nano-bench/<label>/<phase>`:
218
+
219
+ - Start mark: `nano-bench/<label>/<phase>:start`
220
+ - Measure: `nano-bench/<label>/<phase>` (with the start mark as its start)
221
+
222
+ Phases are:
223
+
224
+ - `find-level` — calibration (auto-discovery of batch size).
225
+ - `series` — sequential sample collection.
226
+ - `series-par` — parallel sample collection (when `--parallel` is set).
227
+
228
+ The CLI uses each function's exported name as the label, so multiple benchmarks in one run produce distinct entries (e.g., `nano-bench/strings/find-level`, `nano-bench/backticks/series`, etc.).
229
+
230
+ ### Consumer example
231
+
232
+ ```js
233
+ import {PerformanceObserver} from 'node:perf_hooks';
234
+
235
+ const obs = new PerformanceObserver(list => {
236
+ for (const e of list.getEntries()) {
237
+ console.log(`${e.name}: ${e.duration.toFixed(2)} ms`);
238
+ }
239
+ });
240
+ obs.observe({entryTypes: ['measure']});
241
+ ```
242
+
243
+ ### Library API
244
+
245
+ The orchestrating runner functions accept an `observe` option (`boolean | string`):
246
+
247
+ - `false` / `undefined` — no instrumentation (default).
248
+ - `true` — emit marks with the default label `"default"`.
249
+ - string — emit marks with the given label.
250
+
251
+ The option is supported by `findLevel`, `benchmarkSeries`, `benchmarkSeriesPar`, `measure`, and `measurePar`. `measure` / `measurePar` thread it through to the inner `findLevel` and `benchmarkSeries` / `benchmarkSeriesPar` calls so a single `observe` argument produces both calibration and sample-collection entries.
252
+
253
+ ```js
254
+ import {measure} from 'nano-benchmark/bench/runner.js';
255
+
256
+ const fn = n => { let s = 0; for (let i = 0; i < n; ++i) s += i; };
257
+ const stats = await measure(fn, {nSeries: 50, observe: 'sum-loop'});
258
+ ```
259
+
260
+ ### Cost
261
+
262
+ Marks have a small fixed cost per phase (one `performance.mark` + one `performance.measure` per phase boundary, not per sample). Per-sample timing remains pure `performance.now()` deltas, so observe-mode does not measurably affect benchmark accuracy. The default is off purely to keep the perf timeline buffer empty for users who don't need the integration.
263
+
264
+ The `nano-watch` CLI deliberately does **not** expose `--observe` because its sample loop is unbounded by default; library users who want to instrument continuous monitoring should manage their own buffer (e.g., `performance.clearMarks()` periodically) via the library API.
package/llms.txt CHANGED
@@ -30,7 +30,7 @@ npx nano-bench benchmark.js
30
30
  npx nano-bench -s 200 -b 2000 -a 0.01 benchmark.js
31
31
  ```
32
32
 
33
- Options: `--ms` (measurement time, default 50), `--iterations` (overrides --ms), `--samples` (default 100), `--bootstrap` (default 1000), `--alpha` (significance level, default 0.05), `--parallel`, `--export` (default "default"), `--self`.
33
+ Options: `--ms` (measurement time, default 50), `--iterations` (overrides --ms), `--samples` (default 100), `--bootstrap` (default 1000), `--alpha` (significance level, default 0.05), `--parallel`, `--observe` (emit User Timing marks at phase boundaries), `--export` (default "default"), `--self`.
34
34
 
35
35
  ### nano-watch
36
36
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nano-benchmark",
3
- "version": "1.0.14",
4
- "description": "CLI micro-benchmarking with nonparametric statistics and significance testing.",
3
+ "version": "1.0.16",
4
+ "description": "CLI micro-benchmarking for Node, Deno, and Bun with nonparametric statistics and significance testing.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
7
7
  "exports": {
@@ -23,7 +23,8 @@
23
23
  "test:seq:bun": "bun run `tape6-seq --self` --flags FO",
24
24
  "test:seq:deno": "deno run -A `tape6-seq --self` --flags FO",
25
25
  "lint": "prettier --check .",
26
- "lint:fix": "prettier --write ."
26
+ "lint:fix": "prettier --write .",
27
+ "js-check": "tsc --project tsconfig.check.json"
27
28
  },
28
29
  "repository": {
29
30
  "type": "git",
@@ -33,14 +34,20 @@
33
34
  "benchmark",
34
35
  "micro-benchmark",
35
36
  "performance",
36
- "profiling",
37
37
  "statistics",
38
+ "nonparametric",
38
39
  "significance",
40
+ "confidence-interval",
39
41
  "bootstrap",
40
42
  "mann-whitney",
41
43
  "kruskal-wallis",
42
44
  "cli",
43
- "compare"
45
+ "watch",
46
+ "compare",
47
+ "cross-runtime",
48
+ "nodejs",
49
+ "deno",
50
+ "bun"
44
51
  ],
45
52
  "author": "Eugene Lazutkin <eugene.lazutkin@gmail.com> (https://www.lazutkin.com/)",
46
53
  "license": "BSD-3-Clause",
@@ -60,13 +67,15 @@
60
67
  "llms-full.txt"
61
68
  ],
62
69
  "devDependencies": {
63
- "prettier": "^3.8.1",
64
- "tape-six": "^1.7.12",
65
- "tape-six-proc": "^1.2.7"
70
+ "@types/node": "^25.6.0",
71
+ "prettier": "^3.8.3",
72
+ "tape-six": "^1.9.0",
73
+ "tape-six-proc": "^1.2.9",
74
+ "typescript": "^6.0.3"
66
75
  },
67
76
  "dependencies": {
68
77
  "commander": "^14.0.3",
69
- "console-toolkit": "^1.2.14"
78
+ "console-toolkit": "^1.3.0"
70
79
  },
71
80
  "tape6": {
72
81
  "tests": [
@@ -1,5 +1,21 @@
1
1
  import {performance} from 'node:perf_hooks';
2
2
 
3
+ /**
4
+ * @typedef {boolean | string} Observe
5
+ * false / undefined — no instrumentation; true — emit marks with label "default";
6
+ * string — emit marks with the given label.
7
+ */
8
+
9
+ const makeObserver = (observe, defaultLabel) => {
10
+ if (!observe) return null;
11
+ const label = typeof observe === 'string' ? observe : defaultLabel;
12
+ const prefix = `nano-bench/${label}`;
13
+ return {
14
+ mark: phase => performance.mark(`${prefix}/${phase}:start`),
15
+ measure: phase => performance.measure(`${prefix}/${phase}`, `${prefix}/${phase}:start`)
16
+ };
17
+ };
18
+
3
19
  export const nextLevel = n => {
4
20
  if (n < 1) return 1;
5
21
  let exp = 0;
@@ -20,33 +36,45 @@ export const nextLevel = n => {
20
36
  return n;
21
37
  };
22
38
 
23
- export const findLevel = (fn, {threshold = 20, startFrom = 1, timeout = 5} = {}, report) =>
24
- new Promise((resolve, reject) => {
25
- const bench = async n => {
26
- report && (await report('finding-level', {n}));
27
- try {
28
- const start = performance.now(),
29
- result = fn(n),
30
- finish = performance.now();
31
- if (result && typeof result.then == 'function') {
32
- // thenable
33
- result.then(async () => {
34
- const finish = performance.now();
35
- if (finish - start >= threshold) return resolve(n);
36
- report && (await report('finding-level-next', {n, time: finish - start}));
37
- setTimeout(bench, timeout, nextLevel(n));
38
- }, reject);
39
- return;
39
+ /**
40
+ * @param {{threshold?: number, startFrom?: number, timeout?: number, observe?: Observe}} [opts]
41
+ * @param {Function} [report]
42
+ */
43
+ export const findLevel = async (fn, opts = {}, report) => {
44
+ const {threshold = 20, startFrom = 1, timeout = 5, observe} = opts;
45
+ const obs = makeObserver(observe, 'default');
46
+ obs?.mark('find-level');
47
+ try {
48
+ return await new Promise((resolve, reject) => {
49
+ const bench = async n => {
50
+ report && (await report('finding-level', {n}));
51
+ try {
52
+ const start = performance.now(),
53
+ result = fn(n),
54
+ finish = performance.now();
55
+ if (result && typeof result.then == 'function') {
56
+ // thenable
57
+ result.then(async () => {
58
+ const finish = performance.now();
59
+ if (finish - start >= threshold) return resolve(n);
60
+ report && (await report('finding-level-next', {n, time: finish - start}));
61
+ setTimeout(bench, timeout, nextLevel(n));
62
+ }, reject);
63
+ return;
64
+ }
65
+ if (finish - start >= threshold) return resolve(n);
66
+ report && (await report('finding-level-next', {n, time: finish - start}));
67
+ setTimeout(bench, timeout, nextLevel(n));
68
+ } catch (error) {
69
+ reject(error);
40
70
  }
41
- if (finish - start >= threshold) return resolve(n);
42
- report && (await report('finding-level-next', {n, time: finish - start}));
43
- setTimeout(bench, timeout, nextLevel(n));
44
- } catch (error) {
45
- reject(error);
46
- }
47
- };
48
- bench(startFrom);
49
- });
71
+ };
72
+ bench(startFrom);
73
+ });
74
+ } finally {
75
+ obs?.measure('find-level');
76
+ }
77
+ };
50
78
 
51
79
  export const benchmark = (fn, n) =>
52
80
  new Promise((resolve, reject) => {
@@ -68,42 +96,72 @@ export const benchmark = (fn, n) =>
68
96
  }
69
97
  });
70
98
 
71
- export const benchmarkSeries = async (
72
- fn,
73
- n,
74
- {nSeries = 100, timeout = 5, DataArray = Array} = {}
75
- ) => {
76
- const data = new DataArray(nSeries);
77
-
78
- const bench = async (nSeries, resolve, reject) => {
79
- --nSeries;
80
- try {
81
- data[nSeries] = await benchmark(fn, n);
82
- if (nSeries) {
83
- setTimeout(bench, timeout, nSeries, resolve, reject);
84
- } else {
85
- resolve();
99
+ /**
100
+ * @param {{nSeries?: number, timeout?: number, DataArray?: ArrayConstructor, observe?: Observe}} [opts]
101
+ */
102
+ export const benchmarkSeries = async (fn, n, opts = {}) => {
103
+ const {nSeries = 100, timeout = 5, DataArray = Array, observe} = opts;
104
+ const obs = makeObserver(observe, 'default');
105
+ obs?.mark('series');
106
+ try {
107
+ const data = new DataArray(nSeries);
108
+
109
+ const bench = async (nSeries, resolve, reject) => {
110
+ --nSeries;
111
+ try {
112
+ data[nSeries] = await benchmark(fn, n);
113
+ if (nSeries) {
114
+ setTimeout(bench, timeout, nSeries, resolve, reject);
115
+ } else {
116
+ resolve();
117
+ }
118
+ } catch (error) {
119
+ reject(error);
86
120
  }
87
- } catch (error) {
88
- reject(error);
89
- }
90
- };
121
+ };
91
122
 
92
- await new Promise((resolve, reject) => bench(nSeries, resolve, reject));
123
+ await new Promise((resolve, reject) => bench(nSeries, resolve, reject));
93
124
 
94
- return data;
125
+ return data;
126
+ } finally {
127
+ obs?.measure('series');
128
+ }
95
129
  };
96
130
 
97
- export const benchmarkSeriesPar = async (fn, n, {nSeries = 100, DataArray = Array} = {}) => {
98
- const benchmarks = [];
99
- for (; nSeries > 0; --nSeries) benchmarks.push(benchmark(fn, n));
100
- const results = await Promise.all(benchmarks);
101
- return DataArray === Array ? results : DataArray.from(results);
131
+ /**
132
+ * @param {{nSeries?: number, DataArray?: ArrayConstructor, observe?: Observe}} [opts]
133
+ */
134
+ export const benchmarkSeriesPar = async (fn, n, opts = {}) => {
135
+ let {nSeries = 100} = opts;
136
+ const {DataArray = Array, observe} = opts;
137
+ const obs = makeObserver(observe, 'default');
138
+ obs?.mark('series-par');
139
+ try {
140
+ const benchmarks = [];
141
+ for (; nSeries > 0; --nSeries) benchmarks.push(benchmark(fn, n));
142
+ const results = await Promise.all(benchmarks);
143
+ return DataArray === Array ? results : DataArray.from(results);
144
+ } finally {
145
+ obs?.measure('series-par');
146
+ }
102
147
  };
103
148
 
149
+ /**
150
+ * @typedef {object} StatsInit
151
+ * @property {number[]} data
152
+ * @property {number} reps
153
+ * @property {number} [time]
154
+ * @property {boolean} [sorted]
155
+ */
156
+
104
157
  export class Stats {
158
+ /** @param {StatsInit} object */
105
159
  constructor(object) {
106
- Object.assign(this, object);
160
+ /** @type {number[]} */
161
+ this.data = object.data;
162
+ this.reps = object.reps;
163
+ this.time = object.time;
164
+ this.sorted = object.sorted ?? false;
107
165
  }
108
166
 
109
167
  static sortNumbersAsc = (a, b) => a - b;
@@ -134,34 +192,52 @@ export class Stats {
134
192
  }
135
193
  }
136
194
 
137
- export const measure = async (
138
- fn,
139
- {nSeries = 100, threshold = 20, startFrom = 1, timeout = 5, DataArray = Array} = {},
140
- report
141
- ) => {
195
+ /**
196
+ * @param {{nSeries?: number, threshold?: number, startFrom?: number, timeout?: number, DataArray?: ArrayConstructor, observe?: Observe}} [opts]
197
+ * @param {Function} [report]
198
+ */
199
+ export const measure = async (fn, opts = {}, report) => {
200
+ const {
201
+ nSeries = 100,
202
+ threshold = 20,
203
+ startFrom = 1,
204
+ timeout = 5,
205
+ DataArray = Array,
206
+ observe
207
+ } = opts;
142
208
  report?.('finding-reps');
143
- const reps = startFrom < 0 ? -startFrom : await findLevel(fn, {threshold, startFrom, timeout});
209
+ const reps =
210
+ startFrom < 0 ? -startFrom : await findLevel(fn, {threshold, startFrom, timeout, observe});
144
211
  report?.('found-reps', {reps});
145
212
  report?.('starting-benchmarks', {nSeries, reps});
146
213
  const start = performance.now(),
147
- data = await benchmarkSeries(fn, reps, {nSeries, timeout, DataArray}),
214
+ data = await benchmarkSeries(fn, reps, {nSeries, timeout, DataArray, observe}),
148
215
  finish = performance.now(),
149
216
  result = {data, reps, time: finish - start};
150
217
  report?.('finished-benchmarks', {...result, nSeries});
151
218
  return new Stats(result);
152
219
  };
153
220
 
154
- export const measurePar = async (
155
- fn,
156
- {nSeries = 100, threshold = 20, startFrom = 1, timeout = 5, DataArray = Array} = {},
157
- report
158
- ) => {
221
+ /**
222
+ * @param {{nSeries?: number, threshold?: number, startFrom?: number, timeout?: number, DataArray?: ArrayConstructor, observe?: Observe}} [opts]
223
+ * @param {Function} [report]
224
+ */
225
+ export const measurePar = async (fn, opts = {}, report) => {
226
+ const {
227
+ nSeries = 100,
228
+ threshold = 20,
229
+ startFrom = 1,
230
+ timeout = 5,
231
+ DataArray = Array,
232
+ observe
233
+ } = opts;
159
234
  report?.('finding-reps');
160
- const reps = startFrom < 0 ? -startFrom : await findLevel(fn, {threshold, startFrom, timeout});
235
+ const reps =
236
+ startFrom < 0 ? -startFrom : await findLevel(fn, {threshold, startFrom, timeout, observe});
161
237
  report?.('found-reps', {reps});
162
238
  report?.('starting-benchmarks', {nSeries, reps});
163
239
  const start = performance.now(),
164
- data = await benchmarkSeriesPar(fn, reps, {nSeries, DataArray}),
240
+ data = await benchmarkSeriesPar(fn, reps, {nSeries, DataArray, observe}),
165
241
  finish = performance.now(),
166
242
  result = {data, reps, time: finish - start};
167
243
  report?.('finished-benchmarks', {...result, nSeries});