@paulmillr/jsbt
Version:
JS Build Tools: build, benchmark, test libs and apps
186 lines (185 loc) • 6.71 kB
JavaScript
const maxSamples = 2 ** 26;
const _c = String.fromCharCode(27);
const red = _c + '[31m';
const green = _c + '[32m';
const blue = _c + '[34m';
const reset = _c + '[0m';
const units = [
{ symbol: 'min', val: 60n * 10n ** 9n, threshold: 5n },
{ symbol: 's', val: 10n ** 9n, threshold: 10n },
{ symbol: 'ms', val: 10n ** 6n, threshold: 1n },
{ symbol: 'μs', val: 10n ** 3n, threshold: 1n },
{ symbol: 'ns', val: 0n, threshold: 1n },
];
const SECOND = units[1].val;
let MAX_RUN_TIME = 1n * SECOND; // 1 second
function setMaxRunTime(val) {
if (!val || val < 0.1 || val > 600)
throw new Error('must be between 0.1 and 600 sec');
let tenth = BigInt(val * 10);
MAX_RUN_TIME = (tenth * SECOND) / 10n;
}
function printOutput(...str) {
// @ts-ignore
console.log(...str);
}
function logMem() {
const mapping = {
heapTotal: 'heap',
heapUsed: 'used',
external: 'ext',
arrayBuffers: 'arr',
};
// @ts-ignore
const vals = Object.entries(process.memoryUsage())
.filter((entry) => {
const [k, v] = entry;
return v > 100000 && k !== 'external';
})
.map((entry) => {
const [k, v] = entry;
return `${mapping[k] || k}=${`${(v / 1000000).toFixed(1)}mb`}`;
});
printOutput('RAM:', vals.join(' '));
}
// T-Distribution two-tailed critical values for 95% confidence.
// http://www.itl.nist.gov/div898/handbook/eda/section3/eda3672.htm
// prettier-ignore
const tTable = {
'1': 12.706, '2': 4.303, '3': 3.182, '4': 2.776, '5': 2.571, '6': 2.447,
'7': 2.365, '8': 2.306, '9': 2.262, '10': 2.228, '11': 2.201, '12': 2.179,
'13': 2.16, '14': 2.145, '15': 2.131, '16': 2.12, '17': 2.11, '18': 2.101,
'19': 2.093, '20': 2.086, '21': 2.08, '22': 2.074, '23': 2.069, '24': 2.064,
'25': 2.06, '26': 2.056, '27': 2.052, '28': 2.048, '29': 2.045, '30': 2.042,
'infinity': 1.96
};
const formatter = Intl.NumberFormat('en-US');
// duration formatter
function formatDuration(duration) {
for (let i = 0; i < units.length; i++) {
const { symbol, threshold, val } = units[i];
if (duration >= val * threshold) {
const div = val === 0n ? 1n : val;
return (duration / div).toString() + symbol;
}
}
throw new Error('Invalid duration ' + duration);
}
function calcSum(list, isBig = true) {
// @ts-ignore
return list.reduce((a, b) => a + b, (isBig ? 0n : 0));
}
function isFirstBig(list) {
return list.length > 0 && typeof list[0] === 'bigint';
}
function calcMean(list) {
const len = list.length;
const isBig = isFirstBig(list);
const tlen = isBig ? BigInt(len) : len;
// @ts-ignore
return calcSum(list, isBig) / tlen;
}
function calcDeviation(list) {
const isBig = isFirstBig(list);
const mean = calcMean(list);
const square = isBig ? (a) => a ** 2n : (a) => a ** 2;
// @ts-ignore
const diffs = list.map((val) => square(val - mean));
const variance = Number(calcSum(diffs, isBig)) / list.length - 1;
return Math.sqrt(variance);
}
function calcCorrelation(x, y) {
const isBig = isFirstBig(x);
const checker = isBig ? (a) => typeof a === 'bigint' : (a) => typeof a === 'number';
const err = `expected array of ${isBig ? 'bigints' : 'numbers'}`;
if (!x.every(checker))
throw new Error('x: ' + err);
if (!y.every(checker))
throw new Error('y: ' + err);
const meanX = calcMean(x);
const meanY = calcMean(y);
const sum = calcSum(x.map((val, i) => (val - meanX) * (y[i] - meanY)), isBig);
const observation = Number(sum) / (calcDeviation(x) * calcDeviation(y));
return observation / (x.length - 1);
}
// Mutates array by sorting it
function calcStats(list) {
list.sort((a, b) => Number(a - b));
const samples = list.length;
const mean = calcMean(list);
const median = list[Math.floor(samples / 2)];
const min = list[0];
const max = list[samples - 1];
// Compute the standard error of the mean
// a.k.a. the standard deviation of the sampling distribution of the sample mean
const sem = calcDeviation(list) / Math.sqrt(samples);
const df = samples - 1; // degrees of freedom
// @ts-ignore
const critical = tTable[Math.round(df) || 1] || tTable.infinity; // critical value
const moe = sem * critical; // margin of error
const rme = (moe / Number(mean)) * 100 || 0; // relative margin of error
const formatted = `${red}± ${rme.toFixed(2)}% (${formatDuration(min)}..${formatDuration(max)})${reset}`;
return { rme, min, max, mean, median, formatted };
}
function getTime() {
// @ts-ignore
return process.hrtime.bigint();
}
async function benchmarkRaw(callback, samples = maxSamples) {
if (typeof callback !== 'function')
throw new Error('callback must be a function');
if (typeof samples !== 'number' || samples <= 0 || samples > maxSamples)
throw new Error('samples must be a number');
// measurements contain sample timings
// `new Array(30_000_000)` pre-allocation is in some cases more efficient for
// garbage collection than growing array size continuously.
const measurements = [];
let total = 0n;
for (let i = 0; i < samples; i++) {
const start = getTime();
const val = callback(i);
if (val instanceof Promise)
await val;
const stop = getTime();
const diff = stop - start;
measurements.push(diff);
total += diff;
if (total >= MAX_RUN_TIME)
break;
}
const stats = calcStats(measurements);
const perItemStr = formatDuration(stats.mean);
const sec = units[1].val;
const perSec = sec / stats.mean;
const perSecStr = formatter.format(sec / stats.mean);
return { stats, perSecStr, perSec, perItemStr, measurements };
}
export async function bench(label, fn, samples = maxSamples) {
if (typeof label !== 'string')
throw new Error('label must be a string');
const { stats, perSecStr, perItemStr, measurements } = await benchmarkRaw(fn, samples);
let str = `${label} `;
let perItemStrClr = `${blue}${perItemStr}${reset}`;
if (samples === 1) {
str += perItemStrClr;
}
else {
str += `x ${green}${perSecStr}${reset} ops/sec @ ${perItemStrClr}/op`;
}
if (stats.rme >= 1)
str += ` ${stats.formatted}`;
printOutput(str);
measurements.length = 0; // Destroy the list, simplify the life for garbage collector
return;
}
export default bench;
export const utils = {
getTime,
setMaxRunTime,
logMem,
formatDuration,
calcStats,
calcDeviation,
calcCorrelation,
benchmarkRaw,
};