summaryrefslogtreecommitdiff
path: root/std/testing/bench.ts
diff options
context:
space:
mode:
Diffstat (limited to 'std/testing/bench.ts')
-rw-r--r--std/testing/bench.ts179
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);
+ }
+}