overtake 1.3.2 → 1.4.0
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 +8 -0
- package/build/executor.cjs +12 -5
- package/build/executor.cjs.map +1 -1
- package/build/executor.d.ts +1 -0
- package/build/executor.js +13 -6
- package/build/executor.js.map +1 -1
- package/build/index.cjs +58 -16
- package/build/index.cjs.map +1 -1
- package/build/index.js +58 -16
- package/build/index.js.map +1 -1
- package/build/reporter.cjs +63 -116
- package/build/reporter.cjs.map +1 -1
- package/build/reporter.js +65 -118
- package/build/reporter.js.map +1 -1
- package/build/runner.cjs +23 -19
- package/build/runner.cjs.map +1 -1
- package/build/runner.js +23 -19
- package/build/runner.js.map +1 -1
- package/build/types.cjs +1 -1
- package/build/types.cjs.map +1 -1
- package/build/types.d.ts +1 -1
- package/build/types.js +1 -1
- package/build/types.js.map +1 -1
- package/build/utils.cjs +53 -0
- package/build/utils.cjs.map +1 -1
- package/build/utils.d.ts +2 -0
- package/build/utils.js +48 -1
- package/build/utils.js.map +1 -1
- package/build/worker.cjs +6 -9
- package/build/worker.cjs.map +1 -1
- package/build/worker.js +7 -10
- package/build/worker.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/assert-no-closure.js +134 -0
- package/src/executor.ts +12 -7
- package/src/index.ts +50 -11
- package/src/reporter.ts +62 -118
- package/src/runner.ts +26 -20
- package/src/types.ts +1 -1
- package/src/utils.ts +56 -1
- package/src/worker.ts +7 -9
package/build/worker.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/worker.ts"],"sourcesContent":["import { workerData } from 'node:worker_threads';\nimport { SourceTextModule, SyntheticModule
|
|
1
|
+
{"version":3,"sources":["../src/worker.ts"],"sourcesContent":["import { workerData } from 'node:worker_threads';\nimport { SourceTextModule, SyntheticModule } from 'node:vm';\nimport { createRequire } from 'node:module';\nimport { isAbsolute } from 'node:path';\nimport { fileURLToPath, pathToFileURL } from 'node:url';\nimport { benchmark } from './runner.js';\nimport { WorkerOptions } from './types.js';\n\nconst {\n benchmarkUrl,\n setupCode,\n teardownCode,\n preCode,\n runCode,\n postCode,\n data,\n\n warmupCycles,\n minCycles,\n absThreshold,\n relThreshold,\n gcObserver = true,\n\n durationsSAB,\n controlSAB,\n}: WorkerOptions = workerData;\n\nconst serialize = (code?: string) => (code ? code : 'undefined');\n\nconst resolvedBenchmarkUrl = typeof benchmarkUrl === 'string' ? benchmarkUrl : pathToFileURL(process.cwd()).href;\nconst benchmarkDirUrl = new URL('.', resolvedBenchmarkUrl).href;\nconst requireFrom = createRequire(fileURLToPath(new URL('benchmark.js', benchmarkDirUrl)));\n\nconst resolveSpecifier = (specifier: string) => {\n if (specifier.startsWith('file:')) {\n return specifier;\n }\n if (specifier.startsWith('./') || specifier.startsWith('../')) {\n return new URL(specifier, benchmarkDirUrl).href;\n }\n if (isAbsolute(specifier)) {\n return pathToFileURL(specifier).href;\n }\n try {\n return requireFrom.resolve(specifier);\n } catch {\n return specifier;\n }\n};\n\nconst source = `\nexport const setup = ${serialize(setupCode)};\nexport const teardown = ${serialize(teardownCode)};\nexport const pre = ${serialize(preCode)};\nexport const run = ${serialize(runCode)};\nexport const post = ${serialize(postCode)};\n `;\n\nconst imports = new Map<string, SyntheticModule>();\n\nconst createSyntheticModule = (moduleExports: unknown, exportNames: string[], identifier: string) => {\n const mod = new SyntheticModule(\n exportNames,\n () => {\n for (const name of exportNames) {\n if (name === 'default') {\n mod.setExport(name, moduleExports);\n continue;\n }\n mod.setExport(name, (moduleExports as Record<string, unknown>)[name]);\n }\n },\n { identifier },\n );\n return mod;\n};\n\nconst isCjsModule = (target: string) => target.endsWith('.cjs') || target.endsWith('.cts');\n\nconst toRequireTarget = (target: string) => (target.startsWith('file:') ? fileURLToPath(target) : target);\n\nconst loadModule = async (target: string) => {\n const cached = imports.get(target);\n if (cached) return cached;\n\n if (isCjsModule(target)) {\n const required = requireFrom(toRequireTarget(target));\n const exportNames = required && (typeof required === 'object' || typeof required === 'function') ? Object.keys(required) : [];\n if (!exportNames.includes('default')) {\n exportNames.push('default');\n }\n const mod = createSyntheticModule(required, exportNames, target);\n imports.set(target, mod);\n return mod;\n }\n\n const importedModule = await import(target);\n const exportNames = Object.keys(importedModule);\n const mod = createSyntheticModule(importedModule, exportNames, target);\n imports.set(target, mod);\n return mod;\n};\n\nconst loadDynamicModule = async (target: string) => {\n const mod = await loadModule(target);\n if (mod.status !== 'evaluated') {\n await mod.evaluate();\n }\n return mod;\n};\nconst mod = new SourceTextModule(source, {\n identifier: resolvedBenchmarkUrl,\n initializeImportMeta(meta) {\n meta.url = resolvedBenchmarkUrl;\n },\n importModuleDynamically(specifier) {\n const resolved = resolveSpecifier(specifier);\n return loadDynamicModule(resolved);\n },\n});\n\nawait mod.link(async (specifier) => loadModule(resolveSpecifier(specifier)));\n\nawait mod.evaluate();\nconst { setup, teardown, pre, run, post } = mod.namespace as any;\n\nif (!run) {\n throw new Error('Benchmark run function is required');\n}\n\nprocess.exitCode = await benchmark({\n setup,\n teardown,\n pre,\n run,\n post,\n data,\n\n warmupCycles,\n minCycles,\n absThreshold,\n relThreshold,\n gcObserver,\n\n durationsSAB,\n controlSAB,\n});\n"],"names":["benchmarkUrl","setupCode","teardownCode","preCode","runCode","postCode","data","warmupCycles","minCycles","absThreshold","relThreshold","gcObserver","durationsSAB","controlSAB","workerData","serialize","code","resolvedBenchmarkUrl","pathToFileURL","process","cwd","href","benchmarkDirUrl","URL","requireFrom","createRequire","fileURLToPath","resolveSpecifier","specifier","startsWith","isAbsolute","resolve","source","imports","Map","createSyntheticModule","moduleExports","exportNames","identifier","mod","SyntheticModule","name","setExport","isCjsModule","target","endsWith","toRequireTarget","loadModule","cached","get","required","Object","keys","includes","push","set","importedModule","loadDynamicModule","status","evaluate","SourceTextModule","initializeImportMeta","meta","url","importModuleDynamically","resolved","link","setup","teardown","pre","run","post","namespace","Error","exitCode","benchmark"],"mappings":"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"}
|
package/build/worker.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { workerData } from 'node:worker_threads';
|
|
2
|
-
import { SourceTextModule, SyntheticModule
|
|
2
|
+
import { SourceTextModule, SyntheticModule } from 'node:vm';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
import { isAbsolute } from 'node:path';
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
@@ -19,7 +19,11 @@ const resolveSpecifier = (specifier)=>{
|
|
|
19
19
|
if (isAbsolute(specifier)) {
|
|
20
20
|
return pathToFileURL(specifier).href;
|
|
21
21
|
}
|
|
22
|
-
|
|
22
|
+
try {
|
|
23
|
+
return requireFrom.resolve(specifier);
|
|
24
|
+
} catch {
|
|
25
|
+
return specifier;
|
|
26
|
+
}
|
|
23
27
|
};
|
|
24
28
|
const source = `
|
|
25
29
|
export const setup = ${serialize(setupCode)};
|
|
@@ -28,11 +32,6 @@ export const pre = ${serialize(preCode)};
|
|
|
28
32
|
export const run = ${serialize(runCode)};
|
|
29
33
|
export const post = ${serialize(postCode)};
|
|
30
34
|
`;
|
|
31
|
-
const globals = Object.create(null);
|
|
32
|
-
for (const k of Object.getOwnPropertyNames(globalThis)){
|
|
33
|
-
globals[k] = globalThis[k];
|
|
34
|
-
}
|
|
35
|
-
const context = createContext(globals);
|
|
36
35
|
const imports = new Map();
|
|
37
36
|
const createSyntheticModule = (moduleExports, exportNames, identifier)=>{
|
|
38
37
|
const mod = new SyntheticModule(exportNames, ()=>{
|
|
@@ -44,8 +43,7 @@ const createSyntheticModule = (moduleExports, exportNames, identifier)=>{
|
|
|
44
43
|
mod.setExport(name, moduleExports[name]);
|
|
45
44
|
}
|
|
46
45
|
}, {
|
|
47
|
-
identifier
|
|
48
|
-
context
|
|
46
|
+
identifier
|
|
49
47
|
});
|
|
50
48
|
return mod;
|
|
51
49
|
};
|
|
@@ -79,7 +77,6 @@ const loadDynamicModule = async (target)=>{
|
|
|
79
77
|
};
|
|
80
78
|
const mod = new SourceTextModule(source, {
|
|
81
79
|
identifier: resolvedBenchmarkUrl,
|
|
82
|
-
context,
|
|
83
80
|
initializeImportMeta (meta) {
|
|
84
81
|
meta.url = resolvedBenchmarkUrl;
|
|
85
82
|
},
|
package/build/worker.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/worker.ts"],"sourcesContent":["import { workerData } from 'node:worker_threads';\nimport { SourceTextModule, SyntheticModule
|
|
1
|
+
{"version":3,"sources":["../src/worker.ts"],"sourcesContent":["import { workerData } from 'node:worker_threads';\nimport { SourceTextModule, SyntheticModule } from 'node:vm';\nimport { createRequire } from 'node:module';\nimport { isAbsolute } from 'node:path';\nimport { fileURLToPath, pathToFileURL } from 'node:url';\nimport { benchmark } from './runner.js';\nimport { WorkerOptions } from './types.js';\n\nconst {\n benchmarkUrl,\n setupCode,\n teardownCode,\n preCode,\n runCode,\n postCode,\n data,\n\n warmupCycles,\n minCycles,\n absThreshold,\n relThreshold,\n gcObserver = true,\n\n durationsSAB,\n controlSAB,\n}: WorkerOptions = workerData;\n\nconst serialize = (code?: string) => (code ? code : 'undefined');\n\nconst resolvedBenchmarkUrl = typeof benchmarkUrl === 'string' ? benchmarkUrl : pathToFileURL(process.cwd()).href;\nconst benchmarkDirUrl = new URL('.', resolvedBenchmarkUrl).href;\nconst requireFrom = createRequire(fileURLToPath(new URL('benchmark.js', benchmarkDirUrl)));\n\nconst resolveSpecifier = (specifier: string) => {\n if (specifier.startsWith('file:')) {\n return specifier;\n }\n if (specifier.startsWith('./') || specifier.startsWith('../')) {\n return new URL(specifier, benchmarkDirUrl).href;\n }\n if (isAbsolute(specifier)) {\n return pathToFileURL(specifier).href;\n }\n try {\n return requireFrom.resolve(specifier);\n } catch {\n return specifier;\n }\n};\n\nconst source = `\nexport const setup = ${serialize(setupCode)};\nexport const teardown = ${serialize(teardownCode)};\nexport const pre = ${serialize(preCode)};\nexport const run = ${serialize(runCode)};\nexport const post = ${serialize(postCode)};\n `;\n\nconst imports = new Map<string, SyntheticModule>();\n\nconst createSyntheticModule = (moduleExports: unknown, exportNames: string[], identifier: string) => {\n const mod = new SyntheticModule(\n exportNames,\n () => {\n for (const name of exportNames) {\n if (name === 'default') {\n mod.setExport(name, moduleExports);\n continue;\n }\n mod.setExport(name, (moduleExports as Record<string, unknown>)[name]);\n }\n },\n { identifier },\n );\n return mod;\n};\n\nconst isCjsModule = (target: string) => target.endsWith('.cjs') || target.endsWith('.cts');\n\nconst toRequireTarget = (target: string) => (target.startsWith('file:') ? fileURLToPath(target) : target);\n\nconst loadModule = async (target: string) => {\n const cached = imports.get(target);\n if (cached) return cached;\n\n if (isCjsModule(target)) {\n const required = requireFrom(toRequireTarget(target));\n const exportNames = required && (typeof required === 'object' || typeof required === 'function') ? Object.keys(required) : [];\n if (!exportNames.includes('default')) {\n exportNames.push('default');\n }\n const mod = createSyntheticModule(required, exportNames, target);\n imports.set(target, mod);\n return mod;\n }\n\n const importedModule = await import(target);\n const exportNames = Object.keys(importedModule);\n const mod = createSyntheticModule(importedModule, exportNames, target);\n imports.set(target, mod);\n return mod;\n};\n\nconst loadDynamicModule = async (target: string) => {\n const mod = await loadModule(target);\n if (mod.status !== 'evaluated') {\n await mod.evaluate();\n }\n return mod;\n};\nconst mod = new SourceTextModule(source, {\n identifier: resolvedBenchmarkUrl,\n initializeImportMeta(meta) {\n meta.url = resolvedBenchmarkUrl;\n },\n importModuleDynamically(specifier) {\n const resolved = resolveSpecifier(specifier);\n return loadDynamicModule(resolved);\n },\n});\n\nawait mod.link(async (specifier) => loadModule(resolveSpecifier(specifier)));\n\nawait mod.evaluate();\nconst { setup, teardown, pre, run, post } = mod.namespace as any;\n\nif (!run) {\n throw new Error('Benchmark run function is required');\n}\n\nprocess.exitCode = await benchmark({\n setup,\n teardown,\n pre,\n run,\n post,\n data,\n\n warmupCycles,\n minCycles,\n absThreshold,\n relThreshold,\n gcObserver,\n\n durationsSAB,\n controlSAB,\n});\n"],"names":["workerData","SourceTextModule","SyntheticModule","createRequire","isAbsolute","fileURLToPath","pathToFileURL","benchmark","benchmarkUrl","setupCode","teardownCode","preCode","runCode","postCode","data","warmupCycles","minCycles","absThreshold","relThreshold","gcObserver","durationsSAB","controlSAB","serialize","code","resolvedBenchmarkUrl","process","cwd","href","benchmarkDirUrl","URL","requireFrom","resolveSpecifier","specifier","startsWith","resolve","source","imports","Map","createSyntheticModule","moduleExports","exportNames","identifier","mod","name","setExport","isCjsModule","target","endsWith","toRequireTarget","loadModule","cached","get","required","Object","keys","includes","push","set","importedModule","loadDynamicModule","status","evaluate","initializeImportMeta","meta","url","importModuleDynamically","resolved","link","setup","teardown","pre","run","post","namespace","Error","exitCode"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { assertNoClosure } from '../utils.js';
|
|
2
|
+
|
|
3
|
+
describe('assertNoClosure', () => {
|
|
4
|
+
describe('allows functions without closures', () => {
|
|
5
|
+
test('arrow with params only', () => {
|
|
6
|
+
expect(() => assertNoClosure('(x) => x * 2', 'run')).not.toThrow();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test('arrow with local variables', () => {
|
|
10
|
+
expect(() => assertNoClosure('(ctx, input) => { const y = ctx.value + input; return y; }', 'run')).not.toThrow();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('function expression', () => {
|
|
14
|
+
expect(() => assertNoClosure('function(x) { return x + 1; }', 'run')).not.toThrow();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('named function expression', () => {
|
|
18
|
+
expect(() => assertNoClosure('function run(x) { return x + 1; }', 'run')).not.toThrow();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('async arrow', () => {
|
|
22
|
+
expect(() => assertNoClosure('async (ctx) => { const r = await fetch("url"); return r; }', 'run')).not.toThrow();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('destructured params', () => {
|
|
26
|
+
expect(() => assertNoClosure('({a, b: c}, [d, ...e]) => a + c + d + e.length', 'run')).not.toThrow();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('nested function declaration', () => {
|
|
30
|
+
expect(() => assertNoClosure('(arr) => { function helper(x) { return x * 2; } return arr.map(helper); }', 'run')).not.toThrow();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('for-of loop variable', () => {
|
|
34
|
+
expect(() => assertNoClosure('(arr) => { let sum = 0; for (const x of arr) sum += x; return sum; }', 'run')).not.toThrow();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('member access on params', () => {
|
|
38
|
+
expect(() => assertNoClosure('(ctx) => ctx.data.map(x => x.value)', 'run')).not.toThrow();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('globals like console, Buffer, Math, Array', () => {
|
|
42
|
+
expect(() => assertNoClosure('(ctx) => { console.log(Math.max(...ctx)); return Buffer.from(Array.of(1)); }', 'run')).not.toThrow();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('try-catch with error binding', () => {
|
|
46
|
+
expect(() => assertNoClosure('(ctx) => { try { return ctx(); } catch (e) { return e; } }', 'run')).not.toThrow();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('class expression', () => {
|
|
50
|
+
expect(() => assertNoClosure('() => { class Foo { bar() { return 1; } } return new Foo(); }', 'run')).not.toThrow();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('label statements', () => {
|
|
54
|
+
expect(() => assertNoClosure('() => { outer: for (let i = 0; i < 10; i++) { break outer; } }', 'run')).not.toThrow();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('detects closures', () => {
|
|
59
|
+
test('single closed-over variable', () => {
|
|
60
|
+
expect(() => assertNoClosure('(x) => x + closedOver', 'run')).toThrow(/closedOver/);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('multiple closed-over variables', () => {
|
|
64
|
+
expect(() => {
|
|
65
|
+
assertNoClosure('(ctx) => sharedData.filter(x => x > threshold)', 'run');
|
|
66
|
+
}).toThrow(/sharedData/);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('closed-over function call', () => {
|
|
70
|
+
expect(() => assertNoClosure('(ctx) => helper(ctx)', 'run')).toThrow(/helper/);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('closed-over array', () => {
|
|
74
|
+
expect(() => assertNoClosure('() => myArray.map(x => x * 2)', 'run')).toThrow(/myArray/);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('computed member access with outer variable', () => {
|
|
78
|
+
expect(() => assertNoClosure('(obj) => obj[key]', 'run')).toThrow(/key/);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('variable used as argument', () => {
|
|
82
|
+
expect(() => assertNoClosure('() => JSON.stringify(config)', 'run')).toThrow(/config/);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('variable in template literal', () => {
|
|
86
|
+
expect(() => assertNoClosure('() => `${prefix}-value`', 'run')).toThrow(/prefix/);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('error message', () => {
|
|
91
|
+
test('includes the function name', () => {
|
|
92
|
+
expect(() => assertNoClosure('() => x', 'setup')).toThrow(/"setup"/);
|
|
93
|
+
expect(() => assertNoClosure('() => x', 'run')).toThrow(/"run"/);
|
|
94
|
+
expect(() => assertNoClosure('() => x', 'teardown')).toThrow(/"teardown"/);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('lists all closed-over variables', () => {
|
|
98
|
+
try {
|
|
99
|
+
assertNoClosure('() => a + b + c', 'run');
|
|
100
|
+
expect.unreachable('should have thrown');
|
|
101
|
+
} catch (e) {
|
|
102
|
+
expect(e.message).toMatch(/\ba\b/);
|
|
103
|
+
expect(e.message).toMatch(/\bb\b/);
|
|
104
|
+
expect(e.message).toMatch(/\bc\b/);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('explains the problem and suggests fix', () => {
|
|
109
|
+
try {
|
|
110
|
+
assertNoClosure('() => x', 'run');
|
|
111
|
+
expect.unreachable('should have thrown');
|
|
112
|
+
} catch (e) {
|
|
113
|
+
expect(e.message).toContain('.toString()');
|
|
114
|
+
expect(e.message).toContain('worker');
|
|
115
|
+
expect(e.message).toContain('setup');
|
|
116
|
+
expect(e.message).toContain('data');
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('edge cases', () => {
|
|
122
|
+
test('silently passes on unparseable code', () => {
|
|
123
|
+
expect(() => assertNoClosure('not valid js {{{', 'run')).not.toThrow();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('empty arrow function', () => {
|
|
127
|
+
expect(() => assertNoClosure('() => {}', 'run')).not.toThrow();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('undefined return', () => {
|
|
131
|
+
expect(() => assertNoClosure('() => undefined', 'run')).not.toThrow();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
});
|
package/src/executor.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { once } from 'node:events';
|
|
|
3
3
|
import { queue } from 'async';
|
|
4
4
|
import { pathToFileURL } from 'node:url';
|
|
5
5
|
import { createReport, Report } from './reporter.js';
|
|
6
|
-
import { cmp } from './utils.js';
|
|
6
|
+
import { cmp, assertNoClosure } from './utils.js';
|
|
7
7
|
import {
|
|
8
8
|
ExecutorRunOptions,
|
|
9
9
|
ReportOptions,
|
|
@@ -21,6 +21,7 @@ export type ExecutorReport<R extends ReportTypeList> = Record<R[number], Report>
|
|
|
21
21
|
count: number;
|
|
22
22
|
heapUsedKB: number;
|
|
23
23
|
dceWarning: boolean;
|
|
24
|
+
error?: string;
|
|
24
25
|
};
|
|
25
26
|
|
|
26
27
|
export interface ExecutorOptions<R extends ReportTypeList> extends BenchmarkOptions, ReportOptions<R> {
|
|
@@ -44,6 +45,12 @@ export const createExecutor = <TContext, TInput, R extends ReportTypeList>(optio
|
|
|
44
45
|
const runCode = run.toString()!;
|
|
45
46
|
const postCode = post?.toString();
|
|
46
47
|
|
|
48
|
+
if (setupCode) assertNoClosure(setupCode, 'setup');
|
|
49
|
+
if (teardownCode) assertNoClosure(teardownCode, 'teardown');
|
|
50
|
+
if (preCode) assertNoClosure(preCode, 'pre');
|
|
51
|
+
assertNoClosure(runCode, 'run');
|
|
52
|
+
if (postCode) assertNoClosure(postCode, 'post');
|
|
53
|
+
|
|
47
54
|
const controlSAB = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * CONTROL_SLOTS);
|
|
48
55
|
const durationsSAB = new SharedArrayBuffer(BigUint64Array.BYTES_PER_ELEMENT * maxCycles);
|
|
49
56
|
|
|
@@ -83,17 +90,18 @@ export const createExecutor = <TContext, TInput, R extends ReportTypeList>(optio
|
|
|
83
90
|
const WORKER_TIMEOUT_MS = 300_000;
|
|
84
91
|
const exitPromise = once(worker, 'exit');
|
|
85
92
|
const timeoutId = setTimeout(() => worker.terminate(), WORKER_TIMEOUT_MS);
|
|
93
|
+
let workerError: string | undefined;
|
|
86
94
|
try {
|
|
87
95
|
const [exitCode] = await exitPromise;
|
|
88
96
|
clearTimeout(timeoutId);
|
|
89
97
|
if (progressIntervalId) clearInterval(progressIntervalId);
|
|
90
98
|
if (exitCode !== 0) {
|
|
91
|
-
|
|
99
|
+
workerError = `worker exited with code ${exitCode}`;
|
|
92
100
|
}
|
|
93
101
|
} catch (err) {
|
|
94
102
|
clearTimeout(timeoutId);
|
|
95
103
|
if (progressIntervalId) clearInterval(progressIntervalId);
|
|
96
|
-
|
|
104
|
+
workerError = err instanceof Error ? err.message : String(err);
|
|
97
105
|
}
|
|
98
106
|
|
|
99
107
|
const count = control[Control.INDEX];
|
|
@@ -118,13 +126,10 @@ export const createExecutor = <TContext, TInput, R extends ReportTypeList>(optio
|
|
|
118
126
|
['count', count],
|
|
119
127
|
['heapUsedKB', heapUsedKB],
|
|
120
128
|
['dceWarning', dceWarning],
|
|
129
|
+
['error', workerError],
|
|
121
130
|
]);
|
|
122
131
|
return Object.fromEntries(report);
|
|
123
132
|
}, workers);
|
|
124
133
|
|
|
125
|
-
executor.error((err) => {
|
|
126
|
-
console.error(err);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
134
|
return executor;
|
|
130
135
|
};
|
package/src/index.ts
CHANGED
|
@@ -7,7 +7,7 @@ declare global {
|
|
|
7
7
|
const benchmark: typeof Benchmark.create;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
export const DEFAULT_WORKERS = cpus().length;
|
|
10
|
+
export const DEFAULT_WORKERS = Math.max(1, Math.ceil(cpus().length / 4));
|
|
11
11
|
|
|
12
12
|
export const AsyncFunction = (async () => {}).constructor;
|
|
13
13
|
const BENCHMARK_URL = Symbol.for('overtake.benchmarkUrl');
|
|
@@ -240,7 +240,11 @@ export const printSimpleReports = <R extends ReportTypeList>(reports: TargetRepo
|
|
|
240
240
|
for (const { measure, feeds } of report.measures) {
|
|
241
241
|
console.group('\n', report.target, measure);
|
|
242
242
|
for (const { feed, data } of feeds) {
|
|
243
|
-
const { count, heapUsedKB, dceWarning, ...metrics } = data as Record<string, unknown>;
|
|
243
|
+
const { count, heapUsedKB, dceWarning, error: benchError, ...metrics } = data as Record<string, unknown>;
|
|
244
|
+
if (benchError) {
|
|
245
|
+
console.log(feed, `\x1b[31m[error: ${benchError}]\x1b[0m`);
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
244
248
|
const output = Object.entries(metrics)
|
|
245
249
|
.map(([key, report]) => `${key}: ${(report as { toString(): string }).toString()}`)
|
|
246
250
|
.join('; ');
|
|
@@ -261,7 +265,12 @@ export const printTableReports = <R extends ReportTypeList>(reports: TargetRepor
|
|
|
261
265
|
console.log('\n', report.target, measure);
|
|
262
266
|
const table: Record<string, unknown> = {};
|
|
263
267
|
for (const { feed, data } of feeds) {
|
|
264
|
-
|
|
268
|
+
const { error: benchError } = data as Record<string, unknown>;
|
|
269
|
+
if (benchError) {
|
|
270
|
+
table[feed] = { error: benchError };
|
|
271
|
+
} else {
|
|
272
|
+
table[feed] = Object.fromEntries(Object.entries(data).map(([key, report]) => [key, report.toString()]));
|
|
273
|
+
}
|
|
265
274
|
}
|
|
266
275
|
console.table(table);
|
|
267
276
|
}
|
|
@@ -274,7 +283,12 @@ export const printJSONReports = <R extends ReportTypeList>(reports: TargetReport
|
|
|
274
283
|
for (const { measure, feeds } of report.measures) {
|
|
275
284
|
const row = {} as Record<string, Record<string, string>>;
|
|
276
285
|
for (const { feed, data } of feeds) {
|
|
277
|
-
|
|
286
|
+
const { error: benchError } = data as Record<string, unknown>;
|
|
287
|
+
if (benchError) {
|
|
288
|
+
row[feed] = { error: String(benchError) };
|
|
289
|
+
} else {
|
|
290
|
+
row[feed] = Object.fromEntries(Object.entries(data).map(([key, report]) => [key, report.toString()]));
|
|
291
|
+
}
|
|
278
292
|
}
|
|
279
293
|
output[`${report.target} ${measure}`] = row;
|
|
280
294
|
}
|
|
@@ -288,7 +302,14 @@ export const printMarkdownReports = <R extends ReportTypeList>(reports: TargetRe
|
|
|
288
302
|
console.log(`\n## ${report.target} - ${measure}\n`);
|
|
289
303
|
if (feeds.length === 0) continue;
|
|
290
304
|
|
|
291
|
-
const
|
|
305
|
+
const firstValid = feeds.find((f) => !(f.data as Record<string, unknown>).error);
|
|
306
|
+
if (!firstValid) {
|
|
307
|
+
for (const { feed, data } of feeds) {
|
|
308
|
+
console.log(`| ${feed} | error: ${(data as Record<string, unknown>).error} |`);
|
|
309
|
+
}
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
const keys = Object.keys(firstValid.data).filter((k) => k !== 'count' && k !== 'error');
|
|
292
313
|
const header = ['Feed', ...keys].join(' | ');
|
|
293
314
|
const separator = ['---', ...keys.map(() => '---')].join(' | ');
|
|
294
315
|
|
|
@@ -296,6 +317,10 @@ export const printMarkdownReports = <R extends ReportTypeList>(reports: TargetRe
|
|
|
296
317
|
console.log(`| ${separator} |`);
|
|
297
318
|
|
|
298
319
|
for (const { feed, data } of feeds) {
|
|
320
|
+
if ((data as Record<string, unknown>).error) {
|
|
321
|
+
console.log(`| ${feed} | error: ${(data as Record<string, unknown>).error} |`);
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
299
324
|
const values = keys.map((k) => (data as Record<string, { toString(): string }>)[k]?.toString() ?? '-');
|
|
300
325
|
console.log(`| ${[feed, ...values].join(' | ')} |`);
|
|
301
326
|
}
|
|
@@ -309,18 +334,26 @@ export const printHistogramReports = <R extends ReportTypeList>(reports: TargetR
|
|
|
309
334
|
console.log(`\n${report.target} - ${measure}\n`);
|
|
310
335
|
|
|
311
336
|
const opsKey = 'ops';
|
|
312
|
-
const values = feeds.map((f) =>
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
337
|
+
const values = feeds.map((f) => {
|
|
338
|
+
const { error: benchError } = f.data as Record<string, unknown>;
|
|
339
|
+
return {
|
|
340
|
+
feed: f.feed,
|
|
341
|
+
value: benchError ? 0 : ((f.data as Record<string, { valueOf(): number }>)[opsKey]?.valueOf() ?? 0),
|
|
342
|
+
error: benchError as string | undefined,
|
|
343
|
+
};
|
|
344
|
+
});
|
|
316
345
|
|
|
317
346
|
const maxValue = Math.max(...values.map((v) => v.value));
|
|
318
347
|
const maxLabelLen = Math.max(...values.map((v) => v.feed.length));
|
|
319
348
|
|
|
320
|
-
for (const { feed, value } of values) {
|
|
349
|
+
for (const { feed, value, error } of values) {
|
|
350
|
+
const label = feed.padEnd(maxLabelLen);
|
|
351
|
+
if (error) {
|
|
352
|
+
console.log(` ${label} | \x1b[31m[error: ${error}]\x1b[0m`);
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
321
355
|
const barLen = maxValue > 0 ? Math.round((value / maxValue) * width) : 0;
|
|
322
356
|
const bar = '\u2588'.repeat(barLen);
|
|
323
|
-
const label = feed.padEnd(maxLabelLen);
|
|
324
357
|
const formatted = value.toLocaleString('en-US', { maximumFractionDigits: 2 });
|
|
325
358
|
console.log(` ${label} | ${bar} ${formatted} ops/s`);
|
|
326
359
|
}
|
|
@@ -339,6 +372,7 @@ export const reportsToBaseline = <R extends ReportTypeList>(reports: TargetRepor
|
|
|
339
372
|
for (const report of reports) {
|
|
340
373
|
for (const { measure, feeds } of report.measures) {
|
|
341
374
|
for (const { feed, data } of feeds) {
|
|
375
|
+
if ((data as Record<string, unknown>).error) continue;
|
|
342
376
|
const key = `${report.target}/${measure}/${feed}`;
|
|
343
377
|
results[key] = {};
|
|
344
378
|
for (const [metric, value] of Object.entries(data)) {
|
|
@@ -367,6 +401,11 @@ export const printComparisonReports = <R extends ReportTypeList>(reports: Target
|
|
|
367
401
|
|
|
368
402
|
console.log(` ${feed}:`);
|
|
369
403
|
|
|
404
|
+
if ((data as Record<string, unknown>).error) {
|
|
405
|
+
console.log(` \x1b[31m[error: ${(data as Record<string, unknown>).error}]\x1b[0m`);
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
|
|
370
409
|
for (const [metric, value] of Object.entries(data)) {
|
|
371
410
|
if (metric === 'count') continue;
|
|
372
411
|
const current = (value as { valueOf(): number }).valueOf();
|