UNPKG

perf-insight

Version:

Performance benchmarking tool for NodeJS.

289 lines 8.9 kB
import assert from 'node:assert'; import ora from 'ora'; import { createRunningStdDev } from './sd.mjs'; const defaultTime = 500; // 1/2 second const activeSuites = new Set(); function registerSuite(suite) { activeSuites.add(suite); } export function getActiveSuites() { return Array.from(activeSuites); } export function suite(name, p2, p3) { const description = typeof p2 === 'string' ? p2 : undefined; const suiteFn = typeof p2 === 'function' ? p2 : p3; assert(suiteFn, 'suiteFn must be a function'); return new PerfSuiteImpl(name, description, suiteFn); } export function runSuite(suiteOrName, p2, p3) { if (typeof suiteOrName === 'object') { return suiteOrName.runTests({}); } if (typeof p2 === 'function') { return suite(suiteOrName, p2).runTests({}); } assert(typeof p3 === 'function', 'suiteFn must be a function'); return suite(suiteOrName, p2, p3).runTests({}); } function toError(e) { return e instanceof Error ? e : new Error(String(e)); } function formatResult(result, nameWidth) { const { name, duration, iterations } = result; // const min = sd.min; // const max = sd.max; // const p95 = sd.ok ? sd.p95 : NaN; // const mean = sd.ok ? sd.mean : NaN; const ops = (iterations * 1000) / duration; if (result.error) { return `${name.padEnd(nameWidth)}: ${duration.toFixed(2)}ms\n` + `Error: ${result.error}`; } const msg = `${name.padEnd(nameWidth)}` + ` ${ops.toFixed(2).padStart(8)} ops/sec` + ` ${iterations.toFixed(0).padStart(6)} iterations` + // ` mean: ${mean.toPrecision(5).padStart(8)} ` + // ` p95: ${p95.toPrecision(5).padStart(8)} ` + // ` min/max: ${min.toPrecision(5).padStart(8)}/${max.toPrecision(5).padStart(8)} ` + ` ${duration.toFixed(2).padStart(7)}ms time`; return msg; } // function wait(time: number): Promise<void> { // return new Promise((resolve) => setTimeout(resolve, time)); // } class PerfSuiteImpl { name; description; suiteFn; timeout = defaultTime; constructor(name, description, suiteFn) { this.name = name; this.description = description; this.suiteFn = suiteFn; registerSuite(this); } runTests(options) { return runTests(this, options); } setTimeout(timeout) { this.timeout = timeout ?? this.timeout; return this; } } async function runTests(suite, options, progress) { const stdout = progress?.stdout || process.stdout; const spinner = progress?.spinner || ora({ stream: stdout, discardStdin: false, hideCursor: false }); const log = (msg) => stdout.write(msg + '\n'); const { name, description } = suite; let nameWidth = 0; const reportTestStart = progress?.testStart ?? ((name) => stdout.isTTY && spinner.start(name)); const reportTestEnd = (result) => { if (progress?.testEnd) { if (spinner.isSpinning) { spinner.stop(); } return progress.testEnd(result); } const msg = formatResult(result, nameWidth); if (spinner.isSpinning) { if (result.error) { spinner.fail(msg); } else { spinner.succeed(msg); } } else { if (result.error) { log(`X ${msg}`); } else { log(`✔ ${msg}`); } } }; const reportTestIteration = progress?.testIteration ?? (() => spinner.isSpinning && spinner.render()); const context = { test, prepare, beforeAll, beforeEach, afterAll, afterEach, timeout: suite.timeout || defaultTime, setTimeout: (timeout) => { context.timeout = timeout; }, }; const tests = []; const results = []; const beforeEachFns = []; const afterEachFns = []; const beforeAllFns = []; const afterAllFns = []; function filterTest(name) { if (!options?.tests) { return true; } return options.tests.some((test) => name.toLowerCase().includes(test.toLowerCase())); } function addTest(test) { if (filterTest(test.name)) tests.push(test); } function test(name, method, timeout) { addTest({ name, prepare: () => method, timeout }); } function prepare(prepareFn) { let pending = undefined; function runPrepare() { return (pending ??= Promise.resolve(prepareFn())); } return { test(name, method, timeout) { const fn = async () => { const data = await runPrepare(); return () => method(data); }; addTest({ name, prepare: fn, timeout, }); }, }; } function beforeEach(fn) { beforeEachFns.push(fn); } function afterEach(fn) { afterEachFns.push(fn); } function beforeAll(fn) { beforeAllFns.push(fn); } function afterAll(fn) { afterAllFns.push(fn); } async function runTestAsync(test) { let duration = 0; const runs = []; let iterations = 0; const reportInterval = 100; let interval; let error; let iterationCallbacks = 0; const sd = createRunningStdDev(); let startTime = performance.now(); let nextSd = 0; function reportIteration() { try { ++iterationCallbacks; reportTestIteration(test.name, iterations, duration); } catch (e) { error ??= toError(e); } } try { const method = await test.prepare(); startTime = performance.now(); let lastReport = startTime; const timeout = test.timeout || context.timeout; while (performance.now() - startTime < timeout && !error) { await runBeforeEach(); const startTime = performance.now(); let delta; try { await method(); delta = performance.now() - startTime; } catch (e) { error = toError(e); break; } await runAfterEach(); duration += delta; const now = performance.now(); if (now > nextSd) { sd.push(delta); nextSd = performance.now() + 1; } if (now - lastReport > reportInterval) { reportIteration(); lastReport = performance.now(); } iterations++; } clearInterval(interval); interval = undefined; } catch (e) { error ??= toError(e); } finally { clearInterval(interval); } return { name: test.name, duration, iterations, runs, error, timeout: test.timeout || context.timeout, overhead: performance.now() - startTime - duration, iterationCallbacks, sd, }; } async function runTest(test) { const result = await runTestAsync(test); results.push(result); return result; } async function runBeforeAll() { for (const fn of beforeAllFns) { await fn(); } } async function runAfterAll() { for (const fn of afterAllFns) { await fn(); } } async function runBeforeEach() { for (const fn of beforeEachFns) { await fn(); } } async function runAfterEach() { for (const fn of afterEachFns) { await fn(); } } await suite.suiteFn(createPerfTestFn(context), context); nameWidth = Math.max(...tests.map((t) => t.name.length)); // log(`Running: ${name}:`); if (description) { log(description); } await runBeforeAll(); for (const test of tests) { reportTestStart(test.name); const result = await runTest(test); reportTestEnd(result); } await runAfterAll(); return { name, description, results, hadFailures: results.some((r) => !!r.error), }; } function createPerfTestFn(context) { const { test, ...restContext } = context; const perfTestFn = ((...params) => test(...params)); Object.assign(perfTestFn, restContext); return perfTestFn; } //# sourceMappingURL=perfSuite.mjs.map