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/index.js ADDED
@@ -0,0 +1,375 @@
1
+ import { cpus } from 'node:os';
2
+ import Progress from 'progress';
3
+ import { createExecutor } from './executor.js';
4
+ import { DEFAULT_CYCLES } from './types.js';
5
+ export const DEFAULT_WORKERS = Math.max(1, Math.ceil(cpus().length / 4));
6
+ const BENCHMARK_URL = Symbol.for('overtake.benchmarkUrl');
7
+ export const DEFAULT_REPORT_TYPES = ['ops'];
8
+ const createExecutorErrorReport = (error) => ({
9
+ count: 0,
10
+ heapUsedKB: 0,
11
+ dceWarning: false,
12
+ error: error instanceof Error ? error.message : String(error),
13
+ });
14
+ export class MeasureContext {
15
+ pre;
16
+ post;
17
+ title;
18
+ run;
19
+ constructor(title, run) {
20
+ this.title = title;
21
+ this.run = run;
22
+ }
23
+ }
24
+ export class Measure {
25
+ #ctx;
26
+ constructor(ctx) {
27
+ this.#ctx = ctx;
28
+ }
29
+ pre(fn) {
30
+ this.#ctx.pre = fn;
31
+ return this;
32
+ }
33
+ post(fn) {
34
+ this.#ctx.post = fn;
35
+ return this;
36
+ }
37
+ }
38
+ export class TargetContext {
39
+ teardown;
40
+ measures = [];
41
+ title;
42
+ setup;
43
+ constructor(title, setup) {
44
+ this.title = title;
45
+ this.setup = setup;
46
+ }
47
+ }
48
+ export class Target {
49
+ #ctx;
50
+ constructor(ctx) {
51
+ this.#ctx = ctx;
52
+ }
53
+ teardown(fn) {
54
+ this.#ctx.teardown = fn;
55
+ return this;
56
+ }
57
+ measure(title, run) {
58
+ const measure = new MeasureContext(title, run);
59
+ this.#ctx.measures.push(measure);
60
+ return new Measure(measure);
61
+ }
62
+ }
63
+ export class FeedContext {
64
+ title;
65
+ fn;
66
+ constructor(title, fn) {
67
+ this.title = title;
68
+ this.fn = fn;
69
+ }
70
+ }
71
+ export class Benchmark {
72
+ #targets = [];
73
+ #feeds = [];
74
+ #executed = false;
75
+ static create(title, fn) {
76
+ if (fn) {
77
+ return new Benchmark(title, fn);
78
+ }
79
+ else {
80
+ return new Benchmark(title);
81
+ }
82
+ }
83
+ constructor(title, fn) {
84
+ if (fn) {
85
+ this.feed(title, fn);
86
+ }
87
+ else {
88
+ this.feed(title);
89
+ }
90
+ }
91
+ feed(title, fn) {
92
+ const self = this;
93
+ self.#feeds.push(fn ? new FeedContext(title, fn) : new FeedContext(title));
94
+ return self;
95
+ }
96
+ target(title, setup) {
97
+ const target = new TargetContext(title, setup);
98
+ this.#targets.push(target);
99
+ return new Target(target);
100
+ }
101
+ async execute(options = {}) {
102
+ const { workers = DEFAULT_WORKERS, warmupCycles = 20, maxCycles = DEFAULT_CYCLES, minCycles = 50, absThreshold = 1_000, relThreshold = 0.02, gcObserver = true, reportTypes = DEFAULT_REPORT_TYPES, progress = false, progressInterval = 100, } = options;
103
+ if (this.#executed) {
104
+ throw new Error("Benchmark is executed and can't be reused");
105
+ }
106
+ this.#executed = true;
107
+ const benchmarkUrl = options[BENCHMARK_URL];
108
+ const totalBenchmarks = this.#targets.reduce((acc, t) => acc + t.measures.length * this.#feeds.length, 0);
109
+ const progressMap = new Map();
110
+ let completedBenchmarks = 0;
111
+ let bar = null;
112
+ if (progress && totalBenchmarks > 0) {
113
+ bar = new Progress(' [:bar] :percent :current/:total :label', {
114
+ total: totalBenchmarks * 100,
115
+ width: 30,
116
+ complete: '=',
117
+ incomplete: ' ',
118
+ });
119
+ }
120
+ const onProgress = progress
121
+ ? (info) => {
122
+ progressMap.set(info.id, info.progress);
123
+ const totalProgress = (completedBenchmarks + [...progressMap.values()].reduce((a, b) => a + b, 0)) * 100;
124
+ const label = info.id.length > 30 ? info.id.slice(0, 27) + '...' : info.id;
125
+ bar?.update(totalProgress / (totalBenchmarks * 100), { label });
126
+ }
127
+ : undefined;
128
+ const executor = createExecutor({
129
+ workers,
130
+ warmupCycles,
131
+ maxCycles,
132
+ minCycles,
133
+ absThreshold,
134
+ relThreshold,
135
+ gcObserver,
136
+ reportTypes,
137
+ onProgress,
138
+ progressInterval,
139
+ [BENCHMARK_URL]: benchmarkUrl,
140
+ });
141
+ const reports = [];
142
+ const pendingReports = [];
143
+ try {
144
+ const feedData = await Promise.all(this.#feeds.map(async (feed) => ({ title: feed.title, data: await feed.fn?.() })));
145
+ for (const target of this.#targets) {
146
+ const targetReport = { target: target.title, measures: [] };
147
+ for (const measure of target.measures) {
148
+ const measureReport = { measure: measure.title, feeds: [] };
149
+ for (const feed of feedData) {
150
+ const id = `${target.title}/${measure.title}/${feed.title}`;
151
+ const feedReport = {
152
+ feed: feed.title,
153
+ data: createExecutorErrorReport('Benchmark did not produce a report'),
154
+ };
155
+ measureReport.feeds.push(feedReport);
156
+ pendingReports.push((async () => {
157
+ try {
158
+ feedReport.data = await executor.pushAsync({
159
+ id,
160
+ setup: target.setup,
161
+ teardown: target.teardown,
162
+ pre: measure.pre,
163
+ run: measure.run,
164
+ post: measure.post,
165
+ data: feed.data,
166
+ });
167
+ }
168
+ catch (error) {
169
+ feedReport.data = createExecutorErrorReport(error);
170
+ }
171
+ finally {
172
+ progressMap.delete(id);
173
+ completedBenchmarks++;
174
+ }
175
+ })());
176
+ }
177
+ targetReport.measures.push(measureReport);
178
+ }
179
+ reports.push(targetReport);
180
+ }
181
+ await Promise.all(pendingReports);
182
+ if (bar) {
183
+ bar.update(1, { label: 'done' });
184
+ bar.terminate();
185
+ }
186
+ return reports;
187
+ }
188
+ finally {
189
+ executor.kill();
190
+ }
191
+ }
192
+ }
193
+ export const printSimpleReports = (reports) => {
194
+ for (const report of reports) {
195
+ for (const { measure, feeds } of report.measures) {
196
+ console.group('\n', report.target, measure);
197
+ for (const { feed, data } of feeds) {
198
+ const { count, heapUsedKB, dceWarning, error: benchError, ...metrics } = data;
199
+ if (benchError) {
200
+ console.log(feed, `\x1b[31m[error: ${benchError}]\x1b[0m`);
201
+ continue;
202
+ }
203
+ const output = Object.entries(metrics)
204
+ .map(([key, report]) => `${key}: ${report.toString()}`)
205
+ .join('; ');
206
+ const extras = [];
207
+ if (heapUsedKB)
208
+ extras.push(`heap: ${heapUsedKB}KB`);
209
+ if (dceWarning)
210
+ extras.push('\x1b[33m[DCE warning]\x1b[0m');
211
+ const extrasStr = extras.length > 0 ? ` (${extras.join(', ')})` : '';
212
+ console.log(feed, output + extrasStr);
213
+ }
214
+ console.groupEnd();
215
+ }
216
+ }
217
+ };
218
+ export const printTableReports = (reports) => {
219
+ for (const report of reports) {
220
+ for (const { measure, feeds } of report.measures) {
221
+ console.log('\n', report.target, measure);
222
+ const table = {};
223
+ for (const { feed, data } of feeds) {
224
+ const { error: benchError } = data;
225
+ if (benchError) {
226
+ table[feed] = { error: benchError };
227
+ }
228
+ else {
229
+ table[feed] = Object.fromEntries(Object.entries(data).map(([key, report]) => [key, report.toString()]));
230
+ }
231
+ }
232
+ console.table(table);
233
+ }
234
+ }
235
+ };
236
+ export const printJSONReports = (reports, padding) => {
237
+ const output = {};
238
+ for (const report of reports) {
239
+ for (const { measure, feeds } of report.measures) {
240
+ const row = {};
241
+ for (const { feed, data } of feeds) {
242
+ const { error: benchError } = data;
243
+ if (benchError) {
244
+ row[feed] = { error: String(benchError) };
245
+ }
246
+ else {
247
+ row[feed] = Object.fromEntries(Object.entries(data).map(([key, report]) => [key, report.toString()]));
248
+ }
249
+ }
250
+ output[`${report.target} ${measure}`] = row;
251
+ }
252
+ }
253
+ console.log(JSON.stringify(output, null, padding));
254
+ };
255
+ export const printMarkdownReports = (reports) => {
256
+ for (const report of reports) {
257
+ for (const { measure, feeds } of report.measures) {
258
+ console.log(`\n## ${report.target} - ${measure}\n`);
259
+ if (feeds.length === 0)
260
+ continue;
261
+ const firstValid = feeds.find((f) => !f.data.error);
262
+ if (!firstValid) {
263
+ for (const { feed, data } of feeds) {
264
+ console.log(`| ${feed} | error: ${data.error} |`);
265
+ }
266
+ continue;
267
+ }
268
+ const keys = Object.keys(firstValid.data).filter((k) => k !== 'count' && k !== 'error');
269
+ const header = ['Feed', ...keys].join(' | ');
270
+ const separator = ['---', ...keys.map(() => '---')].join(' | ');
271
+ console.log(`| ${header} |`);
272
+ console.log(`| ${separator} |`);
273
+ for (const { feed, data } of feeds) {
274
+ if (data.error) {
275
+ console.log(`| ${feed} | error: ${data.error} |`);
276
+ continue;
277
+ }
278
+ const values = keys.map((k) => data[k]?.toString() ?? '-');
279
+ console.log(`| ${[feed, ...values].join(' | ')} |`);
280
+ }
281
+ }
282
+ }
283
+ };
284
+ export const printHistogramReports = (reports, width = 40) => {
285
+ for (const report of reports) {
286
+ for (const { measure, feeds } of report.measures) {
287
+ console.log(`\n${report.target} - ${measure}\n`);
288
+ const opsKey = 'ops';
289
+ const values = feeds.map((f) => {
290
+ const { error: benchError } = f.data;
291
+ return {
292
+ feed: f.feed,
293
+ value: benchError ? 0 : (f.data[opsKey]?.valueOf() ?? 0),
294
+ error: benchError,
295
+ };
296
+ });
297
+ const maxValue = Math.max(...values.map((v) => v.value));
298
+ const maxLabelLen = Math.max(...values.map((v) => v.feed.length));
299
+ for (const { feed, value, error } of values) {
300
+ const label = feed.padEnd(maxLabelLen);
301
+ if (error) {
302
+ console.log(` ${label} | \x1b[31m[error: ${error}]\x1b[0m`);
303
+ continue;
304
+ }
305
+ const barLen = maxValue > 0 ? Math.round((value / maxValue) * width) : 0;
306
+ const bar = '\u2588'.repeat(barLen);
307
+ const formatted = value.toLocaleString('en-US', { maximumFractionDigits: 2 });
308
+ console.log(` ${label} | ${bar} ${formatted} ops/s`);
309
+ }
310
+ }
311
+ }
312
+ };
313
+ export const reportsToBaseline = (reports) => {
314
+ const results = {};
315
+ for (const report of reports) {
316
+ for (const { measure, feeds } of report.measures) {
317
+ for (const { feed, data } of feeds) {
318
+ if (data.error)
319
+ continue;
320
+ const key = `${report.target}/${measure}/${feed}`;
321
+ results[key] = {};
322
+ for (const [metric, value] of Object.entries(data)) {
323
+ if (metric !== 'count' && typeof value.valueOf === 'function') {
324
+ results[key][metric] = value.valueOf();
325
+ }
326
+ }
327
+ }
328
+ }
329
+ }
330
+ return {
331
+ version: 1,
332
+ timestamp: new Date().toISOString(),
333
+ results,
334
+ };
335
+ };
336
+ export const printComparisonReports = (reports, baseline, threshold = 5) => {
337
+ for (const report of reports) {
338
+ for (const { measure, feeds } of report.measures) {
339
+ console.log(`\n${report.target} - ${measure}\n`);
340
+ for (const { feed, data } of feeds) {
341
+ const key = `${report.target}/${measure}/${feed}`;
342
+ const baselineData = baseline.results[key];
343
+ console.log(` ${feed}:`);
344
+ if (data.error) {
345
+ console.log(` \x1b[31m[error: ${data.error}]\x1b[0m`);
346
+ continue;
347
+ }
348
+ for (const [metric, value] of Object.entries(data)) {
349
+ if (metric === 'count')
350
+ continue;
351
+ const current = value.valueOf();
352
+ const baselineValue = baselineData?.[metric];
353
+ if (baselineValue !== undefined && baselineValue !== 0) {
354
+ const change = ((current - baselineValue) / baselineValue) * 100;
355
+ const isOps = metric === 'ops';
356
+ const improved = isOps ? change > threshold : change < -threshold;
357
+ const regressed = isOps ? change < -threshold : change > threshold;
358
+ let indicator = ' ';
359
+ if (improved)
360
+ indicator = '\x1b[32m+\x1b[0m';
361
+ else if (regressed)
362
+ indicator = '\x1b[31m!\x1b[0m';
363
+ const changeStr = change >= 0 ? `+${change.toFixed(1)}%` : `${change.toFixed(1)}%`;
364
+ const coloredChange = regressed ? `\x1b[31m${changeStr}\x1b[0m` : improved ? `\x1b[32m${changeStr}\x1b[0m` : changeStr;
365
+ console.log(` ${indicator} ${metric}: ${value.toString()} (${coloredChange})`);
366
+ }
367
+ else {
368
+ console.log(` * ${metric}: ${value.toString()} (new)`);
369
+ }
370
+ }
371
+ }
372
+ }
373
+ }
374
+ console.log(`\nBaseline from: ${baseline.timestamp}`);
375
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,15 @@
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()}`));
@@ -1,4 +1,4 @@
1
- import { type ReportType } from './types.ts';
1
+ import { type ReportType } from './types.js';
2
2
  export declare class Report {
3
3
  readonly type: ReportType;
4
4
  readonly value: bigint;
@@ -0,0 +1,255 @@
1
+ import { div, max, divs, isqrt } from './utils.js';
2
+ import { DURATION_SCALE } from './types.js';
3
+ const units = [
4
+ { unit: 'ns', factor: 1 },
5
+ { unit: 'µs', factor: 1e3 },
6
+ { unit: 'ms', factor: 1e6 },
7
+ { unit: 's', factor: 1e9 },
8
+ { unit: 'm', factor: 60 * 1e9 },
9
+ { unit: 'h', factor: 3600 * 1e9 },
10
+ ];
11
+ function smartFixed(n) {
12
+ return n.toLocaleString(undefined, {
13
+ minimumFractionDigits: 0,
14
+ maximumFractionDigits: 2,
15
+ useGrouping: true,
16
+ });
17
+ }
18
+ export class Report {
19
+ type;
20
+ value;
21
+ uncertainty;
22
+ scale;
23
+ constructor(type, value, uncertainty = 0, scale = 1n) {
24
+ this.type = type;
25
+ this.value = value;
26
+ this.uncertainty = uncertainty;
27
+ this.scale = scale;
28
+ }
29
+ valueOf() {
30
+ return Number(div(this.value, this.scale));
31
+ }
32
+ toString() {
33
+ const uncertainty = this.uncertainty ? ` ± ${smartFixed(this.uncertainty)}%` : '';
34
+ const value = this.valueOf();
35
+ if (this.type === 'ops') {
36
+ return `${smartFixed(value)} ops/s${uncertainty}`;
37
+ }
38
+ if (this.type === 'rme') {
39
+ return `${smartFixed(value)}%`;
40
+ }
41
+ if (this.type === 'variance') {
42
+ let display = value;
43
+ let unit = 'ns²';
44
+ const varianceUnits = [
45
+ { unit: 'ns²', factor: 1 },
46
+ { unit: 'µs²', factor: 1e6 },
47
+ { unit: 'ms²', factor: 1e12 },
48
+ ];
49
+ for (const { unit: u, factor } of varianceUnits) {
50
+ const candidate = value / factor;
51
+ if (candidate < 1000) {
52
+ display = candidate;
53
+ unit = u;
54
+ break;
55
+ }
56
+ }
57
+ return `${smartFixed(display)} ${unit}`;
58
+ }
59
+ let display = value;
60
+ let unit = 'ns';
61
+ for (const { unit: u, factor } of units) {
62
+ const candidate = value / factor;
63
+ if (candidate < 1000) {
64
+ display = candidate;
65
+ unit = u;
66
+ break;
67
+ }
68
+ }
69
+ return `${smartFixed(display)} ${unit}${uncertainty}`;
70
+ }
71
+ }
72
+ const SQRT_SCALE = 1000000n;
73
+ const SQRT_SCALE_SQ = SQRT_SCALE * SQRT_SCALE;
74
+ const Z95_NUM = 196n;
75
+ const Z95_DENOM = 100n;
76
+ export const computeStats = (durations) => {
77
+ let sum = 0n;
78
+ for (const d of durations)
79
+ sum += d;
80
+ const n = BigInt(durations.length);
81
+ const mean = sum / n;
82
+ let ssd = 0n;
83
+ for (const d of durations) {
84
+ const diff = d - mean;
85
+ ssd += diff * diff;
86
+ }
87
+ return { sum, mean, ssd, n };
88
+ };
89
+ export const createReport = (durations, type, stats) => {
90
+ const n = durations.length;
91
+ if (n === 0) {
92
+ return new Report(type, 0n);
93
+ }
94
+ const st = stats ?? computeStats(durations);
95
+ switch (type) {
96
+ case 'min': {
97
+ return new Report(type, durations[0], 0, DURATION_SCALE);
98
+ }
99
+ case 'max': {
100
+ return new Report(type, durations[n - 1], 0, DURATION_SCALE);
101
+ }
102
+ case 'median': {
103
+ const mid = Math.floor(n / 2);
104
+ const med = n % 2 === 0 ? (durations[mid - 1] + durations[mid]) / 2n : durations[mid];
105
+ return new Report(type, med, 0, DURATION_SCALE);
106
+ }
107
+ case 'mode': {
108
+ const freq = new Map();
109
+ let maxCount = 0n;
110
+ let modeVal = durations[0];
111
+ for (const d of durations) {
112
+ const count = (freq.get(d) || 0n) + 1n;
113
+ freq.set(d, count);
114
+ if (count > maxCount) {
115
+ maxCount = count;
116
+ modeVal = d;
117
+ }
118
+ }
119
+ let lower = modeVal;
120
+ let upper = modeVal;
121
+ const firstIdx = durations.indexOf(modeVal);
122
+ const lastIdx = durations.lastIndexOf(modeVal);
123
+ if (firstIdx > 0)
124
+ lower = durations[firstIdx - 1];
125
+ if (lastIdx < n - 1)
126
+ upper = durations[lastIdx + 1];
127
+ const gap = max(modeVal - lower, upper - modeVal);
128
+ const uncertainty = modeVal > 0 ? Number(((gap / 2n) * 100n) / modeVal) : 0;
129
+ return new Report(type, modeVal, uncertainty, DURATION_SCALE);
130
+ }
131
+ case 'ops': {
132
+ const { mean: avgScaled, ssd, n: nBig } = st;
133
+ const nsPerSecScaled = 1000000000n * DURATION_SCALE;
134
+ const raw = Number(nsPerSecScaled) / Number(avgScaled);
135
+ const extra = raw < 1 ? Math.ceil(-Math.log10(raw)) : 0;
136
+ const exp = raw > 100 ? 0 : 2 + extra;
137
+ const scale = 10n ** BigInt(exp);
138
+ const value = avgScaled > 0n ? (nsPerSecScaled * scale) / avgScaled : 0n;
139
+ let uncertainty = 0;
140
+ if (n >= 2 && avgScaled > 0n) {
141
+ const RME_PRECISION = 1000000n;
142
+ const semOverMeanSqScaled = (ssd * RME_PRECISION * RME_PRECISION) / (BigInt(n - 1) * nBig * avgScaled * avgScaled);
143
+ const semOverMeanScaled = isqrt(semOverMeanSqScaled);
144
+ uncertainty = Number(Z95_NUM * semOverMeanScaled) / Number(RME_PRECISION);
145
+ }
146
+ return new Report(type, value, uncertainty, scale);
147
+ }
148
+ case 'mean': {
149
+ const { sum } = st;
150
+ const value = divs(sum, BigInt(n), 1n);
151
+ return new Report(type, value, 0, DURATION_SCALE);
152
+ }
153
+ case 'variance': {
154
+ if (n < 2)
155
+ return new Report(type, 0n, 0, DURATION_SCALE * DURATION_SCALE);
156
+ const { ssd } = st;
157
+ const variance = ssd / BigInt(n - 1);
158
+ return new Report(type, variance, 0, DURATION_SCALE * DURATION_SCALE);
159
+ }
160
+ case 'sd': {
161
+ if (n < 2)
162
+ return new Report(type, 0n, 0, DURATION_SCALE);
163
+ const { ssd } = st;
164
+ const scaledVariance = (ssd * SQRT_SCALE_SQ) / BigInt(n - 1);
165
+ const sdScaled = isqrt(scaledVariance);
166
+ return new Report(type, sdScaled, 0, DURATION_SCALE * SQRT_SCALE);
167
+ }
168
+ case 'sem': {
169
+ if (n < 2)
170
+ return new Report(type, 0n, 0, DURATION_SCALE);
171
+ const { ssd, n: nBig } = st;
172
+ const semSqScaled = (ssd * SQRT_SCALE_SQ) / (BigInt(n - 1) * nBig);
173
+ const semScaled = isqrt(semSqScaled);
174
+ return new Report(type, semScaled, 0, DURATION_SCALE * SQRT_SCALE);
175
+ }
176
+ case 'moe': {
177
+ if (n < 2)
178
+ return new Report(type, 0n, 0, DURATION_SCALE);
179
+ const { ssd, n: nBig } = st;
180
+ const semSqScaled = (ssd * SQRT_SCALE_SQ) / (BigInt(n - 1) * nBig);
181
+ const semScaled = isqrt(semSqScaled);
182
+ const moeScaled = (Z95_NUM * semScaled) / Z95_DENOM;
183
+ return new Report(type, moeScaled, 0, DURATION_SCALE * SQRT_SCALE);
184
+ }
185
+ case 'rme': {
186
+ if (n < 2)
187
+ return new Report(type, 0n);
188
+ const { mean, ssd, n: nBig } = st;
189
+ if (mean === 0n)
190
+ return new Report(type, 0n);
191
+ const RME_PRECISION = 1000000n;
192
+ const semOverMeanSqScaled = (ssd * RME_PRECISION * RME_PRECISION) / (BigInt(n - 1) * nBig * mean * mean);
193
+ const semOverMeanScaled = isqrt(semOverMeanSqScaled);
194
+ const rmeScaled = (Z95_NUM * semOverMeanScaled * 100n) / RME_PRECISION;
195
+ return new Report(type, rmeScaled, 0, 100n);
196
+ }
197
+ case 'mad': {
198
+ const medianIdx = Math.floor(n / 2);
199
+ const median = n % 2 === 1 ? durations[medianIdx] : (durations[medianIdx - 1] + durations[medianIdx]) / 2n;
200
+ const deviations = new BigUint64Array(n);
201
+ for (let i = 0; i < n; i++) {
202
+ const diff = durations[i] > median ? durations[i] - median : median - durations[i];
203
+ deviations[i] = diff;
204
+ }
205
+ deviations.sort();
206
+ const madIdx = Math.floor(n / 2);
207
+ const mad = n % 2 === 1 ? deviations[madIdx] : (deviations[madIdx - 1] + deviations[madIdx]) / 2n;
208
+ return new Report(type, mad, 0, DURATION_SCALE);
209
+ }
210
+ case 'iqr': {
211
+ const q1Idx = Math.floor(n * 0.25);
212
+ const q3Idx = Math.floor(n * 0.75);
213
+ const q1 = durations[q1Idx];
214
+ const q3 = durations[q3Idx];
215
+ const iqr = q3 - q1;
216
+ return new Report(type, iqr, 0, DURATION_SCALE);
217
+ }
218
+ case 'ci_lower': {
219
+ if (n < 2)
220
+ return new Report(type, 0n, 0, DURATION_SCALE);
221
+ const { mean, ssd, n: nBig } = st;
222
+ const semSqScaled = (ssd * SQRT_SCALE_SQ) / (BigInt(n - 1) * nBig);
223
+ const semScaled = isqrt(semSqScaled);
224
+ const moeScaled = (Z95_NUM * semScaled) / Z95_DENOM;
225
+ const ciLowerScaled = mean * SQRT_SCALE - moeScaled;
226
+ return new Report(type, ciLowerScaled > 0n ? ciLowerScaled : 0n, 0, DURATION_SCALE * SQRT_SCALE);
227
+ }
228
+ case 'ci_upper': {
229
+ if (n < 2)
230
+ return new Report(type, 0n, 0, DURATION_SCALE);
231
+ const { mean, ssd, n: nBig } = st;
232
+ const semSqScaled = (ssd * SQRT_SCALE_SQ) / (BigInt(n - 1) * nBig);
233
+ const semScaled = isqrt(semSqScaled);
234
+ const moeScaled = (Z95_NUM * semScaled) / Z95_DENOM;
235
+ const ciUpperScaled = mean * SQRT_SCALE + moeScaled;
236
+ return new Report(type, ciUpperScaled, 0, DURATION_SCALE * SQRT_SCALE);
237
+ }
238
+ default: {
239
+ const p = Number(type.slice(1));
240
+ if (p === 0) {
241
+ return new Report(type, durations[0], 0, DURATION_SCALE);
242
+ }
243
+ if (p === 100) {
244
+ return new Report(type, durations[n - 1], 0, DURATION_SCALE);
245
+ }
246
+ const idx = Math.ceil((p / 100) * n) - 1;
247
+ const value = durations[Math.min(Math.max(idx, 0), n - 1)];
248
+ const prev = idx > 0 ? durations[idx - 1] : value;
249
+ const next = idx < n - 1 ? durations[idx + 1] : value;
250
+ const gap = max(value - prev, next - value);
251
+ const uncertainty = value > 0 ? Number(div(divs(gap, 2n, 10000n), value)) / 100 : 0;
252
+ return new Report(type, value, uncertainty, DURATION_SCALE);
253
+ }
254
+ }
255
+ };
package/build/runner.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { type Options } from './types.ts';
1
+ import { type Options } from './types.js';
2
2
  export declare const benchmark: <TContext, TInput>({ setup, teardown, pre, run: runRaw, post, data, warmupCycles, minCycles, absThreshold, relThreshold, gcObserver, durationsSAB, controlSAB, }: Required<Options<TContext, TInput>>) => Promise<number>;