@thi.ng/bench 3.4.12 → 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 +1 -1
- package/api.js +6 -2
- package/bench.js +20 -31
- package/benchmark.js +49 -51
- package/format/csv.js +19 -18
- package/format/default.js +18 -14
- package/format/markdown.js +40 -38
- package/now.js +8 -33
- package/package.json +8 -5
- package/profiler.js +295 -287
- package/suite.js +16 -13
- package/timed.js +13 -23
package/CHANGELOG.md
CHANGED
package/api.js
CHANGED
package/bench.js
CHANGED
|
@@ -1,34 +1,23 @@
|
|
|
1
1
|
import { timed, timedResult } from "./timed.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
|
|
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
|
};
|
package/format/markdown.js
CHANGED
|
@@ -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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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": "
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
export
|
|
22
|
-
|
|
23
|
-
|
|
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
|
};
|