diff options
Diffstat (limited to 'std/testing/bench.ts')
-rw-r--r-- | std/testing/bench.ts | 179 |
1 files changed, 179 insertions, 0 deletions
diff --git a/std/testing/bench.ts b/std/testing/bench.ts new file mode 100644 index 000000000..3bb62526d --- /dev/null +++ b/std/testing/bench.ts @@ -0,0 +1,179 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. + +const { exit, noColor } = Deno; + +interface BenchmarkClock { + start: number; + stop: number; +} + +/** Provides methods for starting and stopping a benchmark clock. */ +export interface BenchmarkTimer { + start: () => void; + stop: () => void; +} + +/** Defines a benchmark through a named function. */ +export interface BenchmarkFunction { + (b: BenchmarkTimer): void | Promise<void>; + name: string; +} + +/** Defines a benchmark definition with configurable runs. */ +export interface BenchmarkDefinition { + func: BenchmarkFunction; + name: string; + runs?: number; +} + +/** Defines runBenchmark's run constraints by matching benchmark names. */ +export interface BenchmarkRunOptions { + only?: RegExp; + skip?: RegExp; +} + +function red(text: string): string { + return noColor ? text : `\x1b[31m${text}\x1b[0m`; +} + +function blue(text: string): string { + return 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 { + // NaN indicates that a benchmark has not been timed properly + if (!clock.stop) { + throw new Error("The benchmark timer's stop method must be called"); + } else if (!clock.start) { + throw new Error("The benchmark timer's start method must be called"); + } else if (clock.start > clock.stop) { + throw new Error( + "The benchmark timer's start method must be called before its " + + "stop method" + ); + } +} + +function createBenchmarkTimer(clock: BenchmarkClock): BenchmarkTimer { + return { + start(): void { + clock.start = performance.now(); + }, + stop(): void { + clock.stop = performance.now(); + } + }; +} + +const candidates: BenchmarkDefinition[] = []; + +/** Registers a benchmark as a candidate for the runBenchmarks executor. */ +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 + }); + } +} + +/** Runs all registered and non-skipped benchmarks serially. */ +export async function runBenchmarks({ + only = /[^\s]/, + skip = /^\s*$/ +}: BenchmarkRunOptions = {}): Promise<void> { + // Filtering candidates by the "only" and "skip" constraint + const benchmarks: BenchmarkDefinition[] = candidates.filter( + ({ name }): boolean => only.test(name) && !skip.test(name) + ); + // Init main counters and error flag + const filtered = candidates.length - benchmarks.length; + let measured = 0; + let failed = false; + // Setting up a shared benchmark clock and timer + const clock: BenchmarkClock = { start: NaN, stop: NaN }; + const b = createBenchmarkTimer(clock); + // Iterating given benchmark definitions (await-in-loop) + console.log( + "running", + benchmarks.length, + `benchmark${benchmarks.length === 1 ? " ..." : "s ..."}` + ); + for (const { name, runs = 0, func } of benchmarks) { + // See https://github.com/denoland/deno/pull/1452 about groupCollapsed + console.groupCollapsed(`benchmark ${name} ... `); + // Trying benchmark.func + let result = ""; + try { + if (runs === 1) { + // b is a benchmark timer interfacing an unset (NaN) benchmark clock + await func(b); + // Making sure the benchmark was started/stopped properly + assertTiming(clock); + result = `${clock.stop - clock.start}ms`; + } else if (runs > 1) { + // Averaging runs + let pendingRuns = runs; + let totalMs = 0; + // Would be better 2 not run these serially + while (true) { + // b is a benchmark timer interfacing an unset (NaN) benchmark clock + await func(b); + // Making sure the benchmark was started/stopped properly + assertTiming(clock); + // Summing up + totalMs += clock.stop - clock.start; + // Resetting the benchmark clock + clock.start = clock.stop = NaN; + // Once all ran + if (!--pendingRuns) { + result = `${runs} runs avg: ${totalMs / runs}ms`; + break; + } + } + } + } catch (err) { + failed = true; + console.groupEnd(); + console.error(red(err.stack)); + break; + } + // Reporting + console.log(blue(result)); + console.groupEnd(); + measured++; + // Resetting the benchmark clock + clock.start = clock.stop = NaN; + } + // Closing results + console.log( + `benchmark result: ${failed ? red("FAIL") : blue("DONE")}. ` + + `${measured} measured; ${filtered} filtered` + ); + // Making sure the program exit code is not zero in case of failure + if (failed) { + setTimeout((): void => exit(1), 0); + } +} + +/** Runs specified benchmarks if the enclosing script is main. */ +export async function runIfMain( + meta: ImportMeta, + opts?: BenchmarkRunOptions +): Promise<void> { + if (meta.main) { + return runBenchmarks(opts); + } +} |