perf-insight
Version:
Performance benchmarking tool for NodeJS.
289 lines • 8.9 kB
JavaScript
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