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.
- package/README.md +69 -42
- package/dist/{BenchRunner-CSKN9zPy.d.mts → BenchRunner-BzyUfiyB.d.mts} +32 -8
- package/dist/{BrowserHeapSampler-DCeL42RE.mjs → BrowserHeapSampler-B6asLKWQ.mjs} +57 -57
- package/dist/BrowserHeapSampler-B6asLKWQ.mjs.map +1 -0
- package/dist/{GcStats-ByEovUi1.mjs → GcStats-wX7Xyblu.mjs} +15 -15
- package/dist/GcStats-wX7Xyblu.mjs.map +1 -0
- package/dist/HeapSampler-B8dtKHn1.mjs.map +1 -1
- package/dist/{TimingUtils-ClclVQ7E.mjs → TimingUtils-DwOwkc8G.mjs} +225 -225
- package/dist/TimingUtils-DwOwkc8G.mjs.map +1 -0
- package/dist/bin/benchforge.mjs +1 -1
- package/dist/browser/index.js +210 -210
- package/dist/index.d.mts +106 -48
- package/dist/index.mjs +3 -3
- package/dist/runners/WorkerScript.d.mts +1 -1
- package/dist/runners/WorkerScript.mjs +66 -66
- package/dist/runners/WorkerScript.mjs.map +1 -1
- package/dist/{src-HfimYuW_.mjs → src-B-DDaCa9.mjs} +1250 -991
- package/dist/src-B-DDaCa9.mjs.map +1 -0
- package/package.json +4 -3
- package/src/BenchMatrix.ts +125 -125
- package/src/BenchmarkReport.ts +50 -45
- package/src/HtmlDataPrep.ts +21 -21
- package/src/PermutationTest.ts +24 -24
- package/src/StandardSections.ts +45 -45
- package/src/StatisticalUtils.ts +60 -61
- package/src/browser/BrowserGcStats.ts +5 -5
- package/src/browser/BrowserHeapSampler.ts +63 -63
- package/src/cli/CliArgs.ts +20 -6
- package/src/cli/FilterBenchmarks.ts +5 -5
- package/src/cli/RunBenchCLI.ts +533 -476
- package/src/export/JsonExport.ts +10 -10
- package/src/export/PerfettoExport.ts +74 -74
- package/src/export/SpeedscopeExport.ts +202 -0
- package/src/heap-sample/HeapSampleReport.ts +143 -70
- package/src/heap-sample/HeapSampler.ts +55 -12
- package/src/heap-sample/ResolvedProfile.ts +89 -0
- package/src/html/HtmlReport.ts +33 -33
- package/src/html/HtmlTemplate.ts +67 -67
- package/src/html/browser/CIPlot.ts +50 -50
- package/src/html/browser/HistogramKde.ts +13 -13
- package/src/html/browser/LegendUtils.ts +48 -48
- package/src/html/browser/RenderPlots.ts +98 -98
- package/src/html/browser/SampleTimeSeries.ts +79 -79
- package/src/index.ts +6 -0
- package/src/matrix/MatrixFilter.ts +6 -6
- package/src/matrix/MatrixReport.ts +96 -96
- package/src/matrix/VariantLoader.ts +5 -5
- package/src/runners/AdaptiveWrapper.ts +151 -151
- package/src/runners/BasicRunner.ts +175 -175
- package/src/runners/BenchRunner.ts +8 -8
- package/src/runners/GcStats.ts +22 -22
- package/src/runners/RunnerOrchestrator.ts +168 -168
- package/src/runners/WorkerScript.ts +96 -96
- package/src/table-util/Formatters.ts +41 -36
- package/src/table-util/TableReport.ts +122 -122
- package/src/table-util/test/TableValueExtractor.ts +9 -9
- package/src/test/AdaptiveStatistics.integration.ts +7 -39
- package/src/test/HeapAttribution.test.ts +51 -0
- package/src/test/RunBenchCLI.test.ts +36 -11
- package/src/test/TestUtils.ts +24 -24
- package/src/test/fixtures/fn-export-bench.ts +3 -0
- package/src/test/fixtures/suite-export-bench.ts +16 -0
- package/src/tests/BenchMatrix.test.ts +12 -12
- package/src/tests/MatrixFilter.test.ts +15 -15
- package/dist/BrowserHeapSampler-DCeL42RE.mjs.map +0 -1
- package/dist/GcStats-ByEovUi1.mjs.map +0 -1
- package/dist/TimingUtils-ClclVQ7E.mjs.map +0 -1
- package/dist/src-HfimYuW_.mjs.map +0 -1
package/src/html/HtmlTemplate.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
}
|