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.
Files changed (68) hide show
  1. package/README.md +69 -42
  2. package/dist/{BenchRunner-CSKN9zPy.d.mts → BenchRunner-BzyUfiyB.d.mts} +32 -8
  3. package/dist/{BrowserHeapSampler-DCeL42RE.mjs → BrowserHeapSampler-B6asLKWQ.mjs} +57 -57
  4. package/dist/BrowserHeapSampler-B6asLKWQ.mjs.map +1 -0
  5. package/dist/{GcStats-ByEovUi1.mjs → GcStats-wX7Xyblu.mjs} +15 -15
  6. package/dist/GcStats-wX7Xyblu.mjs.map +1 -0
  7. package/dist/HeapSampler-B8dtKHn1.mjs.map +1 -1
  8. package/dist/{TimingUtils-ClclVQ7E.mjs → TimingUtils-DwOwkc8G.mjs} +225 -225
  9. package/dist/TimingUtils-DwOwkc8G.mjs.map +1 -0
  10. package/dist/bin/benchforge.mjs +1 -1
  11. package/dist/browser/index.js +210 -210
  12. package/dist/index.d.mts +106 -48
  13. package/dist/index.mjs +3 -3
  14. package/dist/runners/WorkerScript.d.mts +1 -1
  15. package/dist/runners/WorkerScript.mjs +66 -66
  16. package/dist/runners/WorkerScript.mjs.map +1 -1
  17. package/dist/{src-HfimYuW_.mjs → src-B-DDaCa9.mjs} +1250 -991
  18. package/dist/src-B-DDaCa9.mjs.map +1 -0
  19. package/package.json +4 -3
  20. package/src/BenchMatrix.ts +125 -125
  21. package/src/BenchmarkReport.ts +50 -45
  22. package/src/HtmlDataPrep.ts +21 -21
  23. package/src/PermutationTest.ts +24 -24
  24. package/src/StandardSections.ts +45 -45
  25. package/src/StatisticalUtils.ts +60 -61
  26. package/src/browser/BrowserGcStats.ts +5 -5
  27. package/src/browser/BrowserHeapSampler.ts +63 -63
  28. package/src/cli/CliArgs.ts +20 -6
  29. package/src/cli/FilterBenchmarks.ts +5 -5
  30. package/src/cli/RunBenchCLI.ts +533 -476
  31. package/src/export/JsonExport.ts +10 -10
  32. package/src/export/PerfettoExport.ts +74 -74
  33. package/src/export/SpeedscopeExport.ts +202 -0
  34. package/src/heap-sample/HeapSampleReport.ts +143 -70
  35. package/src/heap-sample/HeapSampler.ts +55 -12
  36. package/src/heap-sample/ResolvedProfile.ts +89 -0
  37. package/src/html/HtmlReport.ts +33 -33
  38. package/src/html/HtmlTemplate.ts +67 -67
  39. package/src/html/browser/CIPlot.ts +50 -50
  40. package/src/html/browser/HistogramKde.ts +13 -13
  41. package/src/html/browser/LegendUtils.ts +48 -48
  42. package/src/html/browser/RenderPlots.ts +98 -98
  43. package/src/html/browser/SampleTimeSeries.ts +79 -79
  44. package/src/index.ts +6 -0
  45. package/src/matrix/MatrixFilter.ts +6 -6
  46. package/src/matrix/MatrixReport.ts +96 -96
  47. package/src/matrix/VariantLoader.ts +5 -5
  48. package/src/runners/AdaptiveWrapper.ts +151 -151
  49. package/src/runners/BasicRunner.ts +175 -175
  50. package/src/runners/BenchRunner.ts +8 -8
  51. package/src/runners/GcStats.ts +22 -22
  52. package/src/runners/RunnerOrchestrator.ts +168 -168
  53. package/src/runners/WorkerScript.ts +96 -96
  54. package/src/table-util/Formatters.ts +41 -36
  55. package/src/table-util/TableReport.ts +122 -122
  56. package/src/table-util/test/TableValueExtractor.ts +9 -9
  57. package/src/test/AdaptiveStatistics.integration.ts +7 -39
  58. package/src/test/HeapAttribution.test.ts +51 -0
  59. package/src/test/RunBenchCLI.test.ts +36 -11
  60. package/src/test/TestUtils.ts +24 -24
  61. package/src/test/fixtures/fn-export-bench.ts +3 -0
  62. package/src/test/fixtures/suite-export-bench.ts +16 -0
  63. package/src/tests/BenchMatrix.test.ts +12 -12
  64. package/src/tests/MatrixFilter.test.ts +15 -15
  65. package/dist/BrowserHeapSampler-DCeL42RE.mjs.map +0 -1
  66. package/dist/GcStats-ByEovUi1.mjs.map +0 -1
  67. package/dist/TimingUtils-ClclVQ7E.mjs.map +0 -1
  68. 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
- const minTime = 1000;
12
- const maxTime = 10000;
13
- const targetConfidence = 95;
14
- const fallbackThreshold = 80;
15
- const windowSize = 50;
16
- const stability = 0.05; // 5% drift threshold (was 2%, too strict for real benchmarks)
17
- const initialBatch = 100;
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
- export interface AdaptiveOptions extends RunnerOptions {
35
- adaptive?: boolean;
36
- minTime?: number;
37
- maxTime?: number;
38
- targetConfidence?: number;
39
- convergence?: number; // Confidence threshold (0-100)
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
- }