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,12 @@ import * as Plot from "@observablehq/plot";
2
2
  import * as d3 from "d3";
3
3
 
4
4
  //#region src/html/browser/CIPlot.ts
5
+ const defaultMargin = {
6
+ top: 22,
7
+ right: 12,
8
+ bottom: 22,
9
+ left: 12
10
+ };
5
11
  const defaultOpts = {
6
12
  width: 260,
7
13
  height: 85,
@@ -23,12 +29,7 @@ const colors = {
23
29
  stroke: "#3b82f6"
24
30
  }
25
31
  };
26
- const defaultMargin = {
27
- top: 22,
28
- right: 12,
29
- bottom: 22,
30
- left: 12
31
- };
32
+ const formatPct$1 = (v) => (v >= 0 ? "+" : "") + v.toFixed(0) + "%";
32
33
  /** Create a small distribution plot showing histogram with CI shading */
33
34
  function createDistributionPlot(histogram, ci, pointEstimate, options = {}) {
34
35
  const opts = {
@@ -68,6 +69,13 @@ function buildLayout(width, height) {
68
69
  }
69
70
  };
70
71
  }
72
+ function createSvg(w, h) {
73
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
74
+ svg.setAttribute("width", String(w));
75
+ svg.setAttribute("height", String(h));
76
+ if (w && h) svg.setAttribute("viewBox", `0 0 ${w} ${h}`);
77
+ return svg;
78
+ }
71
79
  /** Compute x/y scale functions mapping data values to SVG coordinates */
72
80
  function buildScales(histogram, ci, layout) {
73
81
  const { margin, plot } = layout;
@@ -140,6 +148,26 @@ function drawCILabels(svg, ci, scales, height) {
140
148
  svg.appendChild(text(scales.x(ci[0]), height - 4, formatPct$1(ci[0]), "middle", "12"));
141
149
  svg.appendChild(text(scales.x(ci[1]), height - 4, formatPct$1(ci[1]), "middle", "12"));
142
150
  }
151
+ function text(x, y, content, anchor = "start", size = "9", fill = "#666", weight = "400") {
152
+ const el = document.createElementNS("http://www.w3.org/2000/svg", "text");
153
+ el.setAttribute("x", String(x));
154
+ el.setAttribute("y", String(y));
155
+ el.setAttribute("text-anchor", anchor);
156
+ el.setAttribute("font-size", size);
157
+ el.setAttribute("font-weight", weight);
158
+ el.setAttribute("fill", fill);
159
+ el.textContent = content;
160
+ return el;
161
+ }
162
+ function rect(x, y, w, h, attrs) {
163
+ const el = document.createElementNS("http://www.w3.org/2000/svg", "rect");
164
+ el.setAttribute("x", String(x));
165
+ el.setAttribute("y", String(y));
166
+ el.setAttribute("width", String(w));
167
+ el.setAttribute("height", String(h));
168
+ setAttrs(el, attrs);
169
+ return el;
170
+ }
143
171
  /** Apply gaussian kernel smoothing to histogram bins */
144
172
  function gaussianSmooth(bins, sigma) {
145
173
  return bins.map((bin, i) => {
@@ -156,23 +184,6 @@ function gaussianSmooth(bins, sigma) {
156
184
  };
157
185
  });
158
186
  }
159
- const formatPct$1 = (v) => (v >= 0 ? "+" : "") + v.toFixed(0) + "%";
160
- function createSvg(w, h) {
161
- const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
162
- svg.setAttribute("width", String(w));
163
- svg.setAttribute("height", String(h));
164
- if (w && h) svg.setAttribute("viewBox", `0 0 ${w} ${h}`);
165
- return svg;
166
- }
167
- function rect(x, y, w, h, attrs) {
168
- const el = document.createElementNS("http://www.w3.org/2000/svg", "rect");
169
- el.setAttribute("x", String(x));
170
- el.setAttribute("y", String(y));
171
- el.setAttribute("width", String(w));
172
- el.setAttribute("height", String(h));
173
- setAttrs(el, attrs);
174
- return el;
175
- }
176
187
  function path(d, attrs) {
177
188
  const el = document.createElementNS("http://www.w3.org/2000/svg", "path");
178
189
  el.setAttribute("d", d);
@@ -188,17 +199,6 @@ function line(x1, y1, x2, y2, attrs) {
188
199
  setAttrs(el, attrs);
189
200
  return el;
190
201
  }
191
- function text(x, y, content, anchor = "start", size = "9", fill = "#666", weight = "400") {
192
- const el = document.createElementNS("http://www.w3.org/2000/svg", "text");
193
- el.setAttribute("x", String(x));
194
- el.setAttribute("y", String(y));
195
- el.setAttribute("text-anchor", anchor);
196
- el.setAttribute("font-size", size);
197
- el.setAttribute("font-weight", weight);
198
- el.setAttribute("fill", fill);
199
- el.textContent = content;
200
- return el;
201
- }
202
202
  /** Set SVG attributes, converting camelCase keys to kebab-case */
203
203
  function setAttrs(el, attrs) {
204
204
  for (const [k, v] of Object.entries(attrs)) el.setAttribute(k.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase()), v);
@@ -206,6 +206,25 @@ function setAttrs(el, attrs) {
206
206
 
207
207
  //#endregion
208
208
  //#region src/html/browser/LegendUtils.ts
209
+ /** Build complete legend marks array */
210
+ function buildLegend(bounds, items) {
211
+ const xRange = bounds.xMax - bounds.xMin;
212
+ const legendX = bounds.xMin + xRange * .68;
213
+ const textX = legendX + xRange * .04;
214
+ const getY = (i) => bounds.yMax * .98 - i * (bounds.yMax * .08);
215
+ const marks = [legendBackground(bounds)];
216
+ for (let i = 0; i < items.length; i++) {
217
+ const pos = {
218
+ legendX,
219
+ y: getY(i),
220
+ textX,
221
+ xRange,
222
+ yMax: bounds.yMax
223
+ };
224
+ marks.push(symbolMark(pos, items[i]), textMark(pos, items[i].label));
225
+ }
226
+ return marks;
227
+ }
209
228
  /** Draw a semi-transparent white background behind the legend area */
210
229
  function legendBackground(bounds) {
211
230
  const xRange = bounds.xMax - bounds.xMin;
@@ -226,6 +245,30 @@ function legendBackground(bounds) {
226
245
  strokeWidth: 1
227
246
  });
228
247
  }
248
+ function symbolMark(pos, item) {
249
+ switch (item.style) {
250
+ case "filled-dot": return dotMark(pos.legendX, pos.y, item.color, true);
251
+ case "hollow-dot": return dotMark(pos.legendX, pos.y, item.color, false);
252
+ case "vertical-bar": return verticalBarMark(pos, item.color);
253
+ case "vertical-line": return verticalLineMark(pos, item.color, item.strokeDash);
254
+ case "rect": return rectMark(pos, item.color);
255
+ }
256
+ }
257
+ function textMark(pos, label) {
258
+ const data = [{
259
+ x: pos.textX,
260
+ y: pos.y,
261
+ text: label
262
+ }];
263
+ return Plot.text(data, {
264
+ x: "x",
265
+ y: "y",
266
+ text: "text",
267
+ fontSize: 11,
268
+ textAnchor: "start",
269
+ fill: "#333"
270
+ });
271
+ }
229
272
  function dotMark(x, y, color, filled) {
230
273
  return Plot.dot([{
231
274
  x,
@@ -293,49 +336,6 @@ function rectMark(pos, color) {
293
336
  };
294
337
  return Plot.rect(data, opts);
295
338
  }
296
- function symbolMark(pos, item) {
297
- switch (item.style) {
298
- case "filled-dot": return dotMark(pos.legendX, pos.y, item.color, true);
299
- case "hollow-dot": return dotMark(pos.legendX, pos.y, item.color, false);
300
- case "vertical-bar": return verticalBarMark(pos, item.color);
301
- case "vertical-line": return verticalLineMark(pos, item.color, item.strokeDash);
302
- case "rect": return rectMark(pos, item.color);
303
- }
304
- }
305
- function textMark(pos, label) {
306
- const data = [{
307
- x: pos.textX,
308
- y: pos.y,
309
- text: label
310
- }];
311
- return Plot.text(data, {
312
- x: "x",
313
- y: "y",
314
- text: "text",
315
- fontSize: 11,
316
- textAnchor: "start",
317
- fill: "#333"
318
- });
319
- }
320
- /** Build complete legend marks array */
321
- function buildLegend(bounds, items) {
322
- const xRange = bounds.xMax - bounds.xMin;
323
- const legendX = bounds.xMin + xRange * .68;
324
- const textX = legendX + xRange * .04;
325
- const getY = (i) => bounds.yMax * .98 - i * (bounds.yMax * .08);
326
- const marks = [legendBackground(bounds)];
327
- for (let i = 0; i < items.length; i++) {
328
- const pos = {
329
- legendX,
330
- y: getY(i),
331
- textX,
332
- xRange,
333
- yMax: bounds.yMax
334
- };
335
- marks.push(symbolMark(pos, items[i]), textMark(pos, items[i].label));
336
- }
337
- return marks;
338
- }
339
339
 
340
340
  //#endregion
341
341
  //#region src/html/browser/HistogramKde.ts
@@ -386,17 +386,6 @@ function createHistogramKde(allSamples, benchmarkNames) {
386
386
  ]
387
387
  });
388
388
  }
389
- function buildColorData(benchmarkNames) {
390
- const scheme = d3.schemeObservable10;
391
- return {
392
- colorMap: new Map(benchmarkNames.map((name, i) => [name, scheme[i % 10]])),
393
- legendItems: benchmarkNames.map((name, i) => ({
394
- color: scheme[i % 10],
395
- label: name,
396
- style: "vertical-bar"
397
- }))
398
- };
399
- }
400
389
  /** Bin samples into grouped histogram bars for each benchmark */
401
390
  function buildBarData(allSamples, benchmarkNames) {
402
391
  const sorted = allSamples.map((d) => d.value).sort((a, b) => a - b);
@@ -436,6 +425,17 @@ function buildBarData(allSamples, benchmarkNames) {
436
425
  yMax: (d3.max(barData, (d) => d.count) || 1) * 1.15
437
426
  };
438
427
  }
428
+ function buildColorData(benchmarkNames) {
429
+ const scheme = d3.schemeObservable10;
430
+ return {
431
+ colorMap: new Map(benchmarkNames.map((name, i) => [name, scheme[i % 10]])),
432
+ legendItems: benchmarkNames.map((name, i) => ({
433
+ color: scheme[i % 10],
434
+ label: name,
435
+ style: "vertical-bar"
436
+ }))
437
+ };
438
+ }
439
439
 
440
440
  //#endregion
441
441
  //#region src/html/browser/SampleTimeSeries.ts
@@ -531,59 +531,6 @@ function buildPlotContext(timeSeries) {
531
531
  benchmarks
532
532
  };
533
533
  }
534
- function buildSampleData(timeSeries, benchmarks) {
535
- const result = [];
536
- for (const benchmark of benchmarks) {
537
- const isBaseline = benchmark.includes("(baseline)");
538
- for (const d of timeSeries.filter((t) => t.benchmark === benchmark)) {
539
- const optTier = d.optStatus !== void 0 ? OPT_STATUS_NAMES[d.optStatus] || "unknown" : null;
540
- result.push({
541
- benchmark,
542
- sample: d.iteration,
543
- value: d.value,
544
- isBaseline,
545
- isWarmup: d.isWarmup || false,
546
- optTier
547
- });
548
- }
549
- }
550
- return result;
551
- }
552
- /** Pick display unit (ns/us/ms) based on average value magnitude */
553
- function getTimeUnit(values) {
554
- const avg = d3.mean(values);
555
- const fmt0 = (d) => d3.format(",.0f")(d);
556
- const fmt1 = (d) => d3.format(",.1f")(d);
557
- if (avg < .001) return {
558
- unitSuffix: "ns",
559
- convertValue: (ms) => ms * 1e6,
560
- formatValue: fmt0
561
- };
562
- if (avg < 1) return {
563
- unitSuffix: "μs",
564
- convertValue: (ms) => ms * 1e3,
565
- formatValue: fmt1
566
- };
567
- return {
568
- unitSuffix: "ms",
569
- convertValue: (ms) => ms,
570
- formatValue: fmt1
571
- };
572
- }
573
- /** Compute Y axis range with padding, snapping yMin to a round number */
574
- function computeYRange(values) {
575
- const dataMin = d3.min(values);
576
- const dataMax = d3.max(values);
577
- const dataRange = dataMax - dataMin;
578
- let yMin = dataMin - dataRange * .15;
579
- const magnitude = 10 ** Math.floor(Math.log10(Math.abs(yMin)));
580
- yMin = Math.floor(yMin / magnitude) * magnitude;
581
- if (dataMin > 0 && yMin < 0) yMin = 0;
582
- return {
583
- yMin,
584
- yMax: dataMax + dataRange * .05
585
- };
586
- }
587
534
  /** Scale heap byte values into the plot's Y coordinate range */
588
535
  function prepareHeapData(heapSeries, yMin, yMax) {
589
536
  if (heapSeries.length === 0) return [];
@@ -721,6 +668,59 @@ function buildLegendItems(hasWarmup, gcCount, pauseCount, hasHeap, optTiers, ben
721
668
  }
722
669
  return items;
723
670
  }
671
+ function buildSampleData(timeSeries, benchmarks) {
672
+ const result = [];
673
+ for (const benchmark of benchmarks) {
674
+ const isBaseline = benchmark.includes("(baseline)");
675
+ for (const d of timeSeries.filter((t) => t.benchmark === benchmark)) {
676
+ const optTier = d.optStatus !== void 0 ? OPT_STATUS_NAMES[d.optStatus] || "unknown" : null;
677
+ result.push({
678
+ benchmark,
679
+ sample: d.iteration,
680
+ value: d.value,
681
+ isBaseline,
682
+ isWarmup: d.isWarmup || false,
683
+ optTier
684
+ });
685
+ }
686
+ }
687
+ return result;
688
+ }
689
+ /** Pick display unit (ns/us/ms) based on average value magnitude */
690
+ function getTimeUnit(values) {
691
+ const avg = d3.mean(values);
692
+ const fmt0 = (d) => d3.format(",.0f")(d);
693
+ const fmt1 = (d) => d3.format(",.1f")(d);
694
+ if (avg < .001) return {
695
+ unitSuffix: "ns",
696
+ convertValue: (ms) => ms * 1e6,
697
+ formatValue: fmt0
698
+ };
699
+ if (avg < 1) return {
700
+ unitSuffix: "μs",
701
+ convertValue: (ms) => ms * 1e3,
702
+ formatValue: fmt1
703
+ };
704
+ return {
705
+ unitSuffix: "ms",
706
+ convertValue: (ms) => ms,
707
+ formatValue: fmt1
708
+ };
709
+ }
710
+ /** Compute Y axis range with padding, snapping yMin to a round number */
711
+ function computeYRange(values) {
712
+ const dataMin = d3.min(values);
713
+ const dataMax = d3.max(values);
714
+ const dataRange = dataMax - dataMin;
715
+ let yMin = dataMin - dataRange * .15;
716
+ const magnitude = 10 ** Math.floor(Math.log10(Math.abs(yMin)));
717
+ yMin = Math.floor(yMin / magnitude) * magnitude;
718
+ if (dataMin > 0 && yMin < 0) yMin = 0;
719
+ return {
720
+ yMin,
721
+ yMax: dataMax + dataRange * .05
722
+ };
723
+ }
724
724
 
725
725
  //#endregion
726
726
  //#region src/html/browser/RenderPlots.ts
@@ -752,12 +752,9 @@ function renderGroup(group, groupIndex, gcEnabled) {
752
752
  const statsContainer = document.querySelector(`#stats-${groupIndex}`);
753
753
  if (statsContainer) statsContainer.innerHTML = benchmarks.map((b) => generateStatsHtml(b, gcEnabled)).join("");
754
754
  }
755
- /** Clear a container element and append a freshly created plot */
756
- function renderToContainer(selector, condition, create) {
757
- const container = document.querySelector(selector);
758
- if (!container || !condition) return;
759
- container.innerHTML = "";
760
- container.appendChild(create());
755
+ function showError(groupIndex, message) {
756
+ const container = document.querySelector(`#group-${groupIndex}`);
757
+ if (container) container.innerHTML = `<div class="error">${message}</div>`;
761
758
  }
762
759
  /** Combine baseline and benchmarks into a single list with display names */
763
760
  function prepareBenchmarks(group) {
@@ -787,6 +784,62 @@ function flattenSamples(benchmarks) {
787
784
  for (const b of benchmarks) if (b.samples?.length) flattenBenchmark(b, result);
788
785
  return result;
789
786
  }
787
+ /** Clear a container element and append a freshly created plot */
788
+ function renderToContainer(selector, condition, create) {
789
+ const container = document.querySelector(selector);
790
+ if (!container || !condition) return;
791
+ container.innerHTML = "";
792
+ container.appendChild(create());
793
+ }
794
+ function generateStatsHtml(b, gcEnabled) {
795
+ const ciHtml = generateCIHtml(b.comparisonCI);
796
+ if (b.sectionStats?.length) {
797
+ const statsHtml = (gcEnabled ? b.sectionStats : b.sectionStats.filter((s) => s.groupTitle !== "gc")).map((stat) => `
798
+ <div class="stat-item">
799
+ <div class="stat-label">${stat.groupTitle ? stat.groupTitle + " " : ""}${stat.label}</div>
800
+ <div class="stat-value">${stat.value}</div>
801
+ </div>
802
+ `).join("");
803
+ return `
804
+ <div class="summary-stats">
805
+ <h3 style="margin-bottom: 10px; color: #333;">${b.name}</h3>
806
+ <div class="stats-grid">${ciHtml}${statsHtml}</div>
807
+ </div>
808
+ `;
809
+ }
810
+ return `
811
+ <div class="summary-stats">
812
+ <h3 style="margin-bottom: 10px; color: #333;">${b.name}</h3>
813
+ <div class="stats-grid">
814
+ ${ciHtml}
815
+ <div class="stat-item">
816
+ <div class="stat-label">Min</div>
817
+ <div class="stat-value">${b.stats.min.toFixed(3)}ms</div>
818
+ </div>
819
+ <div class="stat-item">
820
+ <div class="stat-label">Median</div>
821
+ <div class="stat-value">${b.stats.p50.toFixed(3)}ms</div>
822
+ </div>
823
+ <div class="stat-item">
824
+ <div class="stat-label">Mean</div>
825
+ <div class="stat-value">${b.stats.avg.toFixed(3)}ms</div>
826
+ </div>
827
+ <div class="stat-item">
828
+ <div class="stat-label">Max</div>
829
+ <div class="stat-value">${b.stats.max.toFixed(3)}ms</div>
830
+ </div>
831
+ <div class="stat-item">
832
+ <div class="stat-label">P75</div>
833
+ <div class="stat-value">${b.stats.p75.toFixed(3)}ms</div>
834
+ </div>
835
+ <div class="stat-item">
836
+ <div class="stat-label">P99</div>
837
+ <div class="stat-value">${b.stats.p99.toFixed(3)}ms</div>
838
+ </div>
839
+ </div>
840
+ </div>
841
+ `;
842
+ }
790
843
  /** Extract time series, heap, GC, and pause data from one benchmark */
791
844
  function flattenBenchmark(b, out) {
792
845
  const warmupCount = b.warmupSamples?.length || 0;
@@ -834,22 +887,6 @@ function flattenBenchmark(b, out) {
834
887
  });
835
888
  });
836
889
  }
837
- function cumulativeSum(arr) {
838
- const result = [];
839
- let sum = 0;
840
- for (const v of arr) {
841
- sum += v;
842
- result.push(sum);
843
- }
844
- return result;
845
- }
846
- function showError(groupIndex, message) {
847
- const container = document.querySelector(`#group-${groupIndex}`);
848
- if (container) container.innerHTML = `<div class="error">${message}</div>`;
849
- }
850
- function formatPct(v) {
851
- return (v >= 0 ? "+" : "") + v.toFixed(1) + "%";
852
- }
853
890
  function generateCIHtml(ci) {
854
891
  if (!ci) return "";
855
892
  const text = `${formatPct(ci.percent)} [${formatPct(ci.ci[0])}, ${formatPct(ci.ci[1])}]`;
@@ -860,54 +897,17 @@ function generateCIHtml(ci) {
860
897
  </div>
861
898
  `;
862
899
  }
863
- function generateStatsHtml(b, gcEnabled) {
864
- const ciHtml = generateCIHtml(b.comparisonCI);
865
- if (b.sectionStats?.length) {
866
- const statsHtml = (gcEnabled ? b.sectionStats : b.sectionStats.filter((s) => s.groupTitle !== "gc")).map((stat) => `
867
- <div class="stat-item">
868
- <div class="stat-label">${stat.groupTitle ? stat.groupTitle + " " : ""}${stat.label}</div>
869
- <div class="stat-value">${stat.value}</div>
870
- </div>
871
- `).join("");
872
- return `
873
- <div class="summary-stats">
874
- <h3 style="margin-bottom: 10px; color: #333;">${b.name}</h3>
875
- <div class="stats-grid">${ciHtml}${statsHtml}</div>
876
- </div>
877
- `;
900
+ function cumulativeSum(arr) {
901
+ const result = [];
902
+ let sum = 0;
903
+ for (const v of arr) {
904
+ sum += v;
905
+ result.push(sum);
878
906
  }
879
- return `
880
- <div class="summary-stats">
881
- <h3 style="margin-bottom: 10px; color: #333;">${b.name}</h3>
882
- <div class="stats-grid">
883
- ${ciHtml}
884
- <div class="stat-item">
885
- <div class="stat-label">Min</div>
886
- <div class="stat-value">${b.stats.min.toFixed(3)}ms</div>
887
- </div>
888
- <div class="stat-item">
889
- <div class="stat-label">Median</div>
890
- <div class="stat-value">${b.stats.p50.toFixed(3)}ms</div>
891
- </div>
892
- <div class="stat-item">
893
- <div class="stat-label">Mean</div>
894
- <div class="stat-value">${b.stats.avg.toFixed(3)}ms</div>
895
- </div>
896
- <div class="stat-item">
897
- <div class="stat-label">Max</div>
898
- <div class="stat-value">${b.stats.max.toFixed(3)}ms</div>
899
- </div>
900
- <div class="stat-item">
901
- <div class="stat-label">P75</div>
902
- <div class="stat-value">${b.stats.p75.toFixed(3)}ms</div>
903
- </div>
904
- <div class="stat-item">
905
- <div class="stat-label">P99</div>
906
- <div class="stat-value">${b.stats.p99.toFixed(3)}ms</div>
907
- </div>
908
- </div>
909
- </div>
910
- `;
907
+ return result;
908
+ }
909
+ function formatPct(v) {
910
+ return (v >= 0 ? "+" : "") + v.toFixed(1) + "%";
911
911
  }
912
912
 
913
913
  //#endregion