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,28 +8,15 @@ import type {
8
8
  import type { BenchRunner, RunnerOptions } from "./BenchRunner.ts";
9
9
  import { executeBenchmark } from "./BenchRunner.ts";
10
10
 
11
- /**
12
- * Wait time after gc() for V8 to stabilize (ms).
13
- *
14
- * V8 has 4 compilation tiers: Ignition (interpreter) -> Sparkplug (baseline) ->
15
- * Maglev (mid-tier optimizer) -> TurboFan (full optimizer). Tiering thresholds:
16
- * - Ignition -> Sparkplug: 8 invocations
17
- * - Sparkplug -> Maglev: 500 invocations
18
- * - Maglev -> TurboFan: 6000 invocations
19
- *
20
- * Optimization compilation happens on background threads and requires idle time
21
- * on the main thread to complete. Without sufficient warmup + settle time,
22
- * benchmarks exhibit bimodal timing: slow Sparkplug samples (~30% slower) mixed
23
- * with fast optimized samples.
24
- *
25
- * The warmup iterations trigger the optimization decision, then gcSettleTime
26
- * provides idle time for background compilation to finish before measurement.
27
- *
28
- * @see https://v8.dev/blog/sparkplug
29
- * @see https://v8.dev/blog/maglev
30
- * @see https://v8.dev/blog/background-compilation
31
- */
32
- const gcSettleTime = 1000;
11
+ export type SampleTimeStats = {
12
+ min: number;
13
+ max: number;
14
+ avg: number;
15
+ p50: number;
16
+ p75: number;
17
+ p99: number;
18
+ p999: number;
19
+ };
33
20
 
34
21
  type CollectParams<T = unknown> = {
35
22
  benchmark: BenchmarkSpec<T>;
@@ -56,14 +43,68 @@ type CollectResult = {
56
43
  pausePoints: PausePoint[]; // where pauses occurred
57
44
  };
58
45
 
59
- export type SampleTimeStats = {
60
- min: number;
61
- max: number;
62
- avg: number;
63
- p50: number;
64
- p75: number;
65
- p99: number;
66
- p999: number;
46
+ type SampleLoopResult = {
47
+ samples: number[];
48
+ heapSamples?: number[];
49
+ timestamps?: number[];
50
+ optStatuses: number[];
51
+ pausePoints: PausePoint[];
52
+ };
53
+
54
+ type SampleArrays = {
55
+ samples: number[];
56
+ timestamps: number[];
57
+ heapSamples: number[];
58
+ optStatuses: number[];
59
+ pausePoints: PausePoint[];
60
+ };
61
+
62
+ /**
63
+ * Wait time after gc() for V8 to stabilize (ms).
64
+ *
65
+ * V8 has 4 compilation tiers: Ignition (interpreter) -> Sparkplug (baseline) ->
66
+ * Maglev (mid-tier optimizer) -> TurboFan (full optimizer). Tiering thresholds:
67
+ * - Ignition -> Sparkplug: 8 invocations
68
+ * - Sparkplug -> Maglev: 500 invocations
69
+ * - Maglev -> TurboFan: 6000 invocations
70
+ *
71
+ * Optimization compilation happens on background threads and requires idle time
72
+ * on the main thread to complete. Without sufficient warmup + settle time,
73
+ * benchmarks exhibit bimodal timing: slow Sparkplug samples (~30% slower) mixed
74
+ * with fast optimized samples.
75
+ *
76
+ * The warmup iterations trigger the optimization decision, then gcSettleTime
77
+ * provides idle time for background compilation to finish before measurement.
78
+ *
79
+ * @see https://v8.dev/blog/sparkplug
80
+ * @see https://v8.dev/blog/maglev
81
+ * @see https://v8.dev/blog/background-compilation
82
+ */
83
+ const gcSettleTime = 1000;
84
+
85
+ const defaultCollectOptions = {
86
+ maxTime: 5000,
87
+ maxIterations: 1000000,
88
+ warmup: 0,
89
+ traceOpt: false,
90
+ noSettle: false,
91
+ };
92
+
93
+ /**
94
+ * V8 optimization status bit meanings:
95
+ * Bit 0 (1): is_function
96
+ * Bit 4 (16): is_optimized (TurboFan)
97
+ * Bit 5 (32): is_optimized (Maglev)
98
+ * Bit 7 (128): is_baseline (Sparkplug)
99
+ * Bit 3 (8): maybe_deoptimized
100
+ */
101
+ const statusNames: Record<number, string> = {
102
+ 1: "interpreted",
103
+ 129: "sparkplug", // 1 + 128
104
+ 17: "turbofan", // 1 + 16
105
+ 33: "maglev", // 1 + 32
106
+ 49: "turbofan+maglev", // 1 + 16 + 32
107
+ 32769: "optimized", // common optimized status
67
108
  };
68
109
 
69
110
  /** @return runner with time and iteration limits */
@@ -79,27 +120,18 @@ export class BasicRunner implements BenchRunner {
79
120
  }
80
121
  }
81
122
 
82
- const defaultCollectOptions = {
83
- maxTime: 5000,
84
- maxIterations: 1000000,
85
- warmup: 0,
86
- traceOpt: false,
87
- noSettle: false,
88
- };
89
-
90
- function buildMeasuredResults(name: string, c: CollectResult): MeasuredResults {
91
- const time = computeStats(c.samples);
123
+ /** @return percentiles and basic statistics */
124
+ export function computeStats(samples: number[]): SampleTimeStats {
125
+ const sorted = [...samples].sort((a, b) => a - b);
126
+ const avg = samples.reduce((sum, s) => sum + s, 0) / samples.length;
92
127
  return {
93
- name,
94
- samples: c.samples,
95
- warmupSamples: c.warmupSamples,
96
- heapSamples: c.heapSamples,
97
- timestamps: c.timestamps,
98
- time,
99
- heapSize: { avg: c.heapGrowth, min: c.heapGrowth, max: c.heapGrowth },
100
- optStatus: c.optStatus,
101
- optSamples: c.optSamples,
102
- pausePoints: c.pausePoints,
128
+ min: sorted[0],
129
+ max: sorted[sorted.length - 1],
130
+ avg,
131
+ p50: percentile(sorted, 0.5),
132
+ p75: percentile(sorted, 0.75),
133
+ p99: percentile(sorted, 0.99),
134
+ p999: percentile(sorted, 0.999),
103
135
  };
104
136
  }
105
137
 
@@ -136,6 +168,34 @@ async function collectSamples<T>(p: CollectParams<T>): Promise<CollectResult> {
136
168
  };
137
169
  }
138
170
 
171
+ function buildMeasuredResults(name: string, c: CollectResult): MeasuredResults {
172
+ const time = computeStats(c.samples);
173
+ return {
174
+ name,
175
+ samples: c.samples,
176
+ warmupSamples: c.warmupSamples,
177
+ heapSamples: c.heapSamples,
178
+ timestamps: c.timestamps,
179
+ time,
180
+ heapSize: { avg: c.heapGrowth, min: c.heapGrowth, max: c.heapGrowth },
181
+ optStatus: c.optStatus,
182
+ optSamples: c.optSamples,
183
+ pausePoints: c.pausePoints,
184
+ };
185
+ }
186
+
187
+ /** @return percentile value with linear interpolation */
188
+ function percentile(sortedArray: number[], p: number): number {
189
+ const index = (sortedArray.length - 1) * p;
190
+ const lower = Math.floor(index);
191
+ const upper = Math.ceil(index);
192
+ const weight = index % 1;
193
+
194
+ if (upper >= sortedArray.length) return sortedArray[sortedArray.length - 1];
195
+
196
+ return sortedArray[lower] * (1 - weight) + sortedArray[upper] * weight;
197
+ }
198
+
139
199
  /** Run warmup iterations with gc + settle time for V8 optimization */
140
200
  async function runWarmup<T>(p: CollectParams<T>): Promise<number[]> {
141
201
  const gc = gcFunction();
@@ -153,55 +213,6 @@ async function runWarmup<T>(p: CollectParams<T>): Promise<number[]> {
153
213
  return samples;
154
214
  }
155
215
 
156
- type SampleLoopResult = {
157
- samples: number[];
158
- heapSamples?: number[];
159
- timestamps?: number[];
160
- optStatuses: number[];
161
- pausePoints: PausePoint[];
162
- };
163
-
164
- /** Estimate sample count for pre-allocation */
165
- function estimateSampleCount(maxTime: number, maxIterations: number): number {
166
- return maxIterations || Math.ceil(maxTime / 0.1); // assume 0.1ms per iteration minimum
167
- }
168
-
169
- type SampleArrays = {
170
- samples: number[];
171
- timestamps: number[];
172
- heapSamples: number[];
173
- optStatuses: number[];
174
- pausePoints: PausePoint[];
175
- };
176
-
177
- /** Pre-allocate arrays to reduce GC pressure during measurement */
178
- function createSampleArrays(
179
- n: number,
180
- trackHeap: boolean,
181
- trackOpt: boolean,
182
- ): SampleArrays {
183
- const arr = (track: boolean) => (track ? new Array<number>(n) : []);
184
- return {
185
- samples: new Array<number>(n),
186
- timestamps: new Array<number>(n),
187
- heapSamples: arr(trackHeap),
188
- optStatuses: arr(trackOpt),
189
- pausePoints: [],
190
- };
191
- }
192
-
193
- /** Trim arrays to actual sample count */
194
- function trimArrays(
195
- a: SampleArrays,
196
- count: number,
197
- trackHeap: boolean,
198
- trackOpt: boolean,
199
- ): void {
200
- a.samples.length = a.timestamps.length = count;
201
- if (trackHeap) a.heapSamples.length = count;
202
- if (trackOpt) a.optStatuses.length = count;
203
- }
204
-
205
216
  /** Collect timing samples with periodic pauses for V8 optimization */
206
217
  async function runSampleLoop<T>(
207
218
  p: CollectParams<T>,
@@ -255,82 +266,6 @@ async function runSampleLoop<T>(
255
266
  };
256
267
  }
257
268
 
258
- /** Check if we should pause at this iteration for V8 optimization */
259
- function shouldPause(
260
- iter: number,
261
- first: number | undefined,
262
- interval: number,
263
- ): boolean {
264
- if (first !== undefined && iter === first) return true;
265
- if (interval <= 0) return false;
266
- if (first === undefined) return iter % interval === 0;
267
- return (iter - first) % interval === 0;
268
- }
269
-
270
- /** @return percentiles and basic statistics */
271
- export function computeStats(samples: number[]): SampleTimeStats {
272
- const sorted = [...samples].sort((a, b) => a - b);
273
- const avg = samples.reduce((sum, s) => sum + s, 0) / samples.length;
274
- return {
275
- min: sorted[0],
276
- max: sorted[sorted.length - 1],
277
- avg,
278
- p50: percentile(sorted, 0.5),
279
- p75: percentile(sorted, 0.75),
280
- p99: percentile(sorted, 0.99),
281
- p999: percentile(sorted, 0.999),
282
- };
283
- }
284
-
285
- /** @return percentile value with linear interpolation */
286
- function percentile(sortedArray: number[], p: number): number {
287
- const index = (sortedArray.length - 1) * p;
288
- const lower = Math.floor(index);
289
- const upper = Math.ceil(index);
290
- const weight = index % 1;
291
-
292
- if (upper >= sortedArray.length) return sortedArray[sortedArray.length - 1];
293
-
294
- return sortedArray[lower] * (1 - weight) + sortedArray[upper] * weight;
295
- }
296
-
297
- /** @return runtime gc() function, or no-op if unavailable */
298
- function gcFunction(): () => void {
299
- const gc = globalThis.gc || (globalThis as any).__gc;
300
- if (gc) return gc;
301
- console.warn("gc() not available, run node/bun with --expose-gc");
302
- return () => {};
303
- }
304
-
305
- /** @return function to get V8 optimization status (requires --allow-natives-syntax) */
306
- function createOptStatusGetter(): ((fn: unknown) => number) | undefined {
307
- try {
308
- // %GetOptimizationStatus returns a bitmask
309
- const getter = new Function("f", "return %GetOptimizationStatus(f)");
310
- getter(() => {});
311
- return getter as (fn: unknown) => number;
312
- } catch {
313
- return undefined;
314
- }
315
- }
316
-
317
- /**
318
- * V8 optimization status bit meanings:
319
- * Bit 0 (1): is_function
320
- * Bit 4 (16): is_optimized (TurboFan)
321
- * Bit 5 (32): is_optimized (Maglev)
322
- * Bit 7 (128): is_baseline (Sparkplug)
323
- * Bit 3 (8): maybe_deoptimized
324
- */
325
- const statusNames: Record<number, string> = {
326
- 1: "interpreted",
327
- 129: "sparkplug", // 1 + 128
328
- 17: "turbofan", // 1 + 16
329
- 33: "maglev", // 1 + 32
330
- 49: "turbofan+maglev", // 1 + 16 + 32
331
- 32769: "optimized", // common optimized status
332
- };
333
-
334
269
  /** @return analysis of V8 optimization status per sample */
335
270
  function analyzeOptStatus(
336
271
  samples: number[],
@@ -362,3 +297,68 @@ function analyzeOptStatus(
362
297
 
363
298
  return { byTier, deoptCount };
364
299
  }
300
+
301
+ /** @return runtime gc() function, or no-op if unavailable */
302
+ function gcFunction(): () => void {
303
+ const gc = globalThis.gc || (globalThis as any).__gc;
304
+ if (gc) return gc;
305
+ console.warn("gc() not available, run node/bun with --expose-gc");
306
+ return () => {};
307
+ }
308
+
309
+ /** @return function to get V8 optimization status (requires --allow-natives-syntax) */
310
+ function createOptStatusGetter(): ((fn: unknown) => number) | undefined {
311
+ try {
312
+ // %GetOptimizationStatus returns a bitmask
313
+ const getter = new Function("f", "return %GetOptimizationStatus(f)");
314
+ getter(() => {});
315
+ return getter as (fn: unknown) => number;
316
+ } catch {
317
+ return undefined;
318
+ }
319
+ }
320
+
321
+ /** Estimate sample count for pre-allocation */
322
+ function estimateSampleCount(maxTime: number, maxIterations: number): number {
323
+ return maxIterations || Math.ceil(maxTime / 0.1); // assume 0.1ms per iteration minimum
324
+ }
325
+
326
+ /** Pre-allocate arrays to reduce GC pressure during measurement */
327
+ function createSampleArrays(
328
+ n: number,
329
+ trackHeap: boolean,
330
+ trackOpt: boolean,
331
+ ): SampleArrays {
332
+ const arr = (track: boolean) => (track ? new Array<number>(n) : []);
333
+ return {
334
+ samples: new Array<number>(n),
335
+ timestamps: new Array<number>(n),
336
+ heapSamples: arr(trackHeap),
337
+ optStatuses: arr(trackOpt),
338
+ pausePoints: [],
339
+ };
340
+ }
341
+
342
+ /** Check if we should pause at this iteration for V8 optimization */
343
+ function shouldPause(
344
+ iter: number,
345
+ first: number | undefined,
346
+ interval: number,
347
+ ): boolean {
348
+ if (first !== undefined && iter === first) return true;
349
+ if (interval <= 0) return false;
350
+ if (first === undefined) return iter % interval === 0;
351
+ return (iter - first) % interval === 0;
352
+ }
353
+
354
+ /** Trim arrays to actual sample count */
355
+ function trimArrays(
356
+ a: SampleArrays,
357
+ count: number,
358
+ trackHeap: boolean,
359
+ trackOpt: boolean,
360
+ ): void {
361
+ a.samples.length = a.timestamps.length = count;
362
+ if (trackHeap) a.heapSamples.length = count;
363
+ if (trackOpt) a.optStatuses.length = count;
364
+ }
@@ -1,14 +1,6 @@
1
1
  import type { BenchmarkSpec } from "../Benchmark.ts";
2
2
  import type { MeasuredResults } from "../MeasuredResults.ts";
3
3
 
4
- /** Execute benchmark with optional parameters */
5
- export function executeBenchmark<T>(
6
- benchmark: BenchmarkSpec<T>,
7
- params?: T,
8
- ): void {
9
- (benchmark.fn as (params?: T) => void)(params);
10
- }
11
-
12
4
  /** Interface for benchmark execution libraries */
13
5
  export interface BenchRunner {
14
6
  runBench<T = unknown>(
@@ -58,3 +50,11 @@ export interface RunnerOptions {
58
50
  /** Heap sampling stack depth */
59
51
  heapDepth?: number;
60
52
  }
53
+
54
+ /** Execute benchmark with optional parameters */
55
+ export function executeBenchmark<T>(
56
+ benchmark: BenchmarkSpec<T>,
57
+ params?: T,
58
+ ): void {
59
+ (benchmark.fn as (params?: T) => void)(params);
60
+ }
@@ -46,28 +46,6 @@ export function parseGcLine(line: string): GcEvent | undefined {
46
46
  return { type, pauseMs, allocated, collected, promoted, survived };
47
47
  }
48
48
 
49
- /** Parse name=value pairs from trace-gc-nvp line */
50
- function parseNvpFields(line: string): Record<string, string> {
51
- const fields: Record<string, string> = {};
52
- // Format: "key=value, key=value, ..." or "key=value key=value"
53
- const matches = line.matchAll(/(\w+)=([^\s,]+)/g);
54
- for (const [, key, value] of matches) {
55
- fields[key] = value;
56
- }
57
- return fields;
58
- }
59
-
60
- /** Map V8 gc type codes to our types */
61
- function parseGcType(gcField: string): GcEvent["type"] {
62
- // V8 uses: s=scavenge, mc=mark-compact, mmc=minor-mc (young gen mark-compact)
63
- if (gcField === "s" || gcField === "scavenge") return "scavenge";
64
- if (gcField === "mc" || gcField === "ms" || gcField === "mark-compact")
65
- return "mark-compact";
66
- if (gcField === "mmc" || gcField === "minor-mc" || gcField === "minor-ms")
67
- return "minor-ms";
68
- return "unknown";
69
- }
70
-
71
49
  /** Aggregate GC events into summary stats */
72
50
  export function aggregateGcStats(events: GcEvent[]): GcStats {
73
51
  let scavenges = 0;
@@ -105,3 +83,25 @@ export function aggregateGcStats(events: GcEvent[]): GcStats {
105
83
  export function emptyGcStats(): GcStats {
106
84
  return { scavenges: 0, markCompacts: 0, totalCollected: 0, gcPauseTime: 0 };
107
85
  }
86
+
87
+ /** Parse name=value pairs from trace-gc-nvp line */
88
+ function parseNvpFields(line: string): Record<string, string> {
89
+ const fields: Record<string, string> = {};
90
+ // Format: "key=value, key=value, ..." or "key=value key=value"
91
+ const matches = line.matchAll(/(\w+)=([^\s,]+)/g);
92
+ for (const [, key, value] of matches) {
93
+ fields[key] = value;
94
+ }
95
+ return fields;
96
+ }
97
+
98
+ /** Map V8 gc type codes to our types */
99
+ function parseGcType(gcField: string): GcEvent["type"] {
100
+ // V8 uses: s=scavenge, mc=mark-compact, mmc=minor-mc (young gen mark-compact)
101
+ if (gcField === "s" || gcField === "scavenge") return "scavenge";
102
+ if (gcField === "mc" || gcField === "ms" || gcField === "mark-compact")
103
+ return "mark-compact";
104
+ if (gcField === "mmc" || gcField === "minor-mc" || gcField === "minor-ms")
105
+ return "minor-ms";
106
+ return "unknown";
107
+ }