Commit c905bedc901 for nodejs

commit c905bedc9019166231dd371964ffb9ff0c78261a
Author: James M Snell <jasnell@gmail.com>
Date:   Sun Oct 11 17:59:17 2026 +0000

    lib: avoid dead code elimination with node:bench

    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 8118a928d0f..31f4c2e22ad 100644
--- a/doc/api/bench.md
+++ b/doc/api/bench.md
@@ -124,8 +124,9 @@ specialize it more narrowly than the workload being modeled. Framework and loop
 overhead can also dominate operations that are too short. To reduce these risks:

 * Make values produced by measured work observable outside the measured
-  interval, for example by validating an aggregate derived from every result.
-  Passing them only through unused local computations is insufficient.
+  interval, for example by validating an aggregate derived from every result
+  or by passing each result to [`doNotOptimize()`][]. Passing them only through
+  unused local computations is insufficient.
 * Perform enough operations in each sample to amortize fixed timer reads and
   calls to `context.start()` and `context.end()`. If loop bookkeeping is material
   relative to one operation, batch multiple operations per iteration and report
@@ -138,7 +139,10 @@ overhead can also dominate operations that are too short. To reduce these risks:

 `node:bench` does not force a particular optimization state or infer whether an
 engine eliminated work. Such controls and diagnostics are runtime-specific and
-heuristic, and do not replace validating the benchmark workload.
+heuristic, and do not replace validating the benchmark workload. The `spec`
+reporter flags rates that are implausibly fast for real work, which often
+indicates eliminated work, but the absence of that warning does not prove the
+work was measured.

 ### Dynamic sampling and variable batches

@@ -311,8 +315,11 @@ 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.
+variation above 5% is reported as `noisy`, an absolute skewness above 1 is
+reported as `skewed`, and a median rate of at least one operation per
+nanosecond is reported as `implausible`, as it usually indicates that the
+measured work was optimized away. 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
@@ -490,6 +497,61 @@ added: v26.9.0

 Alias for `suite()`.

+## `doNotOptimize(value)`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+* `value` {any} A value produced by measured work.
+* Returns: {any} `value`.
+
+Stores `value` where the optimizing compiler cannot prove it is unused, so the
+computation that produced it is not eliminated as dead code. Returning `value`
+allows the call to wrap an expression.
+
+```mjs
+import { bench, doNotOptimize } from 'node:bench';
+
+bench('Math.sqrt', (b) => {
+  const n = 1_000_000;
+  b.start();
+  for (let i = 0; i < n; i++) {
+    doNotOptimize(Math.sqrt(i));
+  }
+  b.end(n);
+});
+```
+
+```cjs
+const { bench, doNotOptimize } = require('node:bench');
+
+bench('Math.sqrt', (b) => {
+  const n = 1_000_000;
+  b.start();
+  for (let i = 0; i < n; i++) {
+    doNotOptimize(Math.sqrt(i));
+  }
+  b.end(n);
+});
+```
+
+The call adds a small, constant cost to every invocation, so it is best suited
+to work that takes longer than a few nanoseconds per call. For cheaper
+operations, accumulate results into a value that is passed to
+`doNotOptimize()` once after the measured region.
+
+`doNotOptimize()` only prevents the result from being discarded. It does not
+prevent the compiler from hoisting loop-invariant work or folding constant
+inputs; vary inputs between iterations when the work would otherwise be
+invariant.
+
+`doNotOptimize()` is intended to be called only from benchmark functions while
+a runner is executing them. Non-number values are retained until the current
+sample finishes, after which the runner releases them. When called outside a
+benchmark function, the most recent non-number value remains reachable until
+`doNotOptimize()` is called again.
+
 ## `before(fn)`

 <!-- YAML
@@ -918,6 +980,7 @@ A completed benchmark result contains:
 [`--bench-gc`]: cli.md#--bench-gcmode
 [`context.record()`]: #contextrecordsample
 [`createRunner()`]: #createrunneroptions
+[`doNotOptimize()`]: #donotoptimizevalue
 [`os`]: os.md
 [`run()`]: #runoptions
 [benchmark reporters]: #benchmark-reporters
diff --git a/lib/bench.js b/lib/bench.js
index 96e199773d9..9cee01524a6 100644
--- a/lib/bench.js
+++ b/lib/bench.js
@@ -18,6 +18,7 @@ const {
   run,
   runFile,
 } = require('internal/bench_runner/runner');
+const { doNotOptimize } = require('internal/bench_runner/benchmark');

 if (process.env.NODE_BENCH_CONTEXT !== 'child' ||
     typeof process.send !== 'function') {
@@ -33,6 +34,7 @@ ObjectAssign(module.exports, {
   bench,
   createRunner,
   describe: suite,
+  doNotOptimize,
   run,
   runFile,
   suite,
diff --git a/lib/internal/bench_runner/benchmark.js b/lib/internal/bench_runner/benchmark.js
index f332b523f4b..114feb38817 100644
--- a/lib/internal/bench_runner/benchmark.js
+++ b/lib/internal/bench_runner/benchmark.js
@@ -7,6 +7,7 @@ const {
   ArrayPrototypeReverse,
   ArrayPrototypeSlice,
   ArrayPrototypeSort,
+  Float64Array,
   JSONStringify,
   MathFloor,
   MathMax,
@@ -57,6 +58,25 @@ const kEmptyNamePath = ObjectFreeze([]);
 const kEmptyParams = ObjectFreeze({ __proto__: null });
 const kEmptyTags = ObjectFreeze([]);

+// doNotOptimize() stores values in reachable storage so that the optimizing
+// compiler cannot prove the computation that produced them is unused. Numbers
+// use a typed array so that storing a double does not allocate a HeapNumber.
+const numberSink = new Float64Array(1);
+const valueSink = [undefined];
+
+function doNotOptimize(value) {
+  if (typeof value === 'number') {
+    numberSink[0] = value;
+  } else {
+    valueSink[0] = value;
+  }
+  return value;
+}
+
+function clearSink() {
+  valueSink[0] = undefined;
+}
+
 function validateSkip(skip) {
   if (skip !== undefined && typeof skip !== 'boolean' &&
       typeof skip !== 'string') {
@@ -578,6 +598,8 @@ module.exports = {
   Bench,
   BenchContext,
   Suite,
+  clearSink,
+  doNotOptimize,
   kGcModes,
   normalizeArgs,
   summarizeSamples,
diff --git a/lib/internal/bench_runner/harness.js b/lib/internal/bench_runner/harness.js
index 8d649b22a42..e9afe90ce50 100644
--- a/lib/internal/bench_runner/harness.js
+++ b/lib/internal/bench_runner/harness.js
@@ -57,6 +57,7 @@ const {
   Bench,
   BenchContext,
   Suite,
+  clearSink,
   kGcModes,
   normalizeArgs,
   summarizeSamples,
@@ -974,6 +975,7 @@ class Harness {
             }
             const outcome = await this.#runSample(
               benchmark, controller.signal, phase, index);
+            clearSink();
             if (!outcome.failed) {
               checkDeadline();
               if (controller.signal.aborted) {
diff --git a/lib/internal/bench_runner/reporter/spec.js b/lib/internal/bench_runner/reporter/spec.js
index 6663360f592..cf3ba088548 100644
--- a/lib/internal/bench_runner/reporter/spec.js
+++ b/lib/internal/bench_runner/reporter/spec.js
@@ -17,6 +17,10 @@ const {
   collectEnvironment,
 } = require('internal/bench_runner/environment');

+// One operation per nanosecond is faster than almost any real operation can
+// run, which usually means the optimizing compiler removed the measured work.
+const kImplausibleRate = 1e9;
+
 const kHeader =
   'benchmark | samples | mean rate | 95% CI | median rate | warning\n';

@@ -118,6 +122,10 @@ function formatWarning(summary) {
   if (NumberIsFinite(summary.skewness) && MathAbs(summary.skewness) > 1) {
     ArrayPrototypePush(warnings, 'skewed');
   }
+  if (NumberIsFinite(summary.median) &&
+      summary.median >= kImplausibleRate) {
+    ArrayPrototypePush(warnings, 'implausible');
+  }
   return ArrayPrototypeJoin(warnings, ', ');
 }

diff --git a/test/parallel/test-bench-do-not-optimize.js b/test/parallel/test-bench-do-not-optimize.js
new file mode 100644
index 00000000000..015fa123aa0
--- /dev/null
+++ b/test/parallel/test-bench-do-not-optimize.js
@@ -0,0 +1,60 @@
+// Flags: --expose-gc --experimental-bench --no-warnings
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const { Readable } = require('stream');
+const { createRunner, doNotOptimize } = require('node:bench');
+const { spec } = require('node:bench/reporters');
+
+// The value is returned unchanged.
+for (const value of [
+  0, -0, 1.5, NaN, Infinity, 1n, 'string', true, null, undefined,
+  Symbol('symbol'), {}, [], () => {},
+]) {
+  assert.ok(Object.is(doNotOptimize(value), value));
+}
+
+function result(name, median) {
+  return {
+    type: 'bench:complete',
+    data: {
+      name,
+      params: {},
+      samples: [{}],
+      summary: {
+        mean: median,
+        median,
+        coefficientOfVariation: 0,
+        confidenceInterval: { lower: median, upper: median },
+        skewness: 0,
+      },
+    },
+  };
+}
+
+(async () => {
+  // Values are released after the sample that produced them.
+  const runner = createRunner({ yieldBetweenSamples: false });
+  let ref;
+  runner.bench('retained', { samples: 1, warmup: 0 }, common.mustCall((b) => {
+    const value = {};
+    ref = new WeakRef(value);
+    b.start();
+    doNotOptimize(value);
+    b.end();
+  }));
+  await runner.run().toArray();
+  await new Promise(setImmediate);
+  globalThis.gc();
+  assert.strictEqual(ref.deref(), undefined);
+
+  // The spec reporter flags median rates of at least one operation per
+  // nanosecond as implausible.
+  const output = (await Readable.from([
+    result('plausible', 999_999_999),
+    result('implausible', 1e9),
+  ]).compose(spec).toArray()).join('');
+  assert.match(output, /^plausible \|.* \| $/m);
+  assert.match(output, /^implausible \|.* \| implausible$/m);
+})().then(common.mustCall());