@jonahsnider/benchmark
Version:
A Node.js benchmarking library with support for multithreading and TurboFan optimization isolation.
177 lines • 6.01 kB
JavaScript
import assert from 'node:assert/strict';
import { performance } from 'node:perf_hooks';
import { Test } from './test.js';
import { AbortError } from './utils.js';
/**
* A collection of {@link (Test:class)}s that are different implementations of the same thing (ex. different ways of sorting an array).
*
* @example
* ```js
* import { Suite } from '@jonahsnider/benchmark';
*
* const suite = new Suite('concatenation', { warmup: { durationMs: 10_000 }, run: { durationMs: 10_000 } })
* .addTest('+', () => 'a' + 'b')
* .addTest('templates', () => `${'a'}${'b'}`)
* .addTest('.concat()', () => 'a'.concat('b'));
*
* const results = await suite.run();
*
* console.log(results);
* ```
*
* @public
*/
export class Suite {
name;
options;
#tests = new Map();
/**
* The tests in this {@link (Suite:class)}.
*/
// eslint-disable-next-line @typescript-eslint/member-ordering
tests = this.#tests;
/**
* This {@link (Suite:class)}'s filepath, if it was provided.
* Used for running the {@link (Suite:class)} in a separate thread.
*/
get filepath() {
return this.options.filepath;
}
/**
* Creates a new {@link (Suite:class)}.
*
* @example
* ```js
* import { Suite } from '@jonahsnider/benchmark';
*
* const suite = new Suite('concatenation', { warmup: { durationMs: 10_000 }, run: { durationMs: 10_000 } });
* ```
*
* @example
* Suites that specify a filepath can be run in a separate thread in a {@link (Benchmark:class)}.
* ```js
* import { Suite } from '@jonahsnider/benchmark';
*
* const suite = new Suite('concatenation', {
* warmup: { durationMs: 10_000 },
* run: { durationMs: 10_000 },
* filepath: import.meta.url
* });
* ```
*
* @param name - The name of the {@link (Suite:class)}
* @param options - Options for the {@link (Suite:class)}
*/
constructor(name,
/**
* Options for running this {@link (Suite:class)} and its warmup.
*/
options) {
this.name = name;
this.options = options;
}
addTest(testName, fnOrTest) {
assert.ok(!this.#tests.has(testName));
assert.strictEqual(typeof testName, 'string', new TypeError(`The "testName" argument must be of type string.`));
if (fnOrTest instanceof Test) {
this.#tests.set(testName, fnOrTest);
}
else {
assert.strictEqual(typeof fnOrTest, 'function', new TypeError(`The "fn" argument must be of type function.`));
this.#tests.set(testName, new Test(fnOrTest));
}
return this;
}
/**
* Runs this {@link (Suite:class)} using {@link (Suite:class).options}.
*
* @example
* ```js
* const results = await suite.run();
* ```
*
* @example
* Using an `AbortSignal` to cancel the suite:
* ```js
* const ac = new AbortController();
* const signal = ac.signal;
*
* suite
* .run(signal)
* .then(console.log)
* .catch(error => {
* if (error.name === 'AbortError') {
* console.log('The suite was aborted');
* }
* });
*
* ac.abort();
* ```
*
* @returns The results of running this {@link (Suite:class)}
*/
async run(abortSignal) {
this.#clearResults();
await this.#runWarmup(abortSignal);
await this.#runTests(abortSignal);
const results = new Map([...this.#tests.entries()].map(([testName, test]) => [testName, test.histogram]));
return results;
}
#clearResults() {
for (const tests of this.#tests.values()) {
tests.histogram.reset();
}
}
async #runTestsOnce() {
for (const test of this.#tests.values()) {
// eslint-disable-next-line no-await-in-loop
await test.run();
}
}
async #runTestsWithOptions(options, abortSignal) {
if (options.durationMs === undefined) {
for (let count = 0; count < options.trials; count++) {
if (abortSignal?.aborted) {
throw new AbortError();
}
// eslint-disable-next-line no-await-in-loop
await this.#runTestsOnce();
// Periodically yield to the macrotask queue to allow abort signals sent via worker messages to be processed
// Without this, microtasks from fast-running tests can starve the event loop
if (count % 100 === 99) {
// eslint-disable-next-line no-await-in-loop
await new Promise(resolve => {
setImmediate(resolve);
});
}
}
}
else {
const startTime = performance.now();
let iterationCount = 0;
while (performance.now() - startTime < options.durationMs) {
if (abortSignal?.aborted) {
throw new AbortError();
}
// eslint-disable-next-line no-await-in-loop
await this.#runTestsOnce();
// Periodically yield to the macrotask queue to allow abort signals sent via worker messages to be processed
// Without this, microtasks from fast-running tests can starve the event loop
if (++iterationCount % 100 === 0) {
// eslint-disable-next-line no-await-in-loop
await new Promise(resolve => {
setImmediate(resolve);
});
}
}
}
}
async #runTests(abortSignal) {
await this.#runTestsWithOptions(this.options.run, abortSignal);
}
async #runWarmup(abortSignal) {
await this.#runTestsWithOptions(this.options.warmup, abortSignal);
this.#clearResults();
}
}
//# sourceMappingURL=suite.js.map