import { assert } from "../_util/assert.ts";import { deepAssign } from "../_util/deep_assign.ts";
interface BenchmarkClock { start: number; stop: number; for?: string;}
export interface BenchmarkTimer { start: () => void; stop: () => void;}
export interface BenchmarkFunction { (b: BenchmarkTimer): void | Promise<void>; name: string;}
export interface BenchmarkDefinition { func: BenchmarkFunction; name: string; runs?: number;}
export interface BenchmarkRunOptions { only?: RegExp; skip?: RegExp; silent?: boolean;}
export interface BenchmarkClearOptions { only?: RegExp; skip?: RegExp;}
export interface BenchmarkResult { name: string; totalMs: number; runsCount: number; measuredRunsAvgMs: number; measuredRunsMs: number[];}
export interface BenchmarkRunResult { filtered: number; results: BenchmarkResult[];}
export interface BenchmarkRunProgress extends BenchmarkRunResult { queued?: Array<{ name: string; runsCount: number }>; running?: { name: string; runsCount: number; measuredRunsMs: number[] }; state?: ProgressState;}
export enum ProgressState { BenchmarkingStart = "benchmarking_start", BenchStart = "bench_start", BenchPartialResult = "bench_partial_result", BenchResult = "bench_result", BenchmarkingEnd = "benchmarking_end",}
export class BenchmarkRunError extends Error { benchmarkName?: string; constructor(msg: string, benchmarkName?: string) { super(msg); this.name = "BenchmarkRunError"; this.benchmarkName = benchmarkName; }}
function red(text: string): string { return Deno.noColor ? text : `\x1b[31m${text}\x1b[0m`;}
function blue(text: string): string { return Deno.noColor ? text : `\x1b[34m${text}\x1b[0m`;}
function verifyOr1Run(runs?: number): number { return runs && runs >= 1 && runs !== Infinity ? Math.floor(runs) : 1;}
function assertTiming(clock: BenchmarkClock): void { if (!clock.stop) { throw new BenchmarkRunError( `Running benchmarks FAILED during benchmark named [${clock.for}]. The benchmark timer's stop method must be called`, clock.for, ); } else if (!clock.start) { throw new BenchmarkRunError( `Running benchmarks FAILED during benchmark named [${clock.for}]. The benchmark timer's start method must be called`, clock.for, ); } else if (clock.start > clock.stop) { throw new BenchmarkRunError( `Running benchmarks FAILED during benchmark named [${clock.for}]. The benchmark timer's start method must be called before its stop method`, clock.for, ); }}
function createBenchmarkTimer(clock: BenchmarkClock): BenchmarkTimer { return { start(): void { clock.start = performance.now(); }, stop(): void { if (isNaN(clock.start)) { throw new BenchmarkRunError( `Running benchmarks FAILED during benchmark named [${clock.for}]. The benchmark timer's start method must be called before its stop method`, clock.for, ); } clock.stop = performance.now(); }, };}
const candidates: BenchmarkDefinition[] = [];
export function bench( benchmark: BenchmarkDefinition | BenchmarkFunction,): void { if (!benchmark.name) { throw new Error("The benchmark function must not be anonymous"); } if (typeof benchmark === "function") { candidates.push({ name: benchmark.name, runs: 1, func: benchmark }); } else { candidates.push({ name: benchmark.name, runs: verifyOr1Run(benchmark.runs), func: benchmark.func, }); }}
export function clearBenchmarks({ only = /[^\s]/, skip = /$^/,}: BenchmarkClearOptions = {}): void { const keep = candidates.filter( ({ name }): boolean => !only.test(name) || skip.test(name), ); candidates.splice(0, candidates.length); candidates.push(...keep);}
export async function runBenchmarks( { only = /[^\s]/, skip = /^\s*$/, silent }: BenchmarkRunOptions = {}, progressCb?: (progress: BenchmarkRunProgress) => void | Promise<void>,): Promise<BenchmarkRunResult> { const benchmarks: BenchmarkDefinition[] = candidates.filter( ({ name }): boolean => only.test(name) && !skip.test(name), ); const filtered = candidates.length - benchmarks.length; let failError: Error | undefined = undefined; const clock: BenchmarkClock = { start: NaN, stop: NaN }; const b = createBenchmarkTimer(clock);
const progress: BenchmarkRunProgress = { queued: benchmarks.map((bench) => ({ name: bench.name, runsCount: bench.runs!, })), results: [], filtered, state: ProgressState.BenchmarkingStart, };
await publishProgress(progress, ProgressState.BenchmarkingStart, progressCb);
if (!silent) { console.log( "running", benchmarks.length, `benchmark${benchmarks.length === 1 ? " ..." : "s ..."}`, ); }
for (const { name, runs = 0, func } of benchmarks) { if (!silent) { console.groupCollapsed(`benchmark ${name} ... `); }
clock.for = name;
assert(progress.queued); const queueIndex = progress.queued.findIndex( (queued) => queued.name === name && queued.runsCount === runs, ); if (queueIndex != -1) { progress.queued.splice(queueIndex, 1); } progress.running = { name, runsCount: runs, measuredRunsMs: [] }; await publishProgress(progress, ProgressState.BenchStart, progressCb);
let result = ""; try { let pendingRuns = runs; let totalMs = 0;
while (true) { await func(b); assertTiming(clock);
const measuredMs = clock.stop - clock.start;
totalMs += measuredMs; progress.running.measuredRunsMs.push(measuredMs); await publishProgress( progress, ProgressState.BenchPartialResult, progressCb, );
clock.start = clock.stop = NaN; if (!--pendingRuns) { result = runs == 1 ? `${totalMs}ms` : `${runs} runs avg: ${totalMs / runs}ms`; progress.results.push({ name, totalMs, runsCount: runs, measuredRunsAvgMs: totalMs / runs, measuredRunsMs: progress.running.measuredRunsMs, }); delete progress.running; await publishProgress( progress, ProgressState.BenchResult, progressCb, ); break; } } } catch (err) { failError = err;
if (!silent) { console.groupEnd(); console.error(red(err.stack)); }
break; }
if (!silent) { console.log(blue(result)); console.groupEnd(); }
clock.start = clock.stop = NaN; delete clock.for; }
delete progress.queued; await publishProgress(progress, ProgressState.BenchmarkingEnd, progressCb);
if (!silent) { console.log( `benchmark result: ${failError ? red("FAIL") : blue("DONE")}. ` + `${progress.results.length} measured; ${filtered} filtered`, ); }
if (failError) { throw failError; }
const benchmarkRunResult = { filtered, results: progress.results, };
return benchmarkRunResult;}
async function publishProgress( progress: BenchmarkRunProgress, state: ProgressState, progressCb?: (progress: BenchmarkRunProgress) => void | Promise<void>,): Promise<void> { progressCb && (await progressCb(cloneProgressWithState(progress, state)));}
function cloneProgressWithState( progress: BenchmarkRunProgress, state: ProgressState,): BenchmarkRunProgress { return deepAssign({}, progress, { state }) as BenchmarkRunProgress;}