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 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
- await import(identifier);
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
- const reports = await instance.execute({
101
- ...executeOptions,
102
- [BENCHMARK_URL]: identifier,
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 baselineData = reportsToBaseline(reports);
106
- await writeFile(opts['save-baseline'], JSON.stringify(baselineData, null, 2));
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?.toString();
37
- const teardownCode = teardown?.toString();
38
- const preCode = pre?.toString();
39
- const runCode = run.toString();
40
- const postCode = post?.toString();
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 report = reportTypes
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
- return Object.fromEntries(report);
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,2 @@
1
+ export declare function resolve(specifier: string, context: unknown, nextResolve: (...args: unknown[]) => unknown): Promise<unknown>;
2
+ export declare function load(url: string, context: unknown, nextLoad: (...args: unknown[]) => unknown): Promise<unknown>;
@@ -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
+ }
@@ -1,15 +1,2 @@
1
1
  import { register } from 'node:module';
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
- 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
- else {
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
- 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()}`);
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 = (a * scale) / b;
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
- function collectUnresolved(node, result) {
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 === 1 && typeof obj.value === 'string') {
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.1",
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": ">=24"
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
- await import(identifier);
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
- const reports = await instance.execute({
124
- ...executeOptions,
125
- [BENCHMARK_URL]: identifier,
126
- } as typeof executeOptions);
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 baselineData = reportsToBaseline(reports);
130
- await writeFile(opts['save-baseline'], JSON.stringify(baselineData, null, 2));
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?.toString();
72
- const teardownCode = teardown?.toString();
73
- const preCode = pre?.toString();
74
- const runCode = run.toString();
75
- const postCode = post?.toString();
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 report = reportTypes
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
- return Object.fromEntries(report);
163
+ if (workerError) entries.push(['error', workerError]);
164
+ return Object.fromEntries(entries);
162
165
  };
163
166
 
164
- return { pushAsync, kill() {} };
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
+ }
@@ -1,15 +1,3 @@
1
1
  import { register } from 'node:module';
2
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()}`));
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
- 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
- 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 = (a * scale) / b;
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
- function collectUnresolved(node: unknown, result: Set<string>) {
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 === 1 && typeof obj.value === 'string') {
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