diff options
| author | chiefbiiko <noah.anabiik.schwarz@gmail.com> | 2019-02-12 16:55:01 +0100 |
|---|---|---|
| committer | Ryan Dahl <ry@tinyclouds.org> | 2019-02-12 10:55:01 -0500 |
| commit | 967389b5df703a8896a343b99ae1e13aefc89cc2 (patch) | |
| tree | 069716f71b60727cfac626200a38d6920b132143 | |
| parent | 53784dce7cb51e0713f563513479ef4101d8c111 (diff) | |
Add benching (denoland/deno_std#185)
Original: https://github.com/denoland/deno_std/commit/0a160c392521f643237cb4890ff64dac6e5e3a6b
| -rw-r--r-- | benching/example.ts | 29 | ||||
| -rw-r--r-- | benching/mod.ts | 169 | ||||
| -rw-r--r-- | benching/readme.md | 86 | ||||
| -rw-r--r-- | benching/test.ts | 50 | ||||
| -rwxr-xr-x | test.ts | 1 |
5 files changed, 335 insertions, 0 deletions
diff --git a/benching/example.ts b/benching/example.ts new file mode 100644 index 000000000..2388ba5d9 --- /dev/null +++ b/benching/example.ts @@ -0,0 +1,29 @@ +// https://deno.land/x/benching/mod.ts +import { BenchmarkTimer, runBenchmarks, bench } from "mod.ts"; + +// Simple +bench(function forIncrementX1e9(b: BenchmarkTimer) { + b.start(); + for (let i = 0; i < 1e9; i++); + b.stop(); +}); + +// Reporting average measured time for $runs runs of func +bench({ + name: "runs100ForIncrementX1e6", + runs: 100, + func(b: BenchmarkTimer) { + b.start(); + for (let i: number = 0; i < 1e6; i++); + b.stop(); + } +}); + +// Itsabug +bench(function throwing(b) { + b.start(); + // Throws bc the timer's stop method is never called +}); + +// Bench control +runBenchmarks({ skip: /throw/ }); diff --git a/benching/mod.ts b/benching/mod.ts new file mode 100644 index 000000000..be77b52c2 --- /dev/null +++ b/benching/mod.ts @@ -0,0 +1,169 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. + +import { exit, noColor } from "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 type 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 = Date.now(); + }, + stop(): void { + clock.stop = Date.now(); + } + }; +} + +const candidates: Array<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: Array<BenchmarkDefinition> = candidates.filter( + ({ name }) => only.test(name) && !skip.test(name) + ); + // Init main counters and error flag + const filtered: number = candidates.length - benchmarks.length; + let measured: number = 0; + let failed: boolean = false; + // Setting up a shared benchmark clock and timer + const clock: BenchmarkClock = { start: NaN, stop: NaN }; + const b: BenchmarkTimer = createBenchmarkTimer(clock); + // Iterating given benchmark definitions (await-in-loop) + console.log( + "running", + benchmarks.length, + `benchmark${benchmarks.length === 1 ? " ..." : "s ..."}` + ); + for (const { name, runs, func } of benchmarks) { + // See https://github.com/denoland/deno/pull/1452 about groupCollapsed + console.groupCollapsed(`benchmark ${name} ... `); + // Trying benchmark.func + let result: string; + 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: number = 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(() => exit(1), 0); + } +} diff --git a/benching/readme.md b/benching/readme.md new file mode 100644 index 000000000..979d1844c --- /dev/null +++ b/benching/readme.md @@ -0,0 +1,86 @@ +# benching + +Basic benchmarking module. Provides flintstone millisecond resolution. + +## Import + +```ts +import * as benching from "https://deno.land/x/benching/mod.ts"; +``` + +## Usage + +```ts +import { + BenchmarkTimer, + runBenchmarks, + bench +} from "https://deno.land/x/benching/mod.ts"; + +// Simple +bench(function forIncrementX1e9(b: BenchmarkTimer) { + b.start(); + for (let i = 0; i < 1e9; i++); + b.stop(); +}); + +// Reporting average measured time for $runs runs of func +bench({ + name: "runs100ForIncrementX1e6", + runs: 100, + func(b: BenchmarkTimer) { + b.start(); + for (let i: number = 0; i < 1e6; i++); + b.stop(); + } +}); + +// Itsabug +bench(function throwing(b) { + b.start(); + // Throws bc the timer's stop method is never called +}); + +// Bench control +runBenchmarks({ skip: /throw/ }); +``` + +## API + +#### `bench(benchmark: BenchmarkDefinition | BenchmarkFunction): void` + +Registers a benchmark that will be run once `runBenchmarks` is called. + +#### `runBenchmarks(opts?: BenchmarkRunOptions): Promise<void>` + +Runs all registered benchmarks serially. Filtering can be applied by setting +`BenchmarkRunOptions.only` and/or `BenchmarkRunOptions.skip` to regular expressions matching benchmark names. + +#### Other exports + +```ts +/** Provides methods for starting and stopping a benchmark clock. */ +export interface BenchmarkTimer { + start: () => void; + stop: () => void; +} + +/** Defines a benchmark through a named function. */ +export type 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; +} +``` diff --git a/benching/test.ts b/benching/test.ts new file mode 100644 index 000000000..81cbc5e6f --- /dev/null +++ b/benching/test.ts @@ -0,0 +1,50 @@ +import { test } from "../testing/mod.ts"; +import { bench, runBenchmarks, BenchmarkTimer } from "./mod.ts"; + +import "example.ts"; + +test(async function benching() { + bench(function forIncrementX1e9(b: BenchmarkTimer) { + b.start(); + for (let i: number = 0; i < 1e9; i++); + b.stop(); + }); + + bench(function forDecrementX1e9(b: BenchmarkTimer) { + b.start(); + for (let i: number = 1e9; i > 0; i--); + b.stop(); + }); + + bench(async function forAwaitFetchDenolandX10(b: BenchmarkTimer) { + b.start(); + for (let i: number = 0; i < 10; i++) { + await fetch("https://deno.land/"); + } + b.stop(); + }); + + bench(async function promiseAllFetchDenolandX10(b: BenchmarkTimer) { + const urls = new Array(10).fill("https://deno.land/"); + b.start(); + await Promise.all(urls.map((denoland: string) => fetch(denoland))); + b.stop(); + }); + + bench({ + name: "runs100ForIncrementX1e6", + runs: 100, + func(b: BenchmarkTimer) { + b.start(); + for (let i: number = 0; i < 1e6; i++); + b.stop(); + } + }); + + bench(function throwing(b: BenchmarkTimer) { + b.start(); + // Throws bc the timer's stop method is never called + }); + + await runBenchmarks({ skip: /throw/ }); +}); @@ -1,5 +1,6 @@ #!/usr/bin/env deno -A // Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +import "benching/test.ts"; import "colors/test.ts"; import "datetime/test.ts"; import "examples/test.ts"; |
