Commit 7a388150760 for nodejs
commit 7a388150760c88b9d97a8b9e95860e86286980a3
Author: James M Snell <jasnell@gmail.com>
Date: Sun Oct 11 16:54:28 2026 +0000
lib: update node:bench reporters to include sys info
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 86644916553..5f23df07f01 100644
--- a/doc/api/bench.md
+++ b/doc/api/bench.md
@@ -260,13 +260,36 @@ bench('example', (b) => {
run().compose(spec).pipe(process.stdout);
```
-The `spec` reporter buffers results and outputs a concise table containing the
-sample count, mean rate, 95% confidence interval for the mean, median rate, and
-warnings. A coefficient of variation above 5% is reported as `noisy`, and an
-absolute skewness above 1 is reported as `skewed`. The exact human-readable
-format is subject to change.
-
-The `json` reporter emits every lifecycle record as newline-delimited JSON.
+The built-in reporters collect a snapshot of the reporting process's
+environment when they receive their first record. The snapshot contains:
+
+* `node` {string} The value of `process.version`.
+* `v8` {string} The value of `process.versions.v8`.
+* `platform` {string} The value of `process.platform`.
+* `arch` {string} The value of `process.arch`.
+* `os` {Object} The operating system `type`, `release`, and `machine`, as
+ returned by the corresponding [`os`][] functions.
+* `cpus` {Object\[]} The logical CPUs grouped by `model`, with the `count` of
+ each model.
+* `availableParallelism` {number} The value of `os.availableParallelism()`.
+* `memory` {Object} The `total` and `free` system memory in bytes, and the
+ `constrained` memory limit in bytes, or `null` when the process is not
+ constrained below the total system memory.
+* `loadavg` {number\[]} The value of `os.loadavg()`.
+* `execArgv` {string\[]} The value of `process.execArgv`.
+
+With process isolation, reporters run in the parent process. Benchmark children
+are started from the same executable, but their `execArgv` can differ.
+
+The `spec` reporter buffers results and outputs a summary of the environment
+followed by a concise table containing the sample count, mean rate, 95%
+confidence interval for the mean, median rate, and warnings. A coefficient of
+variation above 5% is reported as `noisy`, and an absolute skewness above 1 is
+reported as `skewed`. The exact human-readable format is subject to change.
+
+The `json` reporter emits a `'bench:environment'` record containing the
+environment snapshot before the first lifecycle record, and then emits every
+lifecycle record as newline-delimited JSON.
BigInt values, including `duration_ns`, are encoded as decimal strings. Errors
are represented using their `name`, `message`, `stack`, `code`, `cause`, and
`errors` properties. As required by JSON, non-finite numbers are encoded as
@@ -798,7 +821,8 @@ tags, and parameters described in [benchmark result][], together with:
The plan contains execution settings known to the runner. Runtime version,
operating system, processor, and other environment metadata are intentionally
-left for reporters and higher-level tools to collect.
+left for reporters and higher-level tools to collect. The built-in reporters
+include an [environment snapshot][benchmark reporters].
`'bench:complete'` data contains a [benchmark result][]. A failed result has an
additional `error` property and may contain samples recorded before the error.
@@ -856,6 +880,8 @@ A completed benchmark result contains:
[`context.record()`]: #contextrecordsample
[`createRunner()`]: #createrunneroptions
+[`os`]: os.md
[`run()`]: #runoptions
+[benchmark reporters]: #benchmark-reporters
[benchmark result]: #benchmark-result
[command-line options documentation]: cli.md#--bench
diff --git a/lib/internal/bench_runner/environment.js b/lib/internal/bench_runner/environment.js
new file mode 100644
index 00000000000..17e349c342a
--- /dev/null
+++ b/lib/internal/bench_runner/environment.js
@@ -0,0 +1,61 @@
+'use strict';
+
+const {
+ ArrayPrototypePush,
+ ArrayPrototypeSlice,
+} = primordials;
+
+function groupCpus(cpus) {
+ const groups = [];
+ for (let i = 0; i < cpus.length; i++) {
+ // The reported speed is the current core frequency, which fluctuates
+ // with frequency scaling, so only the model is used for grouping.
+ const { model } = cpus[i];
+ let group;
+ for (let j = 0; j < groups.length; j++) {
+ if (groups[j].model === model) {
+ group = groups[j];
+ break;
+ }
+ }
+ if (group === undefined) {
+ group = { __proto__: null, model, count: 0 };
+ ArrayPrototypePush(groups, group);
+ }
+ group.count++;
+ }
+ return groups;
+}
+
+function collectEnvironment() {
+ const os = require('os');
+ const total = os.totalmem();
+ const constrained = process.constrainedMemory();
+ return {
+ __proto__: null,
+ node: process.version,
+ v8: process.versions.v8,
+ platform: process.platform,
+ arch: process.arch,
+ os: {
+ __proto__: null,
+ type: os.type(),
+ release: os.release(),
+ machine: os.machine(),
+ },
+ cpus: groupCpus(os.cpus()),
+ availableParallelism: os.availableParallelism(),
+ memory: {
+ __proto__: null,
+ total,
+ free: os.freemem(),
+ constrained: constrained > 0 && constrained < total ? constrained : null,
+ },
+ loadavg: os.loadavg(),
+ execArgv: ArrayPrototypeSlice(process.execArgv),
+ };
+}
+
+module.exports = {
+ collectEnvironment,
+};
diff --git a/lib/internal/bench_runner/reporter/json.js b/lib/internal/bench_runner/reporter/json.js
index 3237a3e1b26..4a46b83c5f8 100644
--- a/lib/internal/bench_runner/reporter/json.js
+++ b/lib/internal/bench_runner/reporter/json.js
@@ -9,6 +9,9 @@ const {
String,
} = primordials;
const { isError } = require('internal/util');
+const {
+ collectEnvironment,
+} = require('internal/bench_runner/environment');
function toJSONValue(value, seen) {
if (typeof value === 'bigint') return String(value);
@@ -56,7 +59,16 @@ function stringify(record) {
}
async function* jsonReporter(source) {
+ let environmentReported = false;
for await (const record of source) {
+ if (!environmentReported) {
+ environmentReported = true;
+ yield `${stringify({
+ __proto__: null,
+ type: 'bench:environment',
+ data: collectEnvironment(),
+ })}\n`;
+ }
yield `${stringify(record)}\n`;
}
}
diff --git a/lib/internal/bench_runner/reporter/spec.js b/lib/internal/bench_runner/reporter/spec.js
index 34f5831553f..6663360f592 100644
--- a/lib/internal/bench_runner/reporter/spec.js
+++ b/lib/internal/bench_runner/reporter/spec.js
@@ -13,6 +13,9 @@ const {
StringPrototypeReplaceAll,
} = primordials;
const Transform = require('internal/streams/transform');
+const {
+ collectEnvironment,
+} = require('internal/bench_runner/environment');
const kHeader =
'benchmark | samples | mean rate | 95% CI | median rate | warning\n';
@@ -62,6 +65,50 @@ function formatRate(rate) {
return `${formatted}${prefix} ops/s`;
}
+function formatBytes(bytes) {
+ const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB'];
+ let unit = 0;
+ while (bytes >= 1024 && unit < units.length - 1) {
+ bytes /= 1024;
+ unit++;
+ }
+ return `${NumberPrototypeToFixed(bytes, unit === 0 ? 0 : 1)} ${units[unit]}`;
+}
+
+function formatEnvironment(environment) {
+ const { cpus, memory, os } = environment;
+ let output = `node: ${environment.node} (V8 ${environment.v8}), ` +
+ `${environment.platform} ${environment.arch} ` +
+ `(${os.type} ${os.release})\n`;
+
+ const groups = [];
+ for (let i = 0; i < cpus.length; i++) {
+ ArrayPrototypePush(groups, `${cpus[i].count} x ${cpus[i].model}`);
+ }
+ if (groups.length === 0) ArrayPrototypePush(groups, 'unknown');
+ output += `cpu: ${ArrayPrototypeJoin(groups, ', ')} ` +
+ `(available parallelism: ${environment.availableParallelism})\n`;
+
+ output += `memory: ${formatBytes(memory.total)} total, ` +
+ `${formatBytes(memory.free)} free`;
+ if (memory.constrained !== null) {
+ output += `, ${formatBytes(memory.constrained)} constrained`;
+ }
+ output += '\n';
+
+ const loadavg = [];
+ for (let i = 0; i < environment.loadavg.length; i++) {
+ ArrayPrototypePush(
+ loadavg, NumberPrototypeToFixed(environment.loadavg[i], 2));
+ }
+ output += `load average: ${ArrayPrototypeJoin(loadavg, ', ')}\n`;
+
+ if (environment.execArgv.length > 0) {
+ output += `exec argv: ${ArrayPrototypeJoin(environment.execArgv, ' ')}\n`;
+ }
+ return `${output}\n`;
+}
+
function formatWarning(summary) {
const warnings = [];
if (NumberIsFinite(summary.coefficientOfVariation) &&
@@ -97,6 +144,7 @@ function formatResult(data) {
class SpecReporter extends Transform {
#diagnostics = [];
+ #environment;
#reported = false;
#results = [];
@@ -105,7 +153,7 @@ class SpecReporter extends Transform {
}
#format(summary = undefined) {
- let output = kHeader;
+ let output = formatEnvironment(this.#environment) + kHeader;
for (let i = 0; i < this.#results.length; i++) {
output += formatResult(this.#results[i]);
}
@@ -121,6 +169,7 @@ class SpecReporter extends Transform {
}
_transform({ type, data }, _encoding, callback) {
+ this.#environment ??= collectEnvironment();
switch (type) {
case 'bench:complete':
ArrayPrototypePush(this.#results, data);
diff --git a/test/parallel/test-bench-cli-reporters.js b/test/parallel/test-bench-cli-reporters.js
index 31cee67aaa1..9bc775c7c47 100644
--- a/test/parallel/test-bench-cli-reporters.js
+++ b/test/parallel/test-bench-cli-reporters.js
@@ -125,16 +125,19 @@ for (const { file, status } of [
fixtures.path('bench-runner/a.cjs'),
]);
assert.strictEqual(result.status, 0);
- parseRecords(result);
- assert.match(fs.readFileSync(destination, 'utf8'),
- /^benchmark \| samples \| mean rate/);
+ const records = parseRecords(result);
+ assert.strictEqual(records[0].type, 'bench:environment');
+ const output = fs.readFileSync(destination, 'utf8');
+ assert.match(output, /^node: v\d+/);
+ assert.match(output, /^benchmark \| samples \| mean rate/m);
}
{
const result = spawnBench([fixtures.path('bench-runner/a.cjs')]);
assert.strictEqual(result.status, 0);
assert.strictEqual(result.stderr, '');
- assert.match(result.stdout, /^benchmark \| samples \| mean rate/);
+ assert.match(result.stdout, /^node: v\d+/);
+ assert.match(result.stdout, /^benchmark \| samples \| mean rate/m);
assert.match(result.stdout, /1 completed, 0 failed, 0 skipped/);
}
diff --git a/test/parallel/test-bench-reporters.js b/test/parallel/test-bench-reporters.js
index b5078a40de6..09cde1d46b1 100644
--- a/test/parallel/test-bench-reporters.js
+++ b/test/parallel/test-bench-reporters.js
@@ -4,6 +4,7 @@
const common = require('../common');
const { completeSample } = require('../common/bench');
const assert = require('assert');
+const os = require('os');
const { Readable } = require('stream');
const { bench, run } = require('node:bench');
const { json, spec } = require('node:bench/reporters');
@@ -26,7 +27,25 @@ bench('json failed', { samples: 1 }, () => {
const lines = chunks.join('').trim().split('\n');
const records = lines.map((line) => JSON.parse(line));
- assert.strictEqual(records.length, 8);
+ assert.strictEqual(records.length, 9);
+ const { type: environmentType, data: environment } = records[0];
+ assert.strictEqual(environmentType, 'bench:environment');
+ assert.strictEqual(environment.node, process.version);
+ assert.strictEqual(environment.v8, process.versions.v8);
+ assert.strictEqual(environment.platform, process.platform);
+ assert.strictEqual(environment.arch, process.arch);
+ assert.strictEqual(environment.os.type, os.type());
+ assert.strictEqual(environment.os.release, os.release());
+ assert.strictEqual(environment.os.machine, os.machine());
+ assert.strictEqual(
+ environment.cpus.reduce((total, { count }) => total + count, 0),
+ os.cpus().length);
+ assert.strictEqual(environment.availableParallelism,
+ os.availableParallelism());
+ assert.strictEqual(environment.memory.total, os.totalmem());
+ assert.strictEqual(typeof environment.memory.free, 'number');
+ assert.strictEqual(environment.loadavg.length, 3);
+ assert.deepStrictEqual(environment.execArgv, process.execArgv);
const plans = records.filter(({ type }) => type === 'bench:plan');
assert.deepStrictEqual(plans.map(({ data }) => data.selected), [true, true]);
const sample = records.find(({ type }) => type === 'bench:sample');
@@ -105,7 +124,18 @@ bench('json failed', { samples: 1 }, () => {
},
];
const specChunks = await Readable.from(synthetic).compose(spec).toArray();
- assert.strictEqual(specChunks.join(''),
+ const specOutput = specChunks.join('');
+ const specHeaderEnd = specOutput.indexOf('\n\n') + 2;
+ const specHeader = specOutput.slice(0, specHeaderEnd);
+ assert.match(specHeader,
+ new RegExp(`^node: ${RegExp.escape(process.version)} ` +
+ `\\(V8 ${RegExp.escape(process.versions.v8)}\\), ` +
+ `${process.platform} ${process.arch} `));
+ assert.match(specHeader, /^cpu: .*\(available parallelism: \d+\)$/m);
+ assert.match(specHeader, /^memory: [\d.]+ \w+ total, [\d.]+ \w+ free/m);
+ assert.match(specHeader, /^load average: [\d.]+, [\d.]+, [\d.]+$/m);
+ assert.match(specHeader, /^exec argv: --experimental-bench --no-warnings$/m);
+ assert.strictEqual(specOutput.slice(specHeaderEnd),
'benchmark | samples | mean rate | 95% CI | ' +
'median rate | warning\n' +
'fast [size="small"] | 2 | 1.50k ops/s | ' +
@@ -123,7 +153,8 @@ bench('json failed', { samples: 1 }, () => {
type: 'bench:diagnostic',
data: { aggregate, circular },
}]).compose(json).toArray();
- const jsonEdge = JSON.parse(jsonEdgeChunks.join(''));
+ assert.strictEqual(jsonEdgeChunks.length, 2);
+ const jsonEdge = JSON.parse(jsonEdgeChunks[1]);
assert.deepStrictEqual(jsonEdge.data.aggregate.errors, ['1']);
assert.strictEqual(jsonEdge.data.circular.self, '[Circular]');
@@ -134,7 +165,10 @@ bench('json failed', { samples: 1 }, () => {
for await (const chunk of json(undefinedRecord())) {
undefinedChunks.push(chunk);
}
- assert.strictEqual(undefinedChunks.join(''), 'null\n');
+ assert.strictEqual(undefinedChunks.length, 2);
+ assert.strictEqual(JSON.parse(undefinedChunks[0]).type,
+ 'bench:environment');
+ assert.strictEqual(undefinedChunks[1], 'null\n');
function result(name, rate) {
return {