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/src/reporter.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { div, max, divs } from './utils.js';
2
- import { ReportType, DURATION_SCALE, Z95 } from './types.js';
1
+ import { div, max, divs, isqrt } from './utils.js';
2
+ import { ReportType, DURATION_SCALE } from './types.js';
3
3
 
4
4
  const units = [
5
5
  { unit: 'ns', factor: 1 },
@@ -70,6 +70,24 @@ export class Report {
70
70
  }
71
71
  }
72
72
 
73
+ const SQRT_SCALE = 1_000_000n;
74
+ const SQRT_SCALE_SQ = SQRT_SCALE * SQRT_SCALE;
75
+ const Z95_NUM = 196n;
76
+ const Z95_DENOM = 100n;
77
+
78
+ const computeStats = (durations: BigUint64Array) => {
79
+ let sum = 0n;
80
+ for (const d of durations) sum += d;
81
+ const n = BigInt(durations.length);
82
+ const mean = sum / n;
83
+ let ssd = 0n;
84
+ for (const d of durations) {
85
+ const diff = d - mean;
86
+ ssd += diff * diff;
87
+ }
88
+ return { sum, mean, ssd, n };
89
+ };
90
+
73
91
  export const createReport = (durations: BigUint64Array, type: ReportType): Report => {
74
92
  const n = durations.length;
75
93
  if (n === 0) {
@@ -112,11 +130,7 @@ export const createReport = (durations: BigUint64Array, type: ReportType): Repor
112
130
  }
113
131
 
114
132
  case 'ops': {
115
- let sum = 0n;
116
- for (const duration of durations) {
117
- sum += duration;
118
- }
119
- const avgScaled = sum / BigInt(n);
133
+ const { mean: avgScaled, ssd, n: nBig } = computeStats(durations);
120
134
  const nsPerSecScaled = 1_000_000_000n * DURATION_SCALE;
121
135
  const raw = Number(nsPerSecScaled) / Number(avgScaled);
122
136
  const extra = raw < 1 ? Math.ceil(-Math.log10(raw)) : 0;
@@ -126,111 +140,61 @@ export const createReport = (durations: BigUint64Array, type: ReportType): Repor
126
140
  const scale = 10n ** BigInt(exp);
127
141
 
128
142
  const value = avgScaled > 0n ? (nsPerSecScaled * scale) / avgScaled : 0n;
129
- const deviation = durations[n - 1] - durations[0];
130
- const uncertainty = avgScaled > 0 ? Number(div(deviation * scale, 2n * avgScaled)) : 0;
143
+ let uncertainty = 0;
144
+ if (n >= 2 && avgScaled > 0n) {
145
+ const RME_PRECISION = 1_000_000n;
146
+ const semOverMeanSqScaled = (ssd * RME_PRECISION * RME_PRECISION) / (BigInt(n - 1) * nBig * avgScaled * avgScaled);
147
+ const semOverMeanScaled = isqrt(semOverMeanSqScaled);
148
+ uncertainty = Number(Z95_NUM * semOverMeanScaled) / Number(RME_PRECISION);
149
+ }
131
150
  return new Report(type, value, uncertainty, scale);
132
151
  }
133
152
  case 'mean': {
134
- let sum = 0n;
135
- for (const duration of durations) {
136
- sum += duration;
137
- }
153
+ const { sum } = computeStats(durations);
138
154
  const value = divs(sum, BigInt(n), 1n);
139
155
  return new Report(type, value, 0, DURATION_SCALE);
140
156
  }
141
157
 
142
158
  case 'variance': {
143
159
  if (n < 2) return new Report(type, 0n, 0, DURATION_SCALE * DURATION_SCALE);
144
- let sum = 0n;
145
- for (const duration of durations) {
146
- sum += duration;
147
- }
148
- const mean = sum / BigInt(n);
149
- let sumSquaredDiff = 0n;
150
- for (const duration of durations) {
151
- const diff = duration - mean;
152
- sumSquaredDiff += diff * diff;
153
- }
154
- const variance = sumSquaredDiff / BigInt(n - 1);
160
+ const { ssd } = computeStats(durations);
161
+ const variance = ssd / BigInt(n - 1);
155
162
  return new Report(type, variance, 0, DURATION_SCALE * DURATION_SCALE);
156
163
  }
157
164
 
158
165
  case 'sd': {
159
166
  if (n < 2) return new Report(type, 0n, 0, DURATION_SCALE);
160
- let sum = 0n;
161
- for (const duration of durations) {
162
- sum += duration;
163
- }
164
- const mean = sum / BigInt(n);
165
- let sumSquaredDiff = 0n;
166
- for (const duration of durations) {
167
- const diff = duration - mean;
168
- sumSquaredDiff += diff * diff;
169
- }
170
- const variance = Number(sumSquaredDiff) / (n - 1);
171
- const sd = Math.sqrt(variance);
172
- const sdScaled = BigInt(Math.round(sd));
173
- return new Report(type, sdScaled, 0, DURATION_SCALE);
167
+ const { ssd } = computeStats(durations);
168
+ const scaledVariance = (ssd * SQRT_SCALE_SQ) / BigInt(n - 1);
169
+ const sdScaled = isqrt(scaledVariance);
170
+ return new Report(type, sdScaled, 0, DURATION_SCALE * SQRT_SCALE);
174
171
  }
175
172
 
176
173
  case 'sem': {
177
174
  if (n < 2) return new Report(type, 0n, 0, DURATION_SCALE);
178
- let sum = 0n;
179
- for (const duration of durations) {
180
- sum += duration;
181
- }
182
- const mean = sum / BigInt(n);
183
- let sumSquaredDiff = 0n;
184
- for (const duration of durations) {
185
- const diff = duration - mean;
186
- sumSquaredDiff += diff * diff;
187
- }
188
- const variance = Number(sumSquaredDiff) / (n - 1);
189
- const sd = Math.sqrt(variance);
190
- const sem = sd / Math.sqrt(n);
191
- const semScaled = BigInt(Math.round(sem));
192
- return new Report(type, semScaled, 0, DURATION_SCALE);
175
+ const { ssd, n: nBig } = computeStats(durations);
176
+ const semSqScaled = (ssd * SQRT_SCALE_SQ) / (BigInt(n - 1) * nBig);
177
+ const semScaled = isqrt(semSqScaled);
178
+ return new Report(type, semScaled, 0, DURATION_SCALE * SQRT_SCALE);
193
179
  }
194
180
 
195
181
  case 'moe': {
196
182
  if (n < 2) return new Report(type, 0n, 0, DURATION_SCALE);
197
- let sum = 0n;
198
- for (const duration of durations) {
199
- sum += duration;
200
- }
201
- const mean = sum / BigInt(n);
202
- let sumSquaredDiff = 0n;
203
- for (const duration of durations) {
204
- const diff = duration - mean;
205
- sumSquaredDiff += diff * diff;
206
- }
207
- const variance = Number(sumSquaredDiff) / (n - 1);
208
- const sd = Math.sqrt(variance);
209
- const sem = sd / Math.sqrt(n);
210
- const moe = Z95 * sem;
211
- const moeScaled = BigInt(Math.round(moe));
212
- return new Report(type, moeScaled, 0, DURATION_SCALE);
183
+ const { ssd, n: nBig } = computeStats(durations);
184
+ const semSqScaled = (ssd * SQRT_SCALE_SQ) / (BigInt(n - 1) * nBig);
185
+ const semScaled = isqrt(semSqScaled);
186
+ const moeScaled = (Z95_NUM * semScaled) / Z95_DENOM;
187
+ return new Report(type, moeScaled, 0, DURATION_SCALE * SQRT_SCALE);
213
188
  }
214
189
 
215
190
  case 'rme': {
216
191
  if (n < 2) return new Report(type, 0n);
217
- let sum = 0n;
218
- for (const duration of durations) {
219
- sum += duration;
220
- }
221
- const mean = Number(sum) / n;
222
- if (mean === 0) return new Report(type, 0n);
223
- let sumSquaredDiff = 0;
224
- for (const duration of durations) {
225
- const diff = Number(duration) - mean;
226
- sumSquaredDiff += diff * diff;
227
- }
228
- const variance = sumSquaredDiff / (n - 1);
229
- const sd = Math.sqrt(variance);
230
- const sem = sd / Math.sqrt(n);
231
- const moe = Z95 * sem;
232
- const rme = (moe / mean) * 100;
233
- const rmeScaled = BigInt(Math.round(rme * 100));
192
+ const { mean, ssd, n: nBig } = computeStats(durations);
193
+ if (mean === 0n) return new Report(type, 0n);
194
+ const RME_PRECISION = 1_000_000n;
195
+ const semOverMeanSqScaled = (ssd * RME_PRECISION * RME_PRECISION) / (BigInt(n - 1) * nBig * mean * mean);
196
+ const semOverMeanScaled = isqrt(semOverMeanSqScaled);
197
+ const rmeScaled = (Z95_NUM * semOverMeanScaled * 100n) / RME_PRECISION;
234
198
  return new Report(type, rmeScaled, 0, 100n);
235
199
  }
236
200
 
@@ -259,42 +223,22 @@ export const createReport = (durations: BigUint64Array, type: ReportType): Repor
259
223
 
260
224
  case 'ci_lower': {
261
225
  if (n < 2) return new Report(type, 0n, 0, DURATION_SCALE);
262
- let sum = 0n;
263
- for (const duration of durations) {
264
- sum += duration;
265
- }
266
- const mean = Number(sum) / n;
267
- let sumSquaredDiff = 0;
268
- for (const duration of durations) {
269
- const diff = Number(duration) - mean;
270
- sumSquaredDiff += diff * diff;
271
- }
272
- const variance = sumSquaredDiff / (n - 1);
273
- const sd = Math.sqrt(variance);
274
- const sem = sd / Math.sqrt(n);
275
- const moe = Z95 * sem;
276
- const ciLower = Math.max(0, mean - moe);
277
- return new Report(type, BigInt(Math.round(ciLower)), 0, DURATION_SCALE);
226
+ const { mean, ssd, n: nBig } = computeStats(durations);
227
+ const semSqScaled = (ssd * SQRT_SCALE_SQ) / (BigInt(n - 1) * nBig);
228
+ const semScaled = isqrt(semSqScaled);
229
+ const moeScaled = (Z95_NUM * semScaled) / Z95_DENOM;
230
+ const ciLowerScaled = mean * SQRT_SCALE - moeScaled;
231
+ return new Report(type, ciLowerScaled > 0n ? ciLowerScaled : 0n, 0, DURATION_SCALE * SQRT_SCALE);
278
232
  }
279
233
 
280
234
  case 'ci_upper': {
281
235
  if (n < 2) return new Report(type, 0n, 0, DURATION_SCALE);
282
- let sum = 0n;
283
- for (const duration of durations) {
284
- sum += duration;
285
- }
286
- const mean = Number(sum) / n;
287
- let sumSquaredDiff = 0;
288
- for (const duration of durations) {
289
- const diff = Number(duration) - mean;
290
- sumSquaredDiff += diff * diff;
291
- }
292
- const variance = sumSquaredDiff / (n - 1);
293
- const sd = Math.sqrt(variance);
294
- const sem = sd / Math.sqrt(n);
295
- const moe = Z95 * sem;
296
- const ciUpper = mean + moe;
297
- return new Report(type, BigInt(Math.round(ciUpper)), 0, DURATION_SCALE);
236
+ const { mean, ssd, n: nBig } = computeStats(durations);
237
+ const semSqScaled = (ssd * SQRT_SCALE_SQ) / (BigInt(n - 1) * nBig);
238
+ const semScaled = isqrt(semSqScaled);
239
+ const moeScaled = (Z95_NUM * semScaled) / Z95_DENOM;
240
+ const ciUpperScaled = mean * SQRT_SCALE + moeScaled;
241
+ return new Report(type, ciUpperScaled, 0, DURATION_SCALE * SQRT_SCALE);
298
242
  }
299
243
 
300
244
  default: {
package/src/runner.ts CHANGED
@@ -7,7 +7,7 @@ const COMPLETE_VALUE = 100_00;
7
7
 
8
8
  const hr = process.hrtime.bigint.bind(process.hrtime);
9
9
 
10
- const sink = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
10
+ const sink = new Int32Array(1);
11
11
  const consume = (value: unknown) => {
12
12
  let payload = 0;
13
13
  switch (typeof value) {
@@ -32,7 +32,7 @@ const consume = (value: unknown) => {
32
32
  default:
33
33
  payload = -1;
34
34
  }
35
- Atomics.xor(sink, 0, payload);
35
+ sink[0] ^= payload;
36
36
  };
37
37
 
38
38
  const runSync = (run: Function, overhead: bigint) => {
@@ -518,13 +518,23 @@ export const benchmark = async <TContext, TInput>({
518
518
  }
519
519
 
520
520
  let i = 0;
521
- let mean = 0n;
522
- let m2 = 0n;
521
+ const WELFORD_SCALE = 1_000_000n;
522
+ let meanS = 0n;
523
+ let m2S = 0n;
523
524
  const outlierWindow: number[] = [];
524
525
  let skipped = 0;
525
526
  const maxSkipped = maxCycles * 10;
526
527
  let disableFiltering = false;
527
528
 
529
+ const absThScaled = BigInt(Math.round(absThreshold)) * WELFORD_SCALE;
530
+ const absThSq = absThScaled * absThScaled;
531
+ const REL_PRECISION = 1_000_000n;
532
+ const relThBigint = BigInt(Math.round(relThreshold * Number(REL_PRECISION)));
533
+ const relThSq = relThBigint * relThBigint;
534
+ const relPrecSq = REL_PRECISION * REL_PRECISION;
535
+ const Z95_SQ_NUM = 38416n;
536
+ const Z95_SQ_DENOM = 10000n;
537
+
528
538
  while (true) {
529
539
  if (i >= maxCycles) break;
530
540
  if (!disableFiltering && skipped >= maxSkipped) {
@@ -583,26 +593,28 @@ export const benchmark = async <TContext, TInput>({
583
593
  }
584
594
 
585
595
  const durationNumber = Number(sampleDuration);
586
- pushWindow(outlierWindow, durationNumber, OUTLIER_WINDOW);
587
596
  if (!disableFiltering) {
588
597
  const { median, iqr } = medianAndIqr(outlierWindow);
598
+ pushWindow(outlierWindow, durationNumber, OUTLIER_WINDOW);
589
599
  const maxAllowed = median + OUTLIER_IQR_MULTIPLIER * iqr || Number.POSITIVE_INFINITY;
590
600
  if (outlierWindow.length >= 8 && durationNumber > maxAllowed && durationNumber - median > OUTLIER_ABS_THRESHOLD) {
591
601
  skipped++;
592
602
  continue;
593
603
  }
594
604
 
595
- const meanNumber = Number(mean);
605
+ const meanNumber = Number(meanS / WELFORD_SCALE);
596
606
  if (i >= 8 && meanNumber > 0 && durationNumber > OUTLIER_MULTIPLIER * meanNumber && durationNumber - meanNumber > OUTLIER_ABS_THRESHOLD) {
597
607
  skipped++;
598
608
  continue;
599
609
  }
610
+ } else {
611
+ pushWindow(outlierWindow, durationNumber, OUTLIER_WINDOW);
600
612
  }
601
613
 
602
614
  durations[i++] = sampleDuration;
603
- const delta = sampleDuration - mean;
604
- mean += delta / BigInt(i);
605
- m2 += delta * (sampleDuration - mean);
615
+ const deltaS = sampleDuration * WELFORD_SCALE - meanS;
616
+ meanS += deltaS / BigInt(i);
617
+ m2S += deltaS * (sampleDuration * WELFORD_SCALE - meanS);
606
618
 
607
619
  const progress = (i / maxCycles) * COMPLETE_VALUE;
608
620
  if (i % PROGRESS_STRIDE === 0) {
@@ -610,17 +622,11 @@ export const benchmark = async <TContext, TInput>({
610
622
  }
611
623
 
612
624
  if (i >= minCycles) {
613
- const variance = Number(m2) / (i - 1);
614
- const stddev = Math.sqrt(variance);
615
- if (stddev <= Number(absThreshold)) {
616
- break;
617
- }
618
-
619
- const meanNum = Number(mean);
620
- const cov = stddev / (meanNum || 1);
621
- if (cov <= relThreshold) {
622
- break;
623
- }
625
+ if (m2S <= absThSq * BigInt(i - 1)) break;
626
+ // RME convergence: Z95 * sem/mean <= relThreshold
627
+ // Z95^2 * m2S / (n*(n-1)*meanS^2) <= relThreshold^2
628
+ const ni = BigInt(i);
629
+ if (meanS !== 0n && Z95_SQ_NUM * m2S * relPrecSq <= relThSq * ni * (ni - 1n) * meanS * meanS * Z95_SQ_DENOM) break;
624
630
  }
625
631
  }
626
632
 
package/src/types.ts CHANGED
@@ -107,7 +107,7 @@ export enum Control {
107
107
  }
108
108
 
109
109
  export const CONTROL_SLOTS = Object.values(Control).length / 2;
110
- export const DEFAULT_CYCLES = 1_000;
110
+ export const DEFAULT_CYCLES = 10_000;
111
111
  export const Z95 = 1.96;
112
112
  export const DURATION_SCALE = 1000n;
113
113
  export const COMPLETE_VALUE = 100_00;
package/src/utils.ts CHANGED
@@ -1,4 +1,16 @@
1
- import { transform } from '@swc/core';
1
+ import { transform, parseSync } from '@swc/core';
2
+
3
+ export const isqrt = (n: bigint): bigint => {
4
+ if (n < 0n) throw new RangeError('Square root of negative');
5
+ if (n < 2n) return n;
6
+ let x = n;
7
+ let y = (x + 1n) >> 1n;
8
+ while (y < x) {
9
+ x = y;
10
+ y = (x + n / x) >> 1n;
11
+ }
12
+ return x;
13
+ };
2
14
 
3
15
  export const abs = (value: bigint) => {
4
16
  if (value < 0n) {
@@ -66,6 +78,49 @@ export class ScaledBigInt {
66
78
  }
67
79
  }
68
80
 
81
+ const KNOWN_GLOBALS = new Set(Object.getOwnPropertyNames(globalThis));
82
+ KNOWN_GLOBALS.add('arguments');
83
+
84
+ function collectUnresolved(node: unknown, result: Set<string>) {
85
+ if (!node || typeof node !== 'object') return;
86
+ if (Array.isArray(node)) {
87
+ for (const item of node) collectUnresolved(item, result);
88
+ return;
89
+ }
90
+ const obj = node as Record<string, unknown>;
91
+ if (obj.type === 'Identifier' && obj.ctxt === 1 && typeof obj.value === 'string') {
92
+ result.add(obj.value);
93
+ }
94
+ for (const key of Object.keys(obj)) {
95
+ if (key === 'span') continue;
96
+ collectUnresolved(obj[key], result);
97
+ }
98
+ }
99
+
100
+ export function assertNoClosure(code: string, name: string): void {
101
+ let ast;
102
+ try {
103
+ ast = parseSync(`var __fn = ${code}`, { syntax: 'ecmascript', target: 'esnext' });
104
+ } catch {
105
+ return;
106
+ }
107
+ const unresolved = new Set<string>();
108
+ collectUnresolved(ast, unresolved);
109
+ for (const g of KNOWN_GLOBALS) unresolved.delete(g);
110
+ if (unresolved.size === 0) return;
111
+
112
+ const vars = [...unresolved].join(', ');
113
+ throw new Error(
114
+ `Benchmark "${name}" function references outer-scope variables: ${vars}\n\n` +
115
+ `Benchmark functions are serialized with .toString() and executed in an isolated\n` +
116
+ `worker thread. Closed-over variables from the original module scope are not\n` +
117
+ `available in the worker and will cause a ReferenceError at runtime.\n\n` +
118
+ `To fix this, move the referenced values into:\n` +
119
+ ` - "setup" function (returned value becomes the first argument of run/pre/post)\n` +
120
+ ` - "data" option (passed as the second argument of run/pre/post)`,
121
+ );
122
+ }
123
+
69
124
  export const transpile = async (code: string): Promise<string> => {
70
125
  const output = await transform(code, {
71
126
  filename: 'benchmark.ts',
package/src/worker.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { workerData } from 'node:worker_threads';
2
- import { SourceTextModule, SyntheticModule, createContext } from 'node:vm';
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';
@@ -41,7 +41,11 @@ const resolveSpecifier = (specifier: string) => {
41
41
  if (isAbsolute(specifier)) {
42
42
  return pathToFileURL(specifier).href;
43
43
  }
44
- return requireFrom.resolve(specifier);
44
+ try {
45
+ return requireFrom.resolve(specifier);
46
+ } catch {
47
+ return specifier;
48
+ }
45
49
  };
46
50
 
47
51
  const source = `
@@ -52,11 +56,6 @@ export const run = ${serialize(runCode)};
52
56
  export const post = ${serialize(postCode)};
53
57
  `;
54
58
 
55
- const globals = Object.create(null);
56
- for (const k of Object.getOwnPropertyNames(globalThis)) {
57
- globals[k] = (globalThis as any)[k];
58
- }
59
- const context = createContext(globals);
60
59
  const imports = new Map<string, SyntheticModule>();
61
60
 
62
61
  const createSyntheticModule = (moduleExports: unknown, exportNames: string[], identifier: string) => {
@@ -71,7 +70,7 @@ const createSyntheticModule = (moduleExports: unknown, exportNames: string[], id
71
70
  mod.setExport(name, (moduleExports as Record<string, unknown>)[name]);
72
71
  }
73
72
  },
74
- { identifier, context },
73
+ { identifier },
75
74
  );
76
75
  return mod;
77
76
  };
@@ -111,7 +110,6 @@ const loadDynamicModule = async (target: string) => {
111
110
  };
112
111
  const mod = new SourceTextModule(source, {
113
112
  identifier: resolvedBenchmarkUrl,
114
- context,
115
113
  initializeImportMeta(meta) {
116
114
  meta.url = resolvedBenchmarkUrl;
117
115
  },