@thi.ng/bench 3.4.13 → 3.4.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Change Log
2
2
 
3
- - **Last updated**: 2023-12-03T12:13:31Z
3
+ - **Last updated**: 2023-12-11T10:07:09Z
4
4
  - **Generator**: [thi.ng/monopub](https://thi.ng/monopub)
5
5
 
6
6
  All notable changes to this project will be documented in this file.
package/api.js CHANGED
@@ -1,2 +1,6 @@
1
- export const FLOAT = (x) => x.toFixed(2);
2
- export const EMPTY = () => "";
1
+ const FLOAT = (x) => x.toFixed(2);
2
+ const EMPTY = () => "";
3
+ export {
4
+ EMPTY,
5
+ FLOAT
6
+ };
package/bench.js CHANGED
@@ -1,34 +1,23 @@
1
1
  import { timed, timedResult } from "./timed.js";
2
- /**
3
- * Executes given function `n` times, prints elapsed time to console and
4
- * returns last result from fn. The optional `prefix` will be displayed
5
- * with the output, allowing to label different measurements.
6
- *
7
- * @param fn - function to time
8
- * @param n - number of iterations
9
- */
10
- export const bench = (fn, n = 1e6, prefix = "") => {
11
- let res;
12
- return timed(() => {
13
- while (n-- > 0) {
14
- res = fn();
15
- }
16
- return res;
17
- }, prefix);
2
+ const bench = (fn, n = 1e6, prefix = "") => {
3
+ let res;
4
+ return timed(() => {
5
+ while (n-- > 0) {
6
+ res = fn();
7
+ }
8
+ return res;
9
+ }, prefix);
18
10
  };
19
- /**
20
- * Similar to {@link bench}, but produces no output and instead returns
21
- * tuple of `fn`'s last result and the grand total time measurement.
22
- *
23
- * @param fn - function to time
24
- * @param n - number of iterations
25
- */
26
- export const benchResult = (fn, n = 1e6) => {
27
- let res;
28
- return timedResult(() => {
29
- while (n-- > 0) {
30
- res = fn();
31
- }
32
- return res;
33
- });
11
+ const benchResult = (fn, n = 1e6) => {
12
+ let res;
13
+ return timedResult(() => {
14
+ while (n-- > 0) {
15
+ res = fn();
16
+ }
17
+ return res;
18
+ });
19
+ };
20
+ export {
21
+ bench,
22
+ benchResult
34
23
  };
package/benchmark.js CHANGED
@@ -1,55 +1,53 @@
1
1
  import { benchResult } from "./bench.js";
2
2
  import { FORMAT_DEFAULT } from "./format/default.js";
3
- export const DEFAULT_OPTS = {
4
- title: "benchmark",
5
- iter: 1e3,
6
- size: 1,
7
- warmup: 10,
8
- output: true,
9
- format: FORMAT_DEFAULT,
3
+ const DEFAULT_OPTS = {
4
+ title: "benchmark",
5
+ iter: 1e3,
6
+ size: 1,
7
+ warmup: 10,
8
+ output: true,
9
+ format: FORMAT_DEFAULT
10
10
  };
11
- export const benchmark = (fn, opts) => {
12
- const _opts = { ...DEFAULT_OPTS, ...opts };
13
- const { iter, size, warmup, output, format } = _opts;
14
- output && outputString(format.start(_opts));
15
- const t = benchResult(fn, warmup * size)[1];
16
- output && outputString(format.warmup(t, _opts));
17
- const samples = [];
18
- for (let i = iter; i-- > 0;) {
19
- samples.push(benchResult(fn, size)[1]);
20
- }
21
- samples.sort((a, b) => a - b);
22
- const total = samples.reduce((acc, x) => acc + x, 0);
23
- const mean = total / iter;
24
- const median = samples[iter >> 1];
25
- const min = samples[0];
26
- const max = samples[iter - 1];
27
- const q1 = samples[Math.ceil(iter * 0.25)];
28
- const q3 = samples[Math.ceil(iter * 0.75)];
29
- const sd = (Math.sqrt(samples.reduce((acc, x) => acc + (mean - x) ** 2, 0) / iter) /
30
- mean) *
31
- 100;
32
- const res = {
33
- title: _opts.title,
34
- iter,
35
- size,
36
- total,
37
- mean,
38
- median,
39
- min,
40
- max,
41
- q1,
42
- q3,
43
- sd,
44
- };
45
- output && outputString(format.result(res));
46
- return res;
11
+ const benchmark = (fn, opts) => {
12
+ const _opts = { ...DEFAULT_OPTS, ...opts };
13
+ const { iter, size, warmup, output, format } = _opts;
14
+ output && outputString(format.start(_opts));
15
+ const t = benchResult(fn, warmup * size)[1];
16
+ output && outputString(format.warmup(t, _opts));
17
+ const samples = [];
18
+ for (let i = iter; i-- > 0; ) {
19
+ samples.push(benchResult(fn, size)[1]);
20
+ }
21
+ samples.sort((a, b) => a - b);
22
+ const total = samples.reduce((acc, x) => acc + x, 0);
23
+ const mean = total / iter;
24
+ const median = samples[iter >> 1];
25
+ const min = samples[0];
26
+ const max = samples[iter - 1];
27
+ const q1 = samples[Math.ceil(iter * 0.25)];
28
+ const q3 = samples[Math.ceil(iter * 0.75)];
29
+ const sd = Math.sqrt(
30
+ samples.reduce((acc, x) => acc + (mean - x) ** 2, 0) / iter
31
+ ) / mean * 100;
32
+ const res = {
33
+ title: _opts.title,
34
+ iter,
35
+ size,
36
+ total,
37
+ mean,
38
+ median,
39
+ min,
40
+ max,
41
+ q1,
42
+ q3,
43
+ sd
44
+ };
45
+ output && outputString(format.result(res));
46
+ return res;
47
+ };
48
+ const outputString = (str) => str !== "" && console.log(str);
49
+ export {
50
+ DEFAULT_OPTS,
51
+ benchmark,
52
+ outputString
47
53
  };
48
- /**
49
- * Only outputs non-empty strings to console.
50
- *
51
- * @param str
52
- *
53
- * @internal
54
- */
55
- export const outputString = (str) => str !== "" && console.log(str);
package/format/csv.js CHANGED
@@ -1,20 +1,21 @@
1
1
  import { EMPTY, FLOAT } from "../api.js";
2
- export const FORMAT_CSV = {
3
- prefix: () => `Title,Iterations,Size,Total,Mean,Median,Min,Max,Q1,Q3,SD%`,
4
- start: EMPTY,
5
- warmup: EMPTY,
6
- result: (res) => `"${res.title}",${res.iter},${res.size},${[
7
- res.total,
8
- res.mean,
9
- res.median,
10
- res.min,
11
- res.max,
12
- res.q1,
13
- res.q3,
14
- res.sd,
15
- ]
16
- .map(FLOAT)
17
- .join(",")}`,
18
- total: EMPTY,
19
- suffix: EMPTY,
2
+ const FORMAT_CSV = {
3
+ prefix: () => `Title,Iterations,Size,Total,Mean,Median,Min,Max,Q1,Q3,SD%`,
4
+ start: EMPTY,
5
+ warmup: EMPTY,
6
+ result: (res) => `"${res.title}",${res.iter},${res.size},${[
7
+ res.total,
8
+ res.mean,
9
+ res.median,
10
+ res.min,
11
+ res.max,
12
+ res.q1,
13
+ res.q3,
14
+ res.sd
15
+ ].map(FLOAT).join(",")}`,
16
+ total: EMPTY,
17
+ suffix: EMPTY
18
+ };
19
+ export {
20
+ FORMAT_CSV
20
21
  };
package/format/default.js CHANGED
@@ -1,17 +1,21 @@
1
1
  import { EMPTY, FLOAT } from "../api.js";
2
- export const FORMAT_DEFAULT = {
3
- prefix: EMPTY,
4
- start: ({ title }) => `benchmarking: ${title}`,
5
- warmup: (t, { warmup }) => `\twarmup... ${FLOAT(t)}ms (${warmup} runs)`,
6
- result: ({ iter, size, total, mean, median, min, max, q1, q3, sd }) =>
2
+ const FORMAT_DEFAULT = {
3
+ prefix: EMPTY,
4
+ start: ({ title }) => `benchmarking: ${title}`,
5
+ warmup: (t, { warmup }) => ` warmup... ${FLOAT(t)}ms (${warmup} runs)`,
6
+ result: ({ iter, size, total, mean, median, min, max, q1, q3, sd }) => (
7
7
  // prettier-ignore
8
- `\ttotal: ${FLOAT(total)}ms, runs: ${iter} (@ ${size} calls/iter)
9
- \tmean: ${FLOAT(mean)}ms, median: ${FLOAT(median)}ms, range: [${FLOAT(min)}..${FLOAT(max)}]
10
- \tq1: ${FLOAT(q1)}ms, q3: ${FLOAT(q3)}ms
11
- \tsd: ${FLOAT(sd)}%`,
12
- total: (res) => {
13
- const fastest = res.slice().sort((a, b) => a.mean - b.mean)[0];
14
- return `Fastest: "${fastest.title}"`;
15
- },
16
- suffix: () => `---`,
8
+ ` total: ${FLOAT(total)}ms, runs: ${iter} (@ ${size} calls/iter)
9
+ mean: ${FLOAT(mean)}ms, median: ${FLOAT(median)}ms, range: [${FLOAT(min)}..${FLOAT(max)}]
10
+ q1: ${FLOAT(q1)}ms, q3: ${FLOAT(q3)}ms
11
+ sd: ${FLOAT(sd)}%`
12
+ ),
13
+ total: (res) => {
14
+ const fastest = res.slice().sort((a, b) => a.mean - b.mean)[0];
15
+ return `Fastest: "${fastest.title}"`;
16
+ },
17
+ suffix: () => `---`
18
+ };
19
+ export {
20
+ FORMAT_DEFAULT
17
21
  };
@@ -1,13 +1,11 @@
1
1
  import { EMPTY, FLOAT } from "../api.js";
2
2
  const $n = (n, char = "-") => new Array(n).fill(char).join("");
3
3
  const pad = (w) => {
4
- const column = $n(w, " ");
5
- return (x) => {
6
- const s = typeof x === "number" ? FLOAT(x) : x;
7
- return s.length < w
8
- ? column.substr(0, w - s.length) + s
9
- : s.substr(0, w);
10
- };
4
+ const column = $n(w, " ");
5
+ return (x) => {
6
+ const s = typeof x === "number" ? FLOAT(x) : x;
7
+ return s.length < w ? column.substr(0, w - s.length) + s : s.substr(0, w);
8
+ };
11
9
  };
12
10
  const c24 = pad(24);
13
11
  const c12 = pad(12);
@@ -18,35 +16,39 @@ const d8 = $n(7) + ":";
18
16
  const COLUMNS = [c24, c8, c8, c12, c8, c8, c8, c8, c8, c8, c8];
19
17
  const DASHES = [d24, d8, d8, d12, d8, d8, d8, d8, d8, d8, d8];
20
18
  const row = (cols) => `|${cols.map((x, i) => COLUMNS[i](x)).join("|")}|`;
21
- export const FORMAT_MD = {
22
- prefix: () => row([
23
- "Title",
24
- "Iter",
25
- "Size",
26
- "Total",
27
- "Mean",
28
- "Median",
29
- "Min",
30
- "Max",
31
- "Q1",
32
- "Q3",
33
- "SD%",
34
- ]) + `\n|${DASHES.join("|")}|`,
35
- start: EMPTY,
36
- warmup: EMPTY,
37
- result: (res) => row([
38
- res.title,
39
- "" + res.iter,
40
- "" + res.size,
41
- res.total,
42
- res.mean,
43
- res.median,
44
- res.min,
45
- res.max,
46
- res.q1,
47
- res.q3,
48
- res.sd,
49
- ]),
50
- total: EMPTY,
51
- suffix: EMPTY,
19
+ const FORMAT_MD = {
20
+ prefix: () => row([
21
+ "Title",
22
+ "Iter",
23
+ "Size",
24
+ "Total",
25
+ "Mean",
26
+ "Median",
27
+ "Min",
28
+ "Max",
29
+ "Q1",
30
+ "Q3",
31
+ "SD%"
32
+ ]) + `
33
+ |${DASHES.join("|")}|`,
34
+ start: EMPTY,
35
+ warmup: EMPTY,
36
+ result: (res) => row([
37
+ res.title,
38
+ "" + res.iter,
39
+ "" + res.size,
40
+ res.total,
41
+ res.mean,
42
+ res.median,
43
+ res.min,
44
+ res.max,
45
+ res.q1,
46
+ res.q3,
47
+ res.sd
48
+ ]),
49
+ total: EMPTY,
50
+ suffix: EMPTY
51
+ };
52
+ export {
53
+ FORMAT_MD
52
54
  };
package/now.js CHANGED
@@ -1,33 +1,8 @@
1
- /**
2
- * If available, returns wrapper for `process.hrtime.bigint()` else falls back
3
- * to `performance.now()` or lacking that to `Date.now()`. In all cases, returns
4
- * a nanosec-scale timestamp, either as `bigint` or `number`.
5
- */
6
- export const now = typeof BigInt !== "undefined"
7
- ? typeof process !== "undefined" &&
8
- typeof process.hrtime !== "undefined" &&
9
- typeof process.hrtime.bigint === "function"
10
- ? () => process.hrtime.bigint()
11
- : typeof performance !== "undefined"
12
- ? () => BigInt(Math.floor(performance.now() * 1e6))
13
- : () => BigInt(Date.now() * 1e6)
14
- : typeof performance !== "undefined"
15
- ? () => performance.now() * 1e6
16
- : () => Date.now() * 1e6;
17
- /**
18
- * Returns the difference in milliseconds between 2 given {@link Timestamp}s.
19
- * `b` defaults to result of {@link now}.
20
- *
21
- * @param a
22
- * @param b
23
- */
24
- export const timeDiff = (a, b = now()) => (typeof BigInt !== "undefined"
25
- ? Number(b - a)
26
- : b - a) * 1e-6;
27
- /**
28
- * Takes a duration (either a number or bigint) in nanosec-scale (see
29
- * {@link now}) and converts it to a JS number in milliseconds.
30
- *
31
- * @param t
32
- */
33
- export const asMillis = (t) => (typeof t === "bigint" ? Number(t) : t) * 1e-6;
1
+ const now = typeof BigInt !== "undefined" ? typeof process !== "undefined" && typeof process.hrtime !== "undefined" && typeof process.hrtime.bigint === "function" ? () => process.hrtime.bigint() : typeof performance !== "undefined" ? () => BigInt(Math.floor(performance.now() * 1e6)) : () => BigInt(Date.now() * 1e6) : typeof performance !== "undefined" ? () => performance.now() * 1e6 : () => Date.now() * 1e6;
2
+ const timeDiff = (a, b = now()) => (typeof BigInt !== "undefined" ? Number(b - a) : b - a) * 1e-6;
3
+ const asMillis = (t) => (typeof t === "bigint" ? Number(t) : t) * 1e-6;
4
+ export {
5
+ asMillis,
6
+ now,
7
+ timeDiff
8
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thi.ng/bench",
3
- "version": "3.4.13",
3
+ "version": "3.4.14",
4
4
  "description": "Benchmarking & profiling utilities w/ various statistics & formatters (CSV, JSON, Markdown etc.)",
5
5
  "type": "module",
6
6
  "module": "./index.js",
@@ -24,7 +24,9 @@
24
24
  "author": "Karsten Schmidt <k+npm@thi.ng>",
25
25
  "license": "Apache-2.0",
26
26
  "scripts": {
27
- "build": "yarn clean && tsc --declaration",
27
+ "build": "yarn build:esbuild && yarn build:decl",
28
+ "build:decl": "tsc --declaration --emitDeclarationOnly",
29
+ "build:esbuild": "esbuild --format=esm --platform=neutral --target=es2022 --tsconfig=tsconfig.json --outdir=. src/**/*.ts",
28
30
  "clean": "rimraf --glob '*.js' '*.d.ts' '*.map' doc format",
29
31
  "doc": "typedoc --excludePrivate --excludeInternal --out doc src/index.ts",
30
32
  "doc:ae": "mkdir -p .ae/doc .ae/temp && api-extractor run --local --verbose",
@@ -33,12 +35,13 @@
33
35
  "test": "bun test"
34
36
  },
35
37
  "dependencies": {
36
- "@thi.ng/api": "^8.9.11"
38
+ "@thi.ng/api": "^8.9.12"
37
39
  },
38
40
  "devDependencies": {
39
41
  "@microsoft/api-extractor": "^7.38.3",
40
- "@thi.ng/testament": "^0.4.4",
42
+ "@thi.ng/testament": "^0.4.5",
41
43
  "@types/node": "^20.10.2",
44
+ "esbuild": "^0.19.8",
42
45
  "rimraf": "^5.0.5",
43
46
  "tools": "^0.0.1",
44
47
  "typedoc": "^0.25.4",
@@ -116,5 +119,5 @@
116
119
  ],
117
120
  "year": 2018
118
121
  },
119
- "gitHead": "25f2ac8ff795a432a930119661b364d4d93b59a0\n"
122
+ "gitHead": "5e7bafedfc3d53bc131469a28de31dd8e5b4a3ff\n"
120
123
  }
package/profiler.js CHANGED
@@ -1,300 +1,308 @@
1
1
  import { benchResult } from "./bench.js";
2
2
  import { asMillis, now } from "./now.js";
3
- export class Profiler {
4
- _session;
5
- _profiles;
6
- _enabled;
7
- _overhead = 0;
8
- constructor(opts) {
9
- const { warmup, enabled } = {
10
- warmup: 1e6,
11
- enabled: true,
12
- ...opts,
13
- };
14
- this.enable();
15
- if (warmup > 0)
16
- this.warmup(warmup);
17
- enabled ? this.reset() : this.disable();
3
+ class Profiler {
4
+ _session;
5
+ _profiles;
6
+ _enabled;
7
+ _overhead = 0;
8
+ constructor(opts) {
9
+ const { warmup, enabled } = {
10
+ warmup: 1e6,
11
+ enabled: true,
12
+ ...opts
13
+ };
14
+ this.enable();
15
+ if (warmup > 0)
16
+ this.warmup(warmup);
17
+ enabled ? this.reset() : this.disable();
18
+ }
19
+ isEnabled() {
20
+ return this._enabled;
21
+ }
22
+ /**
23
+ * Disables profiler and clears all existing profiles.
24
+ *
25
+ * @remarks
26
+ * Calls to {@link Profiler.start} and {@link Profiler.end} only are no-ops
27
+ * if the profiler is currently disabled.
28
+ */
29
+ disable() {
30
+ if (this._enabled) {
31
+ this._enabled = false;
32
+ this.reset();
18
33
  }
19
- isEnabled() {
20
- return this._enabled;
34
+ }
35
+ /**
36
+ * Enables profiler and clears all existing profiles.
37
+ *
38
+ * @remarks
39
+ * Calls to {@link Profiler.start} and {@link Profiler.end} only are no-ops
40
+ * if the profiler is currently disabled.
41
+ */
42
+ enable() {
43
+ if (!this._enabled) {
44
+ this._enabled = true;
45
+ this.reset();
21
46
  }
22
- /**
23
- * Disables profiler and clears all existing profiles.
24
- *
25
- * @remarks
26
- * Calls to {@link Profiler.start} and {@link Profiler.end} only are no-ops
27
- * if the profiler is currently disabled.
28
- */
29
- disable() {
30
- if (this._enabled) {
31
- this._enabled = false;
32
- this.reset();
33
- }
47
+ }
48
+ /**
49
+ * Resets profiler state and clears all recorded profiles.
50
+ */
51
+ reset() {
52
+ this._profiles = {};
53
+ this._session = void 0;
54
+ return this;
55
+ }
56
+ /**
57
+ * Prepare and return all recorded profiles as object of
58
+ * {@link ProfileResult}s.
59
+ *
60
+ * @remarks
61
+ * Automatically computes and subtracts internal overhead from each
62
+ * profile's total (Overhead is computed during profiler ctor and/or
63
+ * {@link Profiler.warmup}).
64
+ *
65
+ * Also see {@link Profiler.asCSV} to obtain results in CSV format.
66
+ */
67
+ deref() {
68
+ const { _profiles, _session, _overhead } = this;
69
+ if (!_session)
70
+ return {};
71
+ const res = {};
72
+ const sessionTotal = asMillis(_session.total) - _session.calls * _overhead;
73
+ for (let id in _profiles) {
74
+ const profile = _profiles[id];
75
+ const total = asMillis(profile.total) - profile.calls * _overhead;
76
+ const totalPercent = total / sessionTotal * 100;
77
+ const callsPercent = profile.calls / _session.calls * 100;
78
+ res[id] = {
79
+ id,
80
+ total,
81
+ timePerCall: total / profile.calls,
82
+ totalPercent,
83
+ calls: profile.calls,
84
+ callsPercent,
85
+ maxDepth: profile.maxDepth
86
+ };
34
87
  }
35
- /**
36
- * Enables profiler and clears all existing profiles.
37
- *
38
- * @remarks
39
- * Calls to {@link Profiler.start} and {@link Profiler.end} only are no-ops
40
- * if the profiler is currently disabled.
41
- */
42
- enable() {
43
- if (!this._enabled) {
44
- this._enabled = true;
45
- this.reset();
46
- }
88
+ return res;
89
+ }
90
+ /**
91
+ * Start a new profile (or add to an existing ID) by recording current
92
+ * timestamp (via {@link now}), number of calls (to this method and for this
93
+ * ID), as well as max. recursion depth. Use {@link Profiler.end} to
94
+ * stop/update measurements.
95
+ *
96
+ * @remarks
97
+ * Profiling only happens if the profiler is currently enabled, else a
98
+ * no-op.
99
+ *
100
+ * * @example
101
+ * ```ts
102
+ * const profiler = new Profiler();
103
+ *
104
+ * // recursive function
105
+ * const countdown = (n, acc = []) => {
106
+ * profiler.start("countdown");
107
+ * if (n > 0) countdown(n - 1, (acc.push(n),acc));
108
+ * profiler.end("countdown");
109
+ * return acc;
110
+ * }
111
+ *
112
+ * countdown(10);
113
+ * // [ 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 ]
114
+ *
115
+ * countdown(5);
116
+ * // [ 5, 4, 3, 2, 1 ]
117
+ *
118
+ * profiler.deref()
119
+ * // {
120
+ * // countdown: {
121
+ * // id: 'countdown',
122
+ * // total: 0.029665688286,
123
+ * // timePerCall: 0.0017450404874117648,
124
+ * // totalPercent: 96.0872831622525,
125
+ * // calls: 17,
126
+ * // callsPercent: 100,
127
+ * // maxDepth: 11
128
+ * // }
129
+ * // }
130
+ * ```
131
+ *
132
+ * @param id
133
+ */
134
+ start(id) {
135
+ if (!this._enabled)
136
+ return;
137
+ let profile = this._profiles[id];
138
+ const t0 = now();
139
+ if (!profile) {
140
+ this._profiles[id] = this.newProfile(t0);
141
+ } else {
142
+ profile.maxDepth = Math.max(profile.t0.push(t0), profile.maxDepth);
143
+ profile.calls++;
47
144
  }
48
- /**
49
- * Resets profiler state and clears all recorded profiles.
50
- */
51
- reset() {
52
- this._profiles = {};
53
- this._session = undefined;
54
- return this;
145
+ if (!this._session) {
146
+ this._session = this.newProfile(t0);
147
+ } else {
148
+ this._session.calls++;
55
149
  }
56
- /**
57
- * Prepare and return all recorded profiles as object of
58
- * {@link ProfileResult}s.
59
- *
60
- * @remarks
61
- * Automatically computes and subtracts internal overhead from each
62
- * profile's total (Overhead is computed during profiler ctor and/or
63
- * {@link Profiler.warmup}).
64
- *
65
- * Also see {@link Profiler.asCSV} to obtain results in CSV format.
66
- */
67
- deref() {
68
- const { _profiles, _session, _overhead } = this;
69
- if (!_session)
70
- return {};
71
- const res = {};
72
- const sessionTotal = asMillis(_session.total) - _session.calls * _overhead;
73
- for (let id in _profiles) {
74
- const profile = _profiles[id];
75
- const total = asMillis(profile.total) - profile.calls * _overhead;
76
- const totalPercent = (total / sessionTotal) * 100;
77
- const callsPercent = (profile.calls / _session.calls) * 100;
78
- res[id] = {
79
- id,
80
- total: total,
81
- timePerCall: total / profile.calls,
82
- totalPercent,
83
- calls: profile.calls,
84
- callsPercent,
85
- maxDepth: profile.maxDepth,
86
- };
87
- }
88
- return res;
150
+ }
151
+ /**
152
+ * Ends/updates measurements for given profile ID. Throws error if `id` is
153
+ * invalid or if no active profiling iteration exists for this ID (e.g. if
154
+ * this method is called more often than a corresponding
155
+ * {@link Profiler.start}).
156
+ *
157
+ * @remarks
158
+ * Profiling only happens if the profiler is currently enabled, else a
159
+ * no-op.
160
+ *
161
+ * @param id
162
+ */
163
+ end(id) {
164
+ if (!this._enabled)
165
+ return;
166
+ const t = now();
167
+ const profile = this._profiles[id];
168
+ if (!profile)
169
+ throw new Error(`invalid profile ID: ${id}`);
170
+ const t1 = profile.t0.pop();
171
+ if (t1 === void 0)
172
+ throw new Error(`no active profile for ID: ${id}`);
173
+ if (!profile.t0.length) {
174
+ profile.total += t - t1;
175
+ this._session.total += t - t1;
89
176
  }
90
- /**
91
- * Start a new profile (or add to an existing ID) by recording current
92
- * timestamp (via {@link now}), number of calls (to this method and for this
93
- * ID), as well as max. recursion depth. Use {@link Profiler.end} to
94
- * stop/update measurements.
95
- *
96
- * @remarks
97
- * Profiling only happens if the profiler is currently enabled, else a
98
- * no-op.
99
- *
100
- * * @example
101
- * ```ts
102
- * const profiler = new Profiler();
103
- *
104
- * // recursive function
105
- * const countdown = (n, acc = []) => {
106
- * profiler.start("countdown");
107
- * if (n > 0) countdown(n - 1, (acc.push(n),acc));
108
- * profiler.end("countdown");
109
- * return acc;
110
- * }
111
- *
112
- * countdown(10);
113
- * // [ 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 ]
114
- *
115
- * countdown(5);
116
- * // [ 5, 4, 3, 2, 1 ]
117
- *
118
- * profiler.deref()
119
- * // {
120
- * // countdown: {
121
- * // id: 'countdown',
122
- * // total: 0.029665688286,
123
- * // timePerCall: 0.0017450404874117648,
124
- * // totalPercent: 96.0872831622525,
125
- * // calls: 17,
126
- * // callsPercent: 100,
127
- * // maxDepth: 11
128
- * // }
129
- * // }
130
- * ```
131
- *
132
- * @param id
133
- */
134
- start(id) {
135
- if (!this._enabled)
136
- return;
137
- let profile = this._profiles[id];
138
- const t0 = now();
139
- if (!profile) {
140
- this._profiles[id] = this.newProfile(t0);
141
- }
142
- else {
143
- profile.maxDepth = Math.max(profile.t0.push(t0), profile.maxDepth);
144
- profile.calls++;
145
- }
146
- if (!this._session) {
147
- this._session = this.newProfile(t0);
148
- }
149
- else {
150
- this._session.calls++;
151
- }
152
- }
153
- /**
154
- * Ends/updates measurements for given profile ID. Throws error if `id` is
155
- * invalid or if no active profiling iteration exists for this ID (e.g. if
156
- * this method is called more often than a corresponding
157
- * {@link Profiler.start}).
158
- *
159
- * @remarks
160
- * Profiling only happens if the profiler is currently enabled, else a
161
- * no-op.
162
- *
163
- * @param id
164
- */
165
- end(id) {
166
- if (!this._enabled)
167
- return;
168
- const t = now();
169
- const profile = this._profiles[id];
170
- if (!profile)
171
- throw new Error(`invalid profile ID: ${id}`);
172
- const t1 = profile.t0.pop();
173
- if (t1 === undefined)
174
- throw new Error(`no active profile for ID: ${id}`);
175
- if (!profile.t0.length) {
176
- // @ts-ignore num/bigint
177
- profile.total += t - t1;
178
- // @ts-ignore num/bigint
179
- this._session.total += t - t1;
180
- }
181
- }
182
- /**
183
- * Takes a profile `id`, function `fn` and any (optional) arguments. Calls
184
- * `fn` with given args and profiles it using provided ID. Returns result
185
- * of `fn`.
186
- *
187
- * @remarks
188
- * Also see {@link Profiler.wrap}
189
- *
190
- * @param id
191
- * @param fn
192
- */
193
- profile(id, fn, ...args) {
177
+ }
178
+ /**
179
+ * Takes a profile `id`, function `fn` and any (optional) arguments. Calls
180
+ * `fn` with given args and profiles it using provided ID. Returns result
181
+ * of `fn`.
182
+ *
183
+ * @remarks
184
+ * Also see {@link Profiler.wrap}
185
+ *
186
+ * @param id
187
+ * @param fn
188
+ */
189
+ profile(id, fn, ...args) {
190
+ this.start(id);
191
+ const res = fn.apply(null, args);
192
+ this.end(id);
193
+ return res;
194
+ }
195
+ /**
196
+ * Higher-order version of {@link Profiler.profile}. Takes a profile `id`
197
+ * and vararg function `fn`. Returns new function which when called, calls
198
+ * given `fn` and profiles it using provided `id`, then returns result of
199
+ * `fn`.
200
+ *
201
+ * @example
202
+ * ```ts
203
+ * const sum = profiler.wrap(
204
+ * "sum",
205
+ * (vec: number[]) => vec.reduce((acc, x) => acc + x, 0)
206
+ * );
207
+ *
208
+ * sum([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
209
+ * // 55
210
+ *
211
+ * profiler.deref()
212
+ * // {
213
+ * // sum: {
214
+ * // id: 'sum',
215
+ * // total: 0.015644915291,
216
+ * // timePerCall: 0.015644915291,
217
+ * // totalPercent: 100,
218
+ * // calls: 1,
219
+ * // callsPercent: 100,
220
+ * // maxDepth: 1
221
+ * // }
222
+ * // }
223
+ * ```
224
+ *
225
+ * @param id
226
+ * @param fn
227
+ */
228
+ wrap(id, fn) {
229
+ return (...args) => {
230
+ this.start(id);
231
+ const res = fn.apply(null, args);
232
+ this.end(id);
233
+ return res;
234
+ };
235
+ }
236
+ /**
237
+ * Estimates the internal overhead of the {@link Profiler.start} and
238
+ * {@link Profiler.end} methods by performing given number of `iter`ations
239
+ * (distributed over 10 runs) and taking the mean duration of those runs.
240
+ *
241
+ * @remarks
242
+ * The computed overhead (per iteration) will be subtracted from the all
243
+ * recorded profiles (see {@link Profiler.deref} and
244
+ * {@link Profiler.asCSV}).
245
+ *
246
+ * @param iter
247
+ */
248
+ warmup(iter) {
249
+ let total = 0;
250
+ for (let i = 0; i < 10; i++) {
251
+ const id = `prof-${i}`;
252
+ const [_, taken] = benchResult(() => {
194
253
  this.start(id);
195
- const res = fn.apply(null, args);
196
254
  this.end(id);
197
- return res;
198
- }
199
- /**
200
- * Higher-order version of {@link Profiler.profile}. Takes a profile `id`
201
- * and vararg function `fn`. Returns new function which when called, calls
202
- * given `fn` and profiles it using provided `id`, then returns result of
203
- * `fn`.
204
- *
205
- * @example
206
- * ```ts
207
- * const sum = profiler.wrap(
208
- * "sum",
209
- * (vec: number[]) => vec.reduce((acc, x) => acc + x, 0)
210
- * );
211
- *
212
- * sum([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
213
- * // 55
214
- *
215
- * profiler.deref()
216
- * // {
217
- * // sum: {
218
- * // id: 'sum',
219
- * // total: 0.015644915291,
220
- * // timePerCall: 0.015644915291,
221
- * // totalPercent: 100,
222
- * // calls: 1,
223
- * // callsPercent: 100,
224
- * // maxDepth: 1
225
- * // }
226
- * // }
227
- * ```
228
- *
229
- * @param id
230
- * @param fn
231
- */
232
- wrap(id, fn) {
233
- return (...args) => {
234
- this.start(id);
235
- const res = fn.apply(null, args);
236
- this.end(id);
237
- return res;
238
- };
239
- }
240
- /**
241
- * Estimates the internal overhead of the {@link Profiler.start} and
242
- * {@link Profiler.end} methods by performing given number of `iter`ations
243
- * (distributed over 10 runs) and taking the mean duration of those runs.
244
- *
245
- * @remarks
246
- * The computed overhead (per iteration) will be subtracted from the all
247
- * recorded profiles (see {@link Profiler.deref} and
248
- * {@link Profiler.asCSV}).
249
- *
250
- * @param iter
251
- */
252
- warmup(iter) {
253
- let total = 0;
254
- for (let i = 0; i < 10; i++) {
255
- const id = `prof-${i}`;
256
- const [_, taken] = benchResult(() => {
257
- this.start(id);
258
- this.end(id);
259
- }, ~~(iter / 10));
260
- total += taken;
261
- }
262
- this._overhead = total / iter;
263
- }
264
- /**
265
- * Same as {@link Profiler.deref}.
266
- */
267
- toJSON() {
268
- return this.deref();
269
- }
270
- /**
271
- * Returns {@link Profiler.deref} formatted as CSV string.
272
- */
273
- asCSV() {
274
- const res = [
275
- `"id","total (ms)","time/call (ms)","total (%)","calls","calls (%)","max depth"`,
276
- ];
277
- const stats = this.deref();
278
- for (let id of Object.keys(stats).sort()) {
279
- const { total, timePerCall, totalPercent, calls, callsPercent, maxDepth, } = stats[id];
280
- res.push([
281
- `"${id}"`,
282
- total.toFixed(5),
283
- timePerCall.toFixed(5),
284
- totalPercent.toFixed(2),
285
- calls,
286
- callsPercent.toFixed(2),
287
- maxDepth,
288
- ].join(","));
289
- }
290
- return res.join("\n");
255
+ }, ~~(iter / 10));
256
+ total += taken;
291
257
  }
292
- newProfile(t0) {
293
- return {
294
- t0: [t0],
295
- total: typeof t0 === "bigint" ? BigInt(0) : 0,
296
- calls: 1,
297
- maxDepth: 1,
298
- };
258
+ this._overhead = total / iter;
259
+ }
260
+ /**
261
+ * Same as {@link Profiler.deref}.
262
+ */
263
+ toJSON() {
264
+ return this.deref();
265
+ }
266
+ /**
267
+ * Returns {@link Profiler.deref} formatted as CSV string.
268
+ */
269
+ asCSV() {
270
+ const res = [
271
+ `"id","total (ms)","time/call (ms)","total (%)","calls","calls (%)","max depth"`
272
+ ];
273
+ const stats = this.deref();
274
+ for (let id of Object.keys(stats).sort()) {
275
+ const {
276
+ total,
277
+ timePerCall,
278
+ totalPercent,
279
+ calls,
280
+ callsPercent,
281
+ maxDepth
282
+ } = stats[id];
283
+ res.push(
284
+ [
285
+ `"${id}"`,
286
+ total.toFixed(5),
287
+ timePerCall.toFixed(5),
288
+ totalPercent.toFixed(2),
289
+ calls,
290
+ callsPercent.toFixed(2),
291
+ maxDepth
292
+ ].join(",")
293
+ );
299
294
  }
295
+ return res.join("\n");
296
+ }
297
+ newProfile(t0) {
298
+ return {
299
+ t0: [t0],
300
+ total: typeof t0 === "bigint" ? BigInt(0) : 0,
301
+ calls: 1,
302
+ maxDepth: 1
303
+ };
304
+ }
300
305
  }
306
+ export {
307
+ Profiler
308
+ };
package/suite.js CHANGED
@@ -1,15 +1,18 @@
1
1
  import { benchmark, DEFAULT_OPTS, outputString } from "./benchmark.js";
2
- export const suite = (cases, opts) => {
3
- const _opts = {
4
- ...DEFAULT_OPTS,
5
- ...opts,
6
- };
7
- _opts.output && outputString(_opts.format.prefix());
8
- const results = [];
9
- for (let c of cases) {
10
- results.push(benchmark(c.fn, { ..._opts, ...c.opts, title: c.title }));
11
- }
12
- _opts.output && outputString(_opts.format.total(results));
13
- _opts.output && outputString(_opts.format.suffix());
14
- return results;
2
+ const suite = (cases, opts) => {
3
+ const _opts = {
4
+ ...DEFAULT_OPTS,
5
+ ...opts
6
+ };
7
+ _opts.output && outputString(_opts.format.prefix());
8
+ const results = [];
9
+ for (let c of cases) {
10
+ results.push(benchmark(c.fn, { ..._opts, ...c.opts, title: c.title }));
11
+ }
12
+ _opts.output && outputString(_opts.format.total(results));
13
+ _opts.output && outputString(_opts.format.suffix());
14
+ return results;
15
+ };
16
+ export {
17
+ suite
15
18
  };
package/timed.js CHANGED
@@ -1,26 +1,16 @@
1
1
  import { now, timeDiff } from "./now.js";
2
- /**
3
- * Calls function `fn` without args, prints elapsed time and returns
4
- * fn's result. The optional `prefix` will be displayed with the output,
5
- * allowing to label different measurements.
6
- *
7
- * @param fn - function to time
8
- * @param prefix - log prefix
9
- */
10
- export const timed = (fn, prefix = "") => {
11
- const [res, t] = timedResult(fn);
12
- console.log(`${prefix} ${t.toFixed(2)}ms`);
13
- return res;
2
+ const timed = (fn, prefix = "") => {
3
+ const [res, t] = timedResult(fn);
4
+ console.log(`${prefix} ${t.toFixed(2)}ms`);
5
+ return res;
14
6
  };
15
- /**
16
- * Similar to {@link timed}, but produces no output and instead returns
17
- * tuple of `fn`'s result and the time measurement (in milliseconds).
18
- *
19
- * @param fn - function to time
20
- */
21
- export const timedResult = (fn) => {
22
- const t0 = now();
23
- const res = fn();
24
- const t1 = now();
25
- return [res, timeDiff(t0, t1)];
7
+ const timedResult = (fn) => {
8
+ const t0 = now();
9
+ const res = fn();
10
+ const t1 = now();
11
+ return [res, timeDiff(t0, t1)];
12
+ };
13
+ export {
14
+ timed,
15
+ timedResult
26
16
  };