'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 };