Commit e256fcb78be for nodejs
commit e256fcb78be109bec04284105de2508087ba7599
Author: James M Snell <jasnell@gmail.com>
Date: Sun Oct 11 17:38:31 2026 +0000
lib: add gc options to node:bench
Adds option post-warmup and pre-sample gc options
to 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 b457cff50c4..8118a928d0f 100644
--- a/doc/api/bench.md
+++ b/doc/api/bench.md
@@ -90,6 +90,32 @@ system load can all affect results. Keep raw samples when comparing results and
investigate noisy or skewed distributions rather than treating a confidence
interval as a pass/fail threshold.
+### Garbage collection
+
+Warmup invocations can leave garbage on the heap that is collected during the
+first measured samples, charging warmup allocations to measured work. The `gc`
+option forces a full garbage collection outside the measured region:
+
+* `'none'`: No garbage collection is forced. This is the default.
+* `'once'`: A full garbage collection runs after warmup, before the first
+ measured sample. Garbage produced by measured samples is still collected
+ during measurement, so its cost remains part of the results.
+* `'sample'`: A full garbage collection runs before every measured sample. This
+ reduces variance between samples, but samples that are too short to trigger a
+ collection on their own exclude the cost of collecting their garbage, which
+ can make allocation-heavy code appear faster than it is in practice.
+
+Forced collection runs after `beforeEach` hooks and any event loop turn between
+samples, and is excluded from benchmark timeout accounting. The effective mode
+is reported as `gc` in `'bench:plan'` events. The mode can be overridden for
+every benchmark through `createRunner()`, [`run()`][], or [`--bench-gc`][].
+
+Forcing garbage collection requires the `--expose-gc` flag, which also makes
+`globalThis.gc()` available to benchmark code. A benchmark whose effective mode
+is `'once'` or `'sample'` fails when the flag is not set. When `--bench-gc`
+selects one of these modes, the command-line runner adds `--expose-gc` to
+benchmark child processes automatically.
+
### Measurement integrity
A statistically consistent result does not prove that a benchmark measured the
@@ -221,9 +247,10 @@ arbitrary functions or closures to move them between isolates, because closures
cannot be reconstructed with their original lexical environment.
Benchmark files passed to `--bench` should declare benchmarks but must not call
-`run()`. The CLI supports `--bench-name-pattern`, `--bench-samples`,
-`--bench-warmup`, `--bench-reporter`, and `--bench-reporter-destination`. See
-the [command-line options documentation][] for details.
+`run()`. The CLI supports `--bench-gc`, `--bench-name-pattern`,
+`--bench-samples`, `--bench-warmup`, `--bench-reporter`, and
+`--bench-reporter-destination`. See the [command-line options documentation][]
+for details.
Preload modules passed through `--require` or `--import` should not declare
benchmarks. Such declarations are not associated with an entry file and have
@@ -321,6 +348,9 @@ added: v26.9.0
-->
* `options` {Object}
+ * `gc` {string} Overrides the [garbage collection][] mode for every
+ benchmark declared through this runner. One of `'none'`, `'once'`, or
+ `'sample'`.
* `yieldBetweenSamples` {boolean} Schedule an event loop turn between sample
callbacks. Disabling this also prevents timer-based abort signals from
firing between synchronous callbacks. Benchmark timeouts continue to be
@@ -334,8 +364,8 @@ the module-level functions. Call the returned `run()` function to start the
runner and obtain its {BenchmarksStream}.
Each runner can be started once. Its `run()` function accepts the same options
-as the module-level [`run()`][]. `run({ yieldBetweenSamples })` overrides the
-value passed to `createRunner()`. Unlike the module-level runner, a failed
+as the module-level [`run()`][]. `run({ gc })` and `run({ yieldBetweenSamples })`
+override the values passed to `createRunner()`. Unlike the module-level runner, a failed
benchmark in an explicit runner does not change the process exit code.
## `bench([name][, options], fn)`
@@ -350,6 +380,8 @@ added: v26.9.0
* `diagnosticChannels` {Array} String diagnostics channel names, deduplicated
and inherited from containing suites by union. Symbol values in the array
are silently ignored. **Default:** `[]`.
+ * `gc` {string} The [garbage collection][] mode. One of `'none'`, `'once'`,
+ or `'sample'`. **Default:** `'none'`.
* `only` {boolean} When any benchmark or containing suite has `only` set,
benchmarks without `only` in their hierarchy are skipped. **Default:**
`false`.
@@ -511,6 +543,9 @@ added: v26.9.0
-->
* `options` {Object}
+ * `gc` {string} Overrides the [garbage collection][] mode for every
+ benchmark. One of `'none'`, `'once'`, or `'sample'`. **Default:** the value
+ passed to `createRunner()` for an explicit runner.
* `namePattern` {string|RegExp} Only runs benchmarks whose full hierarchical
name matches the pattern. String values are interpreted as JavaScript
regular expressions.
@@ -803,6 +838,8 @@ isolation, all files share one runner and their plans are emitted before any
benchmark executes. Plan data contains the benchmark-scoped identity, location,
tags, and parameters described in [benchmark result][], together with:
+* `gc` {string} The effective [garbage collection][] mode after run-level
+ overrides.
* `diagnosticChannels` {string\[]} The inherited string channel names
subscribed to during each callback.
* `samples` {number} The effective maximum number of measured callback
@@ -878,6 +915,7 @@ A completed benchmark result contains:
interval for the median rate, with `lower` and `upper` properties.
* `skewness` {number} The skewness of the scaled rate histogram.
+[`--bench-gc`]: cli.md#--bench-gcmode
[`context.record()`]: #contextrecordsample
[`createRunner()`]: #createrunneroptions
[`os`]: os.md
@@ -885,3 +923,4 @@ A completed benchmark result contains:
[benchmark reporters]: #benchmark-reporters
[benchmark result]: #benchmark-result
[command-line options documentation]: cli.md#--bench
+[garbage collection]: #garbage-collection
diff --git a/doc/api/cli.md b/doc/api/cli.md
index 5afcc6edaa5..f534aa96e77 100644
--- a/doc/api/cli.md
+++ b/doc/api/cli.md
@@ -554,6 +554,25 @@ details.
This flag cannot be combined with `--test`, `--watch`, `--watch-path`,
`--check`, `--eval`, or `--interactive`.
+### `--bench-gc=mode`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+> Stability: 1 - Experimental
+
+Overrides the garbage collection mode for every selected benchmark. When `mode`
+is `'once'`, a full garbage collection runs before the first measured sample.
+When `mode` is `'sample'`, a full garbage collection runs before every measured
+sample. When `mode` is `'none'`, no garbage collection is forced. Without this
+option, each benchmark uses its own `gc` value, which defaults to `'none'`.
+
+Forcing garbage collection requires the `--expose-gc` flag. With process
+isolation, `--expose-gc` is added to benchmark child processes automatically
+when `mode` is `'once'` or `'sample'`. With `--bench-isolation=none`,
+`--expose-gc` must be passed explicitly.
+
### `--bench-isolation=mode`
<!-- YAML
@@ -4289,6 +4308,7 @@ one is included in the list below.
* `--allow-openssl-store`
* `--allow-wasi`
* `--allow-worker`
+* `--bench-gc`
* `--bench-isolation`
* `--bench-name-pattern`
* `--bench-reporter-destination`
diff --git a/doc/node-config-schema.json b/doc/node-config-schema.json
index 0d2cb83b517..9b9c0f835d0 100644
--- a/doc/node-config-schema.json
+++ b/doc/node-config-schema.json
@@ -54,6 +54,10 @@
"type": "boolean",
"description": "launch benchmark runner on startup"
},
+ "bench-gc": {
+ "type": "string",
+ "description": "run garbage collection before measured benchmark samples"
+ },
"bench-isolation": {
"type": "string",
"description": "configures the type of benchmark isolation used in the benchmark runner"
@@ -191,6 +195,10 @@
"type": "boolean",
"description": "allow worker threads when any permissions are set"
},
+ "bench-gc": {
+ "type": "string",
+ "description": "run garbage collection before measured benchmark samples"
+ },
"bench-isolation": {
"type": "string",
"description": "configures the type of benchmark isolation used in the benchmark runner"
diff --git a/doc/node.1 b/doc/node.1
index 32fd0290582..af10d324353 100644
--- a/doc/node.1
+++ b/doc/node.1
@@ -353,6 +353,17 @@ details.
This flag cannot be combined with \fB--test\fR, \fB--watch\fR, \fB--watch-path\fR,
\fB--check\fR, \fB--eval\fR, or \fB--interactive\fR.
.
+.It Fl -bench-gc Ns = Ns Ar mode
+Overrides the garbage collection mode for every selected benchmark. When \fBmode\fR
+is \fB'once'\fR, a full garbage collection runs before the first measured sample.
+When \fBmode\fR is \fB'sample'\fR, a full garbage collection runs before every measured
+sample. When \fBmode\fR is \fB'none'\fR, no garbage collection is forced. Without this
+option, each benchmark uses its own \fBgc\fR value, which defaults to \fB'none'\fR.
+Forcing garbage collection requires the \fB--expose-gc\fR flag. With process
+isolation, \fB--expose-gc\fR is added to benchmark child processes automatically
+when \fBmode\fR is \fB'once'\fR or \fB'sample'\fR. With \fB--bench-isolation=none\fR,
+\fB--expose-gc\fR must be passed explicitly.
+.
.It Fl -bench-isolation Ns = Ns Ar mode
Configures benchmark file isolation. When \fBmode\fR is \fB'process'\fR, each matching
file runs in a separate child process. This is the default. Files are still run
@@ -2215,6 +2226,8 @@ one is included in the list below.
.It
\fB--allow-worker\fR
.It
+\fB--bench-gc\fR
+.It
\fB--bench-isolation\fR
.It
\fB--bench-name-pattern\fR
diff --git a/lib/internal/bench_runner/benchmark.js b/lib/internal/bench_runner/benchmark.js
index ea47696489a..f332b523f4b 100644
--- a/lib/internal/bench_runner/benchmark.js
+++ b/lib/internal/bench_runner/benchmark.js
@@ -42,6 +42,7 @@ const {
validateInteger,
validateNumber,
validateObject,
+ validateOneOf,
validateString,
validateUint32,
} = require('internal/validators');
@@ -50,6 +51,7 @@ const { structuredClone } = require('internal/worker/js_transferable');
const { bigint: hrtime } = process.hrtime;
const kDefaultSamples = 30;
const kDefaultWarmup = 10;
+const kGcModes = ObjectFreeze(['none', 'once', 'sample']);
const kEmptyDiagnosticChannels = ObjectFreeze([]);
const kEmptyNamePath = ObjectFreeze([]);
const kEmptyParams = ObjectFreeze({ __proto__: null });
@@ -234,6 +236,7 @@ class Bench extends AsyncResource {
const validated = validateNodeOptions(
options, parent.tags, parent.diagnosticChannels);
const {
+ gc = 'none',
params,
samples = kDefaultSamples,
signal,
@@ -241,6 +244,7 @@ class Bench extends AsyncResource {
warmup = kDefaultWarmup,
} = options;
+ validateOneOf(gc, 'options.gc', kGcModes);
validateUint32(samples, 'options.samples', true);
validateUint32(warmup, 'options.warmup');
validateAbortSignal(signal, 'options.signal');
@@ -258,6 +262,7 @@ class Bench extends AsyncResource {
this.skip = validated.skip;
this.tags = validated.tags;
this.params = canonicalizeParams(params);
+ this.gc = gc;
this.samples = samples;
this.warmup = warmup;
this.timeout = timeout;
@@ -573,6 +578,7 @@ module.exports = {
Bench,
BenchContext,
Suite,
+ kGcModes,
normalizeArgs,
summarizeSamples,
};
diff --git a/lib/internal/bench_runner/cli.js b/lib/internal/bench_runner/cli.js
index 5884046e146..4993b44face 100644
--- a/lib/internal/bench_runner/cli.js
+++ b/lib/internal/bench_runner/cli.js
@@ -44,6 +44,9 @@ const { spawn } = require('child_process');
const { createWriteStream, statSync } = require('fs');
const { Glob } = require('internal/fs/glob');
const { getValidatedPath } = require('internal/fs/utils');
+const {
+ kGcModes,
+} = require('internal/bench_runner/benchmark');
const {
BenchmarksStream,
} = require('internal/bench_runner/benchmarks_stream');
@@ -112,6 +115,7 @@ const kFilterArgs = [
'--experimental-default-config-file',
];
const kFilterArgValues = [
+ '--bench-gc',
'--bench-isolation',
'--bench-name-pattern',
'--bench-reporter',
@@ -265,6 +269,7 @@ function parseCommandLine() {
destinations,
fileRunId: isChild && process.env.NODE_BENCH_FILE_RUN_ID ?
process.env.NODE_BENCH_FILE_RUN_ID : undefined,
+ gc: getOptionValue('--bench-gc') || undefined,
isChild,
isolation: getOptionValue('--bench-isolation'),
namePattern: parseNamePattern(getOptionValue('--bench-name-pattern')),
@@ -438,6 +443,7 @@ function validateRecord(record) {
}
if (record.type === 'bench:plan') {
const {
+ gc,
samples,
selected,
skip,
@@ -458,6 +464,7 @@ function validateRecord(record) {
ArrayPrototypeSome(ObjectValues(record.data.params), (value) =>
typeof value !== 'string' && typeof value !== 'boolean' &&
(typeof value !== 'number' || !NumberIsFinite(value))) ||
+ !ArrayPrototypeIncludes(kGcModes, gc) ||
!NumberIsSafeInteger(samples) || samples <= 0 ||
samples > 0xFFFFFFFF ||
!NumberIsSafeInteger(warmup) || warmup < 0 || warmup > 0xFFFFFFFF ||
@@ -604,6 +611,7 @@ async function loadBenchmarkFiles(files, options, modules, onRecord) {
const stream = runBenchmarks({
__proto__: null,
+ gc: options.gc,
namePattern: options.namePattern,
samples: options.samples,
warmup: options.warmup,
@@ -749,6 +757,10 @@ function getChildArgs(path, options) {
getInheritedChildArgs() : ArrayPrototypeSlice(options.execArgv);
ArrayPrototypePush(
args, '--experimental-bench', '--bench', '--bench-isolation=none');
+ if (options.gc !== undefined) {
+ ArrayPrototypePush(args, `--bench-gc=${options.gc}`);
+ if (options.gc !== 'none') ArrayPrototypePush(args, '--expose-gc');
+ }
if (options.namePatternSource.length > 0) {
ArrayPrototypePush(
args, `--bench-name-pattern=${options.namePatternSource}`);
diff --git a/lib/internal/bench_runner/harness.js b/lib/internal/bench_runner/harness.js
index 46d35a0a235..8d649b22a42 100644
--- a/lib/internal/bench_runner/harness.js
+++ b/lib/internal/bench_runner/harness.js
@@ -47,6 +47,7 @@ const {
validateBoolean,
validateFunction,
validateObject,
+ validateOneOf,
validateUint32,
} = require('internal/validators');
const { queueMicrotask } = require('internal/process/task_queues');
@@ -56,6 +57,7 @@ const {
Bench,
BenchContext,
Suite,
+ kGcModes,
normalizeArgs,
summarizeSamples,
} = require('internal/bench_runner/benchmark');
@@ -73,6 +75,23 @@ function createRunId() {
return `${process.pid}:${String(hrtime())}:${nextRunId++}`;
}
+let garbageCollector;
+
+// The gc() function installed by --expose-gc is resolved from a new context
+// so that it reflects the V8 flag rather than a user-replaceable global.
+function getGarbageCollector(mode) {
+ if (garbageCollector === undefined) {
+ const { runInNewContext } = require('vm');
+ const gc = runInNewContext('globalThis.gc');
+ if (typeof gc !== 'function') {
+ throw new ERR_INVALID_STATE(
+ `the '${mode}' gc mode requires the --expose-gc flag`);
+ }
+ garbageCollector = () => { gc(); };
+ }
+ return garbageCollector;
+}
+
function eventLoopTurn() {
return new Promise((resolve) => setImmediate(resolve));
}
@@ -119,9 +138,11 @@ class Harness {
validateObject(options, 'options');
const {
autoRun = true,
+ gc,
yieldBetweenSamples = true,
} = options;
validateBoolean(autoRun, 'options.autoRun');
+ if (gc !== undefined) validateOneOf(gc, 'options.gc', kGcModes);
validateBoolean(
yieldBetweenSamples, 'options.yieldBetweenSamples');
this.#autoRun = autoRun;
@@ -131,6 +152,7 @@ class Harness {
this.entryFile = process.argv?.[1] ?? null;
this.stream = new BenchmarksStream();
this.state = 'collecting';
+ this.gc = gc;
this.namePattern = null;
this.outerSignal = undefined;
this.samples = undefined;
@@ -261,16 +283,22 @@ class Harness {
validateObject(options, 'options');
const {
+ gc,
namePattern,
samples,
signal,
warmup,
yieldBetweenSamples,
} = options;
+ let nextGc = this.gc;
let nextNamePattern = this.namePattern;
let nextSamples = this.samples;
let nextWarmup = this.warmup;
let nextYieldBetweenSamples = this.#yieldBetweenSamples;
+ if (gc !== undefined) {
+ validateOneOf(gc, 'options.gc', kGcModes);
+ nextGc = gc;
+ }
if (namePattern !== undefined) {
if (typeof namePattern === 'string') {
nextNamePattern = new RegExp(namePattern);
@@ -296,6 +324,7 @@ class Harness {
nextYieldBetweenSamples = yieldBetweenSamples;
}
+ this.gc = nextGc;
this.namePattern = nextNamePattern;
this.samples = nextSamples;
this.outerSignal = signal;
@@ -434,6 +463,7 @@ class Harness {
column: benchmark.loc.column,
tags: ArrayPrototypeSlice(benchmark.tags),
params: benchmark.params,
+ gc: this.gc ?? benchmark.gc,
samples: this.samples ?? benchmark.samples,
warmup: this.warmup ?? benchmark.warmup,
timeout: benchmark.timeout === Infinity ? null : benchmark.timeout,
@@ -922,6 +952,8 @@ class Harness {
try {
await this.#runWithStop(benchmark, controller, async (pause) => {
try {
+ const gcMode = this.gc ?? benchmark.gc;
+ const gc = gcMode === 'none' ? null : getGarbageCollector(gcMode);
await this.#runBenchHooks(
benchmark, 'beforeEach', hookContext);
checkDeadline();
@@ -933,6 +965,13 @@ class Harness {
}
const phase = i < warmup ? 'warmup' : 'measurement';
const index = phase === 'warmup' ? i : i - warmup;
+ if (gc !== null && i >= warmup &&
+ (gcMode === 'sample' || i === warmup)) {
+ await pause(gc);
+ if (controller.signal.aborted) {
+ throw controller.signal.reason;
+ }
+ }
const outcome = await this.#runSample(
benchmark, controller.signal, phase, index);
if (!outcome.failed) {
@@ -1096,6 +1135,7 @@ function createRunner(options = kEmptyObject) {
const harness = new Harness({
__proto__: null,
autoRun: false,
+ gc: options.gc,
yieldBetweenSamples: options.yieldBetweenSamples === undefined ?
true : options.yieldBetweenSamples,
});
diff --git a/src/node_options.cc b/src/node_options.cc
index 8c82c0a9f65..3fac3275752 100644
--- a/src/node_options.cc
+++ b/src/node_options.cc
@@ -384,6 +384,10 @@ void EnvironmentOptions::CheckOptions(std::vector<std::string>* errors,
debug_options_.allow_attaching_debugger = false;
}
+ if (!bench_gc.empty() && bench_gc != "none" && bench_gc != "once" &&
+ bench_gc != "sample") {
+ errors->push_back("invalid value for --bench-gc");
+ }
if (has_bench_samples && bench_samples == 0) {
errors->push_back("--bench-samples must be greater than 0");
}
@@ -1149,6 +1153,12 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
false,
OptionNamespaces::kBenchRunnerNamespace);
Implies("--bench", "[has_bench_options]");
+ AddOption("--bench-gc",
+ "run garbage collection before measured benchmark samples",
+ &EnvironmentOptions::bench_gc,
+ kAllowedInEnvvar,
+ OptionNamespaces::kBenchRunnerNamespace);
+ Implies("--bench-gc", "[has_bench_options]");
AddOption("--bench-isolation",
"configures the type of benchmark isolation used in the benchmark "
"runner",
diff --git a/src/node_options.h b/src/node_options.h
index d4d4dfc97a5..4d3585bb8cf 100644
--- a/src/node_options.h
+++ b/src/node_options.h
@@ -147,6 +147,7 @@ class EnvironmentOptions : public Options {
std::string heap_snapshot_signal;
std::string redirect_warnings;
std::string diagnostic_dir;
+ std::string bench_gc;
std::string bench_isolation = "process";
std::string bench_name_pattern;
std::string test_rerun_failures_path;
diff --git a/test/fixtures/bench-runner/gc.cjs b/test/fixtures/bench-runner/gc.cjs
new file mode 100644
index 00000000000..f94688707e5
--- /dev/null
+++ b/test/fixtures/bench-runner/gc.cjs
@@ -0,0 +1,10 @@
+'use strict';
+
+const { bench } = require('node:bench');
+
+const options = { samples: 3, warmup: 2 };
+if (process.env.BENCH_GC_OPTION) options.gc = process.env.BENCH_GC_OPTION;
+
+bench('gc', options, (b) => {
+ b.record({ detail: typeof globalThis.gc, duration_ns: 1n });
+});
diff --git a/test/parallel/test-bench-gc.js b/test/parallel/test-bench-gc.js
new file mode 100644
index 00000000000..5c01cca8659
--- /dev/null
+++ b/test/parallel/test-bench-gc.js
@@ -0,0 +1,126 @@
+// Flags: --expose-gc --experimental-bench --no-warnings
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const { spawnSync } = require('child_process');
+const fixtures = require('../common/fixtures');
+const { bench, createRunner } = require('node:bench');
+
+const spawnTimeout = common.platformTimeout(30_000);
+const fixture = fixtures.path('bench-runner/gc.cjs');
+
+function spawnNode(args, env = undefined) {
+ const result = spawnSync(process.execPath, args, {
+ __proto__: null,
+ encoding: 'utf8',
+ env: { ...process.env, ...env },
+ timeout: spawnTimeout,
+ });
+ assert.ifError(result.error);
+ assert.strictEqual(result.signal, null);
+ return result;
+}
+
+function parseRecords(output) {
+ return output.trim().split('\n').map((line) => JSON.parse(line));
+}
+
+function countForcedCollections(output) {
+ return output.split('\n').filter((line) => /^\[\d+:.*testing/.test(line))
+ .length;
+}
+
+// Invalid modes are rejected when declared or configured.
+for (const gc of ['always', true, null]) {
+ assert.throws(() => bench('invalid', { gc }, () => {}),
+ { code: 'ERR_INVALID_ARG_VALUE' });
+ assert.throws(() => createRunner({ gc }),
+ { code: 'ERR_INVALID_ARG_VALUE' });
+ assert.throws(() => createRunner().run({ gc }),
+ { code: 'ERR_INVALID_ARG_VALUE' });
+}
+
+(async () => {
+ // The effective mode is reported in the plan. run() overrides
+ // createRunner(), which overrides the benchmark option.
+ for (const { runner, run: runGc, expected } of [
+ { expected: ['none', 'once', 'sample'] },
+ { runner: 'sample', expected: ['sample', 'sample', 'sample'] },
+ { runner: 'sample', run: 'none', expected: ['none', 'none', 'none'] },
+ ]) {
+ const instance = createRunner({ gc: runner, yieldBetweenSamples: false });
+ for (const gc of [undefined, 'once', 'sample']) {
+ instance.bench(`gc ${gc}`, { gc, samples: 2, warmup: 1 }, (b) => {
+ b.record({ duration_ns: 1n });
+ });
+ }
+ const records = await instance.run({ gc: runGc }).toArray();
+ const plans = records.filter(({ type }) => type === 'bench:plan');
+ assert.deepStrictEqual(plans.map(({ data }) => data.gc), expected);
+ const completions = records.filter(({ type }) => type === 'bench:complete');
+ assert.deepStrictEqual(
+ completions.map(({ data }) => [data.error, data.samples.length]),
+ [[undefined, 2], [undefined, 2], [undefined, 2]]);
+ }
+
+ // A forced collection runs once before the first measured sample for
+ // 'once', and before every measured sample for 'sample'.
+ for (const { args = [], env, expected } of [
+ { expected: 0 },
+ { env: { BENCH_GC_OPTION: 'once' }, expected: 1 },
+ { env: { BENCH_GC_OPTION: 'sample' }, expected: 3 },
+ { args: ['--bench-gc=once'], expected: 1 },
+ { args: ['--bench-gc=none'], env: { BENCH_GC_OPTION: 'sample' },
+ expected: 0 },
+ ]) {
+ const result = spawnNode([
+ '--expose-gc', '--trace-gc', '--no-warnings', '--experimental-bench',
+ '--bench', '--bench-isolation=none', '--bench-reporter=json',
+ '--bench-reporter-destination=stderr', ...args, fixture,
+ ], env);
+ assert.strictEqual(result.status, 0, result.stderr);
+ assert.strictEqual(countForcedCollections(result.stdout), expected);
+ }
+
+ // Without --expose-gc, benchmarks that request collection fail.
+ {
+ const result = spawnNode([
+ '--no-warnings', '--experimental-bench', '--bench',
+ '--bench-isolation=none', '--bench-reporter=json',
+ '--bench-reporter-destination=stdout', fixture,
+ ], { BENCH_GC_OPTION: 'once' });
+ assert.strictEqual(result.status, 1);
+ const complete = parseRecords(result.stdout)
+ .find(({ type }) => type === 'bench:complete');
+ assert.strictEqual(complete.data.error.code, 'ERR_INVALID_STATE');
+ assert.match(complete.data.error.message,
+ /'once' gc mode requires the --expose-gc flag/);
+ }
+
+ // With process isolation, --bench-gc exposes gc to the child process.
+ {
+ const result = spawnNode([
+ '--no-warnings', '--experimental-bench', '--bench', '--bench-gc=sample',
+ '--bench-reporter=json', '--bench-reporter-destination=stdout', fixture,
+ ]);
+ assert.strictEqual(result.status, 0, result.stderr);
+ const records = parseRecords(result.stdout);
+ const plan = records.find(({ type }) => type === 'bench:plan');
+ assert.strictEqual(plan.data.gc, 'sample');
+ const complete = records.find(({ type }) => type === 'bench:complete');
+ assert.strictEqual(complete.data.error, undefined);
+ assert.deepStrictEqual(
+ complete.data.samples.map(({ detail }) => detail),
+ ['function', 'function', 'function']);
+ }
+
+ // Invalid --bench-gc values are rejected at startup.
+ {
+ const result = spawnNode([
+ '--experimental-bench', '--bench', '--bench-gc=always', fixture,
+ ]);
+ assert.strictEqual(result.status, 9);
+ assert.match(result.stderr, /invalid value for --bench-gc/);
+ }
+})().then(common.mustCall());