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/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
+ }
@@ -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.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": "./src/index.ts"
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 --declaration --emitDeclarationOnly",
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.ts';
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.ts', {
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.ts');
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.ts';
18
- import { REPORT_TYPES } from './types.ts';
19
- import { resolveHookUrl } from './utils.ts';
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
- 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
@@ -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.ts';
5
- import { cmp, assertNoClosure } from './utils.ts';
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.ts';
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?.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');
@@ -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.ts', import.meta.url);
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 { 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
  };
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.ts';
4
- import { type MaybePromise, type StepFn, type SetupFn, type TeardownFn, type FeedFn, type ReportType, type ReportTypeList, DEFAULT_CYCLES, type ProgressInfo } from './types.ts';
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.ts';
2
- import { type ReportType, DURATION_SCALE } from './types.ts';
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.ts';
3
- import { GCWatcher } from './gc-watcher.ts';
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 = (a * scale) / b;
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
- function collectUnresolved(node: unknown, result: Set<string>) {
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 === 1 && typeof obj.value === 'string') {
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.ts';
7
- import { type WorkerOptions } from './types.ts';
8
- import { resolveHookUrl } from './utils.ts';
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
 
package/tsconfig.json CHANGED
@@ -8,7 +8,6 @@
8
8
  "resolveJsonModule": true,
9
9
  "allowJs": true,
10
10
  "moduleResolution": "NodeNext",
11
- "allowImportingTsExtensions": true,
12
11
  "verbatimModuleSyntax": true,
13
12
  "lib": ["ESNext"],
14
13
  "rootDir": "src",