Files
Filip Skokan f9defa61fa lib: fix benchmark record acknowledgement race
The child can receive an acknowledgement and send its next record before
the parent's send callback runs. Clear recordPending before sending the
acknowledgement to avoid rejecting that record as an invalid sequence.

Add a regression test that delays send callbacks until the child sends
its next record or closes.

Signed-off-by: Filip Skokan <panva.ip@gmail.com>
Assisted-by: Codex
PR-URL: https://github.com/nodejs/node/pull/66430
Reviewed-By: James M Snell <jasnell@gmail.com>
2026-10-08 09:58:29 +00:00

1320 lines
40 KiB
JavaScript

'use strict';
const {
ArrayFrom,
ArrayIsArray,
ArrayPrototypeFilter,
ArrayPrototypeIncludes,
ArrayPrototypeJoin,
ArrayPrototypePush,
ArrayPrototypePushApply,
ArrayPrototypeSlice,
ArrayPrototypeSome,
ArrayPrototypeSort,
MathMax,
NumberIsFinite,
NumberIsSafeInteger,
ObjectGetOwnPropertyDescriptor,
ObjectGetPrototypeOf,
ObjectKeys,
ObjectPrototype,
ObjectPrototypeHasOwnProperty,
ObjectValues,
Promise,
PromisePrototypeThen,
PromiseReject,
PromiseResolve,
PromiseWithResolvers,
RegExp,
SafeMap,
SafePromiseAllReturnVoid,
SafeSet,
String,
StringPrototypeIndexOf,
StringPrototypeReplaceAll,
StringPrototypeSlice,
StringPrototypeStartsWith,
StringPrototypeToUpperCase,
SymbolDispose,
uncurryThis,
} = primordials;
const { Buffer } = require('buffer');
const BufferToString = uncurryThis(Buffer.prototype.toString);
const { spawn } = require('child_process');
const { createWriteStream, statSync } = require('fs');
const { Glob } = require('internal/fs/glob');
const { getValidatedPath } = require('internal/fs/utils');
const {
BenchmarksStream,
} = require('internal/bench_runner/benchmarks_stream');
const {
configureRunScope,
createRunId,
runInFileScope,
runBenchmarks,
} = require('internal/bench_runner/harness');
const { deserializeError, serializeError } = require('internal/error_serdes');
const {
AbortError,
codes: {
ERR_ACCESS_DENIED,
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
ERR_INVALID_STATE,
},
} = require('internal/errors');
const { addAbortListener } = require('internal/events/abort_listener');
const {
getCLIOptionsInfo,
getOptionValue,
getOptionsAsFlagsFromBinding,
} = require('internal/options');
const permission = require('internal/process/permission');
const { TIMEOUT_MAX } = require('internal/timers');
const { kEmptyObject } = require('internal/util');
const {
validateAbortSignal,
validateArray,
validateObject,
validateStringWithoutNullBytes,
validateUint32,
} = require('internal/validators');
const { pathToFileURL } = require('internal/url');
const { pipeline } = require('stream/promises');
const { resolve, sep } = require('path');
const { clearTimeout, setTimeout } = require('timers');
const console = require('internal/console/global');
const esmLoader = require('internal/modules/esm/loader');
const { bigint: hrtime } = process.hrtime;
const kBuiltinDestinations = new SafeMap([
['stdout', process.stdout],
['stderr', process.stderr],
]);
const kBuiltinReporters = new SafeMap([
['json', 'internal/bench_runner/reporter/json'],
['spec', 'internal/bench_runner/reporter/spec'],
]);
const kChildAckMessageType = 'node:bench:ack';
const kChildMessageType = 'node:bench:record';
const kEventTypes = new SafeSet([
'bench:plan',
'bench:start',
'bench:sample',
'bench:complete',
'bench:diagnostic',
'bench:summary',
]);
const kFilterArgs = [
'--bench',
'--experimental-bench',
'--experimental-default-config-file',
];
const kFilterArgValues = [
'--bench-isolation',
'--bench-name-pattern',
'--bench-reporter',
'--bench-reporter-destination',
'--bench-samples',
'--bench-warmup',
'--config-file',
'--experimental-config-file',
];
const kIncompatibleExecArgv = new SafeSet([
'--build-sea',
'--build-snapshot',
'--build-snapshot-config',
'--check',
'--completion-bash',
'--eval',
'--experimental-sea-config',
'--help',
'--help-all',
'--input-type',
'--interactive',
'--print',
'--prof-process',
'--run',
'--test',
'--version',
'--v8-options',
'--watch',
'--watch-path',
'-c',
'-e',
'-h',
'-i',
'-p',
'-v',
]);
const kExecArgvWithValue = new SafeSet([
'--eval',
'--input-type',
'--print',
'--run',
'--watch-path',
'-e',
'-p',
]);
const kIPCEnvironmentVariables = new SafeSet([
'NODE_CHANNEL_FD',
'NODE_CHANNEL_SERIALIZATION_MODE',
'NODE_BENCH_CONTEXT',
'NODE_BENCH_FILE_RUN_ID',
'NODE_BENCH_RUN_ID',
'NODE_OPTIONS',
]);
const kForceKillDelay = 1_000;
function createBenchmarkFileList(patterns, cwd) {
if (patterns.length === 0) {
console.error('--bench requires at least one file or glob pattern');
return null;
}
const glob = new Glob(patterns, { __proto__: null, cwd });
const files = ArrayPrototypeFilter(glob.globSync(), (path) => {
try {
return statSync(resolve(cwd, path)).isFile();
} catch {
return false;
}
});
if (files.length === 0) {
console.error(`Could not find '${ArrayPrototypeJoin(patterns, ', ')}'`);
return null;
}
return ArrayPrototypeSort(files);
}
function createChildFileList(patterns, cwd) {
if (patterns.length !== 1) {
console.error('benchmark child process requires exactly one file');
return null;
}
const path = resolve(cwd, patterns[0]);
try {
if (statSync(path).isFile()) return [path];
} catch {
// Fall through to the standard not-found diagnostic.
}
console.error(`Could not find '${patterns[0]}'`);
return null;
}
function createFileScopes(files, options) {
const scopes = [];
for (let i = 0; i < files.length; i++) {
ArrayPrototypePush(scopes, {
__proto__: null,
entryFile: resolve(options.cwd, files[i]),
fileRunId: options.isChild && i === 0 &&
options.fileRunId !== undefined ? options.fileRunId : createRunId(),
});
}
return scopes;
}
function parseNamePattern(value) {
if (value.length === 0) return undefined;
try {
return new RegExp(value);
} catch (error) {
throw new ERR_INVALID_ARG_VALUE(
'--bench-name-pattern', value,
`is an invalid regular expression. ${error.message}`);
}
}
function parseCommandLine() {
const reporters = getOptionValue('--bench-reporter');
const destinations = getOptionValue('--bench-reporter-destination');
const isChild = process.env.NODE_BENCH_CONTEXT === 'child' &&
typeof process.send === 'function';
if (!isChild) {
if (reporters.length === 0 && destinations.length === 0) {
ArrayPrototypePush(reporters, 'spec');
}
if (reporters.length === 1 && destinations.length === 0) {
ArrayPrototypePush(destinations, 'stdout');
}
if (reporters.length !== destinations.length) {
throw new ERR_INVALID_ARG_VALUE(
'--bench-reporter', reporters,
'must match the number of specified ' +
"'--bench-reporter-destination'");
}
}
const samples = getOptionValue('[has_bench_samples]') ?
getOptionValue('--bench-samples') : undefined;
const warmup = getOptionValue('[has_bench_warmup]') ?
getOptionValue('--bench-warmup') : undefined;
if (samples !== undefined) {
validateUint32(samples, '--bench-samples', true);
}
if (warmup !== undefined) {
validateUint32(warmup, '--bench-warmup');
}
return {
__proto__: null,
cwd: process.cwd(),
destinations,
fileRunId: isChild && process.env.NODE_BENCH_FILE_RUN_ID ?
process.env.NODE_BENCH_FILE_RUN_ID : undefined,
isChild,
isolation: getOptionValue('--bench-isolation'),
namePattern: parseNamePattern(getOptionValue('--bench-name-pattern')),
namePatternSource: getOptionValue('--bench-name-pattern'),
reporters,
runId: isChild && process.env.NODE_BENCH_RUN_ID ?
process.env.NODE_BENCH_RUN_ID : undefined,
samples,
warmup,
};
}
async function loadReporter(name) {
const builtin = kBuiltinReporters.get(name);
let reporter;
if (builtin !== undefined) {
reporter = require(builtin);
} else {
let parentURL;
try {
parentURL = pathToFileURL(`${process.cwd()}${sep}`).href;
} catch {
parentURL = 'file:///';
}
const loader = esmLoader.getOrInitializeCascadedLoader();
reporter = await loader.import(name, parentURL, kEmptyObject);
}
reporter = reporter?.default ?? reporter;
if (reporter?.prototype &&
ObjectGetOwnPropertyDescriptor(reporter.prototype, 'constructor')) {
reporter = new reporter();
}
if (!reporter) {
throw new ERR_INVALID_ARG_VALUE(
'--bench-reporter', name, 'is not a valid reporter');
}
return reporter;
}
async function setupReporters(stream, reporters, destinations) {
const state = { __proto__: null, error: undefined, pending: [] };
try {
for (let i = 0; i < reporters.length; i++) {
const reporter = await loadReporter(reporters[i]);
const builtinDestination = kBuiltinDestinations.get(destinations[i]);
const destination = builtinDestination ??
createWriteStream(destinations[i], { __proto__: null, flush: true });
const pending = PromisePrototypeThen(pipeline(
stream,
reporter,
destination,
{ __proto__: null, end: builtinDestination === undefined },
), undefined, (error) => {
state.error ??= error;
});
ArrayPrototypePush(state.pending, pending);
}
} catch (error) {
stream.destroy();
await SafePromiseAllReturnVoid(state.pending);
throw error;
}
return state;
}
async function finishReporters(state) {
await SafePromiseAllReturnVoid(state.pending);
if (state.error !== undefined) throw state.error;
}
function emitRecord(stream, record) {
switch (record.type) {
case 'bench:plan':
return stream.plan(record.data);
case 'bench:start':
return stream.start(record.data);
case 'bench:sample':
return stream.sample(record.data);
case 'bench:complete':
return stream.complete(record.data);
case 'bench:diagnostic':
return stream.diagnostic(record.data);
case 'bench:summary':
return stream.summary(record.data);
default:
throw new ERR_INVALID_ARG_VALUE(
'benchmark event type', record.type, 'is not recognized');
}
}
function emitRecordAndWait(stream, record) {
if (stream.destroyed) {
return PromiseReject(stream.errored ??
new ERR_INVALID_STATE('benchmark output stream is closed'));
}
if (emitRecord(stream, record)) return undefined;
if (stream.destroyed) {
return PromiseReject(stream.errored ??
new ERR_INVALID_STATE('benchmark output stream is closed'));
}
return stream.waitForDrain();
}
function serializeRecord(record) {
if (record.data.error === undefined) return record;
return {
__proto__: null,
type: record.type,
data: {
__proto__: null,
...record.data,
error: serializeError(record.data.error),
},
};
}
function deserializeRecord(record) {
if (record.data.error === undefined) return record;
return {
__proto__: null,
type: record.type,
data: {
__proto__: null,
...record.data,
error: deserializeError(record.data.error),
},
};
}
function isStringArray(value) {
if (!ArrayIsArray(value)) return false;
for (let i = 0; i < value.length; i++) {
if (typeof value[i] !== 'string') return false;
}
return true;
}
function isContextDiagnostic(record) {
return record.type === 'bench:diagnostic' &&
(record.data.benchId !== undefined ||
record.data.phase !== undefined ||
record.data.index !== undefined ||
record.data.detail !== undefined);
}
function validateRecord(record) {
if (record === null || typeof record !== 'object' ||
!kEventTypes.has(record.type) || record.data === null ||
typeof record.data !== 'object' ||
typeof record.data.runId !== 'string' ||
(record.data.fileRunId !== null &&
typeof record.data.fileRunId !== 'string') ||
(record.data.entryFile !== null &&
typeof record.data.entryFile !== 'string')) {
throw new ERR_INVALID_ARG_VALUE(
'benchmark child message', record, 'is not a valid benchmark record');
}
const contextDiagnostic = isContextDiagnostic(record);
if ((record.type === 'bench:plan' || record.type === 'bench:start' ||
record.type === 'bench:sample' || record.type === 'bench:complete' ||
contextDiagnostic) &&
(typeof record.data.fileRunId !== 'string' ||
typeof record.data.benchId !== 'string' ||
(record.data.parentId !== null &&
typeof record.data.parentId !== 'string') ||
typeof record.data.name !== 'string' ||
!isStringArray(record.data.namePath))) {
throw new ERR_INVALID_ARG_VALUE(
'benchmark child message', record, 'is not a valid benchmark record');
}
if (record.type === 'bench:plan') {
const {
samples,
selected,
skip,
timeout,
warmup,
yieldBetweenSamples,
} = record.data;
if (!isStringArray(record.data.diagnosticChannels) ||
typeof record.data.file !== 'string' ||
!NumberIsSafeInteger(record.data.line) || record.data.line < 0 ||
!NumberIsSafeInteger(record.data.column) || record.data.column < 0 ||
!isStringArray(record.data.tags) ||
record.data.params === null ||
typeof record.data.params !== 'object' ||
ArrayIsArray(record.data.params) ||
(ObjectGetPrototypeOf(record.data.params) !== null &&
ObjectGetPrototypeOf(record.data.params) !== ObjectPrototype) ||
ArrayPrototypeSome(ObjectValues(record.data.params), (value) =>
typeof value !== 'string' && typeof value !== 'boolean' &&
(typeof value !== 'number' || !NumberIsFinite(value))) ||
!NumberIsSafeInteger(samples) || samples <= 0 ||
samples > 0xFFFFFFFF ||
!NumberIsSafeInteger(warmup) || warmup < 0 || warmup > 0xFFFFFFFF ||
(timeout !== null &&
(!NumberIsFinite(timeout) || timeout < 0 || timeout > TIMEOUT_MAX)) ||
typeof yieldBetweenSamples !== 'boolean' ||
typeof selected !== 'boolean' ||
(selected && skip !== undefined) ||
(!selected && skip !== true && typeof skip !== 'string')) {
throw new ERR_INVALID_ARG_VALUE(
'benchmark child plan', record.data,
'is not a valid benchmark plan');
}
}
if (contextDiagnostic &&
((record.data.phase !== 'warmup' &&
record.data.phase !== 'measurement') ||
!NumberIsSafeInteger(record.data.index) || record.data.index < 0 ||
record.data.index > 0xFFFFFFFF ||
!ObjectPrototypeHasOwnProperty(record.data, 'message') ||
(record.data.level !== 'info' && record.data.level !== 'warning') ||
typeof record.data.file !== 'string' ||
!NumberIsSafeInteger(record.data.line) || record.data.line < 0 ||
!NumberIsSafeInteger(record.data.column) || record.data.column < 0 ||
record.data.error !== undefined)) {
throw new ERR_INVALID_ARG_VALUE(
'benchmark child diagnostic', record.data,
'is not a valid benchmark diagnostic');
}
if (record.type === 'bench:summary') {
const { counts, duration_ns, success } = record.data;
if (typeof success !== 'boolean' || typeof duration_ns !== 'bigint' ||
counts === null || typeof counts !== 'object' ||
!NumberIsSafeInteger(counts.completed) || counts.completed < 0 ||
!NumberIsSafeInteger(counts.failed) || counts.failed < 0 ||
!NumberIsSafeInteger(counts.skipped) || counts.skipped < 0 ||
!NumberIsSafeInteger(counts.total) || counts.total < 0) {
throw new ERR_INVALID_ARG_VALUE(
'benchmark child summary', record.data,
'is not a valid benchmark summary');
}
}
return record;
}
let nextChildRecordId = 0;
let listeningForAcks = false;
const pendingChildRecordAcks = new SafeMap();
function listenForAcks() {
if (listeningForAcks) return;
listeningForAcks = true;
process.on('message', (message) => {
if (message?.type !== kChildAckMessageType) return;
const pending = pendingChildRecordAcks.get(message.id);
if (pending === undefined) return;
pendingChildRecordAcks.delete(message.id);
pending.resolve();
});
process.once('disconnect', () => {
const error = new ERR_INVALID_STATE(
'benchmark IPC channel closed before acknowledging records');
for (const pending of pendingChildRecordAcks.values()) {
pending.reject(error);
}
pendingChildRecordAcks.clear();
});
}
function sendRecord(record) {
listenForAcks();
const id = nextChildRecordId++;
const acknowledged = PromiseWithResolvers();
pendingChildRecordAcks.set(id, acknowledged);
try {
process.send({
__proto__: null,
id,
type: kChildMessageType,
record: serializeRecord(record),
}, undefined, undefined, (error) => {
if (error) {
pendingChildRecordAcks.delete(id);
acknowledged.reject(error);
}
});
} catch (error) {
pendingChildRecordAcks.delete(id);
acknowledged.reject(error);
}
return acknowledged.promise;
}
function sendAck(child, id) {
return new Promise((resolve, reject) => {
if (!child.connected) {
reject(new ERR_INVALID_STATE(
'benchmark child disconnected before acknowledgement'));
return;
}
child.send({ __proto__: null, id, type: kChildAckMessageType },
undefined, undefined, (error) => {
if (error) reject(error);
else resolve();
});
});
}
async function loadUserImports(options) {
const parentURL = pathToFileURL(`${options.cwd}${sep}`).href;
const loader = esmLoader.getOrInitializeCascadedLoader();
const userImports = getOptionValue('--import');
for (let i = 0; i < userImports.length; i++) {
await loader.import(userImports[i], parentURL, kEmptyObject);
}
return { __proto__: null, loader, parentURL };
}
async function loadBenchmarkFiles(files, options, modules, onRecord) {
const { loader, parentURL } = modules;
let loadFailed = false;
for (let i = 0; i < files.length; i++) {
const file = resolve(options.cwd, files[i]);
try {
await runInFileScope(options.fileScopes[i], () =>
loader.import(pathToFileURL(file), parentURL, kEmptyObject));
} catch (error) {
loadFailed = true;
await onRecord({
__proto__: null,
type: 'bench:diagnostic',
data: {
__proto__: null,
runId: options.runId,
...options.fileScopes[i],
message: error?.message ?? String(error),
error,
level: 'error',
file,
},
});
}
}
const stream = runBenchmarks({
__proto__: null,
namePattern: options.namePattern,
samples: options.samples,
warmup: options.warmup,
}, true);
let summary;
for await (const record of stream) {
if (record.type === 'bench:summary' && (process.exitCode ?? 0) !== 0) {
const scope = files.length === 1 ? options.fileScopes[0] : {
__proto__: null,
entryFile: null,
fileRunId: null,
};
await onRecord({
__proto__: null,
type: 'bench:diagnostic',
data: {
__proto__: null,
runId: options.runId,
...scope,
message: `Benchmark process set exit code ${process.exitCode}`,
level: 'error',
file: files.length === 1 ?
resolve(options.cwd, files[0]) : null,
},
});
}
if (record.type === 'bench:summary' &&
(loadFailed || (process.exitCode ?? 0) !== 0)) {
record.data.success = false;
}
if (record.type === 'bench:summary') {
record.data.file = files.length === 1 ?
resolve(options.cwd, files[0]) : null;
record.data.fileRunId = files.length === 1 ?
options.fileScopes[0].fileRunId : null;
record.data.entryFile = files.length === 1 ?
options.fileScopes[0].entryFile : null;
summary = record.data;
}
await onRecord(record);
}
return summary;
}
function filterExecArgv(arg, index, args) {
const name = getOptionName(arg);
return !ArrayPrototypeIncludes(kFilterArgs, name) &&
!ArrayPrototypeSome(kFilterArgValues, (option) => {
return name === option ||
(option !== '--config-file' &&
option !== '--experimental-config-file' &&
index > 0 &&
StringPrototypeIndexOf(args[index - 1], '=') === -1 &&
getOptionName(args[index - 1]) === option);
});
}
function getOptionName(arg) {
const equals = StringPrototypeIndexOf(arg, '=');
const name = equals === -1 ? arg : StringPrototypeSlice(arg, 0, equals);
return StringPrototypeReplaceAll(name, '_', '-');
}
function getRunFileOptionName(arg) {
if (StringPrototypeStartsWith(arg, '-') &&
!StringPrototypeStartsWith(arg, '--') && arg.length > 2) {
const shortName = StringPrototypeSlice(arg, 0, 2);
if (kIncompatibleExecArgv.has(shortName)) return shortName;
}
return getOptionName(arg);
}
function filterRunFileExecArgv(arg, index, args) {
if (!filterExecArgv(arg, index, args)) return false;
if (!StringPrototypeStartsWith(arg, '-') || arg === '-' || arg === '--') {
return false;
}
const name = getRunFileOptionName(arg);
if (kIncompatibleExecArgv.has(name)) return false;
if (index > 0) {
const previous = getRunFileOptionName(args[index - 1]);
if (kExecArgvWithValue.has(previous) &&
StringPrototypeIndexOf(args[index - 1], '=') === -1) {
return false;
}
}
return true;
}
function runFileOptionRequiresValue(arg) {
const equals = StringPrototypeIndexOf(arg, '=');
let name = getRunFileOptionName(arg);
const { aliases, options } = getCLIOptionsInfo();
let info = options.get(name);
if (info === undefined) {
const alias = aliases.get(name);
if (alias !== undefined) info = options.get(alias[0]);
}
if (info === undefined && StringPrototypeStartsWith(name, '--no-')) {
name = `--${StringPrototypeSlice(name, 5)}`;
info = options.get(name);
}
return info !== undefined && info.type >= 3 &&
(equals === -1 || equals === arg.length - 1);
}
const kOptionAliases = new SafeMap([
['--loader', '--experimental-loader'],
['-C', '--conditions'],
['-r', '--require'],
]);
function getInheritedChildArgs() {
const nodeOptions = getOptionsAsFlagsFromBinding();
const args = ArrayPrototypeFilter(nodeOptions, filterExecArgv);
const nodeOptionNames = new SafeSet();
for (let i = 0; i < nodeOptions.length; i++) {
nodeOptionNames.add(getOptionName(nodeOptions[i]));
}
const unknownExecArgv = ArrayPrototypeFilter(
process.execArgv,
(arg, index, values) => {
if (!filterExecArgv(arg, index, values)) return false;
const optionName = getOptionName(arg);
const name = kOptionAliases.get(optionName) ?? optionName;
if (nodeOptionNames.has(name)) return false;
if (!StringPrototypeStartsWith(arg, '-') && index > 0) {
const previousName = getOptionName(values[index - 1]);
const previous = kOptionAliases.get(previousName) ?? previousName;
if (nodeOptionNames.has(previous)) return false;
}
return true;
},
);
ArrayPrototypePushApply(args, unknownExecArgv);
// Option serialization omits port 0, which would otherwise become 9229.
if (process.debugPort === 0) ArrayPrototypePush(args, '--inspect-port=0');
return args;
}
function getChildArgs(path, options) {
const args = options.execArgv === undefined ?
getInheritedChildArgs() : ArrayPrototypeSlice(options.execArgv);
ArrayPrototypePush(
args, '--experimental-bench', '--bench', '--bench-isolation=none');
if (options.namePatternSource.length > 0) {
ArrayPrototypePush(
args, `--bench-name-pattern=${options.namePatternSource}`);
}
if (options.samples !== undefined) {
ArrayPrototypePush(args, `--bench-samples=${options.samples}`);
}
if (options.warmup !== undefined) {
ArrayPrototypePush(args, `--bench-warmup=${options.warmup}`);
}
ArrayPrototypePush(args, '--', resolve(options.cwd, path));
return args;
}
async function runChild(path, options, scope, onRecord) {
if (options.signal?.aborted) {
return {
__proto__: null,
aborted: true,
error: new AbortError(undefined, {
__proto__: null,
cause: options.signal.reason,
}),
};
}
const resource = resolve(options.cwd, path);
if (permission.isEnabled() &&
!permission.has('fs.read', resource) &&
!permission.isAuditMode()) {
throw new ERR_ACCESS_DENIED(
'Access to this API has been restricted. Use --allow-fs-read to manage permissions.',
'FileSystemRead',
resource,
);
}
const child = spawn(
options.execPath ?? process.execPath,
getChildArgs(path, options),
{
__proto__: null,
cwd: options.cwd,
env: {
__proto__: null,
...(options.env ?? process.env),
NODE_BENCH_CONTEXT: 'child',
NODE_BENCH_FILE_RUN_ID: scope.fileRunId,
NODE_BENCH_RUN_ID: options.runId,
},
serialization: 'advanced',
stdio: ['inherit', 'pipe', 'pipe', 'ipc'],
},
);
let childClosed = false;
let forceKillTimer;
const terminateChild = () => {
if (childClosed) return;
child.kill();
forceKillTimer ??= setTimeout(() => {
child.kill('SIGKILL');
}, kForceKillDelay);
};
const closed = PromiseWithResolvers();
let closeTracked = false;
let spawnError;
try {
child.once('close', (...status) => closed.resolve(status));
closeTracked = true;
child.once('error', (error) => {
spawnError ??= error;
terminateChild();
});
} catch (error) {
terminateChild();
if (closeTracked) {
try {
await closed.promise;
} catch {
// Preserve the setup error.
}
}
throw error;
}
let protocolError;
let abortError;
let aborted = false;
let activeBenchId;
let plansComplete = false;
let recordPending = false;
let summaryReceived = false;
let abortListener;
const closeListener = () => {
terminateChild();
};
try {
if (options.signal !== undefined) {
abortListener = addAbortListener(options.signal, () => {
aborted = true;
abortError = new AbortError(undefined, {
__proto__: null,
cause: options.signal.reason,
});
terminateChild();
});
}
options.output?.once('close', closeListener);
if (options.output?.destroyed) closeListener();
} catch (error) {
terminateChild();
try {
await closed.promise;
} catch {
// Preserve the setup error.
}
childClosed = true;
if (forceKillTimer !== undefined) clearTimeout(forceKillTimer);
abortListener?.[SymbolDispose]();
options.output?.removeListener('close', closeListener);
throw error;
}
const pendingRecords = new SafeSet();
const handleRecord = (record) => {
if (protocolError !== undefined) return;
try {
return onRecord(record);
} catch (error) {
protocolError = error;
terminateChild();
}
};
const trackPending = (pending, source) => {
if (pending === undefined) return;
source?.pause();
const tracked = PromisePrototypeThen(PromiseResolve(pending), () => {
pendingRecords.delete(tracked);
source?.resume();
}, (error) => {
pendingRecords.delete(tracked);
if (!aborted) protocolError ??= error;
terminateChild();
});
pendingRecords.add(tracked);
};
const reportOutput = (source, stream, message) => {
const pending = handleRecord({
__proto__: null,
type: 'bench:diagnostic',
data: {
__proto__: null,
runId: options.runId,
...scope,
message,
level: 'info',
file: path,
stream,
},
});
trackPending(pending, source);
};
try {
child.stdout.setEncoding('utf8');
child.stderr.setEncoding('utf8');
child.stdout.on('data', (data) => {
reportOutput(child.stdout, 'stdout', data);
});
child.stderr.on('data', (data) => {
reportOutput(child.stderr, 'stderr', data);
});
child.on('message', (message) => {
if (message?.type !== kChildMessageType) return;
try {
if (!NumberIsSafeInteger(message.id) || message.id < 0 || recordPending) {
throw new ERR_INVALID_ARG_VALUE(
'benchmark child message', message,
'does not have a valid record sequence');
}
recordPending = true;
const record = deserializeRecord(validateRecord(message.record));
const contextDiagnostic = isContextDiagnostic(record);
if (summaryReceived || (record.type === 'bench:plan' && plansComplete) ||
(record.type === 'bench:start' && activeBenchId !== undefined) ||
((record.type === 'bench:sample' || contextDiagnostic) &&
activeBenchId !== record.data.benchId) ||
(record.type === 'bench:complete' && activeBenchId !== undefined &&
activeBenchId !== record.data.benchId) ||
(record.type === 'bench:summary' && activeBenchId !== undefined)) {
throw new ERR_INVALID_ARG_VALUE(
'benchmark child message', message,
'does not have a valid lifecycle sequence');
}
if (record.type !== 'bench:plan' &&
record.type !== 'bench:diagnostic') {
plansComplete = true;
}
if (record.type === 'bench:start') {
activeBenchId = record.data.benchId;
} else if (record.type === 'bench:complete' &&
activeBenchId === record.data.benchId) {
activeBenchId = undefined;
}
if (record.type === 'bench:summary') summaryReceived = true;
record.data.runId = options.runId;
record.data.fileRunId = scope.fileRunId;
if (record.data.entryFile !== null) {
record.data.entryFile = scope.entryFile;
}
const pending = handleRecord(record);
if (protocolError === undefined) {
trackPending(PromisePrototypeThen(PromiseResolve(pending), () => {
// The child can receive the acknowledgement and send its next
// record before the send callback runs.
recordPending = false;
return sendAck(child, message.id);
}));
}
} catch (error) {
if (!aborted) protocolError ??= error;
terminateChild();
}
});
} catch (error) {
terminateChild();
try {
await closed.promise;
} catch {
// Preserve the setup error.
}
childClosed = true;
if (forceKillTimer !== undefined) clearTimeout(forceKillTimer);
abortListener?.[SymbolDispose]();
options.output?.removeListener('close', closeListener);
throw error;
}
let status;
try {
status = await closed.promise;
} finally {
childClosed = true;
if (forceKillTimer !== undefined) clearTimeout(forceKillTimer);
abortListener?.[SymbolDispose]();
options.output?.removeListener('close', closeListener);
}
const { 0: code, 1: signal } = status;
await SafePromiseAllReturnVoid(ArrayFrom(pendingRecords));
if (!aborted && spawnError !== undefined) throw spawnError;
if (!aborted && protocolError !== undefined) throw protocolError;
return {
__proto__: null,
aborted,
code,
error: abortError,
signal,
};
}
async function runIsolated(files, options, output) {
const counts = {
__proto__: null,
completed: 0,
failed: 0,
skipped: 0,
total: 0,
};
const start = hrtime();
let success = true;
for (let i = 0; i < files.length; i++) {
const path = files[i];
const scope = options.fileScopes[i];
const observedCounts = {
__proto__: null,
completed: 0,
failed: 0,
skipped: 0,
total: 0,
};
let childSummary;
let result;
try {
result = await runChild(path, options, scope, (record) => {
if (record.type === 'bench:summary') {
childSummary = record.data;
return;
}
if (record.type === 'bench:plan') observedCounts.total++;
if (record.type === 'bench:complete') {
if (ObjectPrototypeHasOwnProperty(record.data, 'error')) {
observedCounts.failed++;
} else if (ObjectPrototypeHasOwnProperty(record.data, 'skip')) {
observedCounts.skipped++;
} else {
observedCounts.completed++;
}
}
return emitRecordAndWait(output, record);
});
} catch (error) {
success = false;
await emitRecordAndWait(output, {
__proto__: null,
type: 'bench:diagnostic',
data: {
__proto__: null,
runId: options.runId,
...scope,
message: error.message,
error,
level: 'error',
file: path,
},
});
counts.completed += observedCounts.completed;
counts.failed += observedCounts.failed;
counts.skipped += observedCounts.skipped;
counts.total += MathMax(
observedCounts.total,
observedCounts.completed + observedCounts.failed +
observedCounts.skipped);
continue;
}
if (result.aborted) {
success = false;
await emitRecordAndWait(output, {
__proto__: null,
type: 'bench:diagnostic',
data: {
__proto__: null,
runId: options.runId,
...scope,
message: result.error.message,
error: result.error,
level: 'error',
file: path,
},
});
}
if (childSummary !== undefined) {
success &&= childSummary.success;
counts.completed += childSummary.counts.completed;
counts.failed += childSummary.counts.failed;
counts.skipped += childSummary.counts.skipped;
counts.total += childSummary.counts.total;
} else {
counts.completed += observedCounts.completed;
counts.failed += observedCounts.failed;
counts.skipped += observedCounts.skipped;
counts.total += MathMax(
observedCounts.total,
observedCounts.completed + observedCounts.failed +
observedCounts.skipped);
}
if (!result.aborted && (childSummary === undefined || result.code !== 0 ||
result.signal !== null)) {
success = false;
if (childSummary === undefined || childSummary.success) {
const status = result.signal === null ?
`exit code ${result.code}` : `signal ${result.signal}`;
await emitRecordAndWait(output, {
__proto__: null,
type: 'bench:diagnostic',
data: {
__proto__: null,
runId: options.runId,
...scope,
message: `Benchmark file '${path}' failed with ${status}`,
level: 'error',
file: path,
},
});
}
}
}
const scope = files.length === 1 ? options.fileScopes[0] : null;
const summary = {
__proto__: null,
runId: options.runId,
fileRunId: scope?.fileRunId ?? null,
entryFile: scope?.entryFile ?? null,
success: success && (options.useProcessExitCode === false ||
(process.exitCode ?? 0) === 0),
counts,
duration_ns: hrtime() - start,
file: files.length === 1 ? resolve(options.cwd, files[0]) : null,
};
await emitRecordAndWait(output, {
__proto__: null,
type: 'bench:summary',
data: summary,
});
return summary;
}
function runFile(path, options = kEmptyObject) {
path = getValidatedPath(path);
if (typeof path !== 'string') path = BufferToString(path);
const file = resolve(path);
validateObject(options, 'options');
const {
env = process.env,
execArgv,
signal,
} = options;
let childExecArgv;
if (execArgv !== undefined) {
validateArray(execArgv, 'options.execArgv');
childExecArgv = [];
const length = execArgv.length;
for (let i = 0; i < length; i++) {
const arg = execArgv[i];
validateStringWithoutNullBytes(arg, `options.execArgv[${i}]`);
if (!StringPrototypeStartsWith(arg, '-') || arg === '-' || arg === '--') {
throw new ERR_INVALID_ARG_VALUE(
`options.execArgv[${i}]`, arg,
'must be a Node.js command-line option');
}
if (kIncompatibleExecArgv.has(getRunFileOptionName(arg))) {
throw new ERR_INVALID_ARG_VALUE(
`options.execArgv[${i}]`, arg,
'is not compatible with benchmark execution');
}
if (runFileOptionRequiresValue(arg)) {
const equals = StringPrototypeIndexOf(arg, '=');
if (equals !== -1) {
throw new ERR_INVALID_ARG_VALUE(
`options.execArgv[${i}]`, arg,
'must include a non-empty value');
}
if (i + 1 >= length) {
throw new ERR_INVALID_ARG_VALUE(
`options.execArgv[${i}]`, arg,
'must be followed by a value');
}
ArrayPrototypePush(childExecArgv, arg);
i++;
const value = execArgv[i];
validateStringWithoutNullBytes(value, `options.execArgv[${i}]`);
if (value.length === 0) {
throw new ERR_INVALID_ARG_VALUE(
`options.execArgv[${i}]`, value,
'must not be empty');
}
ArrayPrototypePush(childExecArgv, value);
continue;
}
if (!StringPrototypeStartsWith(arg, '--') &&
StringPrototypeIndexOf(arg, '=') !== -1) {
throw new ERR_INVALID_ARG_VALUE(
`options.execArgv[${i}]`, arg,
'must not use = with a short option');
}
ArrayPrototypePush(childExecArgv, arg);
}
if (ArrayPrototypeFilter(childExecArgv, filterExecArgv).length !==
childExecArgv.length) {
throw new ERR_INVALID_ARG_VALUE(
'options.execArgv', childExecArgv,
'must not contain benchmark runner options');
}
} else {
childExecArgv = ArrayPrototypeFilter(
getInheritedChildArgs(), filterRunFileExecArgv);
}
validateObject(env, 'options.env');
const childEnv = { __proto__: null };
const envKeys = ObjectKeys(env);
for (let i = 0; i < envKeys.length; i++) {
const key = envKeys[i];
validateStringWithoutNullBytes(key, 'options.env key');
const value = env[key];
if (value === undefined) continue;
validateStringWithoutNullBytes(value, `options.env.${key}`);
if (kIPCEnvironmentVariables.has(StringPrototypeToUpperCase(key))) continue;
childEnv[key] = value;
}
validateAbortSignal(signal, 'options.signal');
if (signal !== undefined &&
(typeof signal.addEventListener !== 'function' ||
typeof signal.removeEventListener !== 'function')) {
throw new ERR_INVALID_ARG_TYPE('options.signal', 'AbortSignal', signal);
}
const runId = createRunId();
const scope = {
__proto__: null,
entryFile: file,
fileRunId: createRunId(),
};
const output = new BenchmarksStream();
const runOptions = {
__proto__: null,
cwd: process.cwd(),
env: childEnv,
execPath: process.execPath,
execArgv: childExecArgv,
fileScopes: [scope],
namePatternSource: '',
output,
runId,
signal,
useProcessExitCode: false,
};
const execution = PromisePrototypeThen(PromiseResolve(), () =>
(output.destroyed ? undefined : runIsolated([file], runOptions, output)));
PromisePrototypeThen(execution, () => output.end(),
(error) => output.destroy(error));
return output;
}
async function run(patterns) {
const options = parseCommandLine();
const files = options.isChild ?
createChildFileList(patterns, options.cwd) :
createBenchmarkFileList(patterns, options.cwd);
if (files === null) return { __proto__: null, success: false };
options.runId ??= createRunId();
options.fileScopes = createFileScopes(files, options);
const scope = files.length === 1 ? options.fileScopes[0] : {
__proto__: null,
entryFile: null,
fileRunId: options.runId,
};
configureRunScope({
__proto__: null,
runId: options.runId,
...scope,
});
if (options.isChild) {
try {
const modules = await loadUserImports(options);
return await loadBenchmarkFiles(
files, options, modules, sendRecord);
} finally {
if (process.connected) process.disconnect();
}
}
const output = new BenchmarksStream();
let executionError;
let reporterState;
let summary;
try {
options.modules = await loadUserImports(options);
reporterState = await setupReporters(
output, options.reporters, options.destinations);
if (options.isolation === 'process') {
summary = await runIsolated(files, options, output);
} else {
summary = await loadBenchmarkFiles(
files,
options,
options.modules,
(record) => emitRecordAndWait(output, record),
);
}
} catch (error) {
executionError = error;
} finally {
output.end();
}
if (reporterState !== undefined) await finishReporters(reporterState);
if (executionError !== undefined) throw executionError;
return summary;
}
module.exports = { run, runFile };