overtake 1.3.2 → 2.0.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.
Files changed (56) hide show
  1. package/README.md +12 -15
  2. package/bin/overtake.js +1 -1
  3. package/build/executor.d.ts +8 -3
  4. package/build/index.d.ts +10 -11
  5. package/build/reporter.d.ts +10 -2
  6. package/build/runner.d.ts +1 -1
  7. package/build/types.d.ts +7 -7
  8. package/build/utils.d.ts +3 -17
  9. package/package.json +8 -26
  10. package/src/__tests__/assert-no-closure.ts +135 -0
  11. package/src/__tests__/benchmark-execute.ts +48 -0
  12. package/src/cli.ts +139 -144
  13. package/src/executor.ts +59 -24
  14. package/src/index.ts +135 -68
  15. package/src/reporter.ts +77 -125
  16. package/src/runner.ts +28 -25
  17. package/src/types.ts +9 -9
  18. package/src/utils.ts +62 -46
  19. package/src/worker.ts +13 -12
  20. package/tsconfig.json +3 -1
  21. package/build/cli.cjs +0 -179
  22. package/build/cli.cjs.map +0 -1
  23. package/build/cli.js +0 -134
  24. package/build/cli.js.map +0 -1
  25. package/build/executor.cjs +0 -116
  26. package/build/executor.cjs.map +0 -1
  27. package/build/executor.js +0 -106
  28. package/build/executor.js.map +0 -1
  29. package/build/gc-watcher.cjs +0 -30
  30. package/build/gc-watcher.cjs.map +0 -1
  31. package/build/gc-watcher.js +0 -20
  32. package/build/gc-watcher.js.map +0 -1
  33. package/build/index.cjs +0 -400
  34. package/build/index.cjs.map +0 -1
  35. package/build/index.js +0 -335
  36. package/build/index.js.map +0 -1
  37. package/build/reporter.cjs +0 -364
  38. package/build/reporter.cjs.map +0 -1
  39. package/build/reporter.js +0 -346
  40. package/build/reporter.js.map +0 -1
  41. package/build/runner.cjs +0 -528
  42. package/build/runner.cjs.map +0 -1
  43. package/build/runner.js +0 -518
  44. package/build/runner.js.map +0 -1
  45. package/build/types.cjs +0 -66
  46. package/build/types.cjs.map +0 -1
  47. package/build/types.js +0 -33
  48. package/build/types.js.map +0 -1
  49. package/build/utils.cjs +0 -121
  50. package/build/utils.cjs.map +0 -1
  51. package/build/utils.js +0 -85
  52. package/build/utils.js.map +0 -1
  53. package/build/worker.cjs +0 -158
  54. package/build/worker.cjs.map +0 -1
  55. package/build/worker.js +0 -113
  56. package/build/worker.js.map +0 -1
package/src/index.ts CHANGED
@@ -1,15 +1,14 @@
1
1
  import { cpus } from 'node:os';
2
2
  import Progress from 'progress';
3
- import { createExecutor, ExecutorOptions, ExecutorReport } from './executor.js';
4
- import { MaybePromise, StepFn, SetupFn, TeardownFn, FeedFn, ReportType, ReportTypeList, DEFAULT_CYCLES, ProgressInfo } from './types.js';
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';
5
5
 
6
6
  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
- export const AsyncFunction = (async () => {}).constructor;
13
12
  const BENCHMARK_URL = Symbol.for('overtake.benchmarkUrl');
14
13
 
15
14
  export interface TargetReport<R extends ReportTypeList> {
@@ -30,14 +29,24 @@ export interface FeedReport<R extends ReportTypeList> {
30
29
  export const DEFAULT_REPORT_TYPES = ['ops'] as const;
31
30
  export type DefaultReportTypes = (typeof DEFAULT_REPORT_TYPES)[number];
32
31
 
33
- export class MeasureContext<TContext, TInput> {
34
- public pre?: StepFn<TContext, TInput>;
35
- public post?: StepFn<TContext, TInput>;
32
+ const createExecutorErrorReport = <R extends ReportTypeList>(error: unknown): ExecutorReport<R> =>
33
+ ({
34
+ count: 0,
35
+ heapUsedKB: 0,
36
+ dceWarning: false,
37
+ error: error instanceof Error ? error.message : String(error),
38
+ }) as ExecutorReport<R>;
36
39
 
37
- constructor(
38
- public title: string,
39
- public run: StepFn<TContext, TInput>,
40
- ) {}
40
+ export class MeasureContext<TContext, TInput> {
41
+ pre?: StepFn<TContext, TInput>;
42
+ post?: StepFn<TContext, TInput>;
43
+ title: string;
44
+ run: StepFn<TContext, TInput>;
45
+
46
+ constructor(title: string, run: StepFn<TContext, TInput>) {
47
+ this.title = title;
48
+ this.run = run;
49
+ }
41
50
  }
42
51
 
43
52
  export class Measure<TContext, TInput> {
@@ -58,13 +67,15 @@ export class Measure<TContext, TInput> {
58
67
  }
59
68
 
60
69
  export class TargetContext<TContext, TInput> {
61
- public teardown?: TeardownFn<TContext>;
62
- public measures: MeasureContext<TContext, TInput>[] = [];
63
-
64
- constructor(
65
- readonly title: string,
66
- readonly setup?: SetupFn<MaybePromise<TContext>>,
67
- ) {}
70
+ teardown?: TeardownFn<TContext>;
71
+ measures: MeasureContext<TContext, TInput>[] = [];
72
+ readonly title: string;
73
+ readonly setup?: SetupFn<MaybePromise<TContext>>;
74
+
75
+ constructor(title: string, setup?: SetupFn<MaybePromise<TContext>>) {
76
+ this.title = title;
77
+ this.setup = setup;
78
+ }
68
79
  }
69
80
 
70
81
  export class Target<TContext, TInput> {
@@ -87,10 +98,13 @@ export class Target<TContext, TInput> {
87
98
  }
88
99
 
89
100
  export class FeedContext<TInput> {
90
- constructor(
91
- readonly title: string,
92
- readonly fn?: FeedFn<TInput>,
93
- ) {}
101
+ readonly title: string;
102
+ readonly fn?: FeedFn<TInput>;
103
+
104
+ constructor(title: string, fn?: FeedFn<TInput>) {
105
+ this.title = title;
106
+ this.fn = fn;
107
+ }
94
108
  }
95
109
 
96
110
  export class Benchmark<TInput> {
@@ -136,7 +150,7 @@ export class Benchmark<TInput> {
136
150
  return new Target<TContext, TInput>(target);
137
151
  }
138
152
 
139
- async execute<R extends readonly ReportType[] = typeof DEFAULT_REPORT_TYPES>(options: ExecutorOptions<R> & { progress?: boolean }): Promise<TargetReport<R>[]> {
153
+ async execute<R extends readonly ReportType[] = typeof DEFAULT_REPORT_TYPES>(options: Partial<ExecutorOptions<R>> & { progress?: boolean } = {}): Promise<TargetReport<R>[]> {
140
154
  const {
141
155
  workers = DEFAULT_WORKERS,
142
156
  warmupCycles = 20,
@@ -193,45 +207,59 @@ export class Benchmark<TInput> {
193
207
  } as Required<ExecutorOptions<R>>);
194
208
 
195
209
  const reports: TargetReport<R>[] = [];
196
- for (const target of this.#targets) {
197
- const targetReport: TargetReport<R> = { target: target.title, measures: [] };
198
- for (const measure of target.measures) {
199
- const measureReport: MeasureReport<R> = { measure: measure.title, feeds: [] };
200
- for (const feed of this.#feeds) {
201
- const id = `${target.title}/${measure.title}/${feed.title}`;
202
- const data = await feed.fn?.();
203
- executor
204
- .push<ExecutorReport<R>>({
205
- id,
206
- setup: target.setup,
207
- teardown: target.teardown,
208
- pre: measure.pre,
209
- run: measure.run,
210
- post: measure.post,
211
- data,
212
- })
213
- .then((data) => {
214
- progressMap.delete(id);
215
- completedBenchmarks++;
216
- measureReport.feeds.push({
217
- feed: feed.title,
218
- data,
219
- });
220
- });
210
+ const pendingReports: Promise<void>[] = [];
211
+
212
+ try {
213
+ const feedData = await Promise.all(this.#feeds.map(async (feed) => ({ title: feed.title, data: await feed.fn?.() })));
214
+ for (const target of this.#targets) {
215
+ const targetReport: TargetReport<R> = { target: target.title, measures: [] };
216
+ for (const measure of target.measures) {
217
+ const measureReport: MeasureReport<R> = { measure: measure.title, feeds: [] };
218
+ for (const feed of feedData) {
219
+ const id = `${target.title}/${measure.title}/${feed.title}`;
220
+ const feedReport: FeedReport<R> = {
221
+ feed: feed.title,
222
+ data: createExecutorErrorReport<R>('Benchmark did not produce a report'),
223
+ };
224
+
225
+ measureReport.feeds.push(feedReport);
226
+ pendingReports.push(
227
+ (async () => {
228
+ try {
229
+ feedReport.data = await executor.pushAsync<ExecutorReport<R>>({
230
+ id,
231
+ setup: target.setup,
232
+ teardown: target.teardown,
233
+ pre: measure.pre,
234
+ run: measure.run,
235
+ post: measure.post,
236
+ data: feed.data,
237
+ });
238
+ } catch (error) {
239
+ feedReport.data = createExecutorErrorReport<R>(error);
240
+ } finally {
241
+ progressMap.delete(id);
242
+ completedBenchmarks++;
243
+ }
244
+ })(),
245
+ );
246
+ }
247
+ targetReport.measures.push(measureReport);
221
248
  }
222
- targetReport.measures.push(measureReport);
249
+ reports.push(targetReport);
223
250
  }
224
- reports.push(targetReport);
225
- }
226
- await executor.drain();
227
- executor.kill();
228
251
 
229
- if (bar) {
230
- bar.update(1, { label: 'done' });
231
- bar.terminate();
232
- }
252
+ await Promise.all(pendingReports);
233
253
 
234
- return reports;
254
+ if (bar) {
255
+ bar.update(1, { label: 'done' });
256
+ bar.terminate();
257
+ }
258
+
259
+ return reports;
260
+ } finally {
261
+ executor.kill();
262
+ }
235
263
  }
236
264
  }
237
265
 
@@ -240,7 +268,11 @@ export const printSimpleReports = <R extends ReportTypeList>(reports: TargetRepo
240
268
  for (const { measure, feeds } of report.measures) {
241
269
  console.group('\n', report.target, measure);
242
270
  for (const { feed, data } of feeds) {
243
- const { count, heapUsedKB, dceWarning, ...metrics } = data as Record<string, unknown>;
271
+ const { count, heapUsedKB, dceWarning, error: benchError, ...metrics } = data as Record<string, unknown>;
272
+ if (benchError) {
273
+ console.log(feed, `\x1b[31m[error: ${benchError}]\x1b[0m`);
274
+ continue;
275
+ }
244
276
  const output = Object.entries(metrics)
245
277
  .map(([key, report]) => `${key}: ${(report as { toString(): string }).toString()}`)
246
278
  .join('; ');
@@ -261,7 +293,12 @@ export const printTableReports = <R extends ReportTypeList>(reports: TargetRepor
261
293
  console.log('\n', report.target, measure);
262
294
  const table: Record<string, unknown> = {};
263
295
  for (const { feed, data } of feeds) {
264
- table[feed] = Object.fromEntries(Object.entries(data).map(([key, report]) => [key, report.toString()]));
296
+ const { error: benchError } = data as Record<string, unknown>;
297
+ if (benchError) {
298
+ table[feed] = { error: benchError };
299
+ } else {
300
+ table[feed] = Object.fromEntries(Object.entries(data).map(([key, report]) => [key, report.toString()]));
301
+ }
265
302
  }
266
303
  console.table(table);
267
304
  }
@@ -274,7 +311,12 @@ export const printJSONReports = <R extends ReportTypeList>(reports: TargetReport
274
311
  for (const { measure, feeds } of report.measures) {
275
312
  const row = {} as Record<string, Record<string, string>>;
276
313
  for (const { feed, data } of feeds) {
277
- row[feed] = Object.fromEntries(Object.entries(data).map(([key, report]) => [key, report.toString()]));
314
+ const { error: benchError } = data as Record<string, unknown>;
315
+ if (benchError) {
316
+ row[feed] = { error: String(benchError) };
317
+ } else {
318
+ row[feed] = Object.fromEntries(Object.entries(data).map(([key, report]) => [key, report.toString()]));
319
+ }
278
320
  }
279
321
  output[`${report.target} ${measure}`] = row;
280
322
  }
@@ -288,7 +330,14 @@ export const printMarkdownReports = <R extends ReportTypeList>(reports: TargetRe
288
330
  console.log(`\n## ${report.target} - ${measure}\n`);
289
331
  if (feeds.length === 0) continue;
290
332
 
291
- const keys = Object.keys(feeds[0].data).filter((k) => k !== 'count');
333
+ const firstValid = feeds.find((f) => !(f.data as Record<string, unknown>).error);
334
+ if (!firstValid) {
335
+ for (const { feed, data } of feeds) {
336
+ console.log(`| ${feed} | error: ${(data as Record<string, unknown>).error} |`);
337
+ }
338
+ continue;
339
+ }
340
+ const keys = Object.keys(firstValid.data).filter((k) => k !== 'count' && k !== 'error');
292
341
  const header = ['Feed', ...keys].join(' | ');
293
342
  const separator = ['---', ...keys.map(() => '---')].join(' | ');
294
343
 
@@ -296,6 +345,10 @@ export const printMarkdownReports = <R extends ReportTypeList>(reports: TargetRe
296
345
  console.log(`| ${separator} |`);
297
346
 
298
347
  for (const { feed, data } of feeds) {
348
+ if ((data as Record<string, unknown>).error) {
349
+ console.log(`| ${feed} | error: ${(data as Record<string, unknown>).error} |`);
350
+ continue;
351
+ }
299
352
  const values = keys.map((k) => (data as Record<string, { toString(): string }>)[k]?.toString() ?? '-');
300
353
  console.log(`| ${[feed, ...values].join(' | ')} |`);
301
354
  }
@@ -309,18 +362,26 @@ export const printHistogramReports = <R extends ReportTypeList>(reports: TargetR
309
362
  console.log(`\n${report.target} - ${measure}\n`);
310
363
 
311
364
  const opsKey = 'ops';
312
- const values = feeds.map((f) => ({
313
- feed: f.feed,
314
- value: (f.data as Record<string, { valueOf(): number }>)[opsKey]?.valueOf() ?? 0,
315
- }));
365
+ const values = feeds.map((f) => {
366
+ const { error: benchError } = f.data as Record<string, unknown>;
367
+ return {
368
+ feed: f.feed,
369
+ value: benchError ? 0 : ((f.data as Record<string, { valueOf(): number }>)[opsKey]?.valueOf() ?? 0),
370
+ error: benchError as string | undefined,
371
+ };
372
+ });
316
373
 
317
374
  const maxValue = Math.max(...values.map((v) => v.value));
318
375
  const maxLabelLen = Math.max(...values.map((v) => v.feed.length));
319
376
 
320
- for (const { feed, value } of values) {
377
+ for (const { feed, value, error } of values) {
378
+ const label = feed.padEnd(maxLabelLen);
379
+ if (error) {
380
+ console.log(` ${label} | \x1b[31m[error: ${error}]\x1b[0m`);
381
+ continue;
382
+ }
321
383
  const barLen = maxValue > 0 ? Math.round((value / maxValue) * width) : 0;
322
384
  const bar = '\u2588'.repeat(barLen);
323
- const label = feed.padEnd(maxLabelLen);
324
385
  const formatted = value.toLocaleString('en-US', { maximumFractionDigits: 2 });
325
386
  console.log(` ${label} | ${bar} ${formatted} ops/s`);
326
387
  }
@@ -339,6 +400,7 @@ export const reportsToBaseline = <R extends ReportTypeList>(reports: TargetRepor
339
400
  for (const report of reports) {
340
401
  for (const { measure, feeds } of report.measures) {
341
402
  for (const { feed, data } of feeds) {
403
+ if ((data as Record<string, unknown>).error) continue;
342
404
  const key = `${report.target}/${measure}/${feed}`;
343
405
  results[key] = {};
344
406
  for (const [metric, value] of Object.entries(data)) {
@@ -367,6 +429,11 @@ export const printComparisonReports = <R extends ReportTypeList>(reports: Target
367
429
 
368
430
  console.log(` ${feed}:`);
369
431
 
432
+ if ((data as Record<string, unknown>).error) {
433
+ console.log(` \x1b[31m[error: ${(data as Record<string, unknown>).error}]\x1b[0m`);
434
+ continue;
435
+ }
436
+
370
437
  for (const [metric, value] of Object.entries(data)) {
371
438
  if (metric === 'count') continue;
372
439
  const current = (value as { valueOf(): number }).valueOf();
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.ts';
2
+ import { type ReportType, DURATION_SCALE } from './types.ts';
3
3
 
4
4
  const units = [
5
5
  { unit: 'ns', factor: 1 },
@@ -18,12 +18,17 @@ function smartFixed(n: number): string {
18
18
  });
19
19
  }
20
20
  export class Report {
21
- constructor(
22
- public readonly type: ReportType,
23
- public readonly value: bigint,
24
- public readonly uncertainty: number = 0,
25
- public readonly scale: bigint = 1n,
26
- ) {}
21
+ readonly type: ReportType;
22
+ readonly value: bigint;
23
+ readonly uncertainty: number;
24
+ readonly scale: bigint;
25
+
26
+ constructor(type: ReportType, value: bigint, uncertainty: number = 0, scale: bigint = 1n) {
27
+ this.type = type;
28
+ this.value = value;
29
+ this.uncertainty = uncertainty;
30
+ this.scale = scale;
31
+ }
27
32
  valueOf() {
28
33
  return Number(div(this.value, this.scale));
29
34
  }
@@ -70,11 +75,32 @@ export class Report {
70
75
  }
71
76
  }
72
77
 
73
- export const createReport = (durations: BigUint64Array, type: ReportType): Report => {
78
+ const SQRT_SCALE = 1_000_000n;
79
+ const SQRT_SCALE_SQ = SQRT_SCALE * SQRT_SCALE;
80
+ const Z95_NUM = 196n;
81
+ const Z95_DENOM = 100n;
82
+
83
+ type Stats = { sum: bigint; mean: bigint; ssd: bigint; n: bigint };
84
+
85
+ export const computeStats = (durations: BigUint64Array): Stats => {
86
+ let sum = 0n;
87
+ for (const d of durations) sum += d;
88
+ const n = BigInt(durations.length);
89
+ const mean = sum / n;
90
+ let ssd = 0n;
91
+ for (const d of durations) {
92
+ const diff = d - mean;
93
+ ssd += diff * diff;
94
+ }
95
+ return { sum, mean, ssd, n };
96
+ };
97
+
98
+ export const createReport = (durations: BigUint64Array, type: ReportType, stats?: Stats): Report => {
74
99
  const n = durations.length;
75
100
  if (n === 0) {
76
101
  return new Report(type, 0n);
77
102
  }
103
+ const st = stats ?? computeStats(durations);
78
104
  switch (type) {
79
105
  case 'min': {
80
106
  return new Report(type, durations[0], 0, DURATION_SCALE);
@@ -112,11 +138,7 @@ export const createReport = (durations: BigUint64Array, type: ReportType): Repor
112
138
  }
113
139
 
114
140
  case 'ops': {
115
- let sum = 0n;
116
- for (const duration of durations) {
117
- sum += duration;
118
- }
119
- const avgScaled = sum / BigInt(n);
141
+ const { mean: avgScaled, ssd, n: nBig } = st;
120
142
  const nsPerSecScaled = 1_000_000_000n * DURATION_SCALE;
121
143
  const raw = Number(nsPerSecScaled) / Number(avgScaled);
122
144
  const extra = raw < 1 ? Math.ceil(-Math.log10(raw)) : 0;
@@ -126,111 +148,61 @@ export const createReport = (durations: BigUint64Array, type: ReportType): Repor
126
148
  const scale = 10n ** BigInt(exp);
127
149
 
128
150
  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;
151
+ let uncertainty = 0;
152
+ if (n >= 2 && avgScaled > 0n) {
153
+ const RME_PRECISION = 1_000_000n;
154
+ const semOverMeanSqScaled = (ssd * RME_PRECISION * RME_PRECISION) / (BigInt(n - 1) * nBig * avgScaled * avgScaled);
155
+ const semOverMeanScaled = isqrt(semOverMeanSqScaled);
156
+ uncertainty = Number(Z95_NUM * semOverMeanScaled) / Number(RME_PRECISION);
157
+ }
131
158
  return new Report(type, value, uncertainty, scale);
132
159
  }
133
160
  case 'mean': {
134
- let sum = 0n;
135
- for (const duration of durations) {
136
- sum += duration;
137
- }
161
+ const { sum } = st;
138
162
  const value = divs(sum, BigInt(n), 1n);
139
163
  return new Report(type, value, 0, DURATION_SCALE);
140
164
  }
141
165
 
142
166
  case 'variance': {
143
167
  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);
168
+ const { ssd } = st;
169
+ const variance = ssd / BigInt(n - 1);
155
170
  return new Report(type, variance, 0, DURATION_SCALE * DURATION_SCALE);
156
171
  }
157
172
 
158
173
  case 'sd': {
159
174
  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);
175
+ const { ssd } = st;
176
+ const scaledVariance = (ssd * SQRT_SCALE_SQ) / BigInt(n - 1);
177
+ const sdScaled = isqrt(scaledVariance);
178
+ return new Report(type, sdScaled, 0, DURATION_SCALE * SQRT_SCALE);
174
179
  }
175
180
 
176
181
  case 'sem': {
177
182
  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);
183
+ const { ssd, n: nBig } = st;
184
+ const semSqScaled = (ssd * SQRT_SCALE_SQ) / (BigInt(n - 1) * nBig);
185
+ const semScaled = isqrt(semSqScaled);
186
+ return new Report(type, semScaled, 0, DURATION_SCALE * SQRT_SCALE);
193
187
  }
194
188
 
195
189
  case 'moe': {
196
190
  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);
191
+ const { ssd, n: nBig } = st;
192
+ const semSqScaled = (ssd * SQRT_SCALE_SQ) / (BigInt(n - 1) * nBig);
193
+ const semScaled = isqrt(semSqScaled);
194
+ const moeScaled = (Z95_NUM * semScaled) / Z95_DENOM;
195
+ return new Report(type, moeScaled, 0, DURATION_SCALE * SQRT_SCALE);
213
196
  }
214
197
 
215
198
  case 'rme': {
216
199
  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));
200
+ const { mean, ssd, n: nBig } = st;
201
+ if (mean === 0n) return new Report(type, 0n);
202
+ const RME_PRECISION = 1_000_000n;
203
+ const semOverMeanSqScaled = (ssd * RME_PRECISION * RME_PRECISION) / (BigInt(n - 1) * nBig * mean * mean);
204
+ const semOverMeanScaled = isqrt(semOverMeanSqScaled);
205
+ const rmeScaled = (Z95_NUM * semOverMeanScaled * 100n) / RME_PRECISION;
234
206
  return new Report(type, rmeScaled, 0, 100n);
235
207
  }
236
208
 
@@ -259,42 +231,22 @@ export const createReport = (durations: BigUint64Array, type: ReportType): Repor
259
231
 
260
232
  case 'ci_lower': {
261
233
  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);
234
+ const { mean, ssd, n: nBig } = st;
235
+ const semSqScaled = (ssd * SQRT_SCALE_SQ) / (BigInt(n - 1) * nBig);
236
+ const semScaled = isqrt(semSqScaled);
237
+ const moeScaled = (Z95_NUM * semScaled) / Z95_DENOM;
238
+ const ciLowerScaled = mean * SQRT_SCALE - moeScaled;
239
+ return new Report(type, ciLowerScaled > 0n ? ciLowerScaled : 0n, 0, DURATION_SCALE * SQRT_SCALE);
278
240
  }
279
241
 
280
242
  case 'ci_upper': {
281
243
  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);
244
+ const { mean, ssd, n: nBig } = st;
245
+ const semSqScaled = (ssd * SQRT_SCALE_SQ) / (BigInt(n - 1) * nBig);
246
+ const semScaled = isqrt(semSqScaled);
247
+ const moeScaled = (Z95_NUM * semScaled) / Z95_DENOM;
248
+ const ciUpperScaled = mean * SQRT_SCALE + moeScaled;
249
+ return new Report(type, ciUpperScaled, 0, DURATION_SCALE * SQRT_SCALE);
298
250
  }
299
251
 
300
252
  default: {