cronometro
Version:
Simple benchmarking suite powered by HDR histograms.
150 lines (149 loc) • 4.63 kB
JavaScript
import { pathToFileURL } from 'node:url';
import { isMainThread, Worker, workerData } from 'node:worker_threads';
import { defaultOptions, runnerPath } from "./models.js";
import { printResults } from "./print.js";
export * from "./models.js";
export { printResults } from "./print.js";
export * from "./tracker.js";
function scheduleNextTest(context) {
if (context.current < context.tests.length) {
process.nextTick(() => {
run(context);
});
return;
}
if (context.print) {
const { colors, compare, compareMode } = {
colors: true,
compare: false,
compareMode: 'base',
...(context.print === true ? {} : context.print)
};
printResults(context.results, colors, compare, compareMode);
}
context.callback(null, context.results);
}
function run(context) {
const name = context.tests[context.current][0];
const workerData = {
path: pathToFileURL(process.argv[1]).toString(),
index: context.current,
iterations: context.iterations,
warmup: context.warmup,
errorThreshold: context.errorThreshold
};
let nodeOptions = process.env.NODE_OPTIONS ?? '';
const nodeMajor = Number(process.versions.node.split('.')[0]);
if (nodeMajor < 23) {
nodeOptions += ' --experimental-strip-types ';
}
const worker = new Worker(runnerPath, {
workerData,
env: {
...process.env,
NODE_OPTIONS: nodeOptions
}
});
if (context.onTestStart) {
context.onTestStart(name, workerData, worker);
}
worker.on('error', error => {
context.results[name] = {
success: false,
error,
size: 0,
min: 0,
max: 0,
mean: 0,
stddev: 0,
percentiles: {},
standardError: 0
};
context.current++;
if (context.onTestError) {
context.onTestError(name, error, worker);
}
scheduleNextTest(context);
});
worker.on('message', message => {
if (message.type !== 'cronometro.result') {
return;
}
const result = message.payload;
context.results[name] = result;
context.current++;
if (context.onTestEnd) {
context.onTestEnd(name, result, worker);
}
scheduleNextTest(context);
});
}
export function cronometro(tests, options, cb) {
if (!isMainThread) {
workerData.tests = Object.entries(tests).filter(test => test[1]?.skip !== true);
return;
}
let promise;
let promiseResolve;
let promiseReject;
if (typeof options === 'function') {
cb = options;
options = {};
}
let callback = cb;
if (!callback) {
promise = new Promise((resolve, reject) => {
promiseResolve = resolve;
promiseReject = reject;
});
callback = function (err, results) {
if (err) {
promiseReject(err);
return;
}
promiseResolve(results);
};
}
const { iterations, errorThreshold, print, warmup, onTestStart, onTestEnd, onTestError } = {
...defaultOptions,
...options
};
if (typeof iterations !== 'number' || iterations < 1) {
callback(new Error('The iterations option must be a positive number.'));
return promise;
}
if (typeof errorThreshold !== 'number' || errorThreshold < 0 || errorThreshold > 100) {
callback(new Error('The errorThreshold option must be a number between 0 and 100.'));
return promise;
}
if (onTestStart && typeof onTestStart !== 'function') {
callback(new Error('The onTestStart option must be a function.'));
return promise;
}
if (onTestEnd && typeof onTestEnd !== 'function') {
callback(new Error('The onTestEnd option must be a function.'));
return promise;
}
if (onTestError && typeof onTestError !== 'function') {
callback(new Error('The onTestError option must be a function.'));
return promise;
}
const context = {
warmup,
iterations,
errorThreshold: errorThreshold / 100,
print,
tests: Object.entries(tests).filter(test => test[1]?.skip !== true),
results: {},
current: 0,
callback,
onTestStart,
onTestEnd,
onTestError
};
process.nextTick(() => {
run(context);
});
return promise;
}
export default cronometro;