UNPKG

micro-bmark

Version:

Benchmark your JS projects with nanosecond resolution

231 lines (227 loc) 8.11 kB
/*! micro-bmark - MIT License (c) 2020 Paul Miller, 2010-2016 Mathias Bynens, John-David Dalton, Robert Kieffer from JSLitmus.js */ 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: number): void { 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: any) { // @ts-ignore console.log(...str); } function logMem(): void { const mapping: any = { heapTotal: 'heap', heapUsed: 'used', external: 'ext', arrayBuffers: 'arr', }; // @ts-ignore const vals = Object.entries(process.memoryUsage()) .filter((entry: any) => { const [k, v] = entry; return v > 100000 && k !== 'external'; }) .map((entry: any) => { 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: any): string { 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<T extends number | bigint>(list: T[], isBig = true): T { // @ts-ignore return list.reduce((a, b) => a + b, (isBig ? 0n : 0) as T); } function isFirstBig<T extends number | bigint>(list: T[]): boolean { return list.length > 0 && typeof list[0] === 'bigint'; } function calcMean<T extends number | bigint>(list: T[]): T { const len = list.length; const isBig = isFirstBig(list); const tlen = isBig ? BigInt(len) : len; // @ts-ignore return calcSum(list, isBig) / tlen; } function calcDeviation<T extends number | bigint>(list: T[]): number { const isBig = isFirstBig(list); const mean = calcMean(list); const square = isBig ? (a: bigint) => a ** 2n : (a: number) => 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<T extends number | bigint>(x: T[], y: T[]): number { const isBig = isFirstBig(x); const checker = isBig ? (a: T) => typeof a === 'bigint' : (a: T) => 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<T extends number | bigint>( list: T[] ): { rme: number; min: T; max: T; mean: any; median: T; formatted: string } { list.sort((a, b) => Number(a - b)); const samples = list.length; const mean: T = 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: number = 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 }; } export type BenchStats = { stats: { rme: number; min: number | bigint; max: number | bigint; mean: number | bigint; median: number | bigint; formatted: string; }; perSecStr: string; perSec: bigint; perItemStr: string; measurements: bigint[]; }; function getTime(): bigint { // @ts-ignore return process.hrtime.bigint(); } async function benchmarkRaw(samples: number | undefined, callback: Func): Promise<BenchStats> { if (samples == null) { samples = Number.POSITIVE_INFINITY; } else if (!Number.isSafeInteger(samples) || samples <= 0) { throw new Error('samples must be a number'); } if (typeof callback !== 'function') throw new Error('callback must be a function'); // List containing sample times // pre-allocating using `new Array(1_000_000)` 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 type Func = (iteration?: number) => {}; export function mark(label: string, samples: number, callback: Func): Promise<undefined>; export function mark(label: string, callback: Func): Promise<undefined>; export async function mark( label: string, samplesFN: number | Func, callbackFN?: Func ): Promise<BenchStats | undefined> { if (typeof label !== 'string') throw new Error('label must be a string'); let samples: number | undefined; let callback: Func; if (typeof samplesFN === 'function') { callback = samplesFN; samples = undefined; } else { if (typeof callbackFN !== 'function') throw new Error('callback must be a function'); samples = samplesFN; callback = callbackFN; } const { stats, perSecStr, perItemStr, measurements } = await benchmarkRaw(samples, callback); 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); // Destroy the list, simplify the life for garbage collector measurements.length = 0; return; } export default mark; export const utils: { getTime: typeof getTime; setMaxRunTime: typeof setMaxRunTime, logMem: typeof logMem; formatDuration: typeof formatDuration; calcStats: typeof calcStats; calcDeviation: typeof calcDeviation; calcCorrelation: typeof calcCorrelation; benchmarkRaw: typeof benchmarkRaw; } = { getTime, setMaxRunTime, logMem, formatDuration, calcStats, calcDeviation, calcCorrelation, benchmarkRaw, };