Commit e3499df9b41 for nodejs
commit e3499df9b4115f52d96cf510e3b4249825e00041
Author: James M Snell <jasnell@gmail.com>
Date: Sun Oct 11 17:07:02 2026 +0000
lib: default bench operations to 1
b.end(N) ... N defaults to 1
b.report(...) operations defaults to 1
Signed-off-by: James M Snell <jasnell@gmail.com>
Assisted-by: Opencode
PR-URL: https://github.com/nodejs/node/pull/66664
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
diff --git a/doc/api/bench.md b/doc/api/bench.md
index 5f23df07f01..b457cff50c4 100644
--- a/doc/api/bench.md
+++ b/doc/api/bench.md
@@ -71,7 +71,7 @@ to their caller.
Each warmup and measured sample invokes the benchmark function once with a
fresh {BenchContext}. The function must either call `context.start()` and
-`context.end(operations)` exactly once, or call `context.record(sample)` exactly
+`context.end([operations])` exactly once, or call `context.record(sample)` exactly
once to provide an externally measured sample. Setup before `start()` and
cleanup after `end()` are outside the measured region. Promise-returning
functions are awaited.
@@ -665,14 +665,14 @@ added: v26.9.0
Starts the measured region using `process.hrtime.bigint()`. Calling `start()`
more than once is an error.
-### `context.end(operations[, options])`
+### `context.end([operations[, options]])`
<!-- YAML
added: v26.9.0
-->
* `operations` {number} The number of completed operations. Must be a positive
- safe integer.
+ safe integer. **Default:** `1`.
* `options` {Object}
* `detail` {any} Additional structured-cloneable sample data. With CLI process
isolation, it must also be supported by advanced child process
@@ -694,7 +694,7 @@ added: v26.9.0
* `sample` {Object}
* `operations` {number} The number of completed operations. Must be a positive
- safe integer.
+ safe integer. **Default:** `1`.
* `duration_ns` {bigint} An externally measured positive duration in
nanoseconds no greater than `Number.MAX_SAFE_INTEGER`.
* `detail` {any} Additional structured-cloneable sample data. With CLI process
diff --git a/lib/internal/bench_runner/benchmark.js b/lib/internal/bench_runner/benchmark.js
index cea0abc71e5..ea47696489a 100644
--- a/lib/internal/bench_runner/benchmark.js
+++ b/lib/internal/bench_runner/benchmark.js
@@ -349,7 +349,7 @@ class BenchContext {
this.#startTime = hrtime();
}
- end(operations, options = kEmptyObject) {
+ end(operations = 1, options = kEmptyObject) {
const endTime = hrtime();
if (this.#closed) {
this.#invalid = true;
@@ -415,7 +415,7 @@ class BenchContext {
try {
validateObject(sample, 'sample');
- const { detail, duration_ns, operations } = sample;
+ const { detail, duration_ns, operations = 1 } = sample;
validateInteger(
operations, 'sample.operations', 1, NumberMAX_SAFE_INTEGER);
if (typeof duration_ns !== 'bigint') {
diff --git a/test/parallel/test-bench-end-operations.js b/test/parallel/test-bench-end-operations.js
new file mode 100644
index 00000000000..83a51512e1b
--- /dev/null
+++ b/test/parallel/test-bench-end-operations.js
@@ -0,0 +1,91 @@
+// Flags: --experimental-bench --no-warnings
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const { createRunner } = require('node:bench');
+
+function waitForTick() {
+ const started = process.hrtime.bigint();
+ while (process.hrtime.bigint() === started);
+}
+
+const runner = createRunner({ yieldBetweenSamples: false });
+
+runner.bench('default operations', { samples: 1, warmup: 0 },
+ common.mustCall((b) => {
+ b.start();
+ waitForTick();
+ const sample = b.end();
+ assert.strictEqual(sample.operations, 1);
+ }));
+
+runner.bench('undefined operations with options', { samples: 1, warmup: 0 },
+ common.mustCall((b) => {
+ b.start();
+ waitForTick();
+ const sample = b.end(undefined, { detail: 'value' });
+ assert.strictEqual(sample.operations, 1);
+ assert.strictEqual(sample.detail, 'value');
+ }));
+
+const invalid = [0, -1, 0.5, NaN, Infinity, null, '1'];
+for (const operations of invalid) {
+ runner.bench(`invalid ${String(operations)}`, { samples: 1, warmup: 0 },
+ (b) => {
+ b.start();
+ waitForTick();
+ b.end(operations);
+ });
+}
+
+runner.bench('record default operations', { samples: 1, warmup: 0 },
+ common.mustCall((b) => {
+ const sample = b.record({ duration_ns: 10n });
+ assert.strictEqual(sample.operations, 1);
+ assert.strictEqual(sample.rate, 100_000_000);
+ }));
+
+runner.bench('record undefined operations', { samples: 1, warmup: 0 },
+ common.mustCall((b) => {
+ const sample = b.record({ duration_ns: 10n, operations: undefined });
+ assert.strictEqual(sample.operations, 1);
+ }));
+
+for (const operations of invalid) {
+ runner.bench(`record invalid ${String(operations)}`,
+ { samples: 1, warmup: 0 },
+ (b) => b.record({ duration_ns: 10n, operations }));
+}
+
+(async () => {
+ const records = await runner.run().toArray();
+ const byName = new Map(records
+ .filter(({ type }) => type === 'bench:complete')
+ .map(({ data }) => [data.name, data]));
+
+ assert.deepStrictEqual(
+ byName.get('default operations').samples.map(({ operations }) => operations),
+ [1]);
+ assert.deepStrictEqual(
+ byName.get('undefined operations with options').samples
+ .map(({ operations }) => operations),
+ [1]);
+
+ for (const name of ['record default operations',
+ 'record undefined operations']) {
+ assert.deepStrictEqual(
+ byName.get(name).samples.map(({ operations }) => operations), [1]);
+ }
+
+ for (const operations of invalid) {
+ const code = typeof operations === 'number' ?
+ 'ERR_OUT_OF_RANGE' : 'ERR_INVALID_ARG_TYPE';
+ for (const prefix of ['invalid', 'record invalid']) {
+ const { error, samples } =
+ byName.get(`${prefix} ${String(operations)}`);
+ assert.strictEqual(error.code, code);
+ assert.strictEqual(samples.length, 0);
+ }
+ }
+})().then(common.mustCall());