overtake 1.4.0 → 2.0.1
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/README.md +4 -15
- package/build/cli.js +126 -125
- package/build/executor.d.ts +6 -2
- package/build/executor.js +59 -46
- package/build/gc-watcher.js +2 -6
- package/build/index.d.ts +10 -11
- package/build/index.js +153 -155
- package/build/register-hook.d.ts +1 -0
- package/build/register-hook.js +15 -0
- package/build/reporter.d.ts +10 -2
- package/build/reporter.js +176 -214
- package/build/runner.d.ts +1 -1
- package/build/runner.js +128 -119
- package/build/types.d.ts +6 -6
- package/build/types.js +9 -14
- package/build/utils.d.ts +1 -17
- package/build/utils.js +53 -85
- package/build/worker.js +25 -24
- package/package.json +7 -25
- package/src/__tests__/assert-no-closure.ts +135 -0
- package/src/__tests__/benchmark-execute.ts +48 -0
- package/src/cli.ts +137 -142
- package/src/executor.ts +45 -15
- package/src/index.ts +85 -57
- package/src/register-hook.ts +15 -0
- package/src/reporter.ts +26 -18
- package/src/runner.ts +1 -4
- package/src/types.ts +8 -8
- package/src/utils.ts +15 -54
- package/src/worker.ts +5 -2
- package/tsconfig.json +2 -1
- package/build/cli.cjs +0 -179
- package/build/cli.cjs.map +0 -1
- package/build/cli.js.map +0 -1
- package/build/executor.cjs +0 -123
- package/build/executor.cjs.map +0 -1
- package/build/executor.js.map +0 -1
- package/build/gc-watcher.cjs +0 -30
- package/build/gc-watcher.cjs.map +0 -1
- package/build/gc-watcher.js.map +0 -1
- package/build/index.cjs +0 -442
- package/build/index.cjs.map +0 -1
- package/build/index.js.map +0 -1
- package/build/reporter.cjs +0 -311
- package/build/reporter.cjs.map +0 -1
- package/build/reporter.js.map +0 -1
- package/build/runner.cjs +0 -532
- package/build/runner.cjs.map +0 -1
- package/build/runner.js.map +0 -1
- package/build/types.cjs +0 -66
- package/build/types.cjs.map +0 -1
- package/build/types.js.map +0 -1
- package/build/utils.cjs +0 -174
- package/build/utils.cjs.map +0 -1
- package/build/utils.js.map +0 -1
- package/build/worker.cjs +0 -155
- package/build/worker.cjs.map +0 -1
- package/build/worker.js.map +0 -1
- package/src/__tests__/assert-no-closure.js +0 -134
package/build/utils.js
CHANGED
|
@@ -1,22 +1,32 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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;
|
|
5
21
|
let x = n;
|
|
6
|
-
let y = x + 1n >> 1n;
|
|
7
|
-
while(y < x){
|
|
22
|
+
let y = (x + 1n) >> 1n;
|
|
23
|
+
while (y < x) {
|
|
8
24
|
x = y;
|
|
9
|
-
y = x + n / x >> 1n;
|
|
25
|
+
y = (x + n / x) >> 1n;
|
|
10
26
|
}
|
|
11
27
|
return x;
|
|
12
28
|
};
|
|
13
|
-
export const
|
|
14
|
-
if (value < 0n) {
|
|
15
|
-
return -value;
|
|
16
|
-
}
|
|
17
|
-
return value;
|
|
18
|
-
};
|
|
19
|
-
export const cmp = (a, b)=>{
|
|
29
|
+
export const cmp = (a, b) => {
|
|
20
30
|
if (a > b) {
|
|
21
31
|
return 1;
|
|
22
32
|
}
|
|
@@ -25,108 +35,66 @@ export const cmp = (a, b)=>{
|
|
|
25
35
|
}
|
|
26
36
|
return 0;
|
|
27
37
|
};
|
|
28
|
-
export const max = (a, b)=>{
|
|
38
|
+
export const max = (a, b) => {
|
|
29
39
|
if (a > b) {
|
|
30
40
|
return a;
|
|
31
41
|
}
|
|
32
42
|
return b;
|
|
33
43
|
};
|
|
34
|
-
export const divMod = (a, b)=>{
|
|
35
|
-
return {
|
|
36
|
-
quotient: a / b,
|
|
37
|
-
remainder: a % b
|
|
38
|
-
};
|
|
39
|
-
};
|
|
40
44
|
export function div(a, b, decimals = 2) {
|
|
41
|
-
if (b === 0n)
|
|
45
|
+
if (b === 0n)
|
|
46
|
+
throw new RangeError('Division by zero');
|
|
42
47
|
const scale = 10n ** BigInt(decimals);
|
|
43
|
-
const scaled = a * scale / b;
|
|
48
|
+
const scaled = (a * scale) / b;
|
|
44
49
|
const intPart = scaled / scale;
|
|
45
50
|
const fracPart = scaled % scale;
|
|
46
51
|
return `${intPart}.${fracPart.toString().padStart(decimals, '0')}`;
|
|
47
52
|
}
|
|
48
53
|
export function divs(a, b, scale) {
|
|
49
|
-
if (b === 0n)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
export class ScaledBigInt {
|
|
53
|
-
value;
|
|
54
|
-
scale;
|
|
55
|
-
constructor(value, scale){
|
|
56
|
-
this.value = value;
|
|
57
|
-
this.scale = scale;
|
|
58
|
-
}
|
|
59
|
-
add(value) {
|
|
60
|
-
this.value += value * this.scale;
|
|
61
|
-
}
|
|
62
|
-
sub(value) {
|
|
63
|
-
this.value -= value * this.scale;
|
|
64
|
-
}
|
|
65
|
-
div(value) {
|
|
66
|
-
this.value /= value;
|
|
67
|
-
}
|
|
68
|
-
mul(value) {
|
|
69
|
-
this.value *= value;
|
|
70
|
-
}
|
|
71
|
-
unscale() {
|
|
72
|
-
return this.value / this.scale;
|
|
73
|
-
}
|
|
74
|
-
number() {
|
|
75
|
-
return Number(div(this.value, this.scale));
|
|
76
|
-
}
|
|
54
|
+
if (b === 0n)
|
|
55
|
+
throw new RangeError('Division by zero');
|
|
56
|
+
return (a * scale) / b;
|
|
77
57
|
}
|
|
78
58
|
const KNOWN_GLOBALS = new Set(Object.getOwnPropertyNames(globalThis));
|
|
79
59
|
KNOWN_GLOBALS.add('arguments');
|
|
80
60
|
function collectUnresolved(node, result) {
|
|
81
|
-
if (!node || typeof node !== 'object')
|
|
61
|
+
if (!node || typeof node !== 'object')
|
|
62
|
+
return;
|
|
82
63
|
if (Array.isArray(node)) {
|
|
83
|
-
for (const item of node)
|
|
64
|
+
for (const item of node)
|
|
65
|
+
collectUnresolved(item, result);
|
|
84
66
|
return;
|
|
85
67
|
}
|
|
86
68
|
const obj = node;
|
|
87
69
|
if (obj.type === 'Identifier' && obj.ctxt === 1 && typeof obj.value === 'string') {
|
|
88
70
|
result.add(obj.value);
|
|
89
71
|
}
|
|
90
|
-
for (const key of Object.keys(obj)){
|
|
91
|
-
if (key === 'span')
|
|
72
|
+
for (const key of Object.keys(obj)) {
|
|
73
|
+
if (key === 'span')
|
|
74
|
+
continue;
|
|
92
75
|
collectUnresolved(obj[key], result);
|
|
93
76
|
}
|
|
94
77
|
}
|
|
95
78
|
export function assertNoClosure(code, name) {
|
|
96
79
|
let ast;
|
|
97
80
|
try {
|
|
98
|
-
ast = parseSync(`var __fn = ${code}`, {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
});
|
|
102
|
-
} catch {
|
|
81
|
+
ast = parseSync(`var __fn = ${code}`, { syntax: 'ecmascript', target: 'esnext' });
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
103
84
|
return;
|
|
104
85
|
}
|
|
105
86
|
const unresolved = new Set();
|
|
106
87
|
collectUnresolved(ast, unresolved);
|
|
107
|
-
for (const g of KNOWN_GLOBALS)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
].join(', ');
|
|
112
|
-
throw new Error(`Benchmark "${name}" function references outer-scope variables: ${vars}\n\n` +
|
|
88
|
+
for (const g of KNOWN_GLOBALS)
|
|
89
|
+
unresolved.delete(g);
|
|
90
|
+
if (unresolved.size === 0)
|
|
91
|
+
return;
|
|
92
|
+
const vars = [...unresolved].join(', ');
|
|
93
|
+
throw new Error(`Benchmark "${name}" function references outer-scope variables: ${vars}\n\n` +
|
|
94
|
+
`Benchmark functions are serialized with .toString() and executed in an isolated\n` +
|
|
95
|
+
`worker thread. Closed-over variables from the original module scope are not\n` +
|
|
96
|
+
`available in the worker and will cause a ReferenceError at runtime.\n\n` +
|
|
97
|
+
`To fix this, move the referenced values into:\n` +
|
|
98
|
+
` - "setup" function (returned value becomes the first argument of run/pre/post)\n` +
|
|
99
|
+
` - "data" option (passed as the second argument of run/pre/post)`);
|
|
113
100
|
}
|
|
114
|
-
export const transpile = async (code)=>{
|
|
115
|
-
const output = await transform(code, {
|
|
116
|
-
filename: 'benchmark.ts',
|
|
117
|
-
jsc: {
|
|
118
|
-
parser: {
|
|
119
|
-
syntax: 'typescript',
|
|
120
|
-
tsx: false,
|
|
121
|
-
dynamicImport: true
|
|
122
|
-
},
|
|
123
|
-
target: 'esnext'
|
|
124
|
-
},
|
|
125
|
-
module: {
|
|
126
|
-
type: 'es6'
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
return output.code;
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
//# sourceMappingURL=utils.js.map
|
package/build/worker.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { workerData } from 'node:worker_threads';
|
|
2
2
|
import { SourceTextModule, SyntheticModule } from 'node:vm';
|
|
3
|
-
import { createRequire } from 'node:module';
|
|
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
|
|
7
|
-
|
|
8
|
-
|
|
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');
|
|
9
12
|
const resolvedBenchmarkUrl = typeof benchmarkUrl === 'string' ? benchmarkUrl : pathToFileURL(process.cwd()).href;
|
|
10
13
|
const benchmarkDirUrl = new URL('.', resolvedBenchmarkUrl).href;
|
|
11
14
|
const requireFrom = createRequire(fileURLToPath(new URL('benchmark.js', benchmarkDirUrl)));
|
|
12
|
-
const resolveSpecifier = (specifier)=>{
|
|
15
|
+
const resolveSpecifier = (specifier) => {
|
|
13
16
|
if (specifier.startsWith('file:')) {
|
|
14
17
|
return specifier;
|
|
15
18
|
}
|
|
@@ -21,7 +24,8 @@ const resolveSpecifier = (specifier)=>{
|
|
|
21
24
|
}
|
|
22
25
|
try {
|
|
23
26
|
return requireFrom.resolve(specifier);
|
|
24
|
-
}
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
25
29
|
return specifier;
|
|
26
30
|
}
|
|
27
31
|
};
|
|
@@ -33,25 +37,24 @@ export const run = ${serialize(runCode)};
|
|
|
33
37
|
export const post = ${serialize(postCode)};
|
|
34
38
|
`;
|
|
35
39
|
const imports = new Map();
|
|
36
|
-
const createSyntheticModule = (moduleExports, exportNames, identifier)=>{
|
|
37
|
-
const mod = new SyntheticModule(exportNames, ()=>{
|
|
38
|
-
for (const name of exportNames){
|
|
40
|
+
const createSyntheticModule = (moduleExports, exportNames, identifier) => {
|
|
41
|
+
const mod = new SyntheticModule(exportNames, () => {
|
|
42
|
+
for (const name of exportNames) {
|
|
39
43
|
if (name === 'default') {
|
|
40
44
|
mod.setExport(name, moduleExports);
|
|
41
45
|
continue;
|
|
42
46
|
}
|
|
43
47
|
mod.setExport(name, moduleExports[name]);
|
|
44
48
|
}
|
|
45
|
-
}, {
|
|
46
|
-
identifier
|
|
47
|
-
});
|
|
49
|
+
}, { identifier });
|
|
48
50
|
return mod;
|
|
49
51
|
};
|
|
50
|
-
const isCjsModule = (target)=>target.endsWith('.cjs') || target.endsWith('.cts');
|
|
51
|
-
const toRequireTarget = (target)=>target.startsWith('file:') ? fileURLToPath(target) : target;
|
|
52
|
-
const loadModule = async (target)=>{
|
|
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) => {
|
|
53
55
|
const cached = imports.get(target);
|
|
54
|
-
if (cached)
|
|
56
|
+
if (cached)
|
|
57
|
+
return cached;
|
|
55
58
|
if (isCjsModule(target)) {
|
|
56
59
|
const required = requireFrom(toRequireTarget(target));
|
|
57
60
|
const exportNames = required && (typeof required === 'object' || typeof required === 'function') ? Object.keys(required) : [];
|
|
@@ -68,7 +71,7 @@ const loadModule = async (target)=>{
|
|
|
68
71
|
imports.set(target, mod);
|
|
69
72
|
return mod;
|
|
70
73
|
};
|
|
71
|
-
const loadDynamicModule = async (target)=>{
|
|
74
|
+
const loadDynamicModule = async (target) => {
|
|
72
75
|
const mod = await loadModule(target);
|
|
73
76
|
if (mod.status !== 'evaluated') {
|
|
74
77
|
await mod.evaluate();
|
|
@@ -77,15 +80,15 @@ const loadDynamicModule = async (target)=>{
|
|
|
77
80
|
};
|
|
78
81
|
const mod = new SourceTextModule(source, {
|
|
79
82
|
identifier: resolvedBenchmarkUrl,
|
|
80
|
-
initializeImportMeta
|
|
83
|
+
initializeImportMeta(meta) {
|
|
81
84
|
meta.url = resolvedBenchmarkUrl;
|
|
82
85
|
},
|
|
83
|
-
importModuleDynamically
|
|
86
|
+
importModuleDynamically(specifier) {
|
|
84
87
|
const resolved = resolveSpecifier(specifier);
|
|
85
88
|
return loadDynamicModule(resolved);
|
|
86
|
-
}
|
|
89
|
+
},
|
|
87
90
|
});
|
|
88
|
-
await mod.link(async (specifier)=>loadModule(resolveSpecifier(specifier)));
|
|
91
|
+
await mod.link(async (specifier) => loadModule(resolveSpecifier(specifier)));
|
|
89
92
|
await mod.evaluate();
|
|
90
93
|
const { setup, teardown, pre, run, post } = mod.namespace;
|
|
91
94
|
if (!run) {
|
|
@@ -104,7 +107,5 @@ process.exitCode = await benchmark({
|
|
|
104
107
|
relThreshold,
|
|
105
108
|
gcObserver,
|
|
106
109
|
durationsSAB,
|
|
107
|
-
controlSAB
|
|
110
|
+
controlSAB,
|
|
108
111
|
});
|
|
109
|
-
|
|
110
|
-
//# sourceMappingURL=worker.js.map
|
package/package.json
CHANGED
|
@@ -1,28 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "overtake",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "NodeJS performance benchmark",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
7
|
-
"main": "build/index.cjs",
|
|
8
|
-
"module": "build/index.js",
|
|
9
7
|
"exports": {
|
|
10
8
|
"types": "./build/index.d.ts",
|
|
11
|
-
"require": "./build/index.cjs",
|
|
12
9
|
"import": "./build/index.js"
|
|
13
10
|
},
|
|
14
|
-
"typesVersions": {
|
|
15
|
-
"*": {
|
|
16
|
-
"*": [
|
|
17
|
-
"build/index.d.ts"
|
|
18
|
-
]
|
|
19
|
-
}
|
|
20
|
-
},
|
|
21
11
|
"bin": {
|
|
22
12
|
"overtake": "bin/overtake.js"
|
|
23
13
|
},
|
|
24
14
|
"engines": {
|
|
25
|
-
"node": ">=
|
|
15
|
+
"node": ">=24"
|
|
26
16
|
},
|
|
27
17
|
"repository": {
|
|
28
18
|
"type": "git",
|
|
@@ -41,30 +31,22 @@
|
|
|
41
31
|
},
|
|
42
32
|
"homepage": "https://github.com/3axap4eHko/overtake#readme",
|
|
43
33
|
"devDependencies": {
|
|
44
|
-
"@jest/globals": "^30.2.0",
|
|
45
|
-
"@swc/jest": "^0.2.39",
|
|
46
|
-
"@types/async": "^3.2.25",
|
|
47
|
-
"@types/jest": "^30.0.0",
|
|
48
34
|
"@types/node": "^25.3.0",
|
|
49
35
|
"@types/progress": "^2.0.7",
|
|
50
36
|
"husky": "^9.1.7",
|
|
51
|
-
"inop": "^0.9.0",
|
|
52
|
-
"jest": "^30.2.0",
|
|
53
37
|
"overtake": "^1.3.1",
|
|
54
38
|
"prettier": "^3.8.1",
|
|
55
39
|
"pretty-quick": "^4.2.2",
|
|
56
|
-
"typescript": "
|
|
40
|
+
"typescript": "5.9.3"
|
|
57
41
|
},
|
|
58
42
|
"dependencies": {
|
|
59
|
-
"@swc/core": "^1.15.
|
|
60
|
-
"async": "^3.2.6",
|
|
61
|
-
"commander": "^14.0.3",
|
|
62
|
-
"glob": "^13.0.6",
|
|
43
|
+
"@swc/core": "^1.15.18",
|
|
63
44
|
"progress": "^2.0.3"
|
|
64
45
|
},
|
|
65
46
|
"scripts": {
|
|
66
|
-
"build": "rm -rf build &&
|
|
47
|
+
"build": "rm -rf build && tsc",
|
|
67
48
|
"start": "./bin/overtake.js",
|
|
68
|
-
"test": "
|
|
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"
|
|
69
51
|
}
|
|
70
52
|
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { assertNoClosure } from '../utils.js';
|
|
4
|
+
|
|
5
|
+
describe('assertNoClosure', () => {
|
|
6
|
+
describe('allows functions without closures', () => {
|
|
7
|
+
it('arrow with params only', () => {
|
|
8
|
+
assert.doesNotThrow(() => assertNoClosure('(x) => x * 2', 'run'));
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('arrow with local variables', () => {
|
|
12
|
+
assert.doesNotThrow(() => assertNoClosure('(ctx, input) => { const y = ctx.value + input; return y; }', 'run'));
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('function expression', () => {
|
|
16
|
+
assert.doesNotThrow(() => assertNoClosure('function(x) { return x + 1; }', 'run'));
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('named function expression', () => {
|
|
20
|
+
assert.doesNotThrow(() => assertNoClosure('function run(x) { return x + 1; }', 'run'));
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('async arrow', () => {
|
|
24
|
+
assert.doesNotThrow(() => assertNoClosure('async (ctx) => { const r = await fetch("url"); return r; }', 'run'));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('destructured params', () => {
|
|
28
|
+
assert.doesNotThrow(() => assertNoClosure('({a, b: c}, [d, ...e]) => a + c + d + e.length', 'run'));
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('nested function declaration', () => {
|
|
32
|
+
assert.doesNotThrow(() => assertNoClosure('(arr) => { function helper(x) { return x * 2; } return arr.map(helper); }', 'run'));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('for-of loop variable', () => {
|
|
36
|
+
assert.doesNotThrow(() => assertNoClosure('(arr) => { let sum = 0; for (const x of arr) sum += x; return sum; }', 'run'));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('member access on params', () => {
|
|
40
|
+
assert.doesNotThrow(() => assertNoClosure('(ctx) => ctx.data.map(x => x.value)', 'run'));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('globals like console, Buffer, Math, Array', () => {
|
|
44
|
+
assert.doesNotThrow(() => assertNoClosure('(ctx) => { console.log(Math.max(...ctx)); return Buffer.from(Array.of(1)); }', 'run'));
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('try-catch with error binding', () => {
|
|
48
|
+
assert.doesNotThrow(() => assertNoClosure('(ctx) => { try { return ctx(); } catch (e) { return e; } }', 'run'));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('class expression', () => {
|
|
52
|
+
assert.doesNotThrow(() => assertNoClosure('() => { class Foo { bar() { return 1; } } return new Foo(); }', 'run'));
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('label statements', () => {
|
|
56
|
+
assert.doesNotThrow(() => assertNoClosure('() => { outer: for (let i = 0; i < 10; i++) { break outer; } }', 'run'));
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('detects closures', () => {
|
|
61
|
+
it('single closed-over variable', () => {
|
|
62
|
+
assert.throws(() => assertNoClosure('(x) => x + closedOver', 'run'), /closedOver/);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('multiple closed-over variables', () => {
|
|
66
|
+
assert.throws(() => assertNoClosure('(ctx) => sharedData.filter(x => x > threshold)', 'run'), /sharedData/);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('closed-over function call', () => {
|
|
70
|
+
assert.throws(() => assertNoClosure('(ctx) => helper(ctx)', 'run'), /helper/);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('closed-over array', () => {
|
|
74
|
+
assert.throws(() => assertNoClosure('() => myArray.map(x => x * 2)', 'run'), /myArray/);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('computed member access with outer variable', () => {
|
|
78
|
+
assert.throws(() => assertNoClosure('(obj) => obj[key]', 'run'), /key/);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('variable used as argument', () => {
|
|
82
|
+
assert.throws(() => assertNoClosure('() => JSON.stringify(config)', 'run'), /config/);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('variable in template literal', () => {
|
|
86
|
+
assert.throws(() => assertNoClosure('() => `${prefix}-value`', 'run'), /prefix/);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('error message', () => {
|
|
91
|
+
it('includes the function name', () => {
|
|
92
|
+
assert.throws(() => assertNoClosure('() => x', 'setup'), /"setup"/);
|
|
93
|
+
assert.throws(() => assertNoClosure('() => x', 'run'), /"run"/);
|
|
94
|
+
assert.throws(() => assertNoClosure('() => x', 'teardown'), /"teardown"/);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('lists all closed-over variables', () => {
|
|
98
|
+
try {
|
|
99
|
+
assertNoClosure('() => a + b + c', 'run');
|
|
100
|
+
assert.fail('should have thrown');
|
|
101
|
+
} catch (e) {
|
|
102
|
+
assert.match((e as Error).message, /\ba\b/);
|
|
103
|
+
assert.match((e as Error).message, /\bb\b/);
|
|
104
|
+
assert.match((e as Error).message, /\bc\b/);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('explains the problem and suggests fix', () => {
|
|
109
|
+
try {
|
|
110
|
+
assertNoClosure('() => x', 'run');
|
|
111
|
+
assert.fail('should have thrown');
|
|
112
|
+
} catch (e) {
|
|
113
|
+
const msg = (e as Error).message;
|
|
114
|
+
assert.ok(msg.includes('.toString()'));
|
|
115
|
+
assert.ok(msg.includes('worker'));
|
|
116
|
+
assert.ok(msg.includes('setup'));
|
|
117
|
+
assert.ok(msg.includes('data'));
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('edge cases', () => {
|
|
123
|
+
it('silently passes on unparseable code', () => {
|
|
124
|
+
assert.doesNotThrow(() => assertNoClosure('not valid js {{{', 'run'));
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('empty arrow function', () => {
|
|
128
|
+
assert.doesNotThrow(() => assertNoClosure('() => {}', 'run'));
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('undefined return', () => {
|
|
132
|
+
assert.doesNotThrow(() => assertNoClosure('() => undefined', 'run'));
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, it, mock } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
|
|
4
|
+
describe('Benchmark.execute', () => {
|
|
5
|
+
it('returns an error report when executor task setup fails', async () => {
|
|
6
|
+
const pushAsync = mock.fn(() => Promise.reject(new Error('Benchmark "run" function references outer-scope variables: port')));
|
|
7
|
+
const kill = mock.fn();
|
|
8
|
+
|
|
9
|
+
mock.module('../executor.js', {
|
|
10
|
+
namedExports: {
|
|
11
|
+
createExecutor: () => ({ pushAsync, kill }),
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const { Benchmark } = await import('../index.js');
|
|
16
|
+
|
|
17
|
+
const bench = Benchmark.create('feed');
|
|
18
|
+
bench.target('target').measure('run', () => 1);
|
|
19
|
+
|
|
20
|
+
const reports = await bench.execute();
|
|
21
|
+
|
|
22
|
+
assert.deepStrictEqual(reports, [
|
|
23
|
+
{
|
|
24
|
+
target: 'target',
|
|
25
|
+
measures: [
|
|
26
|
+
{
|
|
27
|
+
measure: 'run',
|
|
28
|
+
feeds: [
|
|
29
|
+
{
|
|
30
|
+
feed: 'feed',
|
|
31
|
+
data: {
|
|
32
|
+
count: 0,
|
|
33
|
+
heapUsedKB: 0,
|
|
34
|
+
dceWarning: false,
|
|
35
|
+
error: 'Benchmark "run" function references outer-scope variables: port',
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
]);
|
|
43
|
+
assert.strictEqual(pushAsync.mock.callCount(), 1);
|
|
44
|
+
assert.strictEqual(kill.mock.callCount(), 1);
|
|
45
|
+
|
|
46
|
+
mock.restoreAll();
|
|
47
|
+
});
|
|
48
|
+
});
|