overtake 2.0.0 → 2.0.2
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/bin/overtake.js +1 -1
- package/build/cli.js +157 -0
- package/build/executor.d.ts +2 -2
- package/build/executor.js +139 -0
- package/build/gc-watcher.js +16 -0
- package/build/index.d.ts +2 -2
- package/build/index.js +375 -0
- package/build/register-hook.d.ts +1 -0
- package/build/register-hook.js +15 -0
- package/build/reporter.d.ts +1 -1
- package/build/reporter.js +255 -0
- package/build/runner.d.ts +1 -1
- package/build/runner.js +528 -0
- package/build/types.js +28 -0
- package/build/utils.d.ts +1 -0
- package/build/utils.js +157 -0
- package/build/worker.js +111 -0
- package/package.json +5 -5
- package/src/__tests__/assert-no-closure.ts +1 -1
- package/src/__tests__/benchmark-execute.ts +2 -2
- package/src/cli.ts +33 -11
- package/src/executor.ts +21 -10
- package/src/index.ts +2 -2
- package/src/register-hook.ts +15 -0
- package/src/reporter.ts +2 -2
- package/src/runner.ts +3 -5
- package/src/utils.ts +60 -7
- package/src/worker.ts +3 -3
- package/tsconfig.json +0 -1
package/build/utils.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { parseSync } from '@swc/core';
|
|
2
|
+
async function resolve(s, c, n) {
|
|
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()}`);
|
|
16
|
+
export const isqrt = (n) => {
|
|
17
|
+
if (n < 0n)
|
|
18
|
+
throw new RangeError('Square root of negative');
|
|
19
|
+
if (n < 2n)
|
|
20
|
+
return n;
|
|
21
|
+
let x = n;
|
|
22
|
+
let y = (x + 1n) >> 1n;
|
|
23
|
+
while (y < x) {
|
|
24
|
+
x = y;
|
|
25
|
+
y = (x + n / x) >> 1n;
|
|
26
|
+
}
|
|
27
|
+
return x;
|
|
28
|
+
};
|
|
29
|
+
export const cmp = (a, b) => {
|
|
30
|
+
if (a > b) {
|
|
31
|
+
return 1;
|
|
32
|
+
}
|
|
33
|
+
if (a < b) {
|
|
34
|
+
return -1;
|
|
35
|
+
}
|
|
36
|
+
return 0;
|
|
37
|
+
};
|
|
38
|
+
export const max = (a, b) => {
|
|
39
|
+
if (a > b) {
|
|
40
|
+
return a;
|
|
41
|
+
}
|
|
42
|
+
return b;
|
|
43
|
+
};
|
|
44
|
+
export function div(a, b, decimals = 2) {
|
|
45
|
+
if (b === 0n)
|
|
46
|
+
throw new RangeError('Division by zero');
|
|
47
|
+
const neg = (a < 0n) !== (b < 0n);
|
|
48
|
+
const absA = a < 0n ? -a : a;
|
|
49
|
+
const absB = b < 0n ? -b : b;
|
|
50
|
+
const scale = 10n ** BigInt(decimals);
|
|
51
|
+
const scaled = (absA * scale) / absB;
|
|
52
|
+
const intPart = scaled / scale;
|
|
53
|
+
const fracPart = scaled % scale;
|
|
54
|
+
return `${neg ? '-' : ''}${intPart}.${fracPart.toString().padStart(decimals, '0')}`;
|
|
55
|
+
}
|
|
56
|
+
export function divs(a, b, scale) {
|
|
57
|
+
if (b === 0n)
|
|
58
|
+
throw new RangeError('Division by zero');
|
|
59
|
+
return (a * scale) / b;
|
|
60
|
+
}
|
|
61
|
+
const KNOWN_GLOBALS = new Set(Object.getOwnPropertyNames(globalThis));
|
|
62
|
+
KNOWN_GLOBALS.add('arguments');
|
|
63
|
+
let _unresolvedCtxt;
|
|
64
|
+
function findIdentifierCtxt(node, name) {
|
|
65
|
+
if (!node || typeof node !== 'object')
|
|
66
|
+
return undefined;
|
|
67
|
+
if (Array.isArray(node)) {
|
|
68
|
+
for (const item of node) {
|
|
69
|
+
const r = findIdentifierCtxt(item, name);
|
|
70
|
+
if (r !== undefined)
|
|
71
|
+
return r;
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
const obj = node;
|
|
76
|
+
if (obj.type === 'Identifier' && obj.value === name && typeof obj.ctxt === 'number') {
|
|
77
|
+
return obj.ctxt;
|
|
78
|
+
}
|
|
79
|
+
for (const key of Object.keys(obj)) {
|
|
80
|
+
if (key === 'span')
|
|
81
|
+
continue;
|
|
82
|
+
const r = findIdentifierCtxt(obj[key], name);
|
|
83
|
+
if (r !== undefined)
|
|
84
|
+
return r;
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
function probeUnresolvedCtxt() {
|
|
89
|
+
if (_unresolvedCtxt !== undefined)
|
|
90
|
+
return _unresolvedCtxt;
|
|
91
|
+
try {
|
|
92
|
+
const ast = parseSync('var _ = () => __PROBE__', { syntax: 'ecmascript', target: 'esnext' });
|
|
93
|
+
_unresolvedCtxt = findIdentifierCtxt(ast, '__PROBE__') ?? 1;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
_unresolvedCtxt = 1;
|
|
97
|
+
}
|
|
98
|
+
return _unresolvedCtxt;
|
|
99
|
+
}
|
|
100
|
+
function collectUnresolved(node, ctxt, result) {
|
|
101
|
+
if (!node || typeof node !== 'object')
|
|
102
|
+
return;
|
|
103
|
+
if (Array.isArray(node)) {
|
|
104
|
+
for (const item of node)
|
|
105
|
+
collectUnresolved(item, ctxt, result);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const obj = node;
|
|
109
|
+
if (obj.type === 'Identifier' && obj.ctxt === ctxt && typeof obj.value === 'string') {
|
|
110
|
+
result.add(obj.value);
|
|
111
|
+
}
|
|
112
|
+
for (const key of Object.keys(obj)) {
|
|
113
|
+
if (key === 'span')
|
|
114
|
+
continue;
|
|
115
|
+
collectUnresolved(obj[key], ctxt, result);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
export function normalizeFunction(code) {
|
|
119
|
+
try {
|
|
120
|
+
parseSync(`var __fn = ${code}`, { syntax: 'ecmascript', target: 'esnext' });
|
|
121
|
+
return code;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
const normalized = code.startsWith('async ') ? `async function ${code.slice(6)}` : `function ${code}`;
|
|
125
|
+
try {
|
|
126
|
+
parseSync(`var __fn = ${normalized}`, { syntax: 'ecmascript', target: 'esnext' });
|
|
127
|
+
return normalized;
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return code;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
export function assertNoClosure(code, name) {
|
|
135
|
+
let ast;
|
|
136
|
+
try {
|
|
137
|
+
ast = parseSync(`var __fn = ${code}`, { syntax: 'ecmascript', target: 'esnext' });
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const unresolvedCtxt = probeUnresolvedCtxt();
|
|
143
|
+
const unresolved = new Set();
|
|
144
|
+
collectUnresolved(ast, unresolvedCtxt, unresolved);
|
|
145
|
+
for (const g of KNOWN_GLOBALS)
|
|
146
|
+
unresolved.delete(g);
|
|
147
|
+
if (unresolved.size === 0)
|
|
148
|
+
return;
|
|
149
|
+
const vars = [...unresolved].join(', ');
|
|
150
|
+
throw new Error(`Benchmark "${name}" function references outer-scope variables: ${vars}\n\n` +
|
|
151
|
+
`Benchmark functions are serialized with .toString() and executed in an isolated\n` +
|
|
152
|
+
`worker thread. Closed-over variables from the original module scope are not\n` +
|
|
153
|
+
`available in the worker and will cause a ReferenceError at runtime.\n\n` +
|
|
154
|
+
`To fix this, move the referenced values into:\n` +
|
|
155
|
+
` - "setup" function (returned value becomes the first argument of run/pre/post)\n` +
|
|
156
|
+
` - "data" option (passed as the second argument of run/pre/post)`);
|
|
157
|
+
}
|
package/build/worker.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { workerData } from 'node:worker_threads';
|
|
2
|
+
import { SourceTextModule, SyntheticModule } from 'node:vm';
|
|
3
|
+
import { createRequire, register } from 'node:module';
|
|
4
|
+
import { isAbsolute } from 'node:path';
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
|
+
import { benchmark } from './runner.js';
|
|
7
|
+
import {} from './types.js';
|
|
8
|
+
import { resolveHookUrl } from './utils.js';
|
|
9
|
+
register(resolveHookUrl);
|
|
10
|
+
const { benchmarkUrl, setupCode, teardownCode, preCode, runCode, postCode, data, warmupCycles, minCycles, absThreshold, relThreshold, gcObserver = true, durationsSAB, controlSAB, } = workerData;
|
|
11
|
+
const serialize = (code) => (code ? code : 'undefined');
|
|
12
|
+
const resolvedBenchmarkUrl = typeof benchmarkUrl === 'string' ? benchmarkUrl : pathToFileURL(process.cwd()).href;
|
|
13
|
+
const benchmarkDirUrl = new URL('.', resolvedBenchmarkUrl).href;
|
|
14
|
+
const requireFrom = createRequire(fileURLToPath(new URL('benchmark.js', benchmarkDirUrl)));
|
|
15
|
+
const resolveSpecifier = (specifier) => {
|
|
16
|
+
if (specifier.startsWith('file:')) {
|
|
17
|
+
return specifier;
|
|
18
|
+
}
|
|
19
|
+
if (specifier.startsWith('./') || specifier.startsWith('../')) {
|
|
20
|
+
return new URL(specifier, benchmarkDirUrl).href;
|
|
21
|
+
}
|
|
22
|
+
if (isAbsolute(specifier)) {
|
|
23
|
+
return pathToFileURL(specifier).href;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
return requireFrom.resolve(specifier);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return specifier;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
const source = `
|
|
33
|
+
export const setup = ${serialize(setupCode)};
|
|
34
|
+
export const teardown = ${serialize(teardownCode)};
|
|
35
|
+
export const pre = ${serialize(preCode)};
|
|
36
|
+
export const run = ${serialize(runCode)};
|
|
37
|
+
export const post = ${serialize(postCode)};
|
|
38
|
+
`;
|
|
39
|
+
const imports = new Map();
|
|
40
|
+
const createSyntheticModule = (moduleExports, exportNames, identifier) => {
|
|
41
|
+
const mod = new SyntheticModule(exportNames, () => {
|
|
42
|
+
for (const name of exportNames) {
|
|
43
|
+
if (name === 'default') {
|
|
44
|
+
mod.setExport(name, moduleExports);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
mod.setExport(name, moduleExports[name]);
|
|
48
|
+
}
|
|
49
|
+
}, { identifier });
|
|
50
|
+
return mod;
|
|
51
|
+
};
|
|
52
|
+
const isCjsModule = (target) => target.endsWith('.cjs') || target.endsWith('.cts');
|
|
53
|
+
const toRequireTarget = (target) => (target.startsWith('file:') ? fileURLToPath(target) : target);
|
|
54
|
+
const loadModule = async (target) => {
|
|
55
|
+
const cached = imports.get(target);
|
|
56
|
+
if (cached)
|
|
57
|
+
return cached;
|
|
58
|
+
if (isCjsModule(target)) {
|
|
59
|
+
const required = requireFrom(toRequireTarget(target));
|
|
60
|
+
const exportNames = required && (typeof required === 'object' || typeof required === 'function') ? Object.keys(required) : [];
|
|
61
|
+
if (!exportNames.includes('default')) {
|
|
62
|
+
exportNames.push('default');
|
|
63
|
+
}
|
|
64
|
+
const mod = createSyntheticModule(required, exportNames, target);
|
|
65
|
+
imports.set(target, mod);
|
|
66
|
+
return mod;
|
|
67
|
+
}
|
|
68
|
+
const importedModule = await import(target);
|
|
69
|
+
const exportNames = Object.keys(importedModule);
|
|
70
|
+
const mod = createSyntheticModule(importedModule, exportNames, target);
|
|
71
|
+
imports.set(target, mod);
|
|
72
|
+
return mod;
|
|
73
|
+
};
|
|
74
|
+
const loadDynamicModule = async (target) => {
|
|
75
|
+
const mod = await loadModule(target);
|
|
76
|
+
if (mod.status !== 'evaluated') {
|
|
77
|
+
await mod.evaluate();
|
|
78
|
+
}
|
|
79
|
+
return mod;
|
|
80
|
+
};
|
|
81
|
+
const mod = new SourceTextModule(source, {
|
|
82
|
+
identifier: resolvedBenchmarkUrl,
|
|
83
|
+
initializeImportMeta(meta) {
|
|
84
|
+
meta.url = resolvedBenchmarkUrl;
|
|
85
|
+
},
|
|
86
|
+
importModuleDynamically(specifier) {
|
|
87
|
+
const resolved = resolveSpecifier(specifier);
|
|
88
|
+
return loadDynamicModule(resolved);
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
await mod.link(async (specifier) => loadModule(resolveSpecifier(specifier)));
|
|
92
|
+
await mod.evaluate();
|
|
93
|
+
const { setup, teardown, pre, run, post } = mod.namespace;
|
|
94
|
+
if (!run) {
|
|
95
|
+
throw new Error('Benchmark run function is required');
|
|
96
|
+
}
|
|
97
|
+
process.exitCode = await benchmark({
|
|
98
|
+
setup,
|
|
99
|
+
teardown,
|
|
100
|
+
pre,
|
|
101
|
+
run,
|
|
102
|
+
post,
|
|
103
|
+
data,
|
|
104
|
+
warmupCycles,
|
|
105
|
+
minCycles,
|
|
106
|
+
absThreshold,
|
|
107
|
+
relThreshold,
|
|
108
|
+
gcObserver,
|
|
109
|
+
durationsSAB,
|
|
110
|
+
controlSAB,
|
|
111
|
+
});
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "overtake",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"description": "NodeJS performance benchmark",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
7
7
|
"exports": {
|
|
8
8
|
"types": "./build/index.d.ts",
|
|
9
|
-
"import": "./
|
|
9
|
+
"import": "./build/index.js"
|
|
10
10
|
},
|
|
11
11
|
"bin": {
|
|
12
12
|
"overtake": "bin/overtake.js"
|
|
@@ -44,9 +44,9 @@
|
|
|
44
44
|
"progress": "^2.0.3"
|
|
45
45
|
},
|
|
46
46
|
"scripts": {
|
|
47
|
-
"build": "rm -rf build && tsc
|
|
47
|
+
"build": "rm -rf build && tsc",
|
|
48
48
|
"start": "./bin/overtake.js",
|
|
49
|
-
"test": "node --experimental-test-module-mocks --test src/__tests__/*.ts",
|
|
50
|
-
"test:cov": "node --experimental-test-module-mocks --experimental-test-coverage --test src/__tests__/*.ts"
|
|
49
|
+
"test": "node --experimental-test-module-mocks --import ./src/register-hook.ts --test src/__tests__/*.ts",
|
|
50
|
+
"test:cov": "node --experimental-test-module-mocks --experimental-test-coverage --import ./src/register-hook.ts --test src/__tests__/*.ts"
|
|
51
51
|
}
|
|
52
52
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
|
-
import { assertNoClosure } from '../utils.
|
|
3
|
+
import { assertNoClosure } from '../utils.js';
|
|
4
4
|
|
|
5
5
|
describe('assertNoClosure', () => {
|
|
6
6
|
describe('allows functions without closures', () => {
|
|
@@ -6,13 +6,13 @@ describe('Benchmark.execute', () => {
|
|
|
6
6
|
const pushAsync = mock.fn(() => Promise.reject(new Error('Benchmark "run" function references outer-scope variables: port')));
|
|
7
7
|
const kill = mock.fn();
|
|
8
8
|
|
|
9
|
-
mock.module('../executor.
|
|
9
|
+
mock.module('../executor.js', {
|
|
10
10
|
namedExports: {
|
|
11
11
|
createExecutor: () => ({ pushAsync, kill }),
|
|
12
12
|
},
|
|
13
13
|
});
|
|
14
14
|
|
|
15
|
-
const { Benchmark } = await import('../index.
|
|
15
|
+
const { Benchmark } = await import('../index.js');
|
|
16
16
|
|
|
17
17
|
const bench = Benchmark.create('feed');
|
|
18
18
|
bench.target('target').measure('run', () => 1);
|
package/src/cli.ts
CHANGED
|
@@ -14,9 +14,9 @@ import {
|
|
|
14
14
|
type BaselineData,
|
|
15
15
|
DEFAULT_REPORT_TYPES,
|
|
16
16
|
DEFAULT_WORKERS,
|
|
17
|
-
} from './index.
|
|
18
|
-
import { REPORT_TYPES } from './types.
|
|
19
|
-
import { resolveHookUrl } from './utils.
|
|
17
|
+
} from './index.js';
|
|
18
|
+
import { REPORT_TYPES } from './types.js';
|
|
19
|
+
import { resolveHookUrl } from './utils.js';
|
|
20
20
|
|
|
21
21
|
register(resolveHookUrl);
|
|
22
22
|
|
|
@@ -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
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Worker } from 'node:worker_threads';
|
|
2
2
|
import { once } from 'node:events';
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
|
-
import { createReport, computeStats, Report } from './reporter.
|
|
5
|
-
import { cmp, assertNoClosure } from './utils.
|
|
4
|
+
import { createReport, computeStats, Report } from './reporter.js';
|
|
5
|
+
import { cmp, assertNoClosure, normalizeFunction } from './utils.js';
|
|
6
6
|
import {
|
|
7
7
|
type ExecutorRunOptions,
|
|
8
8
|
type ReportOptions,
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
CONTROL_SLOTS,
|
|
15
15
|
COMPLETE_VALUE,
|
|
16
16
|
type ProgressCallback,
|
|
17
|
-
} from './types.
|
|
17
|
+
} from './types.js';
|
|
18
18
|
|
|
19
19
|
export type ExecutorReport<R extends ReportTypeList> = Record<R[number], Report> & {
|
|
20
20
|
count: number;
|
|
@@ -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');
|
|
@@ -83,7 +84,7 @@ export const createExecutor = <TContext, TInput, R extends ReportTypeList>(optio
|
|
|
83
84
|
const controlSAB = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * CONTROL_SLOTS);
|
|
84
85
|
const durationsSAB = new SharedArrayBuffer(BigUint64Array.BYTES_PER_ELEMENT * maxCycles);
|
|
85
86
|
|
|
86
|
-
const workerFile = new URL('./worker.
|
|
87
|
+
const workerFile = new URL('./worker.js', import.meta.url);
|
|
87
88
|
const workerData: WorkerOptions = {
|
|
88
89
|
benchmarkUrl: resolvedBenchmarkUrl,
|
|
89
90
|
setupCode,
|
|
@@ -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];
|
|
@@ -161,5 +164,13 @@ export const createExecutor = <TContext, TInput, R extends ReportTypeList>(optio
|
|
|
161
164
|
return Object.fromEntries(report);
|
|
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
|
};
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { cpus } from 'node:os';
|
|
2
2
|
import Progress from 'progress';
|
|
3
|
-
import { createExecutor, type ExecutorOptions, type ExecutorReport } from './executor.
|
|
4
|
-
import { type MaybePromise, type StepFn, type SetupFn, type TeardownFn, type FeedFn, type ReportType, type ReportTypeList, DEFAULT_CYCLES, type ProgressInfo } from './types.
|
|
3
|
+
import { createExecutor, type ExecutorOptions, type ExecutorReport } from './executor.js';
|
|
4
|
+
import { type MaybePromise, type StepFn, type SetupFn, type TeardownFn, type FeedFn, type ReportType, type ReportTypeList, DEFAULT_CYCLES, type ProgressInfo } from './types.js';
|
|
5
5
|
|
|
6
6
|
declare global {
|
|
7
7
|
const benchmark: typeof Benchmark.create;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { register } from 'node:module';
|
|
2
|
+
|
|
3
|
+
async function resolve(s: string, c: unknown, n: (...args: unknown[]) => unknown) {
|
|
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()}`));
|
package/src/reporter.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { div, max, divs, isqrt } from './utils.
|
|
2
|
-
import { type ReportType, DURATION_SCALE } from './types.
|
|
1
|
+
import { div, max, divs, isqrt } from './utils.js';
|
|
2
|
+
import { type ReportType, DURATION_SCALE } from './types.js';
|
|
3
3
|
|
|
4
4
|
const units = [
|
|
5
5
|
{ unit: 'ns', factor: 1 },
|
package/src/runner.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { performance, PerformanceObserver } from 'node:perf_hooks';
|
|
2
|
-
import { type Options, Control, DURATION_SCALE, COMPLETE_VALUE, type StepFn } from './types.
|
|
3
|
-
import { GCWatcher } from './gc-watcher.
|
|
2
|
+
import { type Options, Control, DURATION_SCALE, COMPLETE_VALUE, type StepFn } from './types.js';
|
|
3
|
+
import { GCWatcher } from './gc-watcher.js';
|
|
4
4
|
|
|
5
5
|
const hr = process.hrtime.bigint.bind(process.hrtime);
|
|
6
6
|
|
|
@@ -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
|
@@ -45,11 +45,14 @@ export const max = (a: bigint, b: bigint) => {
|
|
|
45
45
|
|
|
46
46
|
export function div(a: bigint, b: bigint, decimals: number = 2): string {
|
|
47
47
|
if (b === 0n) throw new RangeError('Division by zero');
|
|
48
|
+
const neg = a < 0n !== b < 0n;
|
|
49
|
+
const absA = a < 0n ? -a : a;
|
|
50
|
+
const absB = b < 0n ? -b : b;
|
|
48
51
|
const scale = 10n ** BigInt(decimals);
|
|
49
|
-
const scaled = (
|
|
52
|
+
const scaled = (absA * scale) / absB;
|
|
50
53
|
const intPart = scaled / scale;
|
|
51
54
|
const fracPart = scaled % scale;
|
|
52
|
-
return `${intPart}.${fracPart.toString().padStart(decimals, '0')}`;
|
|
55
|
+
return `${neg ? '-' : ''}${intPart}.${fracPart.toString().padStart(decimals, '0')}`;
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
export function divs(a: bigint, b: bigint, scale: bigint): bigint {
|
|
@@ -60,19 +63,68 @@ export function divs(a: bigint, b: bigint, scale: bigint): bigint {
|
|
|
60
63
|
const KNOWN_GLOBALS = new Set(Object.getOwnPropertyNames(globalThis));
|
|
61
64
|
KNOWN_GLOBALS.add('arguments');
|
|
62
65
|
|
|
63
|
-
|
|
66
|
+
let _unresolvedCtxt: number | undefined;
|
|
67
|
+
|
|
68
|
+
function findIdentifierCtxt(node: unknown, name: string): number | undefined {
|
|
69
|
+
if (!node || typeof node !== 'object') return undefined;
|
|
70
|
+
if (Array.isArray(node)) {
|
|
71
|
+
for (const item of node) {
|
|
72
|
+
const r = findIdentifierCtxt(item, name);
|
|
73
|
+
if (r !== undefined) return r;
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
const obj = node as Record<string, unknown>;
|
|
78
|
+
if (obj.type === 'Identifier' && obj.value === name && typeof obj.ctxt === 'number') {
|
|
79
|
+
return obj.ctxt;
|
|
80
|
+
}
|
|
81
|
+
for (const key of Object.keys(obj)) {
|
|
82
|
+
if (key === 'span') continue;
|
|
83
|
+
const r = findIdentifierCtxt(obj[key], name);
|
|
84
|
+
if (r !== undefined) return r;
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function probeUnresolvedCtxt(): number {
|
|
90
|
+
if (_unresolvedCtxt !== undefined) return _unresolvedCtxt;
|
|
91
|
+
try {
|
|
92
|
+
const ast = parseSync('var _ = () => __PROBE__', { syntax: 'ecmascript', target: 'esnext' });
|
|
93
|
+
_unresolvedCtxt = findIdentifierCtxt(ast, '__PROBE__') ?? 1;
|
|
94
|
+
} catch {
|
|
95
|
+
_unresolvedCtxt = 1;
|
|
96
|
+
}
|
|
97
|
+
return _unresolvedCtxt;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function collectUnresolved(node: unknown, ctxt: number, result: Set<string>) {
|
|
64
101
|
if (!node || typeof node !== 'object') return;
|
|
65
102
|
if (Array.isArray(node)) {
|
|
66
|
-
for (const item of node) collectUnresolved(item, result);
|
|
103
|
+
for (const item of node) collectUnresolved(item, ctxt, result);
|
|
67
104
|
return;
|
|
68
105
|
}
|
|
69
106
|
const obj = node as Record<string, unknown>;
|
|
70
|
-
if (obj.type === 'Identifier' && obj.ctxt ===
|
|
107
|
+
if (obj.type === 'Identifier' && obj.ctxt === ctxt && typeof obj.value === 'string') {
|
|
71
108
|
result.add(obj.value);
|
|
72
109
|
}
|
|
73
110
|
for (const key of Object.keys(obj)) {
|
|
74
111
|
if (key === 'span') continue;
|
|
75
|
-
collectUnresolved(obj[key], result);
|
|
112
|
+
collectUnresolved(obj[key], ctxt, result);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function normalizeFunction(code: string): string {
|
|
117
|
+
try {
|
|
118
|
+
parseSync(`var __fn = ${code}`, { syntax: 'ecmascript', target: 'esnext' });
|
|
119
|
+
return code;
|
|
120
|
+
} catch {
|
|
121
|
+
const normalized = code.startsWith('async ') ? `async function ${code.slice(6)}` : `function ${code}`;
|
|
122
|
+
try {
|
|
123
|
+
parseSync(`var __fn = ${normalized}`, { syntax: 'ecmascript', target: 'esnext' });
|
|
124
|
+
return normalized;
|
|
125
|
+
} catch {
|
|
126
|
+
return code;
|
|
127
|
+
}
|
|
76
128
|
}
|
|
77
129
|
}
|
|
78
130
|
|
|
@@ -83,8 +135,9 @@ export function assertNoClosure(code: string, name: string): void {
|
|
|
83
135
|
} catch {
|
|
84
136
|
return;
|
|
85
137
|
}
|
|
138
|
+
const unresolvedCtxt = probeUnresolvedCtxt();
|
|
86
139
|
const unresolved = new Set<string>();
|
|
87
|
-
collectUnresolved(ast, unresolved);
|
|
140
|
+
collectUnresolved(ast, unresolvedCtxt, unresolved);
|
|
88
141
|
for (const g of KNOWN_GLOBALS) unresolved.delete(g);
|
|
89
142
|
if (unresolved.size === 0) return;
|
|
90
143
|
|
package/src/worker.ts
CHANGED
|
@@ -3,9 +3,9 @@ import { SourceTextModule, SyntheticModule } from 'node:vm';
|
|
|
3
3
|
import { createRequire, register } from 'node:module';
|
|
4
4
|
import { isAbsolute } from 'node:path';
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
|
-
import { benchmark } from './runner.
|
|
7
|
-
import { type WorkerOptions } from './types.
|
|
8
|
-
import { resolveHookUrl } from './utils.
|
|
6
|
+
import { benchmark } from './runner.js';
|
|
7
|
+
import { type WorkerOptions } from './types.js';
|
|
8
|
+
import { resolveHookUrl } from './utils.js';
|
|
9
9
|
|
|
10
10
|
register(resolveHookUrl);
|
|
11
11
|
|