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 {