UNPKG

mitata

Version:

benchmark tooling that loves you ❤️

426 lines (350 loc) 14 kB
const AsyncFunction = (async () => { }).constructor; const GeneratorFunction = (function* () { }).constructor; const AsyncGeneratorFunction = (async function* () { }).constructor; export function do_not_optimize(v) { $._ = v; } export const $ = { _: null, __() { return print($._); } }; export async function measure(f, ...args) { return await { fn, iter, yield: generator, [void 0]() { throw new TypeError('expected iterator, generator or one-shot function'); }, }[kind(f)](f, ...args); } export async function generator(gen, opts = {}) { const ctx = { get(name) { return opts.args?.[name] }, }; const g = gen(ctx); const n = await g.next(); let $fn = n.value; if (!n.value?.heap && null != n.value?.heap) opts.heap = false; opts.concurrency ??= n.value?.concurrency ?? opts.args?.concurrency; if (!n.value?.counters && null != n.value?.counters) opts.$counters = false; if (n.done || 'fn' !== kind($fn)) { $fn = n.value?.bench || n.value?.manual; if ('fn' !== kind($fn, true)) throw new TypeError('expected benchmarkable yield from generator'); opts.params ??= {}; const params = $fn.length; opts.manual = !n.value.manual ? false : ('manual' !== n.value.budget ? 'real' : 'manual'); for (let o = 0; o < params; o++) { opts.params[o] = n.value[o]; if ('fn' !== kind(n.value[o])) throw new TypeError('expected function for benchmark parameter'); } } const stats = await fn($fn, opts); if (!(await g.next()).done) throw new TypeError('expected generator to yield once'); return { ...stats, kind: 'yield', }; } export const print = (() => { if (globalThis.console?.log) return globalThis.console.log; if (globalThis.print && !globalThis.document) return globalThis.print; return () => { throw new Error('no print function available'); }; })(); export const gc = (() => { try { return (Bun.gc(true), () => Bun.gc(true)); } catch { } try { return (globalThis.gc(), () => globalThis.gc()); } catch { } try { return (globalThis.__gc(), () => globalThis.__gc()); } catch { } try { return (globalThis.std.gc(), () => globalThis.std.gc()); } catch { } try { return (globalThis.$262.gc(), () => globalThis.$262.gc()); } catch { } try { return (globalThis.tjs.engine.gc.run(), () => globalThis.tjs.engine.gc.run()); } catch { } return Object.assign(globalThis.Graal ? () => new Uint8Array(2 ** 29) : () => new Uint8Array(2 ** 30), { fallback: true }); })(); export const now = (() => { try { // bun Bun.nanoseconds(); return Bun.nanoseconds; } catch { } try { // jsc $.agent.monotonicNow(); return () => 1e6 * $.agent.monotonicNow(); } catch { } try { // 262 agent $262.agent.monotonicNow(); return () => 1e6 * $262.agent.monotonicNow(); } catch { } try { // node/deno/... (v8 inline, anti-deopts) const now = performance.now.bind(performance); now(); return () => 1e6 * now(); } catch { return () => 1e6 * Date.now(); } })(); export function kind(fn, _ = false) { if (!( fn instanceof Function || fn instanceof AsyncFunction || fn instanceof GeneratorFunction || fn instanceof AsyncGeneratorFunction )) return; if ( fn instanceof GeneratorFunction || fn instanceof AsyncGeneratorFunction ) return 'yield'; if ( (_ ? true : (0 === fn.length)) && ( fn instanceof Function || fn instanceof AsyncFunction ) ) return 'fn'; if ( 0 !== fn.length && ( fn instanceof Function || fn instanceof AsyncFunction ) ) return 'iter'; } const k_cpu_time_rescale_heap = 1.1; const k_cpu_time_rescale_inner_gc = 2; export const k_concurrency = 1; export const k_min_samples = 12; export const k_batch_unroll = 4; export const k_max_samples = 1e9; export const k_warmup_samples = 2; export const k_batch_samples = 4096; export const k_samples_threshold = 12; export const k_batch_threshold = 65536; export const k_min_cpu_time = 642 * 1e6; export const k_warmup_threshold = 500_000; function defaults(opts) { opts.gc ??= gc; opts.now ??= now; opts.heap ??= null; opts.params ??= {}; opts.manual ??= false; opts.inner_gc ??= false; opts.$counters ??= false; opts.concurrency ??= k_concurrency; opts.min_samples ??= k_min_samples; opts.max_samples ??= k_max_samples; opts.min_cpu_time ??= k_min_cpu_time; opts.batch_unroll ??= k_batch_unroll; opts.batch_samples ??= k_batch_samples; opts.warmup_samples ??= k_warmup_samples; opts.batch_threshold ??= k_batch_threshold; opts.warmup_threshold ??= k_warmup_threshold; opts.samples_threshold ??= k_samples_threshold; if (opts.heap) opts.min_cpu_time *= k_cpu_time_rescale_heap; if (opts.gc && opts.inner_gc) opts.min_cpu_time *= k_cpu_time_rescale_inner_gc; } export async function fn(fn, opts = {}) { defaults(opts); let async = false; let batch = false; const params = Object.keys(opts.params); warmup: { const $p = new Array(params.length); for (let o = 0; o < params.length; o++) { $p[o] = await opts.params[o](); } const t0 = now(); const r = fn(...$p); let t1 = now(); if (async = r instanceof Promise) (await r, t1 = now()); if ((t1 - t0) <= opts.warmup_threshold) { for (let o = 0; o < opts.warmup_samples; o++) { for (let oo = 0; oo < params.length; oo++) { $p[oo] = await opts.params[oo](); } const t0 = now(); await fn(...$p); const t1 = now(); if (batch = (t1 - t0) <= opts.batch_threshold) break; } } } if (opts.manual) { batch = false; opts.concurrency = 1; } const loop = new AsyncFunction('$fn', '$gc', '$now', '$heap', '$params', '$counters', ` ${!opts.$counters ? '' : 'let _hc = false;'} ${!opts.$counters ? '' : 'try { $counters.init(); _hc = true; } catch {}'} let _ = 0; let t = 0; let samples = new Array(2 ** 20); ${!opts.heap ? '' : 'const heap = { _: 0, total: 0, min: Infinity, max: -Infinity };'} ${!(opts.gc && opts.inner_gc && !opts.gc.fallback) ? '' : 'const gc = { total: 0, min: Infinity, max: -Infinity };'} ${!params.length ? '' : Array.from({ length: params.length }, (_, o) => ` ${Array.from({ length: opts.concurrency }, (_, c) => ` let param_${o}_${c} = ${!batch ? 'null' : `new Array(${opts.batch_samples})`}; `.trim()).join(' ')} `.trim()).join('\n')} ${!opts.gc ? '' : `$gc();`} for (; _ < ${opts.max_samples}; _++) { if (_ >= ${opts.min_samples} && t >= ${opts.min_cpu_time}) break; ${!params.length ? '' : ` ${!batch ? ` ${Array.from({ length: params.length }, (_, o) => ` ${Array.from({ length: opts.concurrency }, (_, c) => ` if ((param_${o}_${c} = $params[${o}]()) instanceof Promise) param_${o}_${c} = await param_${o}_${c}; `.trim()).join(' ')} `.trim()).join('\n')} ` : ` for (let o = 0; o < ${opts.batch_samples}; o++) { ${Array.from({ length: params.length }, (_, o) => ` ${Array.from({ length: opts.concurrency }, (_, c) => ` if ((param_${o}_${c}[o] = $params[${o}]()) instanceof Promise) param_${o}_${c}[o] = await param_${o}_${c}[o]; `.trim()).join(' ')} `.trim()).join('\n')} } `} `} ${!(opts.gc && opts.inner_gc) ? '' : ` igc: { const t0 = $now(); $gc(); t += $now() - t0; } `} ${!opts.manual ? '' : 'let t2 = 0;'} ${!opts.heap ? '' : 'const h0 = $heap();'} ${!opts.$counters ? '' : 'if (_hc) try { $counters.before(); } catch {};'} const t0 = $now(); ${!batch ? ` ${!async ? '' : (1 >= opts.concurrency ? '' : 'await Promise.all([')} ${Array.from({ length: opts.concurrency }, (_, c) => ` ${!opts.manual ? '' : 't2 +='} ${!async ? '' : (1 < opts.concurrency ? '' : 'await')} ${(!params.length ? ` $fn() ` : ` $fn(${Array.from({ length: params.length }, (_, o) => `param_${o}_${c}`).join(', ')}) `).trim()}${!async ? ';' : (1 < opts.concurrency ? ',' : ';')} `.trim()).join('\n')} ${!async ? '' : (1 >= opts.concurrency ? '' : `]);`)} ` : ` for (let o = 0; o < ${(opts.batch_samples / opts.batch_unroll) | 0}; o++) { ${!params.length ? '' : `const param_offset = o * ${opts.batch_unroll};`} ${Array.from({ length: opts.batch_unroll }, (_, u) => ` ${!async ? '' : (1 >= opts.concurrency ? '' : 'await Promise.all([')} ${Array.from({ length: opts.concurrency }, (_, c) => ` ${!async ? '' : (1 < opts.concurrency ? '' : 'await')} ${(!params.length ? ` $fn() ` : ` $fn(${Array.from({ length: params.length }, (_, o) => `param_${o}_${c}[${u === 0 ? '' : `${u} + `}param_offset]`).join(', ')}) `).trim()}${!async ? ';' : (1 < opts.concurrency ? ',' : ';')} `.trim()).join(' ')} ${!async ? '' : (1 >= opts.concurrency ? '' : ']);')} `.trim()).join('\n')} } `} const t1 = $now(); ${!opts.$counters ? '' : 'if (_hc) try { $counters.after(); } catch {};'} ${!opts.heap ? '' : ` heap: { const t0 = $now(); const h1 = ($heap() - h0) ${!batch ? '' : `/ ${opts.batch_samples}`}; t += $now() - t0; if (0 <= h1) { heap._++; heap.total += h1; heap.min = Math.min(h1, heap.min); heap.max = Math.max(h1, heap.max); } } `} ${!(opts.gc && opts.inner_gc && !opts.gc.fallback) ? '' : ` igc: { const t0 = $now(); $gc(); const t1 = $now() - t0; t += t1; gc.total += t1; gc.min = Math.min(t1, gc.min); gc.max = Math.max(t1, gc.max); } `}; const diff = ${opts.manual ? 't2' : 't1 - t0'}; t += ${'manual' === opts.manual ? 't2' : 't1 - t0'}; samples[_] = diff ${!batch ? '' : `/ ${opts.batch_samples}`}; } samples.length = _; samples.sort((a, b) => a - b); if (samples.length > ${opts.samples_threshold}) samples = samples.slice(2, -2); return { samples, min: samples[0], max: samples[samples.length - 1], p25: samples[(.25 * (samples.length - 1)) | 0], p50: samples[(.50 * (samples.length - 1)) | 0], p75: samples[(.75 * (samples.length - 1)) | 0], p99: samples[(.99 * (samples.length - 1)) | 0], p999: samples[(.999 * (samples.length - 1)) | 0], avg: samples.reduce((a, v) => a + v, 0) / samples.length, ticks: samples.length ${!batch ? '' : `* ${opts.batch_samples}`}, ${!opts.heap ? '' : 'heap: { ...heap, avg: heap.total / heap._ },'} ${!(opts.gc && opts.inner_gc && !opts.gc.fallback) ? '' : 'gc: { ...gc, avg: gc.total / _ },'} ${!opts.$counters ? '' : `...(!_hc ? {} : { counters: $counters.translate(${!batch ? 1 : opts.batch_samples}, _) }),`} }; ${!opts.$counters ? '' : 'if (_hc) try { $counters.deinit(); } catch {};'} `); return { kind: 'fn', debug: loop.toString(), ...(await loop(fn, opts.gc, opts.now, opts.heap, opts.params, opts.$counters)), }; } // TODO: update when jit can do zero-cost opt export async function iter(iter, opts = {}) { const _ = {}; defaults(opts); let samples = new Array(2 ** 20); const _i = { next() { return _.next() } }; const ctx = { [Symbol.iterator]() { return _i }, [Symbol.asyncIterator]() { return _i }, get(name) { return opts.args?.[name] }, }; const gen = (function* () { let batch = false; warmup: { const t0 = now(); yield void 0; const t1 = now(); if ((t1 - t0) <= opts.warmup_threshold) { for (let o = 0; o < opts.warmup_samples; o++) { const t0 = now(); yield void 0; const t1 = now(); if (batch = (t1 - t0) <= opts.batch_threshold) break; } } } const loop = new GeneratorFunction('$gc', '$now', '$samples', _.debug = ` let _ = 0; let t = 0; ${!opts.gc ? '' : `$gc();`} for (; _ < ${opts.max_samples}; _++) { if (_ >= ${opts.min_samples} && t >= ${opts.min_cpu_time}) break; ${!(opts.gc && opts.inner_gc) ? '' : ` let inner_gc_cost = 0; igc: { const t0 = $now(); $gc(); inner_gc_cost = $now() - t0; } `} const t0 = $now(); ${!batch ? 'yield void 0;' : ` for (let o = 0; o < ${(opts.batch_samples / opts.batch_unroll) | 0}; o++) { ${new Array(opts.batch_unroll).fill('yield void 0;').join(' ')} } `} const t1 = $now(); const diff = t1 - t0; $samples[_] = diff ${!batch ? '' : `/ ${opts.batch_samples}`}; t += diff ${!(opts.gc && opts.inner_gc) ? '' : '+ inner_gc_cost'}; } $samples.length = _; `)(opts.gc, opts.now, samples); _.batch = batch; _.next = loop.next.bind(loop); yield void 0; })(); await iter((_.next = gen.next.bind(gen), ctx)); if (samples.length < opts.min_samples) throw new TypeError(`expected at least ${opts.min_samples} samples from iterator`); samples.sort((a, b) => a - b); if (samples.length > opts.samples_threshold) samples = samples.slice(2, -2); return { samples, kind: 'iter', debug: _.debug, min: samples[0], max: samples[samples.length - 1], p25: samples[(.25 * (samples.length - 1)) | 0], p50: samples[(.50 * (samples.length - 1)) | 0], p75: samples[(.75 * (samples.length - 1)) | 0], p99: samples[(.99 * (samples.length - 1)) | 0], p999: samples[(.999 * (samples.length - 1)) | 0], avg: samples.reduce((a, v) => a + v, 0) / samples.length, ticks: samples.length * (!_.batch ? 1 : opts.batch_samples), }; }