Skip to content

Commit

Permalink
feat(bench): add types, benchmark(), bigint timestamps, restructure
Browse files Browse the repository at this point in the history
- split module into separate files
- add BenchmarkOpts / BenchmarkResult types
- add benchmark()
- add now() timestamp fn (uses nanosec timer on Node)

BREAKING CHANGE: Though no public API change, this library internally
uses ES BigInt timestamps now (in Node via `process.hrtime.bigint()`).
  • Loading branch information
postspectacular committed Dec 21, 2019
1 parent 41bd4e4 commit e0af94c
Show file tree
Hide file tree
Showing 10 changed files with 302 additions and 72 deletions.
50 changes: 48 additions & 2 deletions packages/bench/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,32 @@ This project is part of the

- [About](#about)
- [Status](#status)
- [Breaking changes](#breaking-changes)
- [Installation](#installation)
- [Dependencies](#dependencies)
- [Usage examples](#usage-examples)
- [API](#api)
- [Benchmarking with statistics](#benchmarking-with-statistics)
- [Authors](#authors)
- [License](#license)

## About

Basic benchmarking utilities.
Benchmarking utilities w/ optional statistics.

### Status

**STABLE** - used in production

### Breaking changes

Though no public API change, since v2.0.0 this library internally uses
ES
[`BigInt`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt)
timestamps (in Node via `process.hrtime.bigint()`). See
[caniuse](https://caniuse.com/#feat=mdn-javascript_builtins_bigint) for
browser support.

## Installation

```bash
Expand Down Expand Up @@ -55,7 +66,7 @@ A selection:
[Generated API docs](https://docs.thi.ng/umbrella/bench/)

```ts
import { timed, bench } from "@thi.ng/bench";
import { timed, bench, benchmark } from "@thi.ng/bench";

// test functions
const fib = (n) => n > 2 ? fib(n - 1) + fib(n - 2) : n > 0 ? 1 : 0;
Expand Down Expand Up @@ -86,6 +97,41 @@ bench(() => fib2(10), 1e6);
// 55
```

### Benchmarking with statistics

The `benchmark()` function executes a number of warmup runs, before
executing the main measurement and producing a number of useful
statistics: mean, median, min/max, 1st/3rd quartile, standard deviation
(as percentage)...

See
[api.ts](https://github.com/thi-ng/umbrella/tree/master/packages/bench/src/api.ts)
for configuration options.

```ts
benchmark(() => fib(40), { title: "fib", iter: 10, warmup: 5 });
// benchmarking: fib
// warmup... 3707.17ms (5 runs)
// executing...
// total: 7333.72ms, runs: 10
// mean: 733.37ms, median: 733.79ms, range: [728.58..743.43]
// q1: 730.98ms, q3: 735.03ms
// sd: 0.54%

// also returns results:
// {
// iter: 10,
// total: 7333.72402,
// mean: 733.372402,
// median: 733.794194,
// min: 728.5808,
// max: 743.432538,
// q1: 730.980115,
// q3: 735.025314,
// sd: 0.542200865574415
// }
```

## Authors

Karsten Schmidt
Expand Down
46 changes: 45 additions & 1 deletion packages/bench/README.tpl.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ ${pkg.description}

${status}

### Breaking changes

Though no public API change, since v2.0.0 this library internally uses
ES
[`BigInt`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt)
timestamps (in Node via `process.hrtime.bigint()`). See
[caniuse](https://caniuse.com/#feat=mdn-javascript_builtins_bigint) for
browser support.

${supportPackages}

${relatedPackages}
Expand All @@ -38,7 +47,7 @@ ${examples}
${docLink}

```ts
import { timed, bench } from "@thi.ng/bench";
import { timed, bench, benchmark } from "@thi.ng/bench";

// test functions
const fib = (n) => n > 2 ? fib(n - 1) + fib(n - 2) : n > 0 ? 1 : 0;
Expand Down Expand Up @@ -69,6 +78,41 @@ bench(() => fib2(10), 1e6);
// 55
```

### Benchmarking with statistics

The `benchmark()` function executes a number of warmup runs, before
executing the main measurement and producing a number of useful
statistics: mean, median, min/max, 1st/3rd quartile, standard deviation
(as percentage)...

See
[api.ts](https://github.com/thi-ng/umbrella/tree/master/packages/bench/src/api.ts)
for configuration options.

```ts
benchmark(() => fib(40), { title: "fib", iter: 10, warmup: 5 });
// benchmarking: fib
// warmup... 3707.17ms (5 runs)
// executing...
// total: 7333.72ms, runs: 10
// mean: 733.37ms, median: 733.79ms, range: [728.58..743.43]
// q1: 730.98ms, q3: 735.03ms
// sd: 0.54%

// also returns results:
// {
// iter: 10,
// total: 7333.72402,
// mean: 733.372402,
// median: 733.794194,
// min: 728.5808,
// max: 743.432538,
// q1: 730.980115,
// q3: 735.025314,
// sd: 0.542200865574415
// }
```

## Authors

${authors}
Expand Down
5 changes: 4 additions & 1 deletion packages/bench/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@thi.ng/bench",
"version": "1.0.11",
"description": "Basic benchmarking utilities",
"description": "Benchmarking utilities w/ optional statistics",
"module": "./index.js",
"main": "./lib/index.js",
"umd:main": "./lib/index.umd.js",
Expand Down Expand Up @@ -39,10 +39,13 @@
},
"keywords": [
"benchmark",
"bigint",
"ES6",
"execution",
"function",
"hrtime",
"measure",
"statistics",
"timing",
"typescript"
],
Expand Down
67 changes: 67 additions & 0 deletions packages/bench/src/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
export type TimingResult<T> = [T, number];

export interface BenchmarkOpts {
/**
* Benchmark title (only used if `print` enabled)
*/
title: string;
/**
* Number of iterations
*
* @defaultValue 1000
*/
iter: number;
/**
* Number of warmup iterations (not included in results).
*
* @defaultValue 10
*/
warmup: number;
/**
* If true, writes progress & results to console.
*
* @defaultValue true
*/
print: boolean;
}

export interface BenchmarkResult {
/**
* Number of iterations
*/
iter: number;
/**
* Total execution time for all runs (in ms)
*/
total: number;
/**
* Mean execution time (in ms)
*/
mean: number;
/**
* Median execution time (in ms)
*/
median: number;
/**
* Min execution time (in ms)
*/
min: number;
/**
* Max execution time (in ms)
*/
max: number;
/**
* First quartile execution time (in ms). I.e. 25% of all runs were
* faster/equal to this measurement.
*/
q1: number;
/**
* Third quartile execution time (in ms). I.e. 25% of all runs were
* equal/slower than this measurement.
*/
q3: number;
/**
* Standard deviation (in percent)
*/
sd: number;
}
37 changes: 37 additions & 0 deletions packages/bench/src/bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { TimingResult } from "./api";
import { timed, timedResult } from "./timed";

/**
* Executes given function `n` times, prints elapsed time to console and
* returns last result from fn. The optional `prefix` will be displayed
* with the output, allowing to label different measurements.
*
* @param fn - function to time
* @param n - number of iterations
*/
export const bench = <T>(fn: () => T, n = 1e6, prefix = "") => {
let res: T;
return timed(() => {
while (n-- > 0) {
res = fn();
}
return res;
}, prefix);
};

/**
* Similar to {@link bench}, but produces no output and instead returns
* tuple of `fn`'s last result and the grand total time measurement.
*
* @param fn - function to time
* @param n - number of iterations
*/
export const benchResult = <T>(fn: () => T, n = 1e6): TimingResult<T> => {
let res: T;
return timedResult(() => {
while (n-- > 0) {
res = fn();
}
return res;
});
};
54 changes: 54 additions & 0 deletions packages/bench/src/benchmark.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { BenchmarkOpts, BenchmarkResult } from "./api";
import { benchResult } from "./bench";
import { timedResult } from "./timed";

export const benchmark = (
fn: () => void,
opts?: Partial<BenchmarkOpts>
): BenchmarkResult => {
opts = { title: "", iter: 1e3, warmup: 10, print: true, ...opts };
const { iter, warmup, print } = opts;
print && console.log(`benchmarking: ${opts.title}`);
const t = benchResult(fn, warmup)[1];
print && console.log(`\twarmup... ${t.toFixed(2)}ms (${warmup} runs)`);
print && console.log("\texecuting...");
const samples: number[] = [];
for (let i = iter!; --i >= 0; ) {
samples.push(timedResult(fn)[1]);
}
samples.sort((a, b) => a - b);
const total = samples.reduce((acc, x) => acc + x, 0);
const mean = total / iter!;
const median = samples[iter! >> 1];
const min = samples[0];
const max = samples[iter! - 1];
const q1 = samples[Math.ceil(iter! * 0.25)];
const q3 = samples[Math.ceil(iter! * 0.75)];
const sd =
(Math.sqrt(
samples.reduce((acc, x) => acc + (mean - x) ** 2, 0) / iter!
) /
mean) *
100;
if (print) {
console.log(`\ttotal: ${total.toFixed(2)}ms, runs: ${iter}`);
console.log(
`\tmean: ${mean.toFixed(2)}ms, median: ${median.toFixed(
2
)}ms, range: [${min.toFixed(2)}..${max.toFixed(2)}]`
);
console.log(`\tq1: ${q1.toFixed(2)}ms, q3: ${q3.toFixed(2)}ms`);
console.log(`\tsd: ${sd.toFixed(2)}%`);
}
return {
iter: iter!,
total,
mean,
median,
min,
max,
q1,
q3,
sd
};
};
Loading

0 comments on commit e0af94c

Please sign in to comment.