Commit 299f76cb734 for nodejs
commit 299f76cb734281cfd12da71ae745875e45e83e33
Author: RafaelGSS <rafael.nunu@hotmail.com>
Date: Mon Oct 5 10:26:02 2026 -0300
lib: default node:bench warmup to 10 invocations
Signed-off-by: RafaelGSS <rafael.nunu@hotmail.com>
PR-URL: https://github.com/nodejs/node/pull/66536
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
Reviewed-By: Bryan English <bryan@bryanenglish.com>
diff --git a/doc/api/bench.md b/doc/api/bench.md
index e06622ddf36..c04ae971523 100644
--- a/doc/api/bench.md
+++ b/doc/api/bench.md
@@ -74,6 +74,12 @@ once to provide an externally measured sample. Setup before `start()` and
cleanup after `end()` are outside the measured region. Promise-returning
functions are awaited.
+By default, ten unreported warmup invocations run before the measured samples
+so that early samples are less likely to be taken before the measured code has
+been optimized. A fixed warmup count does not guarantee a stable optimization
+state; inspect raw `samples` and increase `warmup` when early samples are
+consistently slower. Set `warmup` to `0` to measure from the first invocation.
+
By default, an event loop turn occurs between sample invocations. An embedded
runner can disable this using `yieldBetweenSamples`. The runner executes
benchmarks serially, but it does not provide process isolation. Other work in
@@ -336,7 +342,7 @@ added: v26.9.0
* `timeout` {number} The number of milliseconds after which the benchmark
fails. **Default:** `Infinity`.
* `warmup` {number} The number of unreported callback invocations before
- measured samples. Must be a 32-bit unsigned integer. **Default:** `0`.
+ measured samples. Must be a 32-bit unsigned integer. **Default:** `10`.
* `fn` {Function|AsyncFunction} The benchmark function. It receives a
{BenchContext}.
* Returns: {Promise} Fulfilled with the benchmark result after a top-level
diff --git a/doc/api/cli.md b/doc/api/cli.md
index 172db4ee302..3822685d3d5 100644
--- a/doc/api/cli.md
+++ b/doc/api/cli.md
@@ -630,6 +630,8 @@ added: v26.9.0
Overrides the number of unreported warmup callback invocations for every
selected benchmark. `count` must be an integer between `0` and `4294967295`.
+Without this option, each benchmark uses its own `warmup` value, which defaults
+to `10`.
### `--build-sea=config`
diff --git a/doc/node.1 b/doc/node.1
index a9ef1243fbb..863c468eed1 100644
--- a/doc/node.1
+++ b/doc/node.1
@@ -384,6 +384,8 @@ selected benchmark. A benchmark may finish earlier by calling
.It Fl -bench-warmup Ns = Ns Ar count
Overrides the number of unreported warmup callback invocations for every
selected benchmark. \fBcount\fR must be an integer between \fB0\fR and \fB4294967295\fR.
+Without this option, each benchmark uses its own \fBwarmup\fR value, which defaults
+to \fB10\fR.
.
.It Fl -build-sea Ns = Ns Ar config
Generates a single executable application from a JSON
diff --git a/lib/internal/bench_runner/benchmark.js b/lib/internal/bench_runner/benchmark.js
index ddcf54169a0..0ab3da9f24d 100644
--- a/lib/internal/bench_runner/benchmark.js
+++ b/lib/internal/bench_runner/benchmark.js
@@ -47,7 +47,7 @@ const { structuredClone } = require('internal/worker/js_transferable');
const { bigint: hrtime } = process.hrtime;
const kDefaultSamples = 30;
-const kDefaultWarmup = 0;
+const kDefaultWarmup = 10;
const kEmptyDiagnosticChannels = ObjectFreeze([]);
const kEmptyNamePath = ObjectFreeze([]);
const kEmptyParams = ObjectFreeze({ __proto__: null });
diff --git a/test/fixtures/bench-runner/diagnostic.cjs b/test/fixtures/bench-runner/diagnostic.cjs
index 9e413e943dd..2cb654aa423 100644
--- a/test/fixtures/bench-runner/diagnostic.cjs
+++ b/test/fixtures/bench-runner/diagnostic.cjs
@@ -9,6 +9,7 @@ const diagnosticChannel = channel(channelName);
bench('diagnostic relay', {
diagnosticChannels: [channelName],
samples: 1,
+ warmup: 0,
}, (b) => {
const message = { value: 42n };
diagnosticChannel.publish(message);
diff --git a/test/fixtures/bench-runner/many-records.cjs b/test/fixtures/bench-runner/many-records.cjs
index aacb1782103..c0923a70ea8 100644
--- a/test/fixtures/bench-runner/many-records.cjs
+++ b/test/fixtures/bench-runner/many-records.cjs
@@ -2,7 +2,7 @@
const { bench } = require('node:bench');
-bench('many records', { samples: 30 }, (b) => {
+bench('many records', { samples: 30, warmup: 0 }, (b) => {
process.stdout.write(`${b.index}\n`);
b.record({ duration_ns: 1n, operations: 1 });
});
diff --git a/test/fixtures/bench-runner/recorded-detail.cjs b/test/fixtures/bench-runner/recorded-detail.cjs
index 1643783225d..0ad6f66ed9c 100644
--- a/test/fixtures/bench-runner/recorded-detail.cjs
+++ b/test/fixtures/bench-runner/recorded-detail.cjs
@@ -2,7 +2,7 @@
const { bench } = require('node:bench');
-bench('recorded detail', { samples: 3 }, (b) => {
+bench('recorded detail', { samples: 3, warmup: 0 }, (b) => {
b.record({
__proto__: null,
detail: {
diff --git a/test/fixtures/bench-runner/serial.js b/test/fixtures/bench-runner/serial.js
index 4e6f44c3b67..b177080425c 100644
--- a/test/fixtures/bench-runner/serial.js
+++ b/test/fixtures/bench-runner/serial.js
@@ -9,7 +9,7 @@ module.exports = function register(name) {
fs.writeFileSync(lock, `${process.pid}`, { flag: 'wx' });
process.on('exit', () => fs.unlinkSync(lock));
- bench(name, { samples: 1 }, async (b) => {
+ bench(name, { samples: 1, warmup: 0 }, async (b) => {
b.start();
await setTimeout(25);
b.end(1);
diff --git a/test/fixtures/bench-runner/utf8-output.cjs b/test/fixtures/bench-runner/utf8-output.cjs
index 7dab41cd7c2..6937432a4a8 100644
--- a/test/fixtures/bench-runner/utf8-output.cjs
+++ b/test/fixtures/bench-runner/utf8-output.cjs
@@ -6,7 +6,7 @@ const { bench } = require('node:bench');
const output = Buffer.from('split:\u20ac\n');
process.stdout.write(output.subarray(0, 7));
-bench('UTF-8 output', { samples: 1 }, async (b) => {
+bench('UTF-8 output', { samples: 1, warmup: 0 }, async (b) => {
b.start();
await setTimeout(20);
process.stdout.write(output.subarray(7));
diff --git a/test/parallel/test-bench-auto-run.js b/test/parallel/test-bench-auto-run.js
index 82e91f97b4a..f8a500312bc 100644
--- a/test/parallel/test-bench-auto-run.js
+++ b/test/parallel/test-bench-auto-run.js
@@ -14,7 +14,9 @@ const child = spawnSync(process.execPath, [
]);
assert.strictEqual(child.status, 1);
-const completion = bench('automatic execution', common.mustCall((b) => {
+const completion = bench('automatic execution', {
+ warmup: 0,
+}, common.mustCall((b) => {
b.record({ duration_ns: 1n, operations: 1 });
}, 30));
diff --git a/test/parallel/test-bench-context-control.js b/test/parallel/test-bench-context-control.js
index 56c170211ac..1657e0c169c 100644
--- a/test/parallel/test-bench-context-control.js
+++ b/test/parallel/test-bench-context-control.js
@@ -36,7 +36,7 @@ const { createRunner } = require('node:bench');
}, 4));
const recordedCompletion = runner.bench(
- 'recorded', { samples: 3 }, common.mustCall((b) => {
+ 'recorded', { samples: 3, warmup: 0 }, common.mustCall((b) => {
const detail = { source: 'worker', value: 1n };
const sample = b.record({
__proto__: null,
@@ -64,6 +64,7 @@ const { createRunner } = require('node:bench');
];
const variableCompletion = runner.bench('variable batch', {
samples: variableSamples.length,
+ warmup: 0,
}, common.mustCall((b) => {
b.record(variableSamples[b.index]);
}, variableSamples.length));
@@ -116,6 +117,7 @@ const { createRunner } = require('node:bench');
[slowSample, slowSample, slowSample, fastSample, fastSample];
const slowCompletion = slowRunner.bench('sub-resolution rates', {
samples: slowSamples.length,
+ warmup: 0,
}, common.mustCall((b) => {
b.record(slowSamples[b.index]);
}, slowSamples.length));
diff --git a/test/parallel/test-bench-create-runner.js b/test/parallel/test-bench-create-runner.js
index d12bc5db4b6..c5cef7b0912 100644
--- a/test/parallel/test-bench-create-runner.js
+++ b/test/parallel/test-bench-create-runner.js
@@ -17,12 +17,12 @@ const { setImmediate } = require('timers/promises');
second.before(common.mustCall());
const firstCompletion = first.bench(
- 'same name', { samples: 2 }, common.mustCall((b) => {
+ 'same name', { samples: 2, warmup: 0 }, common.mustCall((b) => {
firstCalls++;
completeSample(b);
}, 2));
const secondCompletion = second.bench(
- 'same name', { samples: 1 }, common.mustCall((b) => {
+ 'same name', { samples: 1, warmup: 0 }, common.mustCall((b) => {
secondCalls++;
completeSample(b);
}));
diff --git a/test/parallel/test-bench-default-warmup.js b/test/parallel/test-bench-default-warmup.js
new file mode 100644
index 00000000000..5ba531bd5a8
--- /dev/null
+++ b/test/parallel/test-bench-default-warmup.js
@@ -0,0 +1,35 @@
+// Flags: --experimental-bench --no-warnings
+'use strict';
+
+const common = require('../common');
+const { completeSample } = require('../common/bench');
+const assert = require('assert');
+const { createRunner } = require('node:bench');
+
+// Benchmarks that do not set `warmup` run ten unreported invocations before
+// the measured samples.
+(async () => {
+ const runner = createRunner({ yieldBetweenSamples: false });
+ const phases = [];
+
+ const completion = runner.bench('default warmup', {
+ samples: 2,
+ }, common.mustCall((b) => {
+ phases.push(b.phase);
+ completeSample(b);
+ }, 12));
+
+ const records = await runner.run().toArray();
+ const result = await completion;
+
+ const plan = records.find(({ type }) => type === 'bench:plan').data;
+ assert.strictEqual(plan.warmup, 10);
+ assert.deepStrictEqual(phases, [
+ ...Array(10).fill('warmup'),
+ 'measurement',
+ 'measurement',
+ ]);
+ assert.strictEqual(result.samples.length, 2);
+ assert.strictEqual(
+ records.filter(({ type }) => type === 'bench:sample').length, 2);
+})().then(common.mustCall());
diff --git a/test/parallel/test-bench-diagnostic-channels.js b/test/parallel/test-bench-diagnostic-channels.js
index 1c6d3f60135..f8afcd753b1 100644
--- a/test/parallel/test-bench-diagnostic-channels.js
+++ b/test/parallel/test-bench-diagnostic-channels.js
@@ -33,6 +33,7 @@ async function testCapture() {
runner.bench('captured', {
diagnosticChannels: [benchmarkName, nestedName],
samples: 1,
+ warmup: 0,
}, common.mustCall((context) => {
const message = { value: 1 };
inheritedChannel.publish(message);
@@ -45,7 +46,10 @@ async function testCapture() {
}));
}));
}));
- runner.bench('not captured', { samples: 1 }, common.mustCall((context) => {
+ runner.bench('not captured', {
+ samples: 1,
+ warmup: 0,
+ }, common.mustCall((context) => {
inheritedChannel.publish({ value: 6 });
recordSample(context);
}));
@@ -79,6 +83,7 @@ async function testUncloneableMessage() {
runner.bench('uncloneable', {
diagnosticChannels: [name],
samples: 1,
+ warmup: 0,
}, common.mustCall((context) => {
channel.publish(() => {});
recordSample(context);
@@ -99,6 +104,7 @@ async function testAbortCleanup() {
diagnosticChannels: [name],
samples: 1,
signal: controller.signal,
+ warmup: 0,
}, common.mustCall((context) => {
controller.abort(new Error('stop'));
assert.strictEqual(channel.hasSubscribers, false);
diff --git a/test/parallel/test-bench-diagnostics.js b/test/parallel/test-bench-diagnostics.js
index b5383f2bdac..1ba8cdcf01f 100644
--- a/test/parallel/test-bench-diagnostics.js
+++ b/test/parallel/test-bench-diagnostics.js
@@ -19,6 +19,7 @@ async function testListenerFailure(callbackError) {
const listenerError = new Error('diagnostic listener failed');
const completion = runner.bench('listener failure', {
samples: 1,
+ warmup: 0,
}, common.mustCall((b) => {
b.diagnostic('listener failure');
if (callbackError !== undefined) throw callbackError;
@@ -40,6 +41,7 @@ async function testAbortDuringDiagnosticDelivery() {
const controller = new AbortController();
const completion = runner.bench('abort diagnostics', {
samples: 1,
+ warmup: 0,
}, common.mustCall((b) => {
for (let i = 0; i < 32; i++) b.diagnostic(`diagnostic ${i}`);
recordSample(b);
@@ -82,6 +84,7 @@ async function testAfterEachFailurePrecedence() {
}));
const completion = runner.bench('afterEach precedence', {
samples: 1,
+ warmup: 0,
}, common.mustCall((b) => {
b.diagnostic('before failures');
throw callbackError;
@@ -112,6 +115,7 @@ async function testAfterEachFailurePrecedence() {
const expectedError = new Error('benchmark failed');
const failed = runner.bench('failed diagnostic', {
samples: 1,
+ warmup: 0,
}, common.mustCall((b) => {
b.diagnostic('before failure', {
detail: { retained: true },
diff --git a/test/parallel/test-bench-errors.js b/test/parallel/test-bench-errors.js
index 13efa51bf92..5363efd6b14 100644
--- a/test/parallel/test-bench-errors.js
+++ b/test/parallel/test-bench-errors.js
@@ -67,7 +67,7 @@ suiteCompletion.then(common.mustCall());
const completions = [];
const sampleNames = [];
let summary;
-const stream = run();
+const stream = run({ warmup: 0 });
stream.on('bench:complete', (result) => completions.push(result));
stream.on('bench:sample', (sample) => sampleNames.push(sample.name));
stream.on('bench:summary', (result) => { summary = result; });
diff --git a/test/parallel/test-bench-filtering.js b/test/parallel/test-bench-filtering.js
index b9a3124d8b7..49954cfa6fb 100644
--- a/test/parallel/test-bench-filtering.js
+++ b/test/parallel/test-bench-filtering.js
@@ -16,7 +16,7 @@ function complete(name) {
}
suite('selected', { only: true }, () => {
- bench('included', { samples: 1 }, complete('included'));
+ bench('included', { samples: 1, warmup: 0 }, complete('included'));
bench.skip('explicitly skipped', { samples: 1 },
common.mustNotCall());
bench('pattern filtered', { samples: 1 },
diff --git a/test/parallel/test-bench-run.js b/test/parallel/test-bench-run.js
index 0107150262d..abf31125500 100644
--- a/test/parallel/test-bench-run.js
+++ b/test/parallel/test-bench-run.js
@@ -47,7 +47,7 @@ const suiteCompletion = suite('group', { tags: ['Group'] }, async () => {
active = false;
}, 3));
- bench('async', { samples: 2 }, common.mustCall(async (b) => {
+ bench('async', { samples: 2, warmup: 0 }, common.mustCall(async (b) => {
assert.strictEqual(active, false);
active = true;
contexts.add(b);
diff --git a/test/parallel/test-bench-stream.js b/test/parallel/test-bench-stream.js
index dbb68f8be97..59d544400dd 100644
--- a/test/parallel/test-bench-stream.js
+++ b/test/parallel/test-bench-stream.js
@@ -25,7 +25,7 @@ async function testReadableBackpressure() {
calls++;
recordSample(b);
});
- const stream = runner.run();
+ const stream = runner.run({ warmup: 0 });
const iterator = stream[Symbol.asyncIterator]();
const first = await iterator.next();
@@ -59,7 +59,7 @@ async function testPlanBackpressure() {
recordSample(b);
}));
}
- const stream = runner.run();
+ const stream = runner.run({ warmup: 0 });
const iterator = stream[Symbol.asyncIterator]();
const first = await iterator.next();
@@ -90,7 +90,7 @@ async function testDestroyWhileBlocked() {
samples: 1,
}, recordSample));
}
- const stream = runner.run();
+ const stream = runner.run({ warmup: 0 });
const unblocked = stream.waitForDrain();
const iterator = stream[Symbol.asyncIterator]();
await iterator.next();
@@ -120,7 +120,7 @@ async function testNamedEventsWithoutReading() {
calls++;
recordSample(b);
});
- const stream = runner.run();
+ const stream = runner.run({ warmup: 0 });
const summary = await new Promise((resolve) => {
stream.once('bench:summary', resolve);
});
@@ -140,7 +140,7 @@ async function testCancellationCompletesBenchmarks() {
const second = runner.bench('continues headlessly', {
samples: 1,
}, recordSample);
- const stream = runner.run();
+ const stream = runner.run({ warmup: 0 });
const iterator = stream[Symbol.asyncIterator]();
await iterator.next();
@@ -162,7 +162,7 @@ async function testDeliveryDoesNotConsumeTimeout() {
samples: 32,
timeout,
}, recordSample);
- const stream = runner.run();
+ const stream = runner.run({ warmup: 0 });
const iterator = stream[Symbol.asyncIterator]();
await iterator.next();
@@ -182,7 +182,7 @@ async function testReportingFailureSettlesBenchmarks() {
const failure = new Error('record listener failed');
const first = runner.bench('reported', { samples: 1 }, recordSample);
const second = runner.bench('settled', { samples: 1 }, recordSample);
- const stream = runner.run();
+ const stream = runner.run({ warmup: 0 });
stream.once('bench:complete', common.mustCall(() => {
throw failure;
}));
@@ -200,7 +200,7 @@ async function testSummaryListenerFailure() {
const completion = runner.bench('summary failure', {
samples: 1,
}, recordSample);
- const stream = runner.run();
+ const stream = runner.run({ warmup: 0 });
const diagnostics = [];
stream.on('bench:diagnostic', (diagnostic) => {
diagnostics.push(diagnostic);
@@ -285,7 +285,7 @@ async function testRecordOwnership() {
const afterTrap = runner.bench('after trapping error', {
samples: 1,
}, recordSample);
- const stream = runner.run();
+ const stream = runner.run({ warmup: 0 });
let eventSample;
let eventComplete;
let eventError;
@@ -386,7 +386,7 @@ function testOperatorsWithoutStreamModule() {
runner.bench('operators', { samples: 1 }, (b) => {
b.record({ operations: 1, duration_ns: 1n });
});
- runner.run()
+ runner.run({ warmup: 0 })
.map((record) => record.type)
.toArray()
.then((types) => console.log(types.includes('bench:complete')));
diff --git a/test/parallel/test-bench-validation.js b/test/parallel/test-bench-validation.js
index 2b508f18666..e95a7ddf646 100644
--- a/test/parallel/test-bench-validation.js
+++ b/test/parallel/test-bench-validation.js
@@ -69,7 +69,7 @@ bench('valid', { samples: 1 }, (b) => {
completeSample(b);
});
-const stream = run();
+const stream = run({ warmup: 0 });
stream.on('bench:start', common.mustCall(() => {
assert.throws(() => bench('late', noop), { code: 'ERR_INVALID_STATE' });
}, 3));
diff --git a/test/parallel/test-bench-yield-between-samples.js b/test/parallel/test-bench-yield-between-samples.js
index 92b51e9409a..9a3409cb19b 100644
--- a/test/parallel/test-bench-yield-between-samples.js
+++ b/test/parallel/test-bench-yield-between-samples.js
@@ -15,7 +15,7 @@ async function observe(factoryOptions, runOptions) {
turnOccurred = true;
});
- runner.bench('yielding', { samples: 2 }, (b) => {
+ runner.bench('yielding', { samples: 2, warmup: 0 }, (b) => {
observed.push(turnOccurred);
completeSample(b);
});