benchforge 0.1.8 → 0.1.11
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/README.md +69 -42
- package/dist/{BenchRunner-CSKN9zPy.d.mts → BenchRunner-BzyUfiyB.d.mts} +32 -8
- package/dist/{BrowserHeapSampler-DCeL42RE.mjs → BrowserHeapSampler-B6asLKWQ.mjs} +57 -57
- package/dist/BrowserHeapSampler-B6asLKWQ.mjs.map +1 -0
- package/dist/{GcStats-ByEovUi1.mjs → GcStats-wX7Xyblu.mjs} +15 -15
- package/dist/GcStats-wX7Xyblu.mjs.map +1 -0
- package/dist/HeapSampler-B8dtKHn1.mjs.map +1 -1
- package/dist/{TimingUtils-ClclVQ7E.mjs → TimingUtils-DwOwkc8G.mjs} +225 -225
- package/dist/TimingUtils-DwOwkc8G.mjs.map +1 -0
- package/dist/bin/benchforge.mjs +1 -1
- package/dist/browser/index.js +210 -210
- package/dist/index.d.mts +106 -48
- package/dist/index.mjs +3 -3
- package/dist/runners/WorkerScript.d.mts +1 -1
- package/dist/runners/WorkerScript.mjs +66 -66
- package/dist/runners/WorkerScript.mjs.map +1 -1
- package/dist/{src-HfimYuW_.mjs → src-B-DDaCa9.mjs} +1250 -991
- package/dist/src-B-DDaCa9.mjs.map +1 -0
- package/package.json +4 -3
- package/src/BenchMatrix.ts +125 -125
- package/src/BenchmarkReport.ts +50 -45
- package/src/HtmlDataPrep.ts +21 -21
- package/src/PermutationTest.ts +24 -24
- package/src/StandardSections.ts +45 -45
- package/src/StatisticalUtils.ts +60 -61
- package/src/browser/BrowserGcStats.ts +5 -5
- package/src/browser/BrowserHeapSampler.ts +63 -63
- package/src/cli/CliArgs.ts +20 -6
- package/src/cli/FilterBenchmarks.ts +5 -5
- package/src/cli/RunBenchCLI.ts +533 -476
- package/src/export/JsonExport.ts +10 -10
- package/src/export/PerfettoExport.ts +74 -74
- package/src/export/SpeedscopeExport.ts +202 -0
- package/src/heap-sample/HeapSampleReport.ts +143 -70
- package/src/heap-sample/HeapSampler.ts +55 -12
- package/src/heap-sample/ResolvedProfile.ts +89 -0
- package/src/html/HtmlReport.ts +33 -33
- package/src/html/HtmlTemplate.ts +67 -67
- package/src/html/browser/CIPlot.ts +50 -50
- package/src/html/browser/HistogramKde.ts +13 -13
- package/src/html/browser/LegendUtils.ts +48 -48
- package/src/html/browser/RenderPlots.ts +98 -98
- package/src/html/browser/SampleTimeSeries.ts +79 -79
- package/src/index.ts +6 -0
- package/src/matrix/MatrixFilter.ts +6 -6
- package/src/matrix/MatrixReport.ts +96 -96
- package/src/matrix/VariantLoader.ts +5 -5
- package/src/runners/AdaptiveWrapper.ts +151 -151
- package/src/runners/BasicRunner.ts +175 -175
- package/src/runners/BenchRunner.ts +8 -8
- package/src/runners/GcStats.ts +22 -22
- package/src/runners/RunnerOrchestrator.ts +168 -168
- package/src/runners/WorkerScript.ts +96 -96
- package/src/table-util/Formatters.ts +41 -36
- package/src/table-util/TableReport.ts +122 -122
- package/src/table-util/test/TableValueExtractor.ts +9 -9
- package/src/test/AdaptiveStatistics.integration.ts +7 -39
- package/src/test/HeapAttribution.test.ts +51 -0
- package/src/test/RunBenchCLI.test.ts +36 -11
- package/src/test/TestUtils.ts +24 -24
- package/src/test/fixtures/fn-export-bench.ts +3 -0
- package/src/test/fixtures/suite-export-bench.ts +16 -0
- package/src/tests/BenchMatrix.test.ts +12 -12
- package/src/tests/MatrixFilter.test.ts +15 -15
- package/dist/BrowserHeapSampler-DCeL42RE.mjs.map +0 -1
- package/dist/GcStats-ByEovUi1.mjs.map +0 -1
- package/dist/TimingUtils-ClclVQ7E.mjs.map +0 -1
- package/dist/src-HfimYuW_.mjs.map +0 -1
|
@@ -8,15 +8,13 @@ import {
|
|
|
8
8
|
import type { BenchRunner, RunnerOptions } from "./BenchRunner.ts";
|
|
9
9
|
import { msToNs } from "./RunnerUtils.ts";
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const continueBatch = 100;
|
|
19
|
-
const continueIterations = 10;
|
|
11
|
+
export interface AdaptiveOptions extends RunnerOptions {
|
|
12
|
+
adaptive?: boolean;
|
|
13
|
+
minTime?: number;
|
|
14
|
+
maxTime?: number;
|
|
15
|
+
targetConfidence?: number;
|
|
16
|
+
convergence?: number; // Confidence threshold (0-100)
|
|
17
|
+
}
|
|
20
18
|
|
|
21
19
|
type Metrics = {
|
|
22
20
|
medianDrift: number;
|
|
@@ -31,13 +29,15 @@ interface ConvergenceResult {
|
|
|
31
29
|
reason: string;
|
|
32
30
|
}
|
|
33
31
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
32
|
+
const minTime = 1000;
|
|
33
|
+
const maxTime = 10000;
|
|
34
|
+
const targetConfidence = 95;
|
|
35
|
+
const fallbackThreshold = 80;
|
|
36
|
+
const windowSize = 50;
|
|
37
|
+
const stability = 0.05; // 5% drift threshold (was 2%, too strict for real benchmarks)
|
|
38
|
+
const initialBatch = 100;
|
|
39
|
+
const continueBatch = 100;
|
|
40
|
+
const continueIterations = 10;
|
|
41
41
|
|
|
42
42
|
/** @return adaptive sampling runner wrapper */
|
|
43
43
|
export function createAdaptiveWrapper(
|
|
@@ -61,6 +61,19 @@ export function createAdaptiveWrapper(
|
|
|
61
61
|
};
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/** @return convergence based on window stability */
|
|
65
|
+
export function checkConvergence(samples: number[]): ConvergenceResult {
|
|
66
|
+
const windowSize = getWindowSize(samples);
|
|
67
|
+
const minSamples = windowSize * 2;
|
|
68
|
+
|
|
69
|
+
if (samples.length < minSamples) {
|
|
70
|
+
return buildProgressResult(samples.length, minSamples);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const metrics = getStability(samples, windowSize);
|
|
74
|
+
return buildConvergence(metrics);
|
|
75
|
+
}
|
|
76
|
+
|
|
64
77
|
/** @return results using adaptive sampling strategy */
|
|
65
78
|
async function runAdaptiveBench<T>(
|
|
66
79
|
baseRunner: BenchRunner,
|
|
@@ -113,6 +126,82 @@ async function runAdaptiveBench<T>(
|
|
|
113
126
|
);
|
|
114
127
|
}
|
|
115
128
|
|
|
129
|
+
/** @return window size scaled to execution time */
|
|
130
|
+
function getWindowSize(samples: number[]): number {
|
|
131
|
+
if (samples.length < 20) return windowSize; // Default for initial samples
|
|
132
|
+
|
|
133
|
+
const recentMs = samples.slice(-20).map(s => s / msToNs);
|
|
134
|
+
const recentMedian = percentile(recentMs, 0.5);
|
|
135
|
+
|
|
136
|
+
// Inverse scaling with execution time
|
|
137
|
+
if (recentMedian < 0.01) return 200; // <10μs
|
|
138
|
+
if (recentMedian < 0.1) return 100; // <100μs
|
|
139
|
+
if (recentMedian < 1) return 50; // <1ms
|
|
140
|
+
if (recentMedian < 10) return 30; // <10ms
|
|
141
|
+
return 20; // >10ms
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** @return progress when samples insufficient */
|
|
145
|
+
function buildProgressResult(
|
|
146
|
+
currentSamples: number,
|
|
147
|
+
minSamples: number,
|
|
148
|
+
): ConvergenceResult {
|
|
149
|
+
return {
|
|
150
|
+
converged: false,
|
|
151
|
+
confidence: (currentSamples / minSamples) * 100,
|
|
152
|
+
reason: `Collecting samples: ${currentSamples}/${minSamples}`,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** @return stability metrics between windows */
|
|
157
|
+
function getStability(samples: number[], windowSize: number): Metrics {
|
|
158
|
+
const recent = samples.slice(-windowSize);
|
|
159
|
+
const previous = samples.slice(-windowSize * 2, -windowSize);
|
|
160
|
+
|
|
161
|
+
const recentMs = recent.map(s => s / msToNs);
|
|
162
|
+
const previousMs = previous.map(s => s / msToNs);
|
|
163
|
+
|
|
164
|
+
const medianRecent = percentile(recentMs, 0.5);
|
|
165
|
+
const medianPrevious = percentile(previousMs, 0.5);
|
|
166
|
+
const medianDrift = Math.abs(medianRecent - medianPrevious) / medianPrevious;
|
|
167
|
+
|
|
168
|
+
const impactRecent = getOutlierImpact(recentMs);
|
|
169
|
+
const impactPrevious = getOutlierImpact(previousMs);
|
|
170
|
+
const impactDrift = Math.abs(impactRecent.ratio - impactPrevious.ratio);
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
medianDrift,
|
|
174
|
+
impactDrift,
|
|
175
|
+
medianStable: medianDrift < stability,
|
|
176
|
+
impactStable: impactDrift < stability,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** @return convergence from stability metrics */
|
|
181
|
+
function buildConvergence(metrics: Metrics): ConvergenceResult {
|
|
182
|
+
const { medianDrift, impactDrift, medianStable, impactStable } = metrics;
|
|
183
|
+
|
|
184
|
+
if (medianStable && impactStable) {
|
|
185
|
+
return {
|
|
186
|
+
converged: true,
|
|
187
|
+
confidence: 100,
|
|
188
|
+
reason: "Stable performance pattern",
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const confidence = Math.min(
|
|
193
|
+
100,
|
|
194
|
+
(1 - medianDrift / stability) * 50 + (1 - impactDrift / stability) * 50,
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
const reason =
|
|
198
|
+
medianDrift > impactDrift
|
|
199
|
+
? `Median drifting: ${(medianDrift * 100).toFixed(1)}%`
|
|
200
|
+
: `Outlier impact changing: ${(impactDrift * 100).toFixed(1)}%`;
|
|
201
|
+
|
|
202
|
+
return { converged: false, confidence: Math.max(0, confidence), reason };
|
|
203
|
+
}
|
|
204
|
+
|
|
116
205
|
/** @return warmupSamples from initial batch */
|
|
117
206
|
async function collectInitial<T>(
|
|
118
207
|
baseRunner: BenchRunner,
|
|
@@ -179,27 +268,6 @@ async function collectAdaptive<T>(
|
|
|
179
268
|
process.stderr.write("\r" + " ".repeat(60) + "\r");
|
|
180
269
|
}
|
|
181
270
|
|
|
182
|
-
/** Append samples one-by-one to avoid stack overflow from spread on large arrays */
|
|
183
|
-
function appendSamples(result: MeasuredResults, samples: number[]): void {
|
|
184
|
-
if (!result.samples?.length) return;
|
|
185
|
-
for (const sample of result.samples) samples.push(sample);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/** @return true if convergence reached or timeout */
|
|
189
|
-
function shouldStop(
|
|
190
|
-
convergence: ConvergenceResult,
|
|
191
|
-
targetConfidence: number,
|
|
192
|
-
elapsedTime: number,
|
|
193
|
-
minTime: number,
|
|
194
|
-
): boolean {
|
|
195
|
-
if (convergence.converged && convergence.confidence >= targetConfidence) {
|
|
196
|
-
return true;
|
|
197
|
-
}
|
|
198
|
-
// After minTime, accept whichever is higher: targetConfidence or fallbackThreshold
|
|
199
|
-
const threshold = Math.max(targetConfidence, fallbackThreshold);
|
|
200
|
-
return elapsedTime >= minTime && convergence.confidence >= threshold;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
271
|
/** @return measured results with convergence metrics */
|
|
204
272
|
function buildResults(
|
|
205
273
|
samplesMs: number[],
|
|
@@ -224,6 +292,52 @@ function buildResults(
|
|
|
224
292
|
];
|
|
225
293
|
}
|
|
226
294
|
|
|
295
|
+
/** @return outlier impact as proportion of total time */
|
|
296
|
+
function getOutlierImpact(samples: number[]): { ratio: number; count: number } {
|
|
297
|
+
if (samples.length === 0) return { ratio: 0, count: 0 };
|
|
298
|
+
|
|
299
|
+
const median = percentile(samples, 0.5);
|
|
300
|
+
const q75 = percentile(samples, 0.75);
|
|
301
|
+
const threshold = median + 1.5 * (q75 - median);
|
|
302
|
+
|
|
303
|
+
let excessTime = 0;
|
|
304
|
+
let count = 0;
|
|
305
|
+
|
|
306
|
+
for (const sample of samples) {
|
|
307
|
+
if (sample > threshold) {
|
|
308
|
+
excessTime += sample - median;
|
|
309
|
+
count++;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const totalTime = samples.reduce((a, b) => a + b, 0);
|
|
314
|
+
return {
|
|
315
|
+
ratio: totalTime > 0 ? excessTime / totalTime : 0,
|
|
316
|
+
count,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Append samples one-by-one to avoid stack overflow from spread on large arrays */
|
|
321
|
+
function appendSamples(result: MeasuredResults, samples: number[]): void {
|
|
322
|
+
if (!result.samples?.length) return;
|
|
323
|
+
for (const sample of result.samples) samples.push(sample);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** @return true if convergence reached or timeout */
|
|
327
|
+
function shouldStop(
|
|
328
|
+
convergence: ConvergenceResult,
|
|
329
|
+
targetConfidence: number,
|
|
330
|
+
elapsedTime: number,
|
|
331
|
+
minTime: number,
|
|
332
|
+
): boolean {
|
|
333
|
+
if (convergence.converged && convergence.confidence >= targetConfidence) {
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
// After minTime, accept whichever is higher: targetConfidence or fallbackThreshold
|
|
337
|
+
const threshold = Math.max(targetConfidence, fallbackThreshold);
|
|
338
|
+
return elapsedTime >= minTime && convergence.confidence >= threshold;
|
|
339
|
+
}
|
|
340
|
+
|
|
227
341
|
/** @return time percentiles and statistics in ms */
|
|
228
342
|
function computeTimeStats(samplesNs: number[]) {
|
|
229
343
|
const samplesMs = samplesNs.map(s => s / msToNs);
|
|
@@ -275,117 +389,3 @@ function getRobustMetrics(samplesMs: number[]) {
|
|
|
275
389
|
outlierRate: impact.ratio,
|
|
276
390
|
};
|
|
277
391
|
}
|
|
278
|
-
|
|
279
|
-
/** @return outlier impact as proportion of total time */
|
|
280
|
-
function getOutlierImpact(samples: number[]): { ratio: number; count: number } {
|
|
281
|
-
if (samples.length === 0) return { ratio: 0, count: 0 };
|
|
282
|
-
|
|
283
|
-
const median = percentile(samples, 0.5);
|
|
284
|
-
const q75 = percentile(samples, 0.75);
|
|
285
|
-
const threshold = median + 1.5 * (q75 - median);
|
|
286
|
-
|
|
287
|
-
let excessTime = 0;
|
|
288
|
-
let count = 0;
|
|
289
|
-
|
|
290
|
-
for (const sample of samples) {
|
|
291
|
-
if (sample > threshold) {
|
|
292
|
-
excessTime += sample - median;
|
|
293
|
-
count++;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const totalTime = samples.reduce((a, b) => a + b, 0);
|
|
298
|
-
return {
|
|
299
|
-
ratio: totalTime > 0 ? excessTime / totalTime : 0,
|
|
300
|
-
count,
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/** @return convergence based on window stability */
|
|
305
|
-
export function checkConvergence(samples: number[]): ConvergenceResult {
|
|
306
|
-
const windowSize = getWindowSize(samples);
|
|
307
|
-
const minSamples = windowSize * 2;
|
|
308
|
-
|
|
309
|
-
if (samples.length < minSamples) {
|
|
310
|
-
return buildProgressResult(samples.length, minSamples);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
const metrics = getStability(samples, windowSize);
|
|
314
|
-
return buildConvergence(metrics);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/** @return progress when samples insufficient */
|
|
318
|
-
function buildProgressResult(
|
|
319
|
-
currentSamples: number,
|
|
320
|
-
minSamples: number,
|
|
321
|
-
): ConvergenceResult {
|
|
322
|
-
return {
|
|
323
|
-
converged: false,
|
|
324
|
-
confidence: (currentSamples / minSamples) * 100,
|
|
325
|
-
reason: `Collecting samples: ${currentSamples}/${minSamples}`,
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/** @return stability metrics between windows */
|
|
330
|
-
function getStability(samples: number[], windowSize: number): Metrics {
|
|
331
|
-
const recent = samples.slice(-windowSize);
|
|
332
|
-
const previous = samples.slice(-windowSize * 2, -windowSize);
|
|
333
|
-
|
|
334
|
-
const recentMs = recent.map(s => s / msToNs);
|
|
335
|
-
const previousMs = previous.map(s => s / msToNs);
|
|
336
|
-
|
|
337
|
-
const medianRecent = percentile(recentMs, 0.5);
|
|
338
|
-
const medianPrevious = percentile(previousMs, 0.5);
|
|
339
|
-
const medianDrift = Math.abs(medianRecent - medianPrevious) / medianPrevious;
|
|
340
|
-
|
|
341
|
-
const impactRecent = getOutlierImpact(recentMs);
|
|
342
|
-
const impactPrevious = getOutlierImpact(previousMs);
|
|
343
|
-
const impactDrift = Math.abs(impactRecent.ratio - impactPrevious.ratio);
|
|
344
|
-
|
|
345
|
-
return {
|
|
346
|
-
medianDrift,
|
|
347
|
-
impactDrift,
|
|
348
|
-
medianStable: medianDrift < stability,
|
|
349
|
-
impactStable: impactDrift < stability,
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/** @return convergence from stability metrics */
|
|
354
|
-
function buildConvergence(metrics: Metrics): ConvergenceResult {
|
|
355
|
-
const { medianDrift, impactDrift, medianStable, impactStable } = metrics;
|
|
356
|
-
|
|
357
|
-
if (medianStable && impactStable) {
|
|
358
|
-
return {
|
|
359
|
-
converged: true,
|
|
360
|
-
confidence: 100,
|
|
361
|
-
reason: "Stable performance pattern",
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
const confidence = Math.min(
|
|
366
|
-
100,
|
|
367
|
-
(1 - medianDrift / stability) * 50 + (1 - impactDrift / stability) * 50,
|
|
368
|
-
);
|
|
369
|
-
|
|
370
|
-
const reason =
|
|
371
|
-
medianDrift > impactDrift
|
|
372
|
-
? `Median drifting: ${(medianDrift * 100).toFixed(1)}%`
|
|
373
|
-
: `Outlier impact changing: ${(impactDrift * 100).toFixed(1)}%`;
|
|
374
|
-
|
|
375
|
-
return { converged: false, confidence: Math.max(0, confidence), reason };
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
/** @return window size scaled to execution time */
|
|
379
|
-
function getWindowSize(samples: number[]): number {
|
|
380
|
-
if (samples.length < 20) return windowSize; // Default for initial samples
|
|
381
|
-
|
|
382
|
-
const recentMs = samples.slice(-20).map(s => s / msToNs);
|
|
383
|
-
const recentMedian = percentile(recentMs, 0.5);
|
|
384
|
-
|
|
385
|
-
// Inverse scaling with execution time
|
|
386
|
-
if (recentMedian < 0.01) return 200; // <10μs
|
|
387
|
-
if (recentMedian < 0.1) return 100; // <100μs
|
|
388
|
-
if (recentMedian < 1) return 50; // <1ms
|
|
389
|
-
if (recentMedian < 10) return 30; // <10ms
|
|
390
|
-
return 20; // >10ms
|
|
391
|
-
}
|