overtake 2.0.1 → 2.0.3
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/build/cli.js +30 -8
- package/build/executor.js +24 -10
- package/build/loader-hook.d.ts +2 -0
- package/build/loader-hook.js +33 -0
- package/build/register-hook.js +1 -14
- package/build/runner.js +1 -4
- package/build/utils.d.ts +1 -0
- package/build/utils.js +65 -21
- package/package.json +2 -2
- package/src/__tests__/fixtures/enum-bench.ts +17 -0
- package/src/__tests__/fixtures/param-property-bench.ts +14 -0
- package/src/__tests__/loader-hook.ts +33 -0
- package/src/cli.ts +30 -8
- package/src/executor.ts +21 -10
- package/src/loader-hook.ts +33 -0
- package/src/register-hook.ts +1 -13
- package/src/runner.ts +1 -3
- package/src/utils.ts +61 -20
package/build/cli.js
CHANGED
|
@@ -83,6 +83,7 @@ if (opts['compare-baseline']) {
|
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
const files = new Set((await Promise.all(patterns.map((pattern) => Array.fromAsync(glob(pattern, { cwd: process.cwd() })).catch(() => [])))).flat());
|
|
86
|
+
const allBaselineResults = {};
|
|
86
87
|
for (const file of files) {
|
|
87
88
|
const stats = await stat(file).catch(() => false);
|
|
88
89
|
if (stats && stats.isFile()) {
|
|
@@ -95,16 +96,28 @@ for (const file of files) {
|
|
|
95
96
|
instance = Benchmark.create(...args);
|
|
96
97
|
return instance;
|
|
97
98
|
};
|
|
98
|
-
|
|
99
|
+
try {
|
|
100
|
+
await import(identifier);
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
console.error(`Error loading ${file}: ${e instanceof Error ? e.message : String(e)}`);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
99
106
|
if (instance) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
107
|
+
let reports;
|
|
108
|
+
try {
|
|
109
|
+
reports = await instance.execute({
|
|
110
|
+
...executeOptions,
|
|
111
|
+
[BENCHMARK_URL]: identifier,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
console.error(`Error executing ${file}: ${e instanceof Error ? e.message : String(e)}`);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
104
118
|
if (opts['save-baseline']) {
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
console.log(`Baseline saved to: ${opts['save-baseline']}`);
|
|
119
|
+
const bd = reportsToBaseline(reports);
|
|
120
|
+
Object.assign(allBaselineResults, bd.results);
|
|
108
121
|
}
|
|
109
122
|
if (baseline) {
|
|
110
123
|
printComparisonReports(reports, baseline);
|
|
@@ -133,3 +146,12 @@ for (const file of files) {
|
|
|
133
146
|
}
|
|
134
147
|
}
|
|
135
148
|
}
|
|
149
|
+
if (opts['save-baseline'] && Object.keys(allBaselineResults).length > 0) {
|
|
150
|
+
const baselineData = {
|
|
151
|
+
version: 1,
|
|
152
|
+
timestamp: new Date().toISOString(),
|
|
153
|
+
results: allBaselineResults,
|
|
154
|
+
};
|
|
155
|
+
await writeFile(opts['save-baseline'], JSON.stringify(baselineData, null, 2));
|
|
156
|
+
console.log(`Baseline saved to: ${opts['save-baseline']}`);
|
|
157
|
+
}
|
package/build/executor.js
CHANGED
|
@@ -2,7 +2,7 @@ import { Worker } from 'node:worker_threads';
|
|
|
2
2
|
import { once } from 'node:events';
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
4
|
import { createReport, computeStats, Report } from './reporter.js';
|
|
5
|
-
import { cmp, assertNoClosure } from './utils.js';
|
|
5
|
+
import { cmp, assertNoClosure, normalizeFunction } from './utils.js';
|
|
6
6
|
import { Control, CONTROL_SLOTS, COMPLETE_VALUE, } from './types.js';
|
|
7
7
|
const BENCHMARK_URL = Symbol.for('overtake.benchmarkUrl');
|
|
8
8
|
export const createExecutor = (options) => {
|
|
@@ -10,6 +10,7 @@ export const createExecutor = (options) => {
|
|
|
10
10
|
const benchmarkUrl = options[BENCHMARK_URL];
|
|
11
11
|
const resolvedBenchmarkUrl = typeof benchmarkUrl === 'string' ? benchmarkUrl : pathToFileURL(process.cwd()).href;
|
|
12
12
|
const pending = [];
|
|
13
|
+
const activeWorkers = new Set();
|
|
13
14
|
let running = 0;
|
|
14
15
|
const schedule = async (task) => {
|
|
15
16
|
running++;
|
|
@@ -33,11 +34,11 @@ export const createExecutor = (options) => {
|
|
|
33
34
|
});
|
|
34
35
|
};
|
|
35
36
|
const runTask = async ({ id, setup, teardown, pre, run, post, data }) => {
|
|
36
|
-
const setupCode = setup
|
|
37
|
-
const teardownCode = teardown
|
|
38
|
-
const preCode = pre
|
|
39
|
-
const runCode = run.toString();
|
|
40
|
-
const postCode = post
|
|
37
|
+
const setupCode = setup ? normalizeFunction(setup.toString()) : undefined;
|
|
38
|
+
const teardownCode = teardown ? normalizeFunction(teardown.toString()) : undefined;
|
|
39
|
+
const preCode = pre ? normalizeFunction(pre.toString()) : undefined;
|
|
40
|
+
const runCode = normalizeFunction(run.toString());
|
|
41
|
+
const postCode = post ? normalizeFunction(post.toString()) : undefined;
|
|
41
42
|
if (setupCode)
|
|
42
43
|
assertNoClosure(setupCode, 'setup');
|
|
43
44
|
if (teardownCode)
|
|
@@ -69,6 +70,7 @@ export const createExecutor = (options) => {
|
|
|
69
70
|
const worker = new Worker(workerFile, {
|
|
70
71
|
workerData,
|
|
71
72
|
});
|
|
73
|
+
activeWorkers.add(worker);
|
|
72
74
|
const control = new Int32Array(controlSAB);
|
|
73
75
|
let progressIntervalId;
|
|
74
76
|
if (onProgress && id) {
|
|
@@ -96,6 +98,7 @@ export const createExecutor = (options) => {
|
|
|
96
98
|
clearInterval(progressIntervalId);
|
|
97
99
|
workerError = err instanceof Error ? err.message : String(err);
|
|
98
100
|
}
|
|
101
|
+
activeWorkers.delete(worker);
|
|
99
102
|
const count = control[Control.INDEX];
|
|
100
103
|
const heapUsedKB = control[Control.HEAP_USED];
|
|
101
104
|
const durations = new BigUint64Array(durationsSAB).slice(0, count).sort(cmp);
|
|
@@ -112,15 +115,26 @@ export const createExecutor = (options) => {
|
|
|
112
115
|
}
|
|
113
116
|
}
|
|
114
117
|
const stats = count > 0 ? computeStats(durations) : undefined;
|
|
115
|
-
const
|
|
118
|
+
const entries = reportTypes
|
|
116
119
|
.map((type) => [type, createReport(durations, type, stats)])
|
|
117
120
|
.concat([
|
|
118
121
|
['count', count],
|
|
119
122
|
['heapUsedKB', heapUsedKB],
|
|
120
123
|
['dceWarning', dceWarning],
|
|
121
|
-
['error', workerError],
|
|
122
124
|
]);
|
|
123
|
-
|
|
125
|
+
if (workerError)
|
|
126
|
+
entries.push(['error', workerError]);
|
|
127
|
+
return Object.fromEntries(entries);
|
|
128
|
+
};
|
|
129
|
+
return {
|
|
130
|
+
pushAsync,
|
|
131
|
+
kill() {
|
|
132
|
+
for (const w of activeWorkers)
|
|
133
|
+
w.terminate();
|
|
134
|
+
activeWorkers.clear();
|
|
135
|
+
for (const p of pending)
|
|
136
|
+
p.reject(new Error('Executor killed'));
|
|
137
|
+
pending.length = 0;
|
|
138
|
+
},
|
|
124
139
|
};
|
|
125
|
-
return { pushAsync, kill() { } };
|
|
126
140
|
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { transformSync } from '@swc/core';
|
|
4
|
+
export async function resolve(specifier, context, nextResolve) {
|
|
5
|
+
try {
|
|
6
|
+
return await nextResolve(specifier, context);
|
|
7
|
+
}
|
|
8
|
+
catch (e) {
|
|
9
|
+
if (specifier.endsWith('.js'))
|
|
10
|
+
try {
|
|
11
|
+
return await nextResolve(specifier.slice(0, -3) + '.ts', context);
|
|
12
|
+
}
|
|
13
|
+
catch { }
|
|
14
|
+
throw e;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export async function load(url, context, nextLoad) {
|
|
18
|
+
if (!url.endsWith('.ts') && !url.endsWith('.mts')) {
|
|
19
|
+
return nextLoad(url, context);
|
|
20
|
+
}
|
|
21
|
+
const filePath = fileURLToPath(url);
|
|
22
|
+
const rawSource = await readFile(filePath, 'utf-8');
|
|
23
|
+
const { code } = transformSync(rawSource, {
|
|
24
|
+
filename: filePath,
|
|
25
|
+
jsc: {
|
|
26
|
+
parser: { syntax: 'typescript' },
|
|
27
|
+
target: 'esnext',
|
|
28
|
+
},
|
|
29
|
+
module: { type: 'es6' },
|
|
30
|
+
sourceMaps: false,
|
|
31
|
+
});
|
|
32
|
+
return { format: 'module', source: code, shortCircuit: true };
|
|
33
|
+
}
|
package/build/register-hook.js
CHANGED
|
@@ -1,15 +1,2 @@
|
|
|
1
1
|
import { register } from 'node:module';
|
|
2
|
-
|
|
3
|
-
try {
|
|
4
|
-
return await n(s, c);
|
|
5
|
-
}
|
|
6
|
-
catch (e) {
|
|
7
|
-
if (s.endsWith('.js'))
|
|
8
|
-
try {
|
|
9
|
-
return await n(s.slice(0, -3) + '.ts', c);
|
|
10
|
-
}
|
|
11
|
-
catch { }
|
|
12
|
-
throw e;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
register('data:text/javascript,' + encodeURIComponent(`export ${resolve.toString()}`));
|
|
2
|
+
register(new URL('../build/loader-hook.js', import.meta.url).href);
|
package/build/runner.js
CHANGED
|
@@ -475,7 +475,6 @@ export const benchmark = async ({ setup, teardown, pre, run: runRaw, post, data,
|
|
|
475
475
|
const durationNumber = Number(sampleDuration);
|
|
476
476
|
if (!disableFiltering) {
|
|
477
477
|
const { median, iqr } = medianAndIqr(outlierWindow);
|
|
478
|
-
pushWindow(outlierWindow, durationNumber, OUTLIER_WINDOW);
|
|
479
478
|
const maxAllowed = median + OUTLIER_IQR_MULTIPLIER * iqr || Number.POSITIVE_INFINITY;
|
|
480
479
|
if (outlierWindow.length >= 8 && durationNumber > maxAllowed && durationNumber - median > OUTLIER_ABS_THRESHOLD) {
|
|
481
480
|
skipped++;
|
|
@@ -487,9 +486,7 @@ export const benchmark = async ({ setup, teardown, pre, run: runRaw, post, data,
|
|
|
487
486
|
continue;
|
|
488
487
|
}
|
|
489
488
|
}
|
|
490
|
-
|
|
491
|
-
pushWindow(outlierWindow, durationNumber, OUTLIER_WINDOW);
|
|
492
|
-
}
|
|
489
|
+
pushWindow(outlierWindow, durationNumber, OUTLIER_WINDOW);
|
|
493
490
|
durations[i++] = sampleDuration;
|
|
494
491
|
const deltaS = sampleDuration * WELFORD_SCALE - meanS;
|
|
495
492
|
meanS += deltaS / BigInt(i);
|
package/build/utils.d.ts
CHANGED
|
@@ -4,4 +4,5 @@ export declare const cmp: (a: bigint | number, b: bigint | number) => number;
|
|
|
4
4
|
export declare const max: (a: bigint, b: bigint) => bigint;
|
|
5
5
|
export declare function div(a: bigint, b: bigint, decimals?: number): string;
|
|
6
6
|
export declare function divs(a: bigint, b: bigint, scale: bigint): bigint;
|
|
7
|
+
export declare function normalizeFunction(code: string): string;
|
|
7
8
|
export declare function assertNoClosure(code: string, name: string): void;
|
package/build/utils.js
CHANGED
|
@@ -1,18 +1,5 @@
|
|
|
1
1
|
import { parseSync } from '@swc/core';
|
|
2
|
-
|
|
3
|
-
try {
|
|
4
|
-
return await n(s, c);
|
|
5
|
-
}
|
|
6
|
-
catch (e) {
|
|
7
|
-
if (s.endsWith('.js'))
|
|
8
|
-
try {
|
|
9
|
-
return await n(s.slice(0, -3) + '.ts', c);
|
|
10
|
-
}
|
|
11
|
-
catch { }
|
|
12
|
-
throw e;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
export const resolveHookUrl = 'data:text/javascript,' + encodeURIComponent(`export ${resolve.toString()}`);
|
|
2
|
+
export const resolveHookUrl = new URL('./loader-hook.js', import.meta.url).href;
|
|
16
3
|
export const isqrt = (n) => {
|
|
17
4
|
if (n < 0n)
|
|
18
5
|
throw new RangeError('Square root of negative');
|
|
@@ -44,11 +31,14 @@ export const max = (a, b) => {
|
|
|
44
31
|
export function div(a, b, decimals = 2) {
|
|
45
32
|
if (b === 0n)
|
|
46
33
|
throw new RangeError('Division by zero');
|
|
34
|
+
const neg = a < 0n !== b < 0n;
|
|
35
|
+
const absA = a < 0n ? -a : a;
|
|
36
|
+
const absB = b < 0n ? -b : b;
|
|
47
37
|
const scale = 10n ** BigInt(decimals);
|
|
48
|
-
const scaled = (
|
|
38
|
+
const scaled = (absA * scale) / absB;
|
|
49
39
|
const intPart = scaled / scale;
|
|
50
40
|
const fracPart = scaled % scale;
|
|
51
|
-
return `${intPart}.${fracPart.toString().padStart(decimals, '0')}`;
|
|
41
|
+
return `${neg ? '-' : ''}${intPart}.${fracPart.toString().padStart(decimals, '0')}`;
|
|
52
42
|
}
|
|
53
43
|
export function divs(a, b, scale) {
|
|
54
44
|
if (b === 0n)
|
|
@@ -57,22 +47,75 @@ export function divs(a, b, scale) {
|
|
|
57
47
|
}
|
|
58
48
|
const KNOWN_GLOBALS = new Set(Object.getOwnPropertyNames(globalThis));
|
|
59
49
|
KNOWN_GLOBALS.add('arguments');
|
|
60
|
-
|
|
50
|
+
let _unresolvedCtxt;
|
|
51
|
+
function findIdentifierCtxt(node, name) {
|
|
52
|
+
if (!node || typeof node !== 'object')
|
|
53
|
+
return undefined;
|
|
54
|
+
if (Array.isArray(node)) {
|
|
55
|
+
for (const item of node) {
|
|
56
|
+
const r = findIdentifierCtxt(item, name);
|
|
57
|
+
if (r !== undefined)
|
|
58
|
+
return r;
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
const obj = node;
|
|
63
|
+
if (obj.type === 'Identifier' && obj.value === name && typeof obj.ctxt === 'number') {
|
|
64
|
+
return obj.ctxt;
|
|
65
|
+
}
|
|
66
|
+
for (const key of Object.keys(obj)) {
|
|
67
|
+
if (key === 'span')
|
|
68
|
+
continue;
|
|
69
|
+
const r = findIdentifierCtxt(obj[key], name);
|
|
70
|
+
if (r !== undefined)
|
|
71
|
+
return r;
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
function probeUnresolvedCtxt() {
|
|
76
|
+
if (_unresolvedCtxt !== undefined)
|
|
77
|
+
return _unresolvedCtxt;
|
|
78
|
+
try {
|
|
79
|
+
const ast = parseSync('var _ = () => __PROBE__', { syntax: 'ecmascript', target: 'esnext' });
|
|
80
|
+
_unresolvedCtxt = findIdentifierCtxt(ast, '__PROBE__') ?? 1;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
_unresolvedCtxt = 1;
|
|
84
|
+
}
|
|
85
|
+
return _unresolvedCtxt;
|
|
86
|
+
}
|
|
87
|
+
function collectUnresolved(node, ctxt, result) {
|
|
61
88
|
if (!node || typeof node !== 'object')
|
|
62
89
|
return;
|
|
63
90
|
if (Array.isArray(node)) {
|
|
64
91
|
for (const item of node)
|
|
65
|
-
collectUnresolved(item, result);
|
|
92
|
+
collectUnresolved(item, ctxt, result);
|
|
66
93
|
return;
|
|
67
94
|
}
|
|
68
95
|
const obj = node;
|
|
69
|
-
if (obj.type === 'Identifier' && obj.ctxt ===
|
|
96
|
+
if (obj.type === 'Identifier' && obj.ctxt === ctxt && typeof obj.value === 'string') {
|
|
70
97
|
result.add(obj.value);
|
|
71
98
|
}
|
|
72
99
|
for (const key of Object.keys(obj)) {
|
|
73
100
|
if (key === 'span')
|
|
74
101
|
continue;
|
|
75
|
-
collectUnresolved(obj[key], result);
|
|
102
|
+
collectUnresolved(obj[key], ctxt, result);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export function normalizeFunction(code) {
|
|
106
|
+
try {
|
|
107
|
+
parseSync(`var __fn = ${code}`, { syntax: 'ecmascript', target: 'esnext' });
|
|
108
|
+
return code;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
const normalized = code.startsWith('async ') ? `async function ${code.slice(6)}` : `function ${code}`;
|
|
112
|
+
try {
|
|
113
|
+
parseSync(`var __fn = ${normalized}`, { syntax: 'ecmascript', target: 'esnext' });
|
|
114
|
+
return normalized;
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return code;
|
|
118
|
+
}
|
|
76
119
|
}
|
|
77
120
|
}
|
|
78
121
|
export function assertNoClosure(code, name) {
|
|
@@ -83,8 +126,9 @@ export function assertNoClosure(code, name) {
|
|
|
83
126
|
catch {
|
|
84
127
|
return;
|
|
85
128
|
}
|
|
129
|
+
const unresolvedCtxt = probeUnresolvedCtxt();
|
|
86
130
|
const unresolved = new Set();
|
|
87
|
-
collectUnresolved(ast, unresolved);
|
|
131
|
+
collectUnresolved(ast, unresolvedCtxt, unresolved);
|
|
88
132
|
for (const g of KNOWN_GLOBALS)
|
|
89
133
|
unresolved.delete(g);
|
|
90
134
|
if (unresolved.size === 0)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "overtake",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.3",
|
|
4
4
|
"description": "NodeJS performance benchmark",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"overtake": "bin/overtake.js"
|
|
13
13
|
},
|
|
14
14
|
"engines": {
|
|
15
|
-
"node": ">=
|
|
15
|
+
"node": ">=22"
|
|
16
16
|
},
|
|
17
17
|
"repository": {
|
|
18
18
|
"type": "git",
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import 'overtake';
|
|
2
|
+
|
|
3
|
+
const suite = benchmark('ops', () => null);
|
|
4
|
+
|
|
5
|
+
const target = suite.target('enum', () => {
|
|
6
|
+
enum Direction {
|
|
7
|
+
Up,
|
|
8
|
+
Down,
|
|
9
|
+
Left,
|
|
10
|
+
Right,
|
|
11
|
+
}
|
|
12
|
+
return { Direction };
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
target.measure('access', ({ Direction }) => {
|
|
16
|
+
return Direction.Up + Direction.Right;
|
|
17
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import 'overtake';
|
|
2
|
+
|
|
3
|
+
const suite = benchmark('ops', () => null);
|
|
4
|
+
|
|
5
|
+
const target = suite.target('param-property', () => {
|
|
6
|
+
class Container {
|
|
7
|
+
constructor(public value: number) {}
|
|
8
|
+
}
|
|
9
|
+
return { Container };
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
target.measure('create', ({ Container }) => {
|
|
13
|
+
return new Container(42);
|
|
14
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { execFile } from 'node:child_process';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const exec = promisify(execFile);
|
|
8
|
+
const overtakeBin = fileURLToPath(new URL('../../bin/overtake.js', import.meta.url));
|
|
9
|
+
const nodeFlags = ['--experimental-vm-modules', '--no-warnings', '--expose-gc'];
|
|
10
|
+
|
|
11
|
+
const runBench = async (fixture: string) => {
|
|
12
|
+
const { stdout } = await exec(process.execPath, [...nodeFlags, overtakeBin, '-f', 'json', '--max-cycles', '50', '--min-cycles', '10', '--warmup-cycles', '5', fixture], {
|
|
13
|
+
timeout: 30_000,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const result = JSON.parse(stdout);
|
|
17
|
+
const key = Object.keys(result)[0];
|
|
18
|
+
assert.ok(key, 'should have at least one benchmark result');
|
|
19
|
+
const feeds = result[key];
|
|
20
|
+
const feed = Object.keys(feeds)[0];
|
|
21
|
+
assert.ok(feeds[feed].ops, 'should have ops metric');
|
|
22
|
+
assert.ok(!feeds[feed].error, 'should not have an error');
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
describe('loader-hook', () => {
|
|
26
|
+
it('loads benchmark files using parameter properties', async () => {
|
|
27
|
+
await runBench(fileURLToPath(new URL('fixtures/param-property-bench.ts', import.meta.url)));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('loads benchmark files using enums', async () => {
|
|
31
|
+
await runBench(fileURLToPath(new URL('fixtures/enum-bench.ts', import.meta.url)));
|
|
32
|
+
});
|
|
33
|
+
});
|
package/src/cli.ts
CHANGED
|
@@ -105,6 +105,8 @@ if (opts['compare-baseline']) {
|
|
|
105
105
|
|
|
106
106
|
const files = new Set((await Promise.all(patterns.map((pattern) => Array.fromAsync(glob(pattern, { cwd: process.cwd() })).catch(() => [] as string[])))).flat());
|
|
107
107
|
|
|
108
|
+
const allBaselineResults: Record<string, Record<string, number>> = {};
|
|
109
|
+
|
|
108
110
|
for (const file of files) {
|
|
109
111
|
const stats = await stat(file).catch(() => false as const);
|
|
110
112
|
if (stats && stats.isFile()) {
|
|
@@ -117,18 +119,28 @@ for (const file of files) {
|
|
|
117
119
|
instance = Benchmark.create(...args);
|
|
118
120
|
return instance;
|
|
119
121
|
};
|
|
120
|
-
|
|
122
|
+
try {
|
|
123
|
+
await import(identifier);
|
|
124
|
+
} catch (e) {
|
|
125
|
+
console.error(`Error loading ${file}: ${e instanceof Error ? e.message : String(e)}`);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
121
128
|
|
|
122
129
|
if (instance) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
130
|
+
let reports;
|
|
131
|
+
try {
|
|
132
|
+
reports = await instance.execute({
|
|
133
|
+
...executeOptions,
|
|
134
|
+
[BENCHMARK_URL]: identifier,
|
|
135
|
+
} as typeof executeOptions);
|
|
136
|
+
} catch (e) {
|
|
137
|
+
console.error(`Error executing ${file}: ${e instanceof Error ? e.message : String(e)}`);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
127
140
|
|
|
128
141
|
if (opts['save-baseline']) {
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
console.log(`Baseline saved to: ${opts['save-baseline']}`);
|
|
142
|
+
const bd = reportsToBaseline(reports);
|
|
143
|
+
Object.assign(allBaselineResults, bd.results);
|
|
132
144
|
}
|
|
133
145
|
|
|
134
146
|
if (baseline) {
|
|
@@ -157,3 +169,13 @@ for (const file of files) {
|
|
|
157
169
|
}
|
|
158
170
|
}
|
|
159
171
|
}
|
|
172
|
+
|
|
173
|
+
if (opts['save-baseline'] && Object.keys(allBaselineResults).length > 0) {
|
|
174
|
+
const baselineData: BaselineData = {
|
|
175
|
+
version: 1,
|
|
176
|
+
timestamp: new Date().toISOString(),
|
|
177
|
+
results: allBaselineResults,
|
|
178
|
+
};
|
|
179
|
+
await writeFile(opts['save-baseline'], JSON.stringify(baselineData, null, 2));
|
|
180
|
+
console.log(`Baseline saved to: ${opts['save-baseline']}`);
|
|
181
|
+
}
|
package/src/executor.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Worker } from 'node:worker_threads';
|
|
|
2
2
|
import { once } from 'node:events';
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
4
|
import { createReport, computeStats, Report } from './reporter.js';
|
|
5
|
-
import { cmp, assertNoClosure } from './utils.js';
|
|
5
|
+
import { cmp, assertNoClosure, normalizeFunction } from './utils.js';
|
|
6
6
|
import {
|
|
7
7
|
type ExecutorRunOptions,
|
|
8
8
|
type ReportOptions,
|
|
@@ -43,6 +43,7 @@ export const createExecutor = <TContext, TInput, R extends ReportTypeList>(optio
|
|
|
43
43
|
const resolvedBenchmarkUrl = typeof benchmarkUrl === 'string' ? benchmarkUrl : pathToFileURL(process.cwd()).href;
|
|
44
44
|
|
|
45
45
|
const pending: { task: ExecutorRunOptions<TContext, TInput>; resolve: (v: unknown) => void; reject: (e: unknown) => void }[] = [];
|
|
46
|
+
const activeWorkers = new Set<Worker>();
|
|
46
47
|
let running = 0;
|
|
47
48
|
|
|
48
49
|
const schedule = async (task: ExecutorRunOptions<TContext, TInput>) => {
|
|
@@ -68,11 +69,11 @@ export const createExecutor = <TContext, TInput, R extends ReportTypeList>(optio
|
|
|
68
69
|
};
|
|
69
70
|
|
|
70
71
|
const runTask = async ({ id, setup, teardown, pre, run, post, data }: ExecutorRunOptions<TContext, TInput>) => {
|
|
71
|
-
const setupCode = setup
|
|
72
|
-
const teardownCode = teardown
|
|
73
|
-
const preCode = pre
|
|
74
|
-
const runCode = run.toString();
|
|
75
|
-
const postCode = post
|
|
72
|
+
const setupCode = setup ? normalizeFunction(setup.toString()) : undefined;
|
|
73
|
+
const teardownCode = teardown ? normalizeFunction(teardown.toString()) : undefined;
|
|
74
|
+
const preCode = pre ? normalizeFunction(pre.toString()) : undefined;
|
|
75
|
+
const runCode = normalizeFunction(run.toString());
|
|
76
|
+
const postCode = post ? normalizeFunction(post.toString()) : undefined;
|
|
76
77
|
|
|
77
78
|
if (setupCode) assertNoClosure(setupCode, 'setup');
|
|
78
79
|
if (teardownCode) assertNoClosure(teardownCode, 'teardown');
|
|
@@ -106,6 +107,7 @@ export const createExecutor = <TContext, TInput, R extends ReportTypeList>(optio
|
|
|
106
107
|
const worker = new Worker(workerFile, {
|
|
107
108
|
workerData,
|
|
108
109
|
});
|
|
110
|
+
activeWorkers.add(worker);
|
|
109
111
|
|
|
110
112
|
const control = new Int32Array(controlSAB);
|
|
111
113
|
let progressIntervalId: ReturnType<typeof setInterval> | undefined;
|
|
@@ -132,6 +134,7 @@ export const createExecutor = <TContext, TInput, R extends ReportTypeList>(optio
|
|
|
132
134
|
if (progressIntervalId) clearInterval(progressIntervalId);
|
|
133
135
|
workerError = err instanceof Error ? err.message : String(err);
|
|
134
136
|
}
|
|
137
|
+
activeWorkers.delete(worker);
|
|
135
138
|
|
|
136
139
|
const count = control[Control.INDEX];
|
|
137
140
|
const heapUsedKB = control[Control.HEAP_USED];
|
|
@@ -150,16 +153,24 @@ export const createExecutor = <TContext, TInput, R extends ReportTypeList>(optio
|
|
|
150
153
|
}
|
|
151
154
|
|
|
152
155
|
const stats = count > 0 ? computeStats(durations) : undefined;
|
|
153
|
-
const
|
|
156
|
+
const entries: [string, unknown][] = reportTypes
|
|
154
157
|
.map<[string, unknown]>((type) => [type, createReport(durations, type, stats)] as [ReportType, Report])
|
|
155
158
|
.concat([
|
|
156
159
|
['count', count],
|
|
157
160
|
['heapUsedKB', heapUsedKB],
|
|
158
161
|
['dceWarning', dceWarning],
|
|
159
|
-
['error', workerError],
|
|
160
162
|
]);
|
|
161
|
-
|
|
163
|
+
if (workerError) entries.push(['error', workerError]);
|
|
164
|
+
return Object.fromEntries(entries);
|
|
162
165
|
};
|
|
163
166
|
|
|
164
|
-
return {
|
|
167
|
+
return {
|
|
168
|
+
pushAsync,
|
|
169
|
+
kill() {
|
|
170
|
+
for (const w of activeWorkers) w.terminate();
|
|
171
|
+
activeWorkers.clear();
|
|
172
|
+
for (const p of pending) p.reject(new Error('Executor killed'));
|
|
173
|
+
pending.length = 0;
|
|
174
|
+
},
|
|
175
|
+
};
|
|
165
176
|
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { transformSync } from '@swc/core';
|
|
4
|
+
|
|
5
|
+
export async function resolve(specifier: string, context: unknown, nextResolve: (...args: unknown[]) => unknown) {
|
|
6
|
+
try {
|
|
7
|
+
return await nextResolve(specifier, context);
|
|
8
|
+
} catch (e) {
|
|
9
|
+
if (specifier.endsWith('.js'))
|
|
10
|
+
try {
|
|
11
|
+
return await nextResolve(specifier.slice(0, -3) + '.ts', context);
|
|
12
|
+
} catch {}
|
|
13
|
+
throw e;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function load(url: string, context: unknown, nextLoad: (...args: unknown[]) => unknown) {
|
|
18
|
+
if (!url.endsWith('.ts') && !url.endsWith('.mts')) {
|
|
19
|
+
return nextLoad(url, context);
|
|
20
|
+
}
|
|
21
|
+
const filePath = fileURLToPath(url);
|
|
22
|
+
const rawSource = await readFile(filePath, 'utf-8');
|
|
23
|
+
const { code } = transformSync(rawSource, {
|
|
24
|
+
filename: filePath,
|
|
25
|
+
jsc: {
|
|
26
|
+
parser: { syntax: 'typescript' },
|
|
27
|
+
target: 'esnext',
|
|
28
|
+
},
|
|
29
|
+
module: { type: 'es6' },
|
|
30
|
+
sourceMaps: false,
|
|
31
|
+
});
|
|
32
|
+
return { format: 'module', source: code, shortCircuit: true };
|
|
33
|
+
}
|
package/src/register-hook.ts
CHANGED
|
@@ -1,15 +1,3 @@
|
|
|
1
1
|
import { register } from 'node:module';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
try {
|
|
5
|
-
return await n(s, c);
|
|
6
|
-
} catch (e) {
|
|
7
|
-
if (s.endsWith('.js'))
|
|
8
|
-
try {
|
|
9
|
-
return await n(s.slice(0, -3) + '.ts', c);
|
|
10
|
-
} catch {}
|
|
11
|
-
throw e;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
register('data:text/javascript,' + encodeURIComponent(`export ${resolve.toString()}`));
|
|
3
|
+
register(new URL('../build/loader-hook.js', import.meta.url).href);
|
package/src/runner.ts
CHANGED
|
@@ -592,7 +592,6 @@ export const benchmark = async <TContext, TInput>({
|
|
|
592
592
|
const durationNumber = Number(sampleDuration);
|
|
593
593
|
if (!disableFiltering) {
|
|
594
594
|
const { median, iqr } = medianAndIqr(outlierWindow);
|
|
595
|
-
pushWindow(outlierWindow, durationNumber, OUTLIER_WINDOW);
|
|
596
595
|
const maxAllowed = median + OUTLIER_IQR_MULTIPLIER * iqr || Number.POSITIVE_INFINITY;
|
|
597
596
|
if (outlierWindow.length >= 8 && durationNumber > maxAllowed && durationNumber - median > OUTLIER_ABS_THRESHOLD) {
|
|
598
597
|
skipped++;
|
|
@@ -604,9 +603,8 @@ export const benchmark = async <TContext, TInput>({
|
|
|
604
603
|
skipped++;
|
|
605
604
|
continue;
|
|
606
605
|
}
|
|
607
|
-
} else {
|
|
608
|
-
pushWindow(outlierWindow, durationNumber, OUTLIER_WINDOW);
|
|
609
606
|
}
|
|
607
|
+
pushWindow(outlierWindow, durationNumber, OUTLIER_WINDOW);
|
|
610
608
|
|
|
611
609
|
durations[i++] = sampleDuration;
|
|
612
610
|
const deltaS = sampleDuration * WELFORD_SCALE - meanS;
|
package/src/utils.ts
CHANGED
|
@@ -1,18 +1,6 @@
|
|
|
1
1
|
import { parseSync } from '@swc/core';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
try {
|
|
5
|
-
return await n(s, c);
|
|
6
|
-
} catch (e) {
|
|
7
|
-
if (s.endsWith('.js'))
|
|
8
|
-
try {
|
|
9
|
-
return await n(s.slice(0, -3) + '.ts', c);
|
|
10
|
-
} catch {}
|
|
11
|
-
throw e;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export const resolveHookUrl = 'data:text/javascript,' + encodeURIComponent(`export ${resolve.toString()}`);
|
|
3
|
+
export const resolveHookUrl = new URL('./loader-hook.js', import.meta.url).href;
|
|
16
4
|
|
|
17
5
|
export const isqrt = (n: bigint): bigint => {
|
|
18
6
|
if (n < 0n) throw new RangeError('Square root of negative');
|
|
@@ -45,11 +33,14 @@ export const max = (a: bigint, b: bigint) => {
|
|
|
45
33
|
|
|
46
34
|
export function div(a: bigint, b: bigint, decimals: number = 2): string {
|
|
47
35
|
if (b === 0n) throw new RangeError('Division by zero');
|
|
36
|
+
const neg = a < 0n !== b < 0n;
|
|
37
|
+
const absA = a < 0n ? -a : a;
|
|
38
|
+
const absB = b < 0n ? -b : b;
|
|
48
39
|
const scale = 10n ** BigInt(decimals);
|
|
49
|
-
const scaled = (
|
|
40
|
+
const scaled = (absA * scale) / absB;
|
|
50
41
|
const intPart = scaled / scale;
|
|
51
42
|
const fracPart = scaled % scale;
|
|
52
|
-
return `${intPart}.${fracPart.toString().padStart(decimals, '0')}`;
|
|
43
|
+
return `${neg ? '-' : ''}${intPart}.${fracPart.toString().padStart(decimals, '0')}`;
|
|
53
44
|
}
|
|
54
45
|
|
|
55
46
|
export function divs(a: bigint, b: bigint, scale: bigint): bigint {
|
|
@@ -60,19 +51,68 @@ export function divs(a: bigint, b: bigint, scale: bigint): bigint {
|
|
|
60
51
|
const KNOWN_GLOBALS = new Set(Object.getOwnPropertyNames(globalThis));
|
|
61
52
|
KNOWN_GLOBALS.add('arguments');
|
|
62
53
|
|
|
63
|
-
|
|
54
|
+
let _unresolvedCtxt: number | undefined;
|
|
55
|
+
|
|
56
|
+
function findIdentifierCtxt(node: unknown, name: string): number | undefined {
|
|
57
|
+
if (!node || typeof node !== 'object') return undefined;
|
|
58
|
+
if (Array.isArray(node)) {
|
|
59
|
+
for (const item of node) {
|
|
60
|
+
const r = findIdentifierCtxt(item, name);
|
|
61
|
+
if (r !== undefined) return r;
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
const obj = node as Record<string, unknown>;
|
|
66
|
+
if (obj.type === 'Identifier' && obj.value === name && typeof obj.ctxt === 'number') {
|
|
67
|
+
return obj.ctxt;
|
|
68
|
+
}
|
|
69
|
+
for (const key of Object.keys(obj)) {
|
|
70
|
+
if (key === 'span') continue;
|
|
71
|
+
const r = findIdentifierCtxt(obj[key], name);
|
|
72
|
+
if (r !== undefined) return r;
|
|
73
|
+
}
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function probeUnresolvedCtxt(): number {
|
|
78
|
+
if (_unresolvedCtxt !== undefined) return _unresolvedCtxt;
|
|
79
|
+
try {
|
|
80
|
+
const ast = parseSync('var _ = () => __PROBE__', { syntax: 'ecmascript', target: 'esnext' });
|
|
81
|
+
_unresolvedCtxt = findIdentifierCtxt(ast, '__PROBE__') ?? 1;
|
|
82
|
+
} catch {
|
|
83
|
+
_unresolvedCtxt = 1;
|
|
84
|
+
}
|
|
85
|
+
return _unresolvedCtxt;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function collectUnresolved(node: unknown, ctxt: number, result: Set<string>) {
|
|
64
89
|
if (!node || typeof node !== 'object') return;
|
|
65
90
|
if (Array.isArray(node)) {
|
|
66
|
-
for (const item of node) collectUnresolved(item, result);
|
|
91
|
+
for (const item of node) collectUnresolved(item, ctxt, result);
|
|
67
92
|
return;
|
|
68
93
|
}
|
|
69
94
|
const obj = node as Record<string, unknown>;
|
|
70
|
-
if (obj.type === 'Identifier' && obj.ctxt ===
|
|
95
|
+
if (obj.type === 'Identifier' && obj.ctxt === ctxt && typeof obj.value === 'string') {
|
|
71
96
|
result.add(obj.value);
|
|
72
97
|
}
|
|
73
98
|
for (const key of Object.keys(obj)) {
|
|
74
99
|
if (key === 'span') continue;
|
|
75
|
-
collectUnresolved(obj[key], result);
|
|
100
|
+
collectUnresolved(obj[key], ctxt, result);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function normalizeFunction(code: string): string {
|
|
105
|
+
try {
|
|
106
|
+
parseSync(`var __fn = ${code}`, { syntax: 'ecmascript', target: 'esnext' });
|
|
107
|
+
return code;
|
|
108
|
+
} catch {
|
|
109
|
+
const normalized = code.startsWith('async ') ? `async function ${code.slice(6)}` : `function ${code}`;
|
|
110
|
+
try {
|
|
111
|
+
parseSync(`var __fn = ${normalized}`, { syntax: 'ecmascript', target: 'esnext' });
|
|
112
|
+
return normalized;
|
|
113
|
+
} catch {
|
|
114
|
+
return code;
|
|
115
|
+
}
|
|
76
116
|
}
|
|
77
117
|
}
|
|
78
118
|
|
|
@@ -83,8 +123,9 @@ export function assertNoClosure(code: string, name: string): void {
|
|
|
83
123
|
} catch {
|
|
84
124
|
return;
|
|
85
125
|
}
|
|
126
|
+
const unresolvedCtxt = probeUnresolvedCtxt();
|
|
86
127
|
const unresolved = new Set<string>();
|
|
87
|
-
collectUnresolved(ast, unresolved);
|
|
128
|
+
collectUnresolved(ast, unresolvedCtxt, unresolved);
|
|
88
129
|
for (const g of KNOWN_GLOBALS) unresolved.delete(g);
|
|
89
130
|
if (unresolved.size === 0) return;
|
|
90
131
|
|