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
@@ -2,6 +2,19 @@ import type { GitVersion, GroupData, ReportData } from "./Types.ts";
2
2
 
3
3
  const skipArgs = new Set(["_", "$0", "html", "export-html"]);
4
4
 
5
+ const badgeLabels = {
6
+ faster: "Faster",
7
+ slower: "Slower",
8
+ uncertain: "Inconclusive",
9
+ };
10
+ const defaultArgs: Record<string, unknown> = {
11
+ worker: true,
12
+ time: 5,
13
+ warmup: 500,
14
+ "pause-interval": 0,
15
+ "pause-duration": 100,
16
+ };
17
+
5
18
  /** Format ISO date as local time with UTC: "Jan 9, 2026, 3:45 PM (2026-01-09T23:45:00Z)" */
6
19
  export function formatDateWithTimezone(isoDate: string): string {
7
20
  const date = new Date(isoDate);
@@ -33,73 +46,6 @@ export function formatRelativeTime(isoDate: string): string {
33
46
  return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
34
47
  }
35
48
 
36
- /** Format git version for display: "abc1234* (5m ago)" */
37
- function formatVersion(version?: GitVersion): string {
38
- if (!version || version.hash === "unknown") return "unknown";
39
- const hashDisplay = version.dirty ? `${version.hash}*` : version.hash;
40
- const timeDisplay = version.date ? formatRelativeTime(version.date) : "";
41
- return timeDisplay ? `${hashDisplay} (${timeDisplay})` : hashDisplay;
42
- }
43
-
44
- /** Render current/baseline version info as an HTML div */
45
- function versionInfoHtml(data: ReportData): string {
46
- const { currentVersion, baselineVersion } = data.metadata;
47
- if (!currentVersion && !baselineVersion) return "";
48
- const parts: string[] = [];
49
- if (currentVersion) parts.push(`Current: ${formatVersion(currentVersion)}`);
50
- if (baselineVersion)
51
- parts.push(`Baseline: ${formatVersion(baselineVersion)}`);
52
- return `<div class="version-info">${parts.join(" | ")}</div>`;
53
- }
54
-
55
- const badgeLabels = {
56
- faster: "Faster",
57
- slower: "Slower",
58
- uncertain: "Inconclusive",
59
- };
60
-
61
- /** Render faster/slower/uncertain badge with CI plot container */
62
- function comparisonBadge(group: GroupData, groupIndex: number): string {
63
- const ci = group.benchmarks[0]?.comparisonCI;
64
- if (!ci) return "";
65
- const label = badgeLabels[ci.direction];
66
- return `
67
- <span class="badge badge-${ci.direction}">${label}</span>
68
- <div id="ci-plot-${groupIndex}" class="ci-plot-container"></div>
69
- `;
70
- }
71
- const defaultArgs: Record<string, unknown> = {
72
- worker: true,
73
- time: 5,
74
- warmup: 500,
75
- "pause-interval": 0,
76
- "pause-duration": 100,
77
- };
78
-
79
- /** @return true if this CLI arg should be hidden from the report header */
80
- function shouldSkipArg(
81
- key: string,
82
- value: unknown,
83
- adaptive: unknown,
84
- ): boolean {
85
- if (skipArgs.has(key) || value === undefined || value === false) return true;
86
- if (defaultArgs[key] === value) return true;
87
- if (!key.includes("-") && key !== key.toLowerCase()) return true; // skip yargs camelCase aliases
88
- if (key === "convergence" && !adaptive) return true;
89
- return false;
90
- }
91
-
92
- /** Reconstruct the CLI invocation string, omitting default/internal args */
93
- function formatCliArgs(args?: Record<string, unknown>): string {
94
- if (!args) return "bb bench";
95
- const parts = ["bb bench"];
96
- for (const [key, value] of Object.entries(args)) {
97
- if (shouldSkipArg(key, value, args.adaptive)) continue;
98
- parts.push(value === true ? `--${key}` : `--${key} ${value}`);
99
- }
100
- return parts.join(" ");
101
- }
102
-
103
49
  /** Generate complete HTML document with embedded data and visualizations */
104
50
  export function generateHtmlDocument(data: ReportData): string {
105
51
  return `<!DOCTYPE html>
@@ -282,3 +228,57 @@ export function generateHtmlDocument(data: ReportData): string {
282
228
  </body>
283
229
  </html>`;
284
230
  }
231
+
232
+ /** Reconstruct the CLI invocation string, omitting default/internal args */
233
+ function formatCliArgs(args?: Record<string, unknown>): string {
234
+ if (!args) return "bb bench";
235
+ const parts = ["bb bench"];
236
+ for (const [key, value] of Object.entries(args)) {
237
+ if (shouldSkipArg(key, value, args.adaptive)) continue;
238
+ parts.push(value === true ? `--${key}` : `--${key} ${value}`);
239
+ }
240
+ return parts.join(" ");
241
+ }
242
+
243
+ /** Render current/baseline version info as an HTML div */
244
+ function versionInfoHtml(data: ReportData): string {
245
+ const { currentVersion, baselineVersion } = data.metadata;
246
+ if (!currentVersion && !baselineVersion) return "";
247
+ const parts: string[] = [];
248
+ if (currentVersion) parts.push(`Current: ${formatVersion(currentVersion)}`);
249
+ if (baselineVersion)
250
+ parts.push(`Baseline: ${formatVersion(baselineVersion)}`);
251
+ return `<div class="version-info">${parts.join(" | ")}</div>`;
252
+ }
253
+
254
+ /** Render faster/slower/uncertain badge with CI plot container */
255
+ function comparisonBadge(group: GroupData, groupIndex: number): string {
256
+ const ci = group.benchmarks[0]?.comparisonCI;
257
+ if (!ci) return "";
258
+ const label = badgeLabels[ci.direction];
259
+ return `
260
+ <span class="badge badge-${ci.direction}">${label}</span>
261
+ <div id="ci-plot-${groupIndex}" class="ci-plot-container"></div>
262
+ `;
263
+ }
264
+
265
+ /** @return true if this CLI arg should be hidden from the report header */
266
+ function shouldSkipArg(
267
+ key: string,
268
+ value: unknown,
269
+ adaptive: unknown,
270
+ ): boolean {
271
+ if (skipArgs.has(key) || value === undefined || value === false) return true;
272
+ if (defaultArgs[key] === value) return true;
273
+ if (!key.includes("-") && key !== key.toLowerCase()) return true; // skip yargs camelCase aliases
274
+ if (key === "convergence" && !adaptive) return true;
275
+ return false;
276
+ }
277
+
278
+ /** Format git version for display: "abc1234* (5m ago)" */
279
+ function formatVersion(version?: GitVersion): string {
280
+ if (!version || version.hash === "unknown") return "unknown";
281
+ const hashDisplay = version.dirty ? `${version.hash}*` : version.hash;
282
+ const timeDisplay = version.date ? formatRelativeTime(version.date) : "";
283
+ return timeDisplay ? `${hashDisplay} (${timeDisplay})` : hashDisplay;
284
+ }
@@ -8,6 +8,15 @@ export interface DistributionPlotOptions {
8
8
  direction?: "faster" | "slower" | "uncertain";
9
9
  }
10
10
 
11
+ type Scales = { x: (v: number) => number; y: (v: number) => number };
12
+ type Layout = {
13
+ width: number;
14
+ height: number;
15
+ margin: typeof defaultMargin;
16
+ plot: { w: number; h: number };
17
+ };
18
+ const defaultMargin = { top: 22, right: 12, bottom: 22, left: 12 };
19
+
11
20
  const defaultOpts: Required<DistributionPlotOptions> = {
12
21
  width: 260,
13
22
  height: 85,
@@ -22,14 +31,7 @@ const colors = {
22
31
  uncertain: { fill: "#dbeafe", stroke: "#3b82f6" },
23
32
  };
24
33
 
25
- type Scales = { x: (v: number) => number; y: (v: number) => number };
26
- type Layout = {
27
- width: number;
28
- height: number;
29
- margin: typeof defaultMargin;
30
- plot: { w: number; h: number };
31
- };
32
- const defaultMargin = { top: 22, right: 12, bottom: 22, left: 12 };
34
+ const formatPct = (v: number) => (v >= 0 ? "+" : "") + v.toFixed(0) + "%";
33
35
 
34
36
  /** Create a small distribution plot showing histogram with CI shading */
35
37
  export function createDistributionPlot(
@@ -76,6 +78,14 @@ function buildLayout(width: number, height: number): Layout {
76
78
  return { width, height, margin, plot: { w, h } };
77
79
  }
78
80
 
81
+ function createSvg(w: number, h: number): SVGSVGElement {
82
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
83
+ svg.setAttribute("width", String(w));
84
+ svg.setAttribute("height", String(h));
85
+ if (w && h) svg.setAttribute("viewBox", `0 0 ${w} ${h}`);
86
+ return svg;
87
+ }
88
+
79
89
  /** Compute x/y scale functions mapping data values to SVG coordinates */
80
90
  function buildScales(
81
91
  histogram: HistogramBin[],
@@ -194,28 +204,24 @@ function drawCILabels(
194
204
  );
195
205
  }
196
206
 
197
- /** Apply gaussian kernel smoothing to histogram bins */
198
- function gaussianSmooth(bins: HistogramBin[], sigma: number): HistogramBin[] {
199
- return bins.map((bin, i) => {
200
- let sum = 0;
201
- let wt = 0;
202
- for (let j = 0; j < bins.length; j++) {
203
- const w = Math.exp(-((i - j) ** 2) / (2 * sigma ** 2));
204
- sum += bins[j].count * w;
205
- wt += w;
206
- }
207
- return { x: bin.x, count: sum / wt };
208
- });
209
- }
210
-
211
- const formatPct = (v: number) => (v >= 0 ? "+" : "") + v.toFixed(0) + "%";
212
-
213
- function createSvg(w: number, h: number): SVGSVGElement {
214
- const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
215
- svg.setAttribute("width", String(w));
216
- svg.setAttribute("height", String(h));
217
- if (w && h) svg.setAttribute("viewBox", `0 0 ${w} ${h}`);
218
- return svg;
207
+ function text(
208
+ x: number,
209
+ y: number,
210
+ content: string,
211
+ anchor = "start",
212
+ size = "9",
213
+ fill = "#666",
214
+ weight = "400",
215
+ ): SVGTextElement {
216
+ const el = document.createElementNS("http://www.w3.org/2000/svg", "text");
217
+ el.setAttribute("x", String(x));
218
+ el.setAttribute("y", String(y));
219
+ el.setAttribute("text-anchor", anchor);
220
+ el.setAttribute("font-size", size);
221
+ el.setAttribute("font-weight", weight);
222
+ el.setAttribute("fill", fill);
223
+ el.textContent = content;
224
+ return el;
219
225
  }
220
226
 
221
227
  function rect(
@@ -234,6 +240,20 @@ function rect(
234
240
  return el;
235
241
  }
236
242
 
243
+ /** Apply gaussian kernel smoothing to histogram bins */
244
+ function gaussianSmooth(bins: HistogramBin[], sigma: number): HistogramBin[] {
245
+ return bins.map((bin, i) => {
246
+ let sum = 0;
247
+ let wt = 0;
248
+ for (let j = 0; j < bins.length; j++) {
249
+ const w = Math.exp(-((i - j) ** 2) / (2 * sigma ** 2));
250
+ sum += bins[j].count * w;
251
+ wt += w;
252
+ }
253
+ return { x: bin.x, count: sum / wt };
254
+ });
255
+ }
256
+
237
257
  function path(d: string, attrs: Record<string, string>): SVGPathElement {
238
258
  const el = document.createElementNS("http://www.w3.org/2000/svg", "path");
239
259
  el.setAttribute("d", d);
@@ -257,26 +277,6 @@ function line(
257
277
  return el;
258
278
  }
259
279
 
260
- function text(
261
- x: number,
262
- y: number,
263
- content: string,
264
- anchor = "start",
265
- size = "9",
266
- fill = "#666",
267
- weight = "400",
268
- ): SVGTextElement {
269
- const el = document.createElementNS("http://www.w3.org/2000/svg", "text");
270
- el.setAttribute("x", String(x));
271
- el.setAttribute("y", String(y));
272
- el.setAttribute("text-anchor", anchor);
273
- el.setAttribute("font-size", size);
274
- el.setAttribute("font-weight", weight);
275
- el.setAttribute("fill", fill);
276
- el.textContent = content;
277
- return el;
278
- }
279
-
280
280
  /** Set SVG attributes, converting camelCase keys to kebab-case */
281
281
  function setAttrs(el: SVGElement, attrs: Record<string, string>): void {
282
282
  for (const [k, v] of Object.entries(attrs))
@@ -54,19 +54,6 @@ export function createHistogramKde(
54
54
  });
55
55
  }
56
56
 
57
- function buildColorData(benchmarkNames: string[]) {
58
- const scheme = (d3 as any).schemeObservable10;
59
- const colorMap = new Map(
60
- benchmarkNames.map((name, i) => [name, scheme[i % 10]]),
61
- );
62
- const legendItems: LegendItem[] = benchmarkNames.map((name, i) => ({
63
- color: scheme[i % 10],
64
- label: name,
65
- style: "vertical-bar",
66
- }));
67
- return { colorMap, legendItems };
68
- }
69
-
70
57
  /** Bin samples into grouped histogram bars for each benchmark */
71
58
  function buildBarData(allSamples: Sample[], benchmarkNames: string[]) {
72
59
  const values = allSamples.map(d => d.value);
@@ -116,3 +103,16 @@ function buildBarData(allSamples: Sample[], benchmarkNames: string[]) {
116
103
 
117
104
  return { barData, binMin, binMax, yMax };
118
105
  }
106
+
107
+ function buildColorData(benchmarkNames: string[]) {
108
+ const scheme = (d3 as any).schemeObservable10;
109
+ const colorMap = new Map(
110
+ benchmarkNames.map((name, i) => [name, scheme[i % 10]]),
111
+ );
112
+ const legendItems: LegendItem[] = benchmarkNames.map((name, i) => ({
113
+ color: scheme[i % 10],
114
+ label: name,
115
+ style: "vertical-bar",
116
+ }));
117
+ return { colorMap, legendItems };
118
+ }
@@ -26,6 +26,27 @@ interface LegendPos {
26
26
  yMax: number;
27
27
  }
28
28
 
29
+ /** Build complete legend marks array */
30
+ export function buildLegend(bounds: LegendBounds, items: LegendItem[]): any[] {
31
+ const xRange = bounds.xMax - bounds.xMin;
32
+ const legendX = bounds.xMin + xRange * 0.68;
33
+ const textX = legendX + xRange * 0.04;
34
+ const getY = (i: number) => bounds.yMax * 0.98 - i * (bounds.yMax * 0.08);
35
+
36
+ const marks: any[] = [legendBackground(bounds)];
37
+ for (let i = 0; i < items.length; i++) {
38
+ const pos: LegendPos = {
39
+ legendX,
40
+ y: getY(i),
41
+ textX,
42
+ xRange,
43
+ yMax: bounds.yMax,
44
+ };
45
+ marks.push(symbolMark(pos, items[i]), textMark(pos, items[i].label));
46
+ }
47
+ return marks;
48
+ }
49
+
29
50
  /** Draw a semi-transparent white background behind the legend area */
30
51
  function legendBackground(bounds: LegendBounds): any {
31
52
  const xRange = bounds.xMax - bounds.xMin;
@@ -50,6 +71,33 @@ function legendBackground(bounds: LegendBounds): any {
50
71
  return Plot.rect(data, opts);
51
72
  }
52
73
 
74
+ function symbolMark(pos: LegendPos, item: LegendItem): any {
75
+ switch (item.style) {
76
+ case "filled-dot":
77
+ return dotMark(pos.legendX, pos.y, item.color, true);
78
+ case "hollow-dot":
79
+ return dotMark(pos.legendX, pos.y, item.color, false);
80
+ case "vertical-bar":
81
+ return verticalBarMark(pos, item.color);
82
+ case "vertical-line":
83
+ return verticalLineMark(pos, item.color, item.strokeDash);
84
+ case "rect":
85
+ return rectMark(pos, item.color);
86
+ }
87
+ }
88
+
89
+ function textMark(pos: LegendPos, label: string): any {
90
+ const data = [{ x: pos.textX, y: pos.y, text: label }];
91
+ return Plot.text(data, {
92
+ x: "x",
93
+ y: "y",
94
+ text: "text",
95
+ fontSize: 11,
96
+ textAnchor: "start",
97
+ fill: "#333",
98
+ });
99
+ }
100
+
53
101
  function dotMark(x: number, y: number, color: string, filled: boolean): any {
54
102
  return Plot.dot(
55
103
  [{ x, y }],
@@ -113,51 +161,3 @@ function rectMark(pos: LegendPos, color: string): any {
113
161
  };
114
162
  return Plot.rect(data, opts);
115
163
  }
116
-
117
- function symbolMark(pos: LegendPos, item: LegendItem): any {
118
- switch (item.style) {
119
- case "filled-dot":
120
- return dotMark(pos.legendX, pos.y, item.color, true);
121
- case "hollow-dot":
122
- return dotMark(pos.legendX, pos.y, item.color, false);
123
- case "vertical-bar":
124
- return verticalBarMark(pos, item.color);
125
- case "vertical-line":
126
- return verticalLineMark(pos, item.color, item.strokeDash);
127
- case "rect":
128
- return rectMark(pos, item.color);
129
- }
130
- }
131
-
132
- function textMark(pos: LegendPos, label: string): any {
133
- const data = [{ x: pos.textX, y: pos.y, text: label }];
134
- return Plot.text(data, {
135
- x: "x",
136
- y: "y",
137
- text: "text",
138
- fontSize: 11,
139
- textAnchor: "start",
140
- fill: "#333",
141
- });
142
- }
143
-
144
- /** Build complete legend marks array */
145
- export function buildLegend(bounds: LegendBounds, items: LegendItem[]): any[] {
146
- const xRange = bounds.xMax - bounds.xMin;
147
- const legendX = bounds.xMin + xRange * 0.68;
148
- const textX = legendX + xRange * 0.04;
149
- const getY = (i: number) => bounds.yMax * 0.98 - i * (bounds.yMax * 0.08);
150
-
151
- const marks: any[] = [legendBackground(bounds)];
152
- for (let i = 0; i < items.length; i++) {
153
- const pos: LegendPos = {
154
- legendX,
155
- y: getY(i),
156
- textX,
157
- xRange,
158
- yMax: bounds.yMax,
159
- };
160
- marks.push(symbolMark(pos, items[i]), textMark(pos, items[i].label));
161
- }
162
- return marks;
163
- }
@@ -11,6 +11,18 @@ import type {
11
11
  TimeSeriesPoint,
12
12
  } from "./Types.ts";
13
13
 
14
+ interface PreparedBenchmark extends BenchmarkEntry {
15
+ name: string;
16
+ }
17
+
18
+ interface FlattenedData {
19
+ allSamples: Sample[];
20
+ timeSeries: TimeSeriesPoint[];
21
+ heapSeries: HeapPoint[];
22
+ allGcEvents: GcEvent[];
23
+ allPausePoints: PausePoint[];
24
+ }
25
+
14
26
  /** Render all plots for the benchmark report */
15
27
  export function renderPlots(data: ReportData): void {
16
28
  const gcEnabled = data.metadata.gcTrackingEnabled ?? false;
@@ -72,20 +84,9 @@ function renderGroup(
72
84
  .join("");
73
85
  }
74
86
 
75
- /** Clear a container element and append a freshly created plot */
76
- function renderToContainer(
77
- selector: string,
78
- condition: boolean,
79
- create: () => SVGSVGElement | HTMLElement,
80
- ): void {
81
- const container = document.querySelector(selector);
82
- if (!container || !condition) return;
83
- container.innerHTML = "";
84
- container.appendChild(create());
85
- }
86
-
87
- interface PreparedBenchmark extends BenchmarkEntry {
88
- name: string;
87
+ function showError(groupIndex: number, message: string): void {
88
+ const container = document.querySelector(`#group-${groupIndex}`);
89
+ if (container) container.innerHTML = `<div class="error">${message}</div>`;
89
90
  }
90
91
 
91
92
  /** Combine baseline and benchmarks into a single list with display names */
@@ -102,14 +103,6 @@ function prepareBenchmarks(
102
103
  return benchmarks;
103
104
  }
104
105
 
105
- interface FlattenedData {
106
- allSamples: Sample[];
107
- timeSeries: TimeSeriesPoint[];
108
- heapSeries: HeapPoint[];
109
- allGcEvents: GcEvent[];
110
- allPausePoints: PausePoint[];
111
- }
112
-
113
106
  function flattenSamples(benchmarks: PreparedBenchmark[]): FlattenedData {
114
107
  const result: FlattenedData = {
115
108
  allSamples: [],
@@ -123,6 +116,78 @@ function flattenSamples(benchmarks: PreparedBenchmark[]): FlattenedData {
123
116
  return result;
124
117
  }
125
118
 
119
+ /** Clear a container element and append a freshly created plot */
120
+ function renderToContainer(
121
+ selector: string,
122
+ condition: boolean,
123
+ create: () => SVGSVGElement | HTMLElement,
124
+ ): void {
125
+ const container = document.querySelector(selector);
126
+ if (!container || !condition) return;
127
+ container.innerHTML = "";
128
+ container.appendChild(create());
129
+ }
130
+
131
+ function generateStatsHtml(b: PreparedBenchmark, gcEnabled: boolean): string {
132
+ const ciHtml = generateCIHtml(b.comparisonCI);
133
+
134
+ if (b.sectionStats?.length) {
135
+ const stats = gcEnabled
136
+ ? b.sectionStats
137
+ : b.sectionStats.filter(s => s.groupTitle !== "gc");
138
+ const statsHtml = stats
139
+ .map(
140
+ stat => `
141
+ <div class="stat-item">
142
+ <div class="stat-label">${stat.groupTitle ? stat.groupTitle + " " : ""}${stat.label}</div>
143
+ <div class="stat-value">${stat.value}</div>
144
+ </div>
145
+ `,
146
+ )
147
+ .join("");
148
+ return `
149
+ <div class="summary-stats">
150
+ <h3 style="margin-bottom: 10px; color: #333;">${b.name}</h3>
151
+ <div class="stats-grid">${ciHtml}${statsHtml}</div>
152
+ </div>
153
+ `;
154
+ }
155
+
156
+ // Fallback to hardcoded stats
157
+ return `
158
+ <div class="summary-stats">
159
+ <h3 style="margin-bottom: 10px; color: #333;">${b.name}</h3>
160
+ <div class="stats-grid">
161
+ ${ciHtml}
162
+ <div class="stat-item">
163
+ <div class="stat-label">Min</div>
164
+ <div class="stat-value">${b.stats.min.toFixed(3)}ms</div>
165
+ </div>
166
+ <div class="stat-item">
167
+ <div class="stat-label">Median</div>
168
+ <div class="stat-value">${b.stats.p50.toFixed(3)}ms</div>
169
+ </div>
170
+ <div class="stat-item">
171
+ <div class="stat-label">Mean</div>
172
+ <div class="stat-value">${b.stats.avg.toFixed(3)}ms</div>
173
+ </div>
174
+ <div class="stat-item">
175
+ <div class="stat-label">Max</div>
176
+ <div class="stat-value">${b.stats.max.toFixed(3)}ms</div>
177
+ </div>
178
+ <div class="stat-item">
179
+ <div class="stat-label">P75</div>
180
+ <div class="stat-value">${b.stats.p75.toFixed(3)}ms</div>
181
+ </div>
182
+ <div class="stat-item">
183
+ <div class="stat-label">P99</div>
184
+ <div class="stat-value">${b.stats.p99.toFixed(3)}ms</div>
185
+ </div>
186
+ </div>
187
+ </div>
188
+ `;
189
+ }
190
+
126
191
  /** Extract time series, heap, GC, and pause data from one benchmark */
127
192
  function flattenBenchmark(b: PreparedBenchmark, out: FlattenedData): void {
128
193
  const warmupCount = b.warmupSamples?.length || 0;
@@ -171,26 +236,6 @@ function flattenBenchmark(b: PreparedBenchmark, out: FlattenedData): void {
171
236
  });
172
237
  }
173
238
 
174
- function cumulativeSum(arr: number[]): number[] {
175
- const result: number[] = [];
176
- let sum = 0;
177
- for (const v of arr) {
178
- sum += v;
179
- result.push(sum);
180
- }
181
- return result;
182
- }
183
-
184
- function showError(groupIndex: number, message: string): void {
185
- const container = document.querySelector(`#group-${groupIndex}`);
186
- if (container) container.innerHTML = `<div class="error">${message}</div>`;
187
- }
188
-
189
- function formatPct(v: number): string {
190
- const sign = v >= 0 ? "+" : "";
191
- return sign + v.toFixed(1) + "%";
192
- }
193
-
194
239
  function generateCIHtml(ci: BenchmarkEntry["comparisonCI"]): string {
195
240
  if (!ci) return "";
196
241
  const text = `${formatPct(ci.percent)} [${formatPct(ci.ci[0])}, ${formatPct(ci.ci[1])}]`;
@@ -202,62 +247,17 @@ function generateCIHtml(ci: BenchmarkEntry["comparisonCI"]): string {
202
247
  `;
203
248
  }
204
249
 
205
- function generateStatsHtml(b: PreparedBenchmark, gcEnabled: boolean): string {
206
- const ciHtml = generateCIHtml(b.comparisonCI);
207
-
208
- if (b.sectionStats?.length) {
209
- const stats = gcEnabled
210
- ? b.sectionStats
211
- : b.sectionStats.filter(s => s.groupTitle !== "gc");
212
- const statsHtml = stats
213
- .map(
214
- stat => `
215
- <div class="stat-item">
216
- <div class="stat-label">${stat.groupTitle ? stat.groupTitle + " " : ""}${stat.label}</div>
217
- <div class="stat-value">${stat.value}</div>
218
- </div>
219
- `,
220
- )
221
- .join("");
222
- return `
223
- <div class="summary-stats">
224
- <h3 style="margin-bottom: 10px; color: #333;">${b.name}</h3>
225
- <div class="stats-grid">${ciHtml}${statsHtml}</div>
226
- </div>
227
- `;
250
+ function cumulativeSum(arr: number[]): number[] {
251
+ const result: number[] = [];
252
+ let sum = 0;
253
+ for (const v of arr) {
254
+ sum += v;
255
+ result.push(sum);
228
256
  }
257
+ return result;
258
+ }
229
259
 
230
- // Fallback to hardcoded stats
231
- return `
232
- <div class="summary-stats">
233
- <h3 style="margin-bottom: 10px; color: #333;">${b.name}</h3>
234
- <div class="stats-grid">
235
- ${ciHtml}
236
- <div class="stat-item">
237
- <div class="stat-label">Min</div>
238
- <div class="stat-value">${b.stats.min.toFixed(3)}ms</div>
239
- </div>
240
- <div class="stat-item">
241
- <div class="stat-label">Median</div>
242
- <div class="stat-value">${b.stats.p50.toFixed(3)}ms</div>
243
- </div>
244
- <div class="stat-item">
245
- <div class="stat-label">Mean</div>
246
- <div class="stat-value">${b.stats.avg.toFixed(3)}ms</div>
247
- </div>
248
- <div class="stat-item">
249
- <div class="stat-label">Max</div>
250
- <div class="stat-value">${b.stats.max.toFixed(3)}ms</div>
251
- </div>
252
- <div class="stat-item">
253
- <div class="stat-label">P75</div>
254
- <div class="stat-value">${b.stats.p75.toFixed(3)}ms</div>
255
- </div>
256
- <div class="stat-item">
257
- <div class="stat-label">P99</div>
258
- <div class="stat-value">${b.stats.p99.toFixed(3)}ms</div>
259
- </div>
260
- </div>
261
- </div>
262
- `;
260
+ function formatPct(v: number): string {
261
+ const sign = v >= 0 ? "+" : "";
262
+ return sign + v.toFixed(1) + "%";
263
263
  }