'use strict';
const {
ArrayPrototypePush,
ArrayPrototypeReverse,
ArrayPrototypeSlice,
BigInt,
FunctionPrototypeCall,
JSONStringify,
MathCeil,
Number,
Promise,
PromisePrototypeThen,
PromiseResolve,
PromiseWithResolvers,
ReflectApply,
RegExp,
RegExpPrototypeExec,
SafeMap,
SafePromiseRace,
String,
SymbolDispose,
} = primordials;
const { getCallerLocation } = internalBinding('util');
const { exitCodes: { kGenericUserError } } = internalBinding('errors');
const { AsyncLocalStorage } = require('async_hooks');
const {
subscribe: subscribeToChannel,
unsubscribe: unsubscribeFromChannel,
} = require('diagnostics_channel');
const { AbortController } = require('internal/abort_controller');
const {
AbortError,
codes: {
ERR_INVALID_ARG_TYPE,
ERR_INVALID_STATE,
ERR_OPERATION_FAILED,
},
} = require('internal/errors');
const { addAbortListener } = require('internal/events/abort_listener');
const {
kEmptyObject,
} = require('internal/util');
const { isRegExp } = require('internal/util/types');
const {
validateAbortSignal,
validateBoolean,
validateFunction,
validateObject,
validateUint32,
} = require('internal/validators');
const { queueMicrotask } = require('internal/process/task_queues');
const { getOptionValue } = require('internal/options');
const { clearTimeout, setImmediate, setTimeout } = require('timers');
const {
Bench,
BenchContext,
Suite,
normalizeArgs,
summarizeSamples,
} = require('internal/bench_runner/benchmark');
const {
BenchmarksStream,
} = require('internal/bench_runner/benchmarks_stream');
const { bigint: hrtime } = process.hrtime;
const kCancellationGracePeriod = 100;
const kHookNames = ['after', 'afterEach', 'before', 'beforeEach'];
const kIsCliRunner = getOptionValue('--bench');
let nextRunId = 0;
function createRunId() {
return `${process.pid}:${String(hrtime())}:${nextRunId++}`;
}
function eventLoopTurn() {
return new Promise((resolve) => setImmediate(resolve));
}
async function settlesPromptly(work) {
const timeout = PromiseWithResolvers();
const timer = setTimeout(
() => timeout.resolve(false), kCancellationGracePeriod);
try {
return await SafePromiseRace([
PromisePrototypeThen(work, () => true, () => true),
timeout.promise,
]);
} finally {
clearTimeout(timer);
}
}
function createAbortError(signal) {
return new AbortError(undefined, { __proto__: null, cause: signal.reason });
}
function createTimeoutError(benchmark) {
return new ERR_OPERATION_FAILED(
`Benchmark timed out after ${benchmark.timeout}ms`);
}
class Harness {
#abortReason = null;
#autoRun;
#buildPromises = [];
#cliRun = false;
#duplicateErrors = new SafeMap();
#explicitRun = false;
#fileScopeStorage = new AsyncLocalStorage();
#hasOnly = false;
#runPromise = null;
#scheduled = false;
#starting = false;
#storage = new AsyncLocalStorage();
#yieldBetweenSamples;
constructor(options = kEmptyObject) {
validateObject(options, 'options');
const {
autoRun = true,
yieldBetweenSamples = true,
} = options;
validateBoolean(autoRun, 'options.autoRun');
validateBoolean(
yieldBetweenSamples, 'options.yieldBetweenSamples');
this.#autoRun = autoRun;
this.#yieldBetweenSamples = yieldBetweenSamples;
this.runId = createRunId();
this.fileRunId = this.runId;
this.entryFile = process.argv?.[1] ?? null;
this.stream = new BenchmarksStream();
this.state = 'collecting';
this.namePattern = null;
this.outerSignal = undefined;
this.samples = undefined;
this.success = true;
this.warmup = undefined;
this.counts = {
__proto__: null,
completed: 0,
failed: 0,
skipped: 0,
total: 0,
};
this.root = new Suite(
this,
null,
'<root>',
kEmptyObject,
undefined,
undefined,
true,
);
}
getFileScope() {
return this.#fileScopeStorage.getStore() ?? null;
}
runInFileScope(scope, fn) {
return this.#fileScopeStorage.run(scope, fn);
}
setRunScope({ entryFile, fileRunId, runId }) {
if (this.state !== 'collecting') {
throw new ERR_INVALID_STATE(
'benchmark execution scope cannot change after execution has started');
}
this.entryFile = entryFile;
this.fileRunId = fileRunId;
this.runId = runId;
}
#ensureCollecting() {
if (this.state === 'building' &&
this.#storage.getStore() instanceof Suite) return;
if (!this.#explicitRun && !this.#starting &&
this.state === 'collecting') return;
throw new ERR_INVALID_STATE(
'benchmarks cannot be declared after execution has started');
}
#getParent() {
const current = this.#storage.getStore();
return current instanceof Suite ? current : this.root;
}
createBench(name, options, fn, overrides = kEmptyObject) {
this.#ensureCollecting();
const normalized = normalizeArgs('benchmark', name, options, fn);
const parent = this.#getParent();
const benchmark = new Bench(
this,
parent,
normalized.name,
{ __proto__: null, ...normalized.options, ...overrides },
normalized.fn,
overrides.loc,
);
ArrayPrototypePush(parent.children, benchmark);
this.#schedule();
return parent.isRoot ? benchmark.completion.promise : PromiseResolve();
}
createSuite(name, options, fn, overrides = kEmptyObject) {
this.#ensureCollecting();
const normalized = normalizeArgs('suite', name, options, fn);
const parent = this.#getParent();
const suite = new Suite(
this,
parent,
normalized.name,
{ __proto__: null, ...normalized.options, ...overrides },
normalized.fn,
overrides.loc,
);
ArrayPrototypePush(parent.children, suite);
this.#buildSuite(suite);
this.#schedule();
return parent.isRoot ? suite.completion.promise : PromiseResolve();
}
createHook(name, fn, options = kEmptyObject) {
this.#ensureCollecting();
validateFunction(fn, 'hook function');
validateObject(options, 'options');
const parent = this.#getParent();
ArrayPrototypePush(parent.hooks[name], {
__proto__: null,
fileScope: parent.isRoot ? this.getFileScope() : parent.fileScope,
fn,
loc: getCallerLocation(),
});
this.#schedule();
}
#buildSuite(suite) {
let result;
try {
result = suite.runInAsyncScope(() => this.#storage.run(
suite,
() => FunctionPrototypeCall(suite.fn),
));
} catch (error) {
suite.buildError = error;
result = undefined;
}
suite.buildPromise = PromisePrototypeThen(
PromiseResolve(result),
undefined,
(error) => {
suite.buildError = error;
},
);
ArrayPrototypePush(this.#buildPromises, suite.buildPromise);
}
configure(options = kEmptyObject) {
validateObject(options, 'options');
const {
namePattern,
samples,
signal,
warmup,
yieldBetweenSamples,
} = options;
let nextNamePattern = this.namePattern;
let nextSamples = this.samples;
let nextWarmup = this.warmup;
let nextYieldBetweenSamples = this.#yieldBetweenSamples;
if (namePattern !== undefined) {
if (typeof namePattern === 'string') {
nextNamePattern = new RegExp(namePattern);
} else if (isRegExp(namePattern)) {
nextNamePattern = new RegExp(namePattern);
} else {
throw new ERR_INVALID_ARG_TYPE(
'options.namePattern', ['string', 'RegExp'], namePattern);
}
}
if (samples !== undefined) {
validateUint32(samples, 'options.samples', true);
nextSamples = samples;
}
validateAbortSignal(signal, 'options.signal');
if (warmup !== undefined) {
validateUint32(warmup, 'options.warmup');
nextWarmup = warmup;
}
if (yieldBetweenSamples !== undefined) {
validateBoolean(
yieldBetweenSamples, 'options.yieldBetweenSamples');
nextYieldBetweenSamples = yieldBetweenSamples;
}
this.namePattern = nextNamePattern;
this.samples = nextSamples;
this.outerSignal = signal;
this.warmup = nextWarmup;
this.#yieldBetweenSamples = nextYieldBetweenSamples;
}
#ensureCanRun() {
if (this.#explicitRun || this.#starting || this.state !== 'collecting') {
throw new ERR_INVALID_STATE('benchmark execution has already started');
}
}
run(options = kEmptyObject, force = false) {
if (kIsCliRunner && !force) {
throw new ERR_INVALID_STATE(
'run() cannot be called from a file run with --bench');
}
this.#ensureCanRun();
this.#starting = true;
try {
this.configure(options);
this.#explicitRun = true;
this.#cliRun = force;
} finally {
this.#starting = false;
}
this.#schedule(force, true);
return this.stream;
}
#schedule(force = false, explicit = false) {
if (!force && kIsCliRunner) return;
if (!explicit && !this.#autoRun) return;
if (this.#scheduled) return;
this.#scheduled = true;
queueMicrotask(() => {
if (this.#runPromise === null) {
if (!this.#explicitRun) this.stream.resume();
this.#runPromise = PromisePrototypeThen(
this.#execute(), undefined, (error) => this.#recover(error));
}
});
}
async #recover(error) {
this.#settleSubtree(this.root, error);
if (this.state !== 'finished') {
try {
await this.#diagnostic(error, undefined, 'error');
} catch {
// The stream can fail while reporting the original error.
}
}
try {
await this.#finish();
} catch {
// Stream failure has already been reported to its consumer.
}
}
#settleSubtree(node, error) {
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i];
if (child.finished) continue;
if (child instanceof Suite) {
this.#settleSubtree(child, error);
child.finished = true;
child.completion.resolve();
} else {
this.success = false;
this.counts.failed++;
const result = this.#createResult(
child, [], { __proto__: null, error });
child.finished = true;
child.result = result;
child.completion.resolve(result);
}
child.emitDestroy();
}
}
async #waitForBuild() {
for (let i = 0; i < this.#buildPromises.length; i++) {
await this.#buildPromises[i];
}
}
#walk(node, callback) {
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i];
callback(child);
if (child instanceof Suite) this.#walk(child, callback);
}
}
#prepare() {
const identities = new SafeMap();
this.#walk(this.root, (node) => {
if (node.only) this.#hasOnly = true;
if (!(node instanceof Bench)) return;
this.counts.total++;
const identity = JSONStringify([
this.#getRecordScope(node).fileRunId,
node.benchId,
]);
const existing = identities.get(identity);
if (existing === undefined) {
identities.set(identity, node);
} else {
this.#duplicateErrors.set(node, new ERR_INVALID_STATE(
`duplicate benchmark identity for "${node.fullName}"`));
}
});
}
async #emitPlans() {
const benchmarks = [];
this.#walk(this.root, (node) => {
if (node instanceof Bench) ArrayPrototypePush(benchmarks, node);
});
for (let i = 0; i < benchmarks.length; i++) {
const benchmark = benchmarks[i];
const skip = this.#getSkip(benchmark);
const data = {
__proto__: null,
...this.#getRecordScope(benchmark),
benchId: benchmark.benchId,
parentId: benchmark.parentId,
name: benchmark.name,
namePath: ArrayPrototypeSlice(benchmark.namePath),
diagnosticChannels: ArrayPrototypeSlice(benchmark.diagnosticChannels),
file: benchmark.loc.file,
line: benchmark.loc.line,
column: benchmark.loc.column,
tags: ArrayPrototypeSlice(benchmark.tags),
params: benchmark.params,
samples: this.samples ?? benchmark.samples,
warmup: this.warmup ?? benchmark.warmup,
timeout: benchmark.timeout === Infinity ? null : benchmark.timeout,
yieldBetweenSamples: this.#yieldBetweenSamples,
selected: skip === null,
};
if (skip !== null) data.skip = skip;
await this.#waitForStream(this.stream.plan(data));
}
}
#hasSelectedAncestor(benchmark) {
for (let current = benchmark; current !== null; current = current.parent) {
if (current.only) return true;
}
return false;
}
#getSkip(benchmark) {
for (let current = benchmark; current !== null; current = current.parent) {
if (current.skip !== undefined && current.skip !== false) {
return current.skip;
}
}
if (this.#hasOnly && !this.#hasSelectedAncestor(benchmark)) return 'only';
if (this.namePattern !== null) {
this.namePattern.lastIndex = 0;
if (RegExpPrototypeExec(this.namePattern, benchmark.fullName) === null) {
return 'name pattern';
}
}
return null;
}
#suiteHasActiveBench(suite) {
for (let i = 0; i < suite.children.length; i++) {
const child = suite.children[i];
if (child instanceof Suite) {
if (this.#suiteHasActiveBench(child)) return true;
} else if (this.#getSkip(child) === null) {
return true;
}
}
return false;
}
async #invoke(resource, store, fn, args) {
const result = resource.runInAsyncScope(() => this.#storage.run(
store,
() => ReflectApply(fn, undefined, args),
));
return PromiseResolve(result);
}
async #runHooks(suite, name, resource, store, context) {
const hooks = suite.hooks[name];
for (let i = 0; i < hooks.length; i++) {
await this.#invoke(resource, store, hooks[i].fn, [context]);
}
}
async #runSuiteHooks(suite, name) {
const context = {
__proto__: null,
name: suite.name,
signal: this.outerSignal,
};
const hooks = suite.hooks[name];
for (let i = 0; i < hooks.length; i++) {
try {
await this.#invoke(suite, suite, hooks[i].fn, [context]);
} catch (error) {
return { __proto__: null, error, hook: hooks[i] };
}
}
return null;
}
#getRecordScope(node = undefined) {
const scope = node?.fileScope;
if (scope !== null && scope !== undefined) {
return {
__proto__: null,
runId: this.runId,
fileRunId: scope.fileRunId,
entryFile: scope.entryFile,
};
}
if (node !== undefined && kIsCliRunner) {
return {
__proto__: null,
runId: this.runId,
fileRunId: this.fileRunId,
entryFile: null,
};
}
return {
__proto__: null,
runId: this.runId,
fileRunId: this.fileRunId,
entryFile: this.entryFile,
};
}
async #waitForStream(canContinue) {
if (canContinue) return;
try {
await this.stream.waitForDrain();
} catch (error) {
if (!this.stream.destroyed) throw error;
}
}
async #diagnostic(error, loc, level = 'info', node = undefined) {
this.success = false;
await this.#waitForStream(this.stream.diagnostic({
__proto__: null,
...this.#getRecordScope(node),
message: error?.message ?? `${error}`,
error,
level,
file: loc?.file ?? loc?.[2],
line: loc?.line ?? loc?.[0],
column: loc?.column ?? loc?.[1],
}));
}
async #emitSampleDiagnostics(benchmark, phase, index, diagnostics) {
for (let i = 0; i < diagnostics.length; i++) {
const diagnostic = diagnostics[i];
const data = {
__proto__: null,
...this.#getRecordScope(benchmark),
benchId: benchmark.benchId,
parentId: benchmark.parentId,
name: benchmark.name,
namePath: ArrayPrototypeSlice(benchmark.namePath),
phase,
index,
message: diagnostic.message,
level: diagnostic.level,
file: benchmark.loc.file,
line: benchmark.loc.line,
column: benchmark.loc.column,
};
if (diagnostic.detail !== undefined) data.detail = diagnostic.detail;
await this.#waitForStream(this.stream.diagnostic(data));
}
}
async #completeSubtree(node, error) {
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i];
if (child instanceof Suite) {
await this.#completeSubtree(child, error);
child.finished = true;
child.completion.resolve();
child.emitDestroy();
} else {
await this.#executeBench(child, error);
}
}
}
async #executeSuite(suite) {
if (this.#abortReason !== null) {
await this.#completeSubtree(suite, this.#abortReason);
suite.finished = true;
suite.completion.resolve();
if (!suite.isRoot) suite.emitDestroy();
return;
}
if (suite.buildError !== null) {
await this.#diagnostic(suite.buildError, suite.loc, 'error', suite);
await this.#completeSubtree(suite, suite.buildError);
suite.finished = true;
suite.completion.resolve();
suite.emitDestroy();
return;
}
const active = this.#suiteHasActiveBench(suite);
let beforeError;
if (active) {
const failure = await this.#runSuiteHooks(suite, 'before');
if (failure !== null) {
beforeError = failure.error;
await this.#diagnostic(
failure.error, failure.hook.loc, 'error', failure.hook);
}
}
if (beforeError !== undefined) {
await this.#completeSubtree(suite, beforeError);
} else {
for (let i = 0; i < suite.children.length; i++) {
const child = suite.children[i];
if (child instanceof Suite) {
await this.#executeSuite(child);
} else {
await this.#executeBench(child);
}
}
}
if (active && this.#abortReason === null) {
const failure = await this.#runSuiteHooks(suite, 'after');
if (failure !== null) {
await this.#diagnostic(
failure.error, failure.hook.loc, 'error', failure.hook);
}
}
suite.finished = true;
suite.completion.resolve();
if (!suite.isRoot) suite.emitDestroy();
}
#getHookSuites(benchmark) {
const suites = [];
for (let current = benchmark.parent; current !== null; current = current.parent) {
ArrayPrototypePush(suites, current);
}
ArrayPrototypeReverse(suites);
return suites;
}
async #runBenchHooks(benchmark, name, context) {
const suites = this.#getHookSuites(benchmark);
if (name === 'afterEach') ArrayPrototypeReverse(suites);
for (let i = 0; i < suites.length; i++) {
await this.#runHooks(
suites[i], name, benchmark, benchmark, context);
}
}
async #runWithStop(benchmark, controller, callback, deadline) {
const signals = [];
if (this.outerSignal !== undefined) {
ArrayPrototypePush(signals, this.outerSignal);
}
if (benchmark.outerSignal !== undefined &&
benchmark.outerSignal !== this.outerSignal) {
ArrayPrototypePush(signals, benchmark.outerSignal);
}
for (let i = 0; i < signals.length; i++) {
if (signals[i].aborted) {
const error = createAbortError(signals[i]);
controller.abort(error);
throw error;
}
}
const stop = PromiseWithResolvers();
const listeners = [];
let pausedWork;
let timer;
for (let i = 0; i < signals.length; i++) {
const signal = signals[i];
ArrayPrototypePush(listeners, addAbortListener(signal, () => {
const error = createAbortError(signal);
controller.abort(error);
stop.reject(error);
}));
}
const armTimer = () => {
let remaining = deadline.value - hrtime();
if (remaining < 0n) remaining = 0n;
timer = setTimeout(() => {
const error = createTimeoutError(benchmark);
controller.abort(error);
stop.reject(error);
}, Number(remaining) / 1e6);
};
if (deadline !== null) armTimer();
const pause = (work) => {
pausedWork = (async () => {
if (deadline === null) return work();
clearTimeout(timer);
timer = undefined;
const start = hrtime();
try {
return await work();
} finally {
deadline.value += hrtime() - start;
if (!controller.signal.aborted) armTimer();
}
})();
return pausedWork;
};
const work = callback(pause);
try {
if (signals.length === 0 && timer === undefined) return await work;
return await SafePromiseRace([work, stop.promise]);
} catch (error) {
try {
await pausedWork;
} catch {
// Preserve the error that stopped benchmark execution.
}
if (!await settlesPromptly(work) && this.#abortReason === null) {
this.#abortReason = new AbortError(
'The benchmark run was aborted because asynchronous work did not ' +
'settle after cancellation',
{ __proto__: null, cause: error },
);
}
throw error;
} finally {
if (timer !== undefined) clearTimeout(timer);
for (let i = 0; i < listeners.length; i++) {
listeners[i][SymbolDispose]();
}
}
}
async #runSample(benchmark, signal, phase, index) {
const diagnostics = [];
const context = new BenchContext(
benchmark, signal, phase, index,
(diagnostic) => ArrayPrototypePush(diagnostics, diagnostic));
const channels = benchmark.diagnosticChannels;
let abortSubscription;
let diagnosticError;
let diagnosticFailed = false;
let subscribed = 0;
const onMessage = (message, name) => {
if (signal.aborted || diagnosticFailed) return;
try {
context.diagnostic({ __proto__: null, name, message });
} catch (error) {
diagnosticError = error;
diagnosticFailed = true;
}
};
const unsubscribe = () => {
while (subscribed > 0) {
subscribed--;
unsubscribeFromChannel(channels[subscribed], onMessage);
}
};
try {
for (let i = 0; i < channels.length; i++) {
subscribeToChannel(channels[i], onMessage);
subscribed++;
}
if (subscribed > 0) {
abortSubscription = addAbortListener(signal, unsubscribe);
}
await this.#invoke(
benchmark, benchmark, benchmark.fn, [context]);
if (diagnosticFailed) throw diagnosticError;
const { done, sample } = context.finish();
return {
__proto__: null,
diagnostics,
done,
failed: false,
sample,
};
} catch (error) {
context.close();
return {
__proto__: null,
diagnostics,
error,
failed: true,
};
} finally {
abortSubscription?.[SymbolDispose]();
unsubscribe();
}
}
#createResult(benchmark, samples, extra = kEmptyObject) {
return {
__proto__: null,
...this.#getRecordScope(benchmark),
benchId: benchmark.benchId,
parentId: benchmark.parentId,
name: benchmark.name,
namePath: ArrayPrototypeSlice(benchmark.namePath),
file: benchmark.loc.file,
line: benchmark.loc.line,
column: benchmark.loc.column,
tags: ArrayPrototypeSlice(benchmark.tags),
params: benchmark.params,
samples,
...extra,
};
}
async #recordResult(benchmark, result) {
benchmark.finished = true;
benchmark.result = result;
let canContinue;
try {
canContinue = this.stream.complete(result);
} finally {
benchmark.completion.resolve(result);
benchmark.emitDestroy();
}
await this.#waitForStream(canContinue);
}
async #executeBench(benchmark, forcedError = undefined) {
const duplicateError = this.#duplicateErrors.get(benchmark);
if (duplicateError !== undefined) {
this.success = false;
this.counts.failed++;
await this.#recordResult(benchmark, this.#createResult(
benchmark,
[],
{ __proto__: null, error: duplicateError },
));
return;
}
const skip = this.#getSkip(benchmark);
if (skip !== null) {
this.counts.skipped++;
await this.#recordResult(benchmark, this.#createResult(
benchmark,
[],
{ __proto__: null, skip },
));
return;
}
if (this.#abortReason !== null) forcedError = this.#abortReason;
if (forcedError !== undefined) {
this.success = false;
this.counts.failed++;
await this.#recordResult(benchmark, this.#createResult(
benchmark,
[],
{ __proto__: null, error: forcedError },
));
return;
}
await this.#waitForStream(this.stream.start({
__proto__: null,
...this.#getRecordScope(benchmark),
benchId: benchmark.benchId,
parentId: benchmark.parentId,
name: benchmark.name,
namePath: ArrayPrototypeSlice(benchmark.namePath),
file: benchmark.loc.file,
line: benchmark.loc.line,
column: benchmark.loc.column,
tags: ArrayPrototypeSlice(benchmark.tags),
params: benchmark.params,
}));
const controller = new AbortController();
const deadline = benchmark.timeout === Infinity ? null : {
__proto__: null,
value: hrtime() + BigInt(MathCeil(benchmark.timeout * 1e6)),
};
const checkDeadline = () => {
if (deadline !== null && hrtime() >= deadline.value) {
const timeoutError = createTimeoutError(benchmark);
controller.abort(timeoutError);
throw timeoutError;
}
};
const samples = [];
const hookContext = {
__proto__: null,
name: benchmark.name,
params: benchmark.params,
signal: controller.signal,
};
let error;
let failed = false;
const runAfterEach = async () => {
await this.#runBenchHooks(
benchmark, 'afterEach', hookContext);
checkDeadline();
};
try {
await this.#runWithStop(benchmark, controller, async (pause) => {
try {
await this.#runBenchHooks(
benchmark, 'beforeEach', hookContext);
checkDeadline();
const warmup = this.warmup ?? benchmark.warmup;
const total = warmup + (this.samples ?? benchmark.samples);
for (let i = 0; i < total; i++) {
if (controller.signal.aborted) {
throw controller.signal.reason;
}
const phase = i < warmup ? 'warmup' : 'measurement';
const index = phase === 'warmup' ? i : i - warmup;
const outcome = await this.#runSample(
benchmark, controller.signal, phase, index);
if (!outcome.failed) {
checkDeadline();
if (controller.signal.aborted) {
throw controller.signal.reason;
}
if (i >= warmup) {
ArrayPrototypePush(samples, outcome.sample);
}
}
let diagnosticError;
let diagnosticFailed = false;
if (outcome.diagnostics.length > 0 &&
!controller.signal.aborted) {
try {
await pause(() => this.#emitSampleDiagnostics(
benchmark, phase, index, outcome.diagnostics));
} catch (cause) {
diagnosticFailed = true;
diagnosticError = cause;
}
}
if (outcome.failed) throw outcome.error;
if (diagnosticFailed) throw diagnosticError;
if (controller.signal.aborted) {
throw controller.signal.reason;
}
if (i >= warmup) {
await pause(() => this.#waitForStream(this.stream.sample({
__proto__: null,
...this.#getRecordScope(benchmark),
benchId: benchmark.benchId,
parentId: benchmark.parentId,
name: benchmark.name,
namePath: ArrayPrototypeSlice(benchmark.namePath),
index: i - warmup,
...outcome.sample,
})));
if (controller.signal.aborted) {
throw controller.signal.reason;
}
}
if (outcome.done) break;
if (i + 1 < total && this.#yieldBetweenSamples) {
await eventLoopTurn();
}
}
} finally {
await runAfterEach();
}
}, deadline);
} catch (cause) {
failed = true;
error = cause;
} finally {
controller.abort();
}
if (failed) {
this.success = false;
this.counts.failed++;
await this.#recordResult(benchmark, this.#createResult(
benchmark,
samples,
{ __proto__: null, error },
));
return;
}
this.counts.completed++;
await this.#recordResult(benchmark, this.#createResult(
benchmark,
samples,
{ __proto__: null, summary: summarizeSamples(samples) },
));
}
async #finish(startTime) {
if (this.state === 'finished') return;
this.state = 'finished';
const duration = startTime === undefined ? 0n : hrtime() - startTime;
try {
await this.#waitForStream(this.stream.summary({
__proto__: null,
...this.#getRecordScope(),
success: this.success,
counts: this.counts,
duration_ns: duration,
file: this.entryFile,
}));
} catch (error) {
try {
await this.#diagnostic(error, undefined, 'error');
} catch {
// The stream can fail while reporting the summary listener error.
}
} finally {
this.stream.end();
this.root.finished = true;
this.root.completion.resolve();
this.root.emitDestroy();
this.#storage.disable();
// Runners created with createRunner() leave the exit code to their
// caller, and the CLI runner derives it from the aggregated summary.
if (this.#autoRun && !this.#cliRun && !this.success) {
process.exitCode ??= kGenericUserError;
}
}
}
async #execute() {
this.state = 'building';
const startTime = hrtime();
await this.#waitForBuild();
this.#fileScopeStorage.disable();
this.#prepare();
this.state = 'running';
await this.#emitPlans();
await this.#executeSuite(this.root);
await this.#finish(startTime);
}
}
let globalHarness;
function lazyHarness() {
globalHarness ??= new Harness();
return globalHarness;
}
function createDeclaration(type, getHarness) {
const declare = (name, options, fn, overrides = kEmptyObject) => {
const harness = getHarness();
const loc = getCallerLocation();
const declarationOptions = { __proto__: null, ...overrides, loc };
return type === 'benchmark' ?
harness.createBench(name, options, fn, declarationOptions) :
harness.createSuite(name, options, fn, declarationOptions);
};
if (type === 'benchmark') {
declare.skip = (name, options, fn) => declare(
name, options, fn, { __proto__: null, skip: true });
declare.only = (name, options, fn) => declare(
name, options, fn, { __proto__: null, only: true });
}
return declare;
}
function createHook(name, getHarness) {
return (fn, options) => getHarness().createHook(name, fn, options);
}
const bench = createDeclaration('benchmark', lazyHarness);
const suite = createDeclaration('suite', lazyHarness);
function createRunner(options = kEmptyObject) {
validateObject(options, 'options');
const harness = new Harness({
__proto__: null,
autoRun: false,
yieldBetweenSamples: options.yieldBetweenSamples === undefined ?
true : options.yieldBetweenSamples,
});
const getHarness = () => harness;
const runnerBench = createDeclaration('benchmark', getHarness);
const runnerSuite = createDeclaration('suite', getHarness);
return {
__proto__: null,
after: createHook(kHookNames[0], getHarness),
afterEach: createHook(kHookNames[1], getHarness),
before: createHook(kHookNames[2], getHarness),
beforeEach: createHook(kHookNames[3], getHarness),
bench: runnerBench,
describe: runnerSuite,
run: (runOptions = kEmptyObject) => harness.run(runOptions),
suite: runnerSuite,
};
}
function configureRunScope(scope) {
lazyHarness().setRunScope(scope);
}
function runInFileScope(scope, fn) {
return lazyHarness().runInFileScope(scope, fn);
}
function runBenchmarks(options, force) {
return lazyHarness().run(options, force);
}
module.exports = {
Harness,
after: createHook(kHookNames[0], lazyHarness),
afterEach: createHook(kHookNames[1], lazyHarness),
before: createHook(kHookNames[2], lazyHarness),
beforeEach: createHook(kHookNames[3], lazyHarness),
bench,
configureRunScope,
createRunId,
createRunner,
runInFileScope,
runBenchmarks,
suite,
};