UNPKG

@jonahsnider/benchmark

Version:

A Node.js benchmarking library with support for multithreading and TurboFan optimization isolation.

177 lines 6.01 kB
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