cronometro
Version:
Simple benchmarking suite powered by HDR histograms.
170 lines (169 loc) • 5.22 kB
JavaScript
import { Tracker } from "./tracker.js";
function noOp() {
// No-op
}
function noSetup(cb) {
cb();
}
function handleTestIteration(context, error) {
// Handle error
if (error) {
context.tracker.error = error;
context.callback(context.tracker.results);
return;
}
// Get some parameters
const { tracker, total, errorThreshold } = context;
// Track results
tracker.track(context.start);
context.executed++;
// Check if stop earlier if we are below the error threshold
const executed = context.executed;
let stop = false;
if (errorThreshold > 0) {
const completedPercentage = Math.floor((executed / total) * 10_000);
// Check if abort the test earlier. It is checked every 5% after 10% of the iterations
if (completedPercentage >= 1000 && completedPercentage % 500 === 0) {
const standardErrorPercentage = tracker.standardError / tracker.histogram.mean;
if (standardErrorPercentage < errorThreshold) {
stop = true;
}
}
}
// If the test is over
if (stop || executed > total) {
context.callback(tracker.results);
return;
}
// Schedule next iteration
process.nextTick(() => {
runTestIteration(context);
});
}
function runTestIteration(context) {
// Execute the function and get the response time - Handle also promises
try {
context.start = process.hrtime.bigint();
const callResult = context.test(context.handler);
// It is a promise, handle it accordingly
if (callResult && typeof callResult.then === 'function') {
callResult.then(() => {
context.handler(null);
}, context.handler);
}
else if (context.test.length === 0) {
// The function is not a promise and has no arguments, so it's sync
context.handler(null);
}
}
catch (error) {
/* c8 ignore start */
// If a error was thrown, only handle if the original function length is 0, which means it's a sync error, otherwise propagate
if (context.test.length === 0) {
context.handler(error);
return;
}
/* c8 ignore end */
throw error;
}
}
function beforeCallback(testContext, err) {
if (err) {
testContext.callback({
success: false,
error: err,
size: 0,
min: 0,
max: 0,
mean: 0,
stddev: 0,
percentiles: {},
standardError: 0
});
return;
}
// Schedule the first run
process.nextTick(() => {
runTestIteration(testContext);
});
}
function afterCallback(result, notifier, cb, err) {
let notifierCode = result.success ? 0 : 1;
if (err) {
notifier({
success: false,
error: err,
size: 0,
min: 0,
max: 0,
mean: 0,
stddev: 0,
percentiles: {},
standardError: 0
});
notifierCode = 1;
}
else {
notifier(result);
}
cb(notifierCode);
}
export function runWorker(context, notifier, cb) {
const { warmup, tests, index, iterations, errorThreshold } = context;
// Require the original file to build tests
const testToRun = tests?.[index];
if (!testToRun) {
throw new Error('No test code exported from the worker thread');
}
const [name, testDefinition] = testToRun;
// Prepare the test
let test = noOp;
let before = noSetup;
let after = noSetup;
if (typeof testDefinition === 'function') {
test = testDefinition;
}
else {
if (typeof testDefinition.test === 'function') {
test = testDefinition.test;
}
if (typeof testDefinition.before === 'function') {
before = testDefinition.before;
}
if (typeof testDefinition.after === 'function') {
after = testDefinition.after;
}
}
// Prepare the context
const testContext = {
name,
test,
errorThreshold,
total: iterations - 1,
executed: 0,
tracker: new Tracker(),
start: BigInt(0),
handler: noOp,
notifier,
callback(result) {
if (warmup) {
context.warmup = false;
runWorker(context, notifier, cb);
return;
}
const callback = afterCallback.bind(null, result, notifier, cb);
const afterResponse = after(callback);
if (afterResponse && typeof afterResponse.then === 'function') {
afterResponse.then(callback, callback);
}
}
};
// Bind the handler to the context
testContext.handler = handleTestIteration.bind(null, testContext);
// Run the test setup, then start the test
const callback = beforeCallback.bind(null, testContext);
const beforeResponse = before(callback);
if (beforeResponse && typeof beforeResponse.then === 'function') {
beforeResponse.then(callback, callback);
}
}