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
@@ -1,13 +1,3 @@
1
- const outlierMultiplier = 1.5; // Tukey's fence multiplier
2
- const bootstrapSamples = 10000;
3
- const confidence = 0.95;
4
-
5
- /** Options for bootstrap resampling methods */
6
- type BootstrapOptions = {
7
- resamples?: number;
8
- confidence?: number;
9
- };
10
-
11
1
  /** Bootstrap estimate with confidence interval and raw resample data */
12
2
  export interface BootstrapResult {
13
3
  estimate: number;
@@ -15,6 +5,32 @@ export interface BootstrapResult {
15
5
  samples: number[];
16
6
  }
17
7
 
8
+ export type CIDirection = "faster" | "slower" | "uncertain";
9
+
10
+ /** Binned histogram for efficient transfer to browser */
11
+ export interface HistogramBin {
12
+ x: number; // bin center
13
+ count: number;
14
+ }
15
+
16
+ /** Bootstrap confidence interval for percentage difference between two samples */
17
+ export interface DifferenceCI {
18
+ percent: number;
19
+ ci: [number, number];
20
+ direction: CIDirection;
21
+ /** Histogram of bootstrap distribution for visualization */
22
+ histogram?: HistogramBin[];
23
+ }
24
+
25
+ /** Options for bootstrap resampling methods */
26
+ type BootstrapOptions = {
27
+ resamples?: number;
28
+ confidence?: number;
29
+ };
30
+ const confidence = 0.95;
31
+ const outlierMultiplier = 1.5; // Tukey's fence multiplier
32
+ const bootstrapSamples = 10000;
33
+
18
34
  /** @return relative standard deviation (coefficient of variation) */
19
35
  export function coefficientOfVariation(samples: number[]): number {
20
36
  const mean = average(samples);
@@ -86,13 +102,6 @@ export function percentile(values: number[], p: number): number {
86
102
  return sorted[Math.max(0, index)];
87
103
  }
88
104
 
89
- /** @return medians from bootstrap resamples */
90
- function generateMedians(samples: number[], resamples: number): number[] {
91
- return Array.from({ length: resamples }, () =>
92
- percentile(createResample(samples), 0.5),
93
- );
94
- }
95
-
96
105
  /** @return bootstrap resample with replacement */
97
106
  export function createResample(samples: number[]): number[] {
98
107
  const n = samples.length;
@@ -100,50 +109,6 @@ export function createResample(samples: number[]): number[] {
100
109
  return Array.from({ length: n }, rand);
101
110
  }
102
111
 
103
- /** @return confidence interval [lower, upper] */
104
- function computeInterval(
105
- medians: number[],
106
- confidence: number,
107
- ): [number, number] {
108
- const alpha = (1 - confidence) / 2;
109
- const lower = percentile(medians, alpha);
110
- const upper = percentile(medians, 1 - alpha);
111
- return [lower, upper];
112
- }
113
-
114
- export type CIDirection = "faster" | "slower" | "uncertain";
115
-
116
- /** Binned histogram for efficient transfer to browser */
117
- export interface HistogramBin {
118
- x: number; // bin center
119
- count: number;
120
- }
121
-
122
- /** Bootstrap confidence interval for percentage difference between two samples */
123
- export interface DifferenceCI {
124
- percent: number;
125
- ci: [number, number];
126
- direction: CIDirection;
127
- /** Histogram of bootstrap distribution for visualization */
128
- histogram?: HistogramBin[];
129
- }
130
-
131
- /** Bin values into histogram for compact visualization */
132
- function binValues(values: number[], binCount = 30): HistogramBin[] {
133
- const sorted = [...values].sort((a, b) => a - b);
134
- const min = sorted[0];
135
- const max = sorted[sorted.length - 1];
136
- if (min === max) return [{ x: min, count: values.length }];
137
-
138
- const step = (max - min) / binCount;
139
- const counts = new Array(binCount).fill(0);
140
- for (const v of values) {
141
- const bin = Math.min(Math.floor((v - min) / step), binCount - 1);
142
- counts[bin]++;
143
- }
144
- return counts.map((count, i) => ({ x: min + (i + 0.5) * step, count }));
145
- }
146
-
147
112
  /** @return bootstrap CI for percentage difference between baseline and current medians */
148
113
  export function bootstrapDifferenceCI(
149
114
  baseline: number[],
@@ -174,3 +139,37 @@ export function bootstrapDifferenceCI(
174
139
  const histogram = binValues(diffs);
175
140
  return { percent: observedPercent, ci, direction, histogram };
176
141
  }
142
+
143
+ /** @return medians from bootstrap resamples */
144
+ function generateMedians(samples: number[], resamples: number): number[] {
145
+ return Array.from({ length: resamples }, () =>
146
+ percentile(createResample(samples), 0.5),
147
+ );
148
+ }
149
+
150
+ /** @return confidence interval [lower, upper] */
151
+ function computeInterval(
152
+ medians: number[],
153
+ confidence: number,
154
+ ): [number, number] {
155
+ const alpha = (1 - confidence) / 2;
156
+ const lower = percentile(medians, alpha);
157
+ const upper = percentile(medians, 1 - alpha);
158
+ return [lower, upper];
159
+ }
160
+
161
+ /** Bin values into histogram for compact visualization */
162
+ function binValues(values: number[], binCount = 30): HistogramBin[] {
163
+ const sorted = [...values].sort((a, b) => a - b);
164
+ const min = sorted[0];
165
+ const max = sorted[sorted.length - 1];
166
+ if (min === max) return [{ x: min, count: values.length }];
167
+
168
+ const step = (max - min) / binCount;
169
+ const counts = new Array(binCount).fill(0);
170
+ for (const v of values) {
171
+ const bin = Math.min(Math.floor((v - min) / step), binCount - 1);
172
+ counts[bin]++;
173
+ }
174
+ return counts.map((count, i) => ({ x: min + (i + 0.5) * step, count }));
175
+ }
@@ -32,13 +32,13 @@ export function parseGcTraceEvents(traceEvents: TraceEvent[]): GcEvent[] {
32
32
  });
33
33
  }
34
34
 
35
+ /** Parse CDP trace events and aggregate into GcStats */
36
+ export function browserGcStats(traceEvents: TraceEvent[]): GcStats {
37
+ return aggregateGcStats(parseGcTraceEvents(traceEvents));
38
+ }
39
+
35
40
  function gcType(name: string): GcEvent["type"] | undefined {
36
41
  if (name === "MinorGC") return "scavenge";
37
42
  if (name === "MajorGC") return "mark-compact";
38
43
  return undefined;
39
44
  }
40
-
41
- /** Parse CDP trace events and aggregate into GcStats */
42
- export function browserGcStats(traceEvents: TraceEvent[]): GcStats {
43
- return aggregateGcStats(parseGcTraceEvents(traceEvents));
44
- }
@@ -93,6 +93,32 @@ export async function profileBrowser(
93
93
  }
94
94
  }
95
95
 
96
+ /** Forward Chrome's stdout/stderr to the terminal so V8 flag output is visible. */
97
+ function pipeChromeOutput(server: BrowserServer): void {
98
+ const proc = server.process();
99
+ const pipe = (stream: NodeJS.ReadableStream | null) =>
100
+ stream?.on("data", (chunk: Buffer) => {
101
+ for (const line of chunk.toString().split("\n")) {
102
+ const text = line.trim();
103
+ if (text) process.stderr.write(`[chrome] ${text}\n`);
104
+ }
105
+ });
106
+ pipe(proc.stdout);
107
+ pipe(proc.stderr);
108
+ }
109
+
110
+ /** Start CDP GC tracing, returns the event collector array. */
111
+ async function startGcTracing(cdp: CDPSession): Promise<TraceEvent[]> {
112
+ const events: TraceEvent[] = [];
113
+ cdp.on("Tracing.dataCollected", ({ value }) => {
114
+ for (const e of value) events.push(e as unknown as TraceEvent);
115
+ });
116
+ await cdp.send("Tracing.start", {
117
+ traceConfig: { includedCategories: ["v8", "v8.gc"] },
118
+ });
119
+ return events;
120
+ }
121
+
96
122
  /** Inject __start/__lap as in-page functions, expose __done for results collection.
97
123
  * __start/__lap are pure in-page (zero CDP overhead). First __start() triggers
98
124
  * instrument start. __done() stops instruments and collects timing data. */
@@ -147,57 +173,6 @@ async function setupLapMode(
147
173
  return { promise, cancel: () => clearTimeout(timer) };
148
174
  }
149
175
 
150
- /** In-page timing functions injected via addInitScript (zero CDP overhead).
151
- * __start/__lap collect timestamps, __done delegates to exposed __benchCollect. */
152
- function injectLapFunctions(): void {
153
- const g = globalThis as any;
154
- g.__benchSamples = [];
155
- g.__benchLastTime = 0;
156
- g.__benchFirstStart = 0;
157
-
158
- g.__start = () => {
159
- const now = performance.now();
160
- g.__benchLastTime = now;
161
- if (!g.__benchFirstStart) {
162
- g.__benchFirstStart = now;
163
- return g.__benchInstrumentStart();
164
- }
165
- };
166
-
167
- g.__lap = () => {
168
- const now = performance.now();
169
- g.__benchSamples.push(now - g.__benchLastTime);
170
- g.__benchLastTime = now;
171
- };
172
-
173
- g.__done = () => {
174
- const wall = g.__benchFirstStart
175
- ? performance.now() - g.__benchFirstStart
176
- : 0;
177
- return g.__benchCollect(g.__benchSamples.slice(), wall);
178
- };
179
- }
180
-
181
- function heapSamplingParams(samplingInterval: number) {
182
- return {
183
- samplingInterval,
184
- includeObjectsCollectedByMajorGC: true,
185
- includeObjectsCollectedByMinorGC: true,
186
- };
187
- }
188
-
189
- /** Start CDP GC tracing, returns the event collector array. */
190
- async function startGcTracing(cdp: CDPSession): Promise<TraceEvent[]> {
191
- const events: TraceEvent[] = [];
192
- cdp.on("Tracing.dataCollected", ({ value }) => {
193
- for (const e of value) events.push(e as unknown as TraceEvent);
194
- });
195
- await cdp.send("Tracing.start", {
196
- traceConfig: { includedCategories: ["v8", "v8.gc"] },
197
- });
198
- return events;
199
- }
200
-
201
176
  /** Bench function mode: run window.__bench in a timed iteration loop. */
202
177
  async function runBenchLoop(
203
178
  page: Page,
@@ -254,18 +229,43 @@ async function collectTracing(
254
229
  return browserGcStats(traceEvents);
255
230
  }
256
231
 
257
- /** Forward Chrome's stdout/stderr to the terminal so V8 flag output is visible. */
258
- function pipeChromeOutput(server: BrowserServer): void {
259
- const proc = server.process();
260
- const pipe = (stream: NodeJS.ReadableStream | null) =>
261
- stream?.on("data", (chunk: Buffer) => {
262
- for (const line of chunk.toString().split("\n")) {
263
- const text = line.trim();
264
- if (text) process.stderr.write(`[chrome] ${text}\n`);
265
- }
266
- });
267
- pipe(proc.stdout);
268
- pipe(proc.stderr);
232
+ function heapSamplingParams(samplingInterval: number) {
233
+ return {
234
+ samplingInterval,
235
+ includeObjectsCollectedByMajorGC: true,
236
+ includeObjectsCollectedByMinorGC: true,
237
+ };
238
+ }
239
+
240
+ /** In-page timing functions injected via addInitScript (zero CDP overhead).
241
+ * __start/__lap collect timestamps, __done delegates to exposed __benchCollect. */
242
+ function injectLapFunctions(): void {
243
+ const g = globalThis as any;
244
+ g.__benchSamples = [];
245
+ g.__benchLastTime = 0;
246
+ g.__benchFirstStart = 0;
247
+
248
+ g.__start = () => {
249
+ const now = performance.now();
250
+ g.__benchLastTime = now;
251
+ if (!g.__benchFirstStart) {
252
+ g.__benchFirstStart = now;
253
+ return g.__benchInstrumentStart();
254
+ }
255
+ };
256
+
257
+ g.__lap = () => {
258
+ const now = performance.now();
259
+ g.__benchSamples.push(now - g.__benchLastTime);
260
+ g.__benchLastTime = now;
261
+ };
262
+
263
+ g.__done = () => {
264
+ const wall = g.__benchFirstStart
265
+ ? performance.now() - g.__benchFirstStart
266
+ : 0;
267
+ return g.__benchCollect(g.__benchSamples.slice(), wall);
268
+ };
269
269
  }
270
270
 
271
271
  export { profileBrowser as profileBrowserHeap };
@@ -1,12 +1,14 @@
1
1
  import type { Argv, InferredOptionTypes } from "yargs";
2
2
  import yargs from "yargs";
3
3
 
4
- export const defaultAdaptiveMaxTime = 20;
5
-
6
4
  export type Configure<T> = (yargs: Argv) => Argv<T>;
7
5
 
8
- /** CLI args type inferred from cliOptions */
9
- export type DefaultCliArgs = InferredOptionTypes<typeof cliOptions>;
6
+ /** CLI args type inferred from cliOptions, plus optional file positional */
7
+ export type DefaultCliArgs = InferredOptionTypes<typeof cliOptions> & {
8
+ file?: string;
9
+ };
10
+
11
+ export const defaultAdaptiveMaxTime = 20;
10
12
 
11
13
  // biome-ignore format: compact option definitions
12
14
  const cliOptions = {
@@ -25,7 +27,9 @@ const cliOptions = {
25
27
  html: { type: "boolean", default: false, describe: "generate HTML report and open in browser" },
26
28
  "export-html": { type: "string", requiresArg: true, describe: "export HTML report to specified file" },
27
29
  json: { type: "string", requiresArg: true, describe: "export benchmark data to JSON file" },
28
- perfetto: { type: "string", requiresArg: true, describe: "export Perfetto trace file (view at ui.perfetto.dev)" },
30
+ "export-perfetto": { type: "string", requiresArg: true, describe: "export Perfetto trace file (view at ui.perfetto.dev)" },
31
+ speedscope: { type: "boolean", default: false, describe: "open heap profile in speedscope (via npx)" },
32
+ "export-speedscope": { type: "string", requiresArg: true, describe: "export heap profile as speedscope JSON" },
29
33
  "trace-opt": { type: "boolean", default: false, describe: "trace V8 optimization tiers (requires --allow-natives-syntax)" },
30
34
  "skip-settle": { type: "boolean", default: false, describe: "skip post-warmup settle time (see V8 optimization cold start)" },
31
35
  "pause-first": { type: "number", describe: "iterations before first pause (then pause-interval applies)" },
@@ -39,6 +43,7 @@ const cliOptions = {
39
43
  "heap-rows": { type: "number", default: 20, describe: "top allocation sites to show" },
40
44
  "heap-stack": { type: "number", default: 3, describe: "call stack depth to display" },
41
45
  "heap-verbose": { type: "boolean", default: false, describe: "verbose output with file:// paths and line numbers" },
46
+ "heap-raw": { type: "boolean", default: false, describe: "dump every raw heap sample (ordinal, size, stack)" },
42
47
  "heap-user-only": { type: "boolean", default: false, describe: "filter to user code only (hide node internals)" },
43
48
  url: { type: "string", requiresArg: true, describe: "page URL for browser profiling (enables browser mode)" },
44
49
  headless: { type: "boolean", default: true, describe: "run browser in headless mode" },
@@ -48,7 +53,16 @@ const cliOptions = {
48
53
 
49
54
  /** @return yargs with standard benchmark options */
50
55
  export function defaultCliArgs(yargsInstance: Argv): Argv<DefaultCliArgs> {
51
- return yargsInstance.options(cliOptions).help().strict();
56
+ return yargsInstance
57
+ .command("$0 [file]", "run benchmarks", y => {
58
+ y.positional("file", {
59
+ type: "string",
60
+ describe: "benchmark file to run",
61
+ });
62
+ })
63
+ .options(cliOptions)
64
+ .help()
65
+ .strict() as Argv<DefaultCliArgs>;
52
66
  }
53
67
 
54
68
  /** @return parsed command line arguments */
@@ -55,14 +55,14 @@ function stripCaseSuffix(name: string): string {
55
55
  return name.replace(/ \[.*?\]$/, "");
56
56
  }
57
57
 
58
- /** Escape regex special characters */
59
- function escapeRegex(str: string): string {
60
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
61
- }
62
-
63
58
  /** Ensure at least one benchmark matches filter */
64
59
  function validateFilteredSuite(groups: BenchGroup[], filter?: string): void {
65
60
  if (groups.every(g => g.benchmarks.length === 0)) {
66
61
  throw new Error(`No benchmarks match filter: "${filter}"`);
67
62
  }
68
63
  }
64
+
65
+ /** Escape regex special characters */
66
+ function escapeRegex(str: string): string {
67
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
68
+ }