summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorchiefbiiko <noah.anabiik.schwarz@gmail.com>2019-02-12 16:55:01 +0100
committerRyan Dahl <ry@tinyclouds.org>2019-02-12 10:55:01 -0500
commit967389b5df703a8896a343b99ae1e13aefc89cc2 (patch)
tree069716f71b60727cfac626200a38d6920b132143
parent53784dce7cb51e0713f563513479ef4101d8c111 (diff)
Add benching (denoland/deno_std#185)
Original: https://github.com/denoland/deno_std/commit/0a160c392521f643237cb4890ff64dac6e5e3a6b
-rw-r--r--benching/example.ts29
-rw-r--r--benching/mod.ts169
-rw-r--r--benching/readme.md86
-rw-r--r--benching/test.ts50
-rwxr-xr-xtest.ts1
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/ });
+});
diff --git a/test.ts b/test.ts
index 92d916e41..da3cecf3c 100755
--- a/test.ts
+++ b/test.ts
@@ -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";