benchforge 0.1.11 → 0.2.4
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/LICENSE +20 -0
- package/README.md +99 -294
- package/bin/benchforge +1 -2
- package/dist/AnalyzeArchive-8NCJhmhS.mjs +145 -0
- package/dist/AnalyzeArchive-8NCJhmhS.mjs.map +1 -0
- package/dist/BenchMatrix-BZVrBB_h.mjs +1050 -0
- package/dist/BenchMatrix-BZVrBB_h.mjs.map +1 -0
- package/dist/{BenchRunner-BzyUfiyB.d.mts → BenchRunner-DglX1NOn.d.mts} +119 -66
- package/dist/CoverageSampler-D5T9DRqe.mjs +27 -0
- package/dist/CoverageSampler-D5T9DRqe.mjs.map +1 -0
- package/dist/Formatters-BWj3d4sv.mjs +95 -0
- package/dist/Formatters-BWj3d4sv.mjs.map +1 -0
- package/dist/{HeapSampler-B8dtKHn1.mjs → HeapSampler-Dq-hpXem.mjs} +4 -4
- package/dist/HeapSampler-Dq-hpXem.mjs.map +1 -0
- package/dist/RunBenchCLI-C17DrJz8.mjs +3075 -0
- package/dist/RunBenchCLI-C17DrJz8.mjs.map +1 -0
- package/dist/StatisticalUtils-BD92crgM.mjs +255 -0
- package/dist/StatisticalUtils-BD92crgM.mjs.map +1 -0
- package/dist/TimeSampler-Ds8n7l2B.mjs +29 -0
- package/dist/TimeSampler-Ds8n7l2B.mjs.map +1 -0
- package/dist/ViewerServer-BJhdnxlN.mjs +639 -0
- package/dist/ViewerServer-BJhdnxlN.mjs.map +1 -0
- package/dist/ViewerServer-CuMNdNBz.mjs +2 -0
- package/dist/bin/benchforge.mjs +4 -5
- package/dist/bin/benchforge.mjs.map +1 -1
- package/dist/index.d.mts +711 -558
- package/dist/index.mjs +98 -3
- package/dist/index.mjs.map +1 -0
- package/dist/runners/WorkerScript.d.mts +12 -4
- package/dist/runners/WorkerScript.mjs +77 -105
- package/dist/runners/WorkerScript.mjs.map +1 -1
- package/dist/viewer/assets/CIPlot-BkOvMoMa.js +1 -0
- package/dist/viewer/assets/HistogramKde-CmSyUFY0.js +1 -0
- package/dist/viewer/assets/LegendUtils-BJpbn_jr.js +55 -0
- package/dist/viewer/assets/SampleTimeSeries-C4VBhXr3.js +1 -0
- package/dist/viewer/assets/index-Br9bp_cX.js +153 -0
- package/dist/viewer/assets/index-NzXXe_CC.css +1 -0
- package/dist/viewer/index.html +19 -0
- package/dist/viewer/speedscope/LICENSE +21 -0
- package/dist/viewer/speedscope/SourceCodePro-Regular.ttf-ILST5JV6.woff2 +0 -0
- package/dist/viewer/speedscope/favicon-16x16-V2DMIAZS.js +2 -0
- package/dist/viewer/speedscope/favicon-16x16-V2DMIAZS.js.map +7 -0
- package/dist/viewer/speedscope/favicon-16x16-VSI62OPJ.png +0 -0
- package/dist/viewer/speedscope/favicon-32x32-3EB2YCUY.png +0 -0
- package/dist/viewer/speedscope/favicon-32x32-THY3JDJL.js +2 -0
- package/dist/viewer/speedscope/favicon-32x32-THY3JDJL.js.map +7 -0
- package/dist/viewer/speedscope/favicon-FOKUP5Y5.ico +0 -0
- package/dist/viewer/speedscope/favicon-M34RF7BI.js +2 -0
- package/dist/viewer/speedscope/favicon-M34RF7BI.js.map +7 -0
- package/dist/viewer/speedscope/file-format-schema.json +274 -0
- package/dist/viewer/speedscope/index.html +19 -0
- package/dist/viewer/speedscope/jfrview_bg-BLJXNNQB.wasm +0 -0
- package/dist/viewer/speedscope/perf-vertx-stacks-01-collapsed-all-ZNUIGAJL.txt +199 -0
- package/dist/viewer/speedscope/release.txt +3 -0
- package/dist/viewer/speedscope/source-code-pro.LICENSE.md +93 -0
- package/dist/viewer/speedscope/speedscope-GHPHNKXC.css +2 -0
- package/dist/viewer/speedscope/speedscope-GHPHNKXC.css.map +7 -0
- package/dist/viewer/speedscope/speedscope-QZFMJ7VP.js +212 -0
- package/dist/viewer/speedscope/speedscope-QZFMJ7VP.js.map +7 -0
- package/package.json +52 -27
- package/src/bin/benchforge.ts +2 -2
- package/src/cli/AnalyzeArchive.ts +232 -0
- package/src/cli/BrowserBench.ts +322 -0
- package/src/cli/CliArgs.ts +164 -51
- package/src/cli/CliExport.ts +179 -0
- package/src/cli/CliOptions.ts +147 -0
- package/src/cli/CliReport.ts +197 -0
- package/src/cli/FilterBenchmarks.ts +18 -30
- package/src/cli/RunBenchCLI.ts +132 -866
- package/src/cli/SuiteRunner.ts +160 -0
- package/src/cli/ViewerServer.ts +282 -0
- package/src/export/AllocExport.ts +121 -0
- package/src/export/ArchiveExport.ts +146 -0
- package/src/export/ArchiveFormat.ts +50 -0
- package/src/export/CoverageExport.ts +148 -0
- package/src/export/EditorUri.ts +10 -0
- package/src/export/PerfettoExport.ts +64 -99
- package/src/export/SpeedscopeTypes.ts +98 -0
- package/src/export/TimeExport.ts +115 -0
- package/src/index.ts +86 -67
- package/src/matrix/BenchMatrix.ts +230 -0
- package/src/matrix/CaseLoader.ts +8 -6
- package/src/matrix/MatrixDirRunner.ts +153 -0
- package/src/matrix/MatrixFilter.ts +49 -47
- package/src/matrix/MatrixInlineRunner.ts +50 -0
- package/src/matrix/MatrixReport.ts +90 -250
- package/src/matrix/VariantLoader.ts +5 -5
- package/src/profiling/browser/BenchLoop.ts +51 -0
- package/src/profiling/browser/BrowserCDP.ts +133 -0
- package/src/profiling/browser/BrowserGcStats.ts +33 -0
- package/src/profiling/browser/BrowserProfiler.ts +160 -0
- package/src/profiling/browser/CdpClient.ts +82 -0
- package/src/profiling/browser/CdpPage.ts +138 -0
- package/src/profiling/browser/ChromeLauncher.ts +158 -0
- package/src/profiling/browser/ChromeTraceEvent.ts +28 -0
- package/src/profiling/browser/PageLoadMode.ts +61 -0
- package/src/profiling/node/CoverageSampler.ts +27 -0
- package/src/profiling/node/CoverageTypes.ts +23 -0
- package/src/profiling/node/HeapSampleReport.ts +261 -0
- package/src/{heap-sample → profiling/node}/HeapSampler.ts +1 -2
- package/src/{heap-sample → profiling/node}/ResolvedProfile.ts +18 -9
- package/src/profiling/node/TimeSampler.ts +57 -0
- package/src/report/BenchmarkReport.ts +146 -0
- package/src/report/Colors.ts +9 -0
- package/src/report/Formatters.ts +110 -0
- package/src/report/GcSections.ts +151 -0
- package/src/{GitUtils.ts → report/GitUtils.ts} +18 -19
- package/src/report/HtmlReport.ts +223 -0
- package/src/report/ParseStats.ts +73 -0
- package/src/report/StandardSections.ts +147 -0
- package/src/report/ViewerSections.ts +286 -0
- package/src/report/text/TableReport.ts +253 -0
- package/src/report/text/TextReport.ts +123 -0
- package/src/runners/AdaptiveWrapper.ts +116 -236
- package/src/runners/BenchRunner.ts +20 -15
- package/src/{Benchmark.ts → runners/BenchmarkSpec.ts} +5 -6
- package/src/runners/CreateRunner.ts +5 -7
- package/src/runners/GcStats.ts +47 -50
- package/src/{MeasuredResults.ts → runners/MeasuredResults.ts} +43 -37
- package/src/runners/MergeBatches.ts +123 -0
- package/src/{NodeGC.ts → runners/NodeGC.ts} +2 -3
- package/src/runners/RunnerOrchestrator.ts +127 -243
- package/src/runners/RunnerUtils.ts +75 -1
- package/src/runners/SampleStats.ts +100 -0
- package/src/runners/TimingRunner.ts +244 -0
- package/src/runners/TimingUtils.ts +3 -2
- package/src/runners/WorkerScript.ts +135 -151
- package/src/stats/BootstrapDifference.ts +282 -0
- package/src/{PermutationTest.ts → stats/PermutationTest.ts} +8 -17
- package/src/stats/StatisticalUtils.ts +445 -0
- package/src/{tests → test}/AdaptiveConvergence.test.ts +10 -10
- package/src/test/AdaptiveRunner.test.ts +39 -41
- package/src/{tests → test}/AdaptiveSampling.test.ts +9 -9
- package/src/test/AdaptiveStatistics.integration.ts +2 -2
- package/src/{tests → test}/BenchMatrix.test.ts +19 -16
- package/src/test/BenchmarkReport.test.ts +63 -13
- package/src/test/BrowserBench.e2e.test.ts +186 -17
- package/src/test/BrowserBench.test.ts +10 -5
- package/src/test/BuildTimeSection.test.ts +130 -0
- package/src/test/CapSamples.test.ts +82 -0
- package/src/test/CoverageExport.test.ts +115 -0
- package/src/test/CoverageSampler.test.ts +33 -0
- package/src/test/HeapAttribution.test.ts +14 -14
- package/src/{tests → test}/MatrixFilter.test.ts +1 -1
- package/src/{tests → test}/MatrixReport.test.ts +1 -1
- package/src/test/PermutationTest.test.ts +1 -1
- package/src/{tests → test}/RealDataValidation.test.ts +6 -6
- package/src/test/RunBenchCLI.test.ts +39 -38
- package/src/test/RunnerOrchestrator.test.ts +12 -12
- package/src/test/StatisticalUtils.test.ts +48 -12
- package/src/{table-util/test → test}/TableReport.test.ts +2 -2
- package/src/test/TestUtils.ts +12 -7
- package/src/test/TimeExport.test.ts +139 -0
- package/src/test/TimeSampler.test.ts +37 -0
- package/src/test/ViewerLive.e2e.test.ts +159 -0
- package/src/test/ViewerStatic.static.e2e.test.ts +137 -0
- package/src/{tests → test}/fixtures/baseline/impl.ts +1 -1
- package/src/{tests → test}/fixtures/bevy30-samples.ts +3 -1
- package/src/test/fixtures/cases/asyncCases.ts +9 -0
- package/src/{tests → test}/fixtures/cases/cases.ts +5 -2
- package/src/test/fixtures/cases/variants/product.ts +2 -0
- package/src/test/fixtures/cases/variants/sum.ts +2 -0
- package/src/test/fixtures/discover/fast.ts +1 -0
- package/src/{tests → test}/fixtures/discover/slow.ts +1 -1
- package/src/test/fixtures/invalid/bad.ts +1 -0
- package/src/test/fixtures/loader/fast.ts +1 -0
- package/src/{tests → test}/fixtures/loader/slow.ts +1 -1
- package/src/test/fixtures/loader/stateful.ts +2 -0
- package/src/test/fixtures/stateful/stateful.ts +2 -0
- package/src/test/fixtures/variants/extra.ts +1 -0
- package/src/test/fixtures/variants/impl.ts +1 -0
- package/src/test/fixtures/worker/fast.ts +1 -0
- package/src/{tests → test}/fixtures/worker/slow.ts +1 -1
- package/src/viewer/DateFormat.ts +30 -0
- package/src/viewer/Helpers.ts +23 -0
- package/src/viewer/LineData.ts +120 -0
- package/src/viewer/Providers.ts +191 -0
- package/src/viewer/ReportData.ts +123 -0
- package/src/viewer/State.ts +49 -0
- package/src/viewer/Theme.ts +15 -0
- package/src/viewer/components/App.tsx +73 -0
- package/src/viewer/components/DropZone.tsx +71 -0
- package/src/viewer/components/LazyPlot.ts +33 -0
- package/src/viewer/components/SamplesPanel.tsx +214 -0
- package/src/viewer/components/Shell.tsx +26 -0
- package/src/viewer/components/SourcePanel.tsx +216 -0
- package/src/viewer/components/SummaryPanel.tsx +332 -0
- package/src/viewer/components/TabBar.tsx +131 -0
- package/src/viewer/components/TabContent.tsx +46 -0
- package/src/viewer/components/ThemeToggle.tsx +50 -0
- package/src/viewer/index.html +20 -0
- package/src/viewer/main.tsx +4 -0
- package/src/viewer/plots/CIPlot.ts +313 -0
- package/src/{html/browser → viewer/plots}/HistogramKde.ts +33 -38
- package/src/viewer/plots/LegendUtils.ts +134 -0
- package/src/viewer/plots/PlotTypes.ts +85 -0
- package/src/viewer/plots/RenderPlots.ts +230 -0
- package/src/viewer/plots/SampleTimeSeries.ts +306 -0
- package/src/viewer/plots/SvgHelpers.ts +136 -0
- package/src/viewer/plots/TimeSeriesMarks.ts +319 -0
- package/src/viewer/report.css +427 -0
- package/src/viewer/shell.css +357 -0
- package/src/viewer/tsconfig.json +11 -0
- package/dist/BrowserHeapSampler-B6asLKWQ.mjs +0 -202
- package/dist/BrowserHeapSampler-B6asLKWQ.mjs.map +0 -1
- package/dist/GcStats-wX7Xyblu.mjs +0 -77
- package/dist/GcStats-wX7Xyblu.mjs.map +0 -1
- package/dist/HeapSampler-B8dtKHn1.mjs.map +0 -1
- package/dist/TimingUtils-DwOwkc8G.mjs +0 -597
- package/dist/TimingUtils-DwOwkc8G.mjs.map +0 -1
- package/dist/browser/index.js +0 -914
- package/dist/src-B-DDaCa9.mjs +0 -3108
- package/dist/src-B-DDaCa9.mjs.map +0 -1
- package/src/BenchMatrix.ts +0 -380
- package/src/BenchmarkReport.ts +0 -161
- package/src/HtmlDataPrep.ts +0 -148
- package/src/StandardSections.ts +0 -261
- package/src/StatisticalUtils.ts +0 -175
- package/src/TypeUtil.ts +0 -8
- package/src/browser/BrowserGcStats.ts +0 -44
- package/src/browser/BrowserHeapSampler.ts +0 -271
- package/src/export/JsonExport.ts +0 -103
- package/src/export/JsonFormat.ts +0 -91
- package/src/export/SpeedscopeExport.ts +0 -202
- package/src/heap-sample/HeapSampleReport.ts +0 -269
- package/src/html/HtmlReport.ts +0 -131
- package/src/html/HtmlTemplate.ts +0 -284
- package/src/html/Types.ts +0 -88
- package/src/html/browser/CIPlot.ts +0 -287
- package/src/html/browser/LegendUtils.ts +0 -163
- package/src/html/browser/RenderPlots.ts +0 -263
- package/src/html/browser/SampleTimeSeries.ts +0 -389
- package/src/html/browser/Types.ts +0 -96
- package/src/html/browser/index.ts +0 -1
- package/src/html/index.ts +0 -17
- package/src/runners/BasicRunner.ts +0 -364
- package/src/table-util/ConvergenceFormatters.ts +0 -19
- package/src/table-util/Formatters.ts +0 -157
- package/src/table-util/README.md +0 -70
- package/src/table-util/TableReport.ts +0 -293
- package/src/tests/fixtures/cases/asyncCases.ts +0 -7
- package/src/tests/fixtures/cases/variants/product.ts +0 -2
- package/src/tests/fixtures/cases/variants/sum.ts +0 -2
- package/src/tests/fixtures/discover/fast.ts +0 -1
- package/src/tests/fixtures/invalid/bad.ts +0 -1
- package/src/tests/fixtures/loader/fast.ts +0 -1
- package/src/tests/fixtures/loader/stateful.ts +0 -2
- package/src/tests/fixtures/stateful/stateful.ts +0 -2
- package/src/tests/fixtures/variants/extra.ts +0 -1
- package/src/tests/fixtures/variants/impl.ts +0 -1
- package/src/tests/fixtures/worker/fast.ts +0 -1
- /package/src/{table-util/test → test}/TableValueExtractor.test.ts +0 -0
- /package/src/{table-util/test → test}/TableValueExtractor.ts +0 -0
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
import pc from "picocolors";
|
|
2
|
-
|
|
3
|
-
import { formatBytes } from "../table-util/Formatters.ts";
|
|
4
|
-
import type { HeapProfile, HeapSample } from "./HeapSampler.ts";
|
|
5
|
-
import {
|
|
6
|
-
type ResolvedFrame,
|
|
7
|
-
type ResolvedProfile,
|
|
8
|
-
resolveProfile,
|
|
9
|
-
} from "./ResolvedProfile.ts";
|
|
10
|
-
|
|
11
|
-
export interface CallFrame {
|
|
12
|
-
fn: string;
|
|
13
|
-
url: string;
|
|
14
|
-
line: number; // 1-indexed for display
|
|
15
|
-
col?: number; // 1-indexed for display
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface HeapSite {
|
|
19
|
-
fn: string;
|
|
20
|
-
url: string;
|
|
21
|
-
line: number; // 1-indexed for display
|
|
22
|
-
col?: number;
|
|
23
|
-
bytes: number;
|
|
24
|
-
stack?: CallFrame[]; // call stack from root to this frame
|
|
25
|
-
samples?: HeapSample[]; // individual allocation samples at this site
|
|
26
|
-
callers?: { stack: CallFrame[]; bytes: number }[]; // distinct caller paths
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export type UserCodeFilter = (site: CallFrame) => boolean;
|
|
30
|
-
|
|
31
|
-
export interface HeapReportOptions {
|
|
32
|
-
topN: number;
|
|
33
|
-
stackDepth?: number;
|
|
34
|
-
verbose?: boolean;
|
|
35
|
-
raw?: boolean; // dump every raw sample
|
|
36
|
-
userOnly?: boolean; // filter to user code only (hide node internals)
|
|
37
|
-
isUserCode?: UserCodeFilter; // predicate for user vs internal code
|
|
38
|
-
totalAll?: number; // total across all nodes (before filtering)
|
|
39
|
-
totalUserCode?: number; // total for user code only
|
|
40
|
-
sampleCount?: number; // number of samples taken
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** Sum selfSize across all nodes in profile (before any filtering) */
|
|
44
|
-
export function totalProfileBytes(profile: HeapProfile): number {
|
|
45
|
-
return resolveProfile(profile).totalBytes;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** Flatten resolved profile into sorted list of allocation sites with call stacks.
|
|
49
|
-
* When raw samples are available, attaches them to corresponding sites. */
|
|
50
|
-
export function flattenProfile(resolved: ResolvedProfile): HeapSite[] {
|
|
51
|
-
const sites: HeapSite[] = [];
|
|
52
|
-
const nodeIdToSites = new Map<number, HeapSite[]>();
|
|
53
|
-
|
|
54
|
-
for (const node of resolved.allocationNodes) {
|
|
55
|
-
const frame = toCallFrame(node.frame);
|
|
56
|
-
const stack = node.stack.map(toCallFrame);
|
|
57
|
-
const site: HeapSite = { ...frame, bytes: node.selfSize, stack };
|
|
58
|
-
sites.push(site);
|
|
59
|
-
const existing = nodeIdToSites.get(node.nodeId);
|
|
60
|
-
if (existing) existing.push(site);
|
|
61
|
-
else nodeIdToSites.set(node.nodeId, [site]);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Attach raw samples to their corresponding sites
|
|
65
|
-
for (const sample of resolved.sortedSamples ?? []) {
|
|
66
|
-
const matchingSites = nodeIdToSites.get(sample.nodeId);
|
|
67
|
-
if (!matchingSites) continue;
|
|
68
|
-
for (const site of matchingSites) {
|
|
69
|
-
if (!site.samples) site.samples = [];
|
|
70
|
-
site.samples.push(sample);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return sites.sort((a, b) => b.bytes - a.bytes);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** Check if site is user code (not node internals) */
|
|
78
|
-
export function isNodeUserCode(site: CallFrame): boolean {
|
|
79
|
-
if (!site.url) return false;
|
|
80
|
-
if (site.url.startsWith("node:")) return false;
|
|
81
|
-
if (site.url.includes("(native)")) return false;
|
|
82
|
-
if (site.url.includes("internal/")) return false;
|
|
83
|
-
return true;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/** Check if site is user code (not browser internals) */
|
|
87
|
-
export function isBrowserUserCode(site: CallFrame): boolean {
|
|
88
|
-
if (!site.url) return false;
|
|
89
|
-
if (site.url.startsWith("chrome-extension://")) return false;
|
|
90
|
-
if (site.url.startsWith("devtools://")) return false;
|
|
91
|
-
if (site.url.includes("(native)")) return false;
|
|
92
|
-
return true;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** Filter sites to user code only */
|
|
96
|
-
export function filterSites(
|
|
97
|
-
sites: HeapSite[],
|
|
98
|
-
isUser: UserCodeFilter = isNodeUserCode,
|
|
99
|
-
): HeapSite[] {
|
|
100
|
-
return sites.filter(isUser);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/** Aggregate sites by location (combine same file:line:col).
|
|
104
|
-
* Tracks distinct caller stacks with byte weights when merging. */
|
|
105
|
-
export function aggregateSites(sites: HeapSite[]): HeapSite[] {
|
|
106
|
-
const byLocation = new Map<string, HeapSite>();
|
|
107
|
-
|
|
108
|
-
for (const site of sites) {
|
|
109
|
-
// When column is unknown, include fn name to avoid merging distinct sites
|
|
110
|
-
const key =
|
|
111
|
-
site.col != null
|
|
112
|
-
? `${site.url}:${site.line}:${site.col}`
|
|
113
|
-
: `${site.url}:${site.line}:?:${site.fn}`;
|
|
114
|
-
const existing = byLocation.get(key);
|
|
115
|
-
if (existing) {
|
|
116
|
-
existing.bytes += site.bytes;
|
|
117
|
-
addCaller(existing, site);
|
|
118
|
-
} else {
|
|
119
|
-
const entry = { ...site };
|
|
120
|
-
if (site.stack) {
|
|
121
|
-
entry.callers = [{ stack: site.stack, bytes: site.bytes }];
|
|
122
|
-
}
|
|
123
|
-
byLocation.set(key, entry);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Sort callers by bytes descending, use top caller as primary stack
|
|
128
|
-
for (const site of byLocation.values()) {
|
|
129
|
-
if (site.callers && site.callers.length > 1) {
|
|
130
|
-
site.callers.sort((a, b) => b.bytes - a.bytes);
|
|
131
|
-
site.stack = site.callers[0].stack;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return [...byLocation.values()].sort((a, b) => b.bytes - a.bytes);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/** Format heap report for console output */
|
|
139
|
-
export function formatHeapReport(
|
|
140
|
-
sites: HeapSite[],
|
|
141
|
-
options: HeapReportOptions,
|
|
142
|
-
): string {
|
|
143
|
-
const { topN, stackDepth = 3, verbose = false } = options;
|
|
144
|
-
const { totalAll, totalUserCode, sampleCount } = options;
|
|
145
|
-
const isUser = options.isUserCode ?? isNodeUserCode;
|
|
146
|
-
const lines: string[] = [];
|
|
147
|
-
lines.push(`Heap allocation sites (top ${topN}, garbage included):`);
|
|
148
|
-
|
|
149
|
-
for (const site of sites.slice(0, topN)) {
|
|
150
|
-
if (verbose) {
|
|
151
|
-
formatVerboseSite(lines, site, stackDepth, isUser);
|
|
152
|
-
} else {
|
|
153
|
-
formatCompactSite(lines, site, stackDepth, isUser);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
lines.push("");
|
|
158
|
-
if (totalAll !== undefined)
|
|
159
|
-
lines.push(`Total (all): ${fmtBytes(totalAll)}`);
|
|
160
|
-
if (totalUserCode !== undefined)
|
|
161
|
-
lines.push(`Total (user-code): ${fmtBytes(totalUserCode)}`);
|
|
162
|
-
if (sampleCount !== undefined)
|
|
163
|
-
lines.push(`Samples: ${sampleCount.toLocaleString()}`);
|
|
164
|
-
|
|
165
|
-
return lines.join("\n");
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/** Get total bytes from sites */
|
|
169
|
-
export function totalBytes(sites: HeapSite[]): number {
|
|
170
|
-
return sites.reduce((sum, s) => sum + s.bytes, 0);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/** Format every raw sample as one line, ordered by ordinal (time).
|
|
174
|
-
* Output is tab-separated for easy piping/grep/diff. */
|
|
175
|
-
export function formatRawSamples(resolved: ResolvedProfile): string {
|
|
176
|
-
if (!resolved.sortedSamples || resolved.sortedSamples.length === 0) {
|
|
177
|
-
return "No raw samples available.";
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const lines: string[] = ["ordinal\tsize\tfunction\tlocation"];
|
|
181
|
-
for (const s of resolved.sortedSamples) {
|
|
182
|
-
const node = resolved.nodeMap.get(s.nodeId);
|
|
183
|
-
const fn = node?.frame.name || "(unknown)";
|
|
184
|
-
const url = node?.frame.url || "";
|
|
185
|
-
const loc = url
|
|
186
|
-
? fmtLoc(url, node!.frame.line, node!.frame.col)
|
|
187
|
-
: "(unknown)";
|
|
188
|
-
lines.push(`${s.ordinal}\t${s.size}\t${fn}\t${loc}`);
|
|
189
|
-
}
|
|
190
|
-
return lines.join("\n");
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function toCallFrame(f: ResolvedFrame): CallFrame {
|
|
194
|
-
return { fn: f.name, url: f.url, line: f.line, col: f.col };
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/** Add a caller stack to an aggregated site, merging if the same path exists */
|
|
198
|
-
function addCaller(existing: HeapSite, site: HeapSite): void {
|
|
199
|
-
if (!site.stack) return;
|
|
200
|
-
if (!existing.callers) {
|
|
201
|
-
existing.callers = [];
|
|
202
|
-
}
|
|
203
|
-
const key = callerKey(site.stack);
|
|
204
|
-
const match = existing.callers.find(c => callerKey(c.stack) === key);
|
|
205
|
-
if (match) {
|
|
206
|
-
match.bytes += site.bytes;
|
|
207
|
-
} else {
|
|
208
|
-
existing.callers.push({ stack: site.stack, bytes: site.bytes });
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/** Verbose multi-line format with file:// paths and line numbers */
|
|
213
|
-
function formatVerboseSite(
|
|
214
|
-
lines: string[],
|
|
215
|
-
site: HeapSite,
|
|
216
|
-
stackDepth: number,
|
|
217
|
-
isUser: UserCodeFilter,
|
|
218
|
-
): void {
|
|
219
|
-
const bytes = fmtBytes(site.bytes).padStart(10);
|
|
220
|
-
const loc = site.url ? fmtLoc(site.url, site.line, site.col) : "(unknown)";
|
|
221
|
-
const dimFn = isUser(site) ? (s: string) => s : pc.dim;
|
|
222
|
-
|
|
223
|
-
lines.push(dimFn(`${bytes} ${site.fn} ${loc}`));
|
|
224
|
-
|
|
225
|
-
if (site.stack && site.stack.length > 1) {
|
|
226
|
-
const callers = site.stack.slice(0, -1).reverse().slice(0, stackDepth);
|
|
227
|
-
for (const frame of callers) {
|
|
228
|
-
if (!frame.url || !isUser(frame)) continue;
|
|
229
|
-
const callerLoc = fmtLoc(frame.url, frame.line, frame.col);
|
|
230
|
-
lines.push(dimFn(` <- ${frame.fn} ${callerLoc}`));
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/** Compact single-line format: `49 MB fn1 <- fn2 <- fn3` */
|
|
236
|
-
function formatCompactSite(
|
|
237
|
-
lines: string[],
|
|
238
|
-
site: HeapSite,
|
|
239
|
-
stackDepth: number,
|
|
240
|
-
isUser: UserCodeFilter,
|
|
241
|
-
): void {
|
|
242
|
-
const bytes = fmtBytes(site.bytes).padStart(10);
|
|
243
|
-
const fns = [site.fn];
|
|
244
|
-
|
|
245
|
-
if (site.stack && site.stack.length > 1) {
|
|
246
|
-
const callers = site.stack.slice(0, -1).reverse().slice(0, stackDepth);
|
|
247
|
-
for (const frame of callers) {
|
|
248
|
-
if (!frame.url || !isUser(frame)) continue;
|
|
249
|
-
fns.push(frame.fn);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const line = `${bytes} ${fns.join(" <- ")}`;
|
|
254
|
-
lines.push(isUser(site) ? line : pc.dim(line));
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
function fmtBytes(bytes: number): string {
|
|
258
|
-
return formatBytes(bytes, { space: true }) ?? `${bytes} B`;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/** Format location, omitting column when unknown */
|
|
262
|
-
function fmtLoc(url: string, line: number, col?: number): string {
|
|
263
|
-
return col != null ? `${url}:${line}:${col}` : `${url}:${line}`;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/** Serialize a call stack for dedup comparison */
|
|
267
|
-
function callerKey(stack: CallFrame[]): string {
|
|
268
|
-
return stack.map(f => `${f.url}:${f.line}:${f.col}`).join("|");
|
|
269
|
-
}
|
package/src/html/HtmlReport.ts
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
-
import { createServer, type Server } from "node:http";
|
|
3
|
-
import { dirname, extname, join } from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
import open from "open";
|
|
6
|
-
import { generateHtmlDocument } from "./HtmlTemplate.ts";
|
|
7
|
-
import type {
|
|
8
|
-
HtmlReportOptions,
|
|
9
|
-
HtmlReportResult,
|
|
10
|
-
ReportData,
|
|
11
|
-
} from "./Types.ts";
|
|
12
|
-
|
|
13
|
-
/** Generate HTML report from prepared data and optionally open in browser */
|
|
14
|
-
export async function generateHtmlReport(
|
|
15
|
-
data: ReportData,
|
|
16
|
-
options: HtmlReportOptions,
|
|
17
|
-
): Promise<HtmlReportResult> {
|
|
18
|
-
const html = generateHtmlDocument(data);
|
|
19
|
-
|
|
20
|
-
const reportDir = options.outputPath || (await createReportDir());
|
|
21
|
-
await mkdir(reportDir, { recursive: true });
|
|
22
|
-
|
|
23
|
-
await writeFile(join(reportDir, "index.html"), html, "utf-8");
|
|
24
|
-
const plots = await loadPlotsBundle();
|
|
25
|
-
await writeFile(join(reportDir, "plots.js"), plots, "utf-8");
|
|
26
|
-
await writeLatestRedirect(reportDir);
|
|
27
|
-
|
|
28
|
-
let server: Server | undefined;
|
|
29
|
-
let closeServer: (() => void) | undefined;
|
|
30
|
-
|
|
31
|
-
if (options.openBrowser) {
|
|
32
|
-
const baseDir = dirname(reportDir);
|
|
33
|
-
const reportName = reportDir.split("/").pop();
|
|
34
|
-
const result = await startReportServer(baseDir, 7979, 7978, 7977);
|
|
35
|
-
server = result.server;
|
|
36
|
-
closeServer = () => result.server.close();
|
|
37
|
-
const openUrl = `http://localhost:${result.port}/${reportName}/`;
|
|
38
|
-
await open(openUrl);
|
|
39
|
-
console.log(`Report opened in browser: ${openUrl}`);
|
|
40
|
-
} else {
|
|
41
|
-
console.log(`Report saved to: ${reportDir}/`);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return { reportDir, server, closeServer };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Create a timestamped report directory under ./bench-report/ */
|
|
48
|
-
async function createReportDir(): Promise<string> {
|
|
49
|
-
const base = "./bench-report";
|
|
50
|
-
await mkdir(base, { recursive: true });
|
|
51
|
-
const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
52
|
-
return join(base, `report-${ts}`);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** Read the pre-built browser plots bundle from dist/ */
|
|
56
|
-
async function loadPlotsBundle(): Promise<string> {
|
|
57
|
-
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
58
|
-
const builtPath = join(thisDir, "browser/index.js");
|
|
59
|
-
const devPath = join(thisDir, "../../dist/browser/index.js");
|
|
60
|
-
try {
|
|
61
|
-
return await readFile(builtPath, "utf-8");
|
|
62
|
-
} catch {}
|
|
63
|
-
return readFile(devPath, "utf-8");
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** Write an index.html in the parent dir that redirects to this report */
|
|
67
|
-
async function writeLatestRedirect(reportDir: string): Promise<void> {
|
|
68
|
-
const baseDir = dirname(reportDir);
|
|
69
|
-
const reportName = reportDir.split("/").pop();
|
|
70
|
-
const html = `<!DOCTYPE html>
|
|
71
|
-
<html><head>
|
|
72
|
-
<meta http-equiv="refresh" content="0; url=./${reportName}/">
|
|
73
|
-
<script>location.href = "./${reportName}/";</script>
|
|
74
|
-
</head><body>
|
|
75
|
-
<a href="./${reportName}/">Latest report</a>
|
|
76
|
-
</body></html>`;
|
|
77
|
-
await writeFile(join(baseDir, "index.html"), html, "utf-8");
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** Start HTTP server for report directory, trying fallback ports if needed */
|
|
81
|
-
async function startReportServer(
|
|
82
|
-
baseDir: string,
|
|
83
|
-
...ports: number[]
|
|
84
|
-
): Promise<{ server: Server; port: number }> {
|
|
85
|
-
const mimeTypes: Record<string, string> = {
|
|
86
|
-
".html": "text/html",
|
|
87
|
-
".js": "application/javascript",
|
|
88
|
-
".css": "text/css",
|
|
89
|
-
".json": "application/json",
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
const server = createServer(async (req, res) => {
|
|
93
|
-
const url = req.url || "/";
|
|
94
|
-
const suffix = url.endsWith("/") ? url + "index.html" : url;
|
|
95
|
-
const filePath = join(baseDir, suffix);
|
|
96
|
-
try {
|
|
97
|
-
const content = await readFile(filePath);
|
|
98
|
-
const mime = mimeTypes[extname(filePath)] || "application/octet-stream";
|
|
99
|
-
res.setHeader("Content-Type", mime);
|
|
100
|
-
res.end(content);
|
|
101
|
-
} catch {
|
|
102
|
-
res.statusCode = 404;
|
|
103
|
-
res.end("Not found");
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
for (const port of ports) {
|
|
108
|
-
try {
|
|
109
|
-
return await tryListen(server, port);
|
|
110
|
-
} catch {
|
|
111
|
-
// Port in use, try next
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
return tryListen(server, 0);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/** Listen on a port, resolving with the actual port or rejecting on error */
|
|
118
|
-
function tryListen(
|
|
119
|
-
server: Server,
|
|
120
|
-
port: number,
|
|
121
|
-
): Promise<{ server: Server; port: number }> {
|
|
122
|
-
return new Promise((resolve, reject) => {
|
|
123
|
-
server.once("error", reject);
|
|
124
|
-
server.listen(port, () => {
|
|
125
|
-
server.removeListener("error", reject);
|
|
126
|
-
const addr = server.address();
|
|
127
|
-
const actualPort = typeof addr === "object" && addr ? addr.port : port;
|
|
128
|
-
resolve({ server, port: actualPort });
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
}
|
package/src/html/HtmlTemplate.ts
DELETED
|
@@ -1,284 +0,0 @@
|
|
|
1
|
-
import type { GitVersion, GroupData, ReportData } from "./Types.ts";
|
|
2
|
-
|
|
3
|
-
const skipArgs = new Set(["_", "$0", "html", "export-html"]);
|
|
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
|
-
|
|
18
|
-
/** Format ISO date as local time with UTC: "Jan 9, 2026, 3:45 PM (2026-01-09T23:45:00Z)" */
|
|
19
|
-
export function formatDateWithTimezone(isoDate: string): string {
|
|
20
|
-
const date = new Date(isoDate);
|
|
21
|
-
const local = date.toLocaleString("en-US", {
|
|
22
|
-
month: "short",
|
|
23
|
-
day: "numeric",
|
|
24
|
-
year: "numeric",
|
|
25
|
-
hour: "numeric",
|
|
26
|
-
minute: "2-digit",
|
|
27
|
-
});
|
|
28
|
-
const utc = date.toISOString().replace(".000Z", "Z");
|
|
29
|
-
return `${local} (${utc})`;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Format relative time: "5m ago", "2h ago", "yesterday", "3 days ago" */
|
|
33
|
-
export function formatRelativeTime(isoDate: string): string {
|
|
34
|
-
const date = new Date(isoDate);
|
|
35
|
-
const now = new Date();
|
|
36
|
-
const diffMs = now.getTime() - date.getTime();
|
|
37
|
-
const diffMins = Math.floor(diffMs / 60000);
|
|
38
|
-
const diffHours = Math.floor(diffMs / 3600000);
|
|
39
|
-
const diffDays = Math.floor(diffMs / 86400000);
|
|
40
|
-
|
|
41
|
-
if (diffMins < 1) return "just now";
|
|
42
|
-
if (diffMins < 60) return `${diffMins}m ago`;
|
|
43
|
-
if (diffHours < 24) return `${diffHours}h ago`;
|
|
44
|
-
if (diffDays === 1) return "yesterday";
|
|
45
|
-
if (diffDays < 30) return `${diffDays} days ago`;
|
|
46
|
-
return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Generate complete HTML document with embedded data and visualizations */
|
|
50
|
-
export function generateHtmlDocument(data: ReportData): string {
|
|
51
|
-
return `<!DOCTYPE html>
|
|
52
|
-
<html lang="en">
|
|
53
|
-
<head>
|
|
54
|
-
<meta charset="UTF-8">
|
|
55
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
56
|
-
<title>Benchmark Report - ${new Date().toLocaleDateString()}</title>
|
|
57
|
-
<style>
|
|
58
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
59
|
-
body {
|
|
60
|
-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
61
|
-
background: #f5f5f5;
|
|
62
|
-
padding: 20px;
|
|
63
|
-
line-height: 1.6;
|
|
64
|
-
}
|
|
65
|
-
.header {
|
|
66
|
-
background: white;
|
|
67
|
-
padding: 10px 15px;
|
|
68
|
-
border-radius: 8px;
|
|
69
|
-
margin-bottom: 20px;
|
|
70
|
-
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
71
|
-
display: flex;
|
|
72
|
-
justify-content: space-between;
|
|
73
|
-
align-items: center;
|
|
74
|
-
}
|
|
75
|
-
h1 { display: none; }
|
|
76
|
-
h2 {
|
|
77
|
-
color: #555;
|
|
78
|
-
margin: 30px 0 20px;
|
|
79
|
-
font-size: 20px;
|
|
80
|
-
border-bottom: 2px solid #e0e0e0;
|
|
81
|
-
padding-bottom: 10px;
|
|
82
|
-
}
|
|
83
|
-
.metadata { color: #666; font-size: 12px; }
|
|
84
|
-
.cli-args {
|
|
85
|
-
font-family: "SF Mono", Monaco, "Consolas", monospace;
|
|
86
|
-
font-size: 11px;
|
|
87
|
-
color: #555;
|
|
88
|
-
background: #f0f0f0;
|
|
89
|
-
padding: 6px 10px;
|
|
90
|
-
border-radius: 4px;
|
|
91
|
-
word-break: break-word;
|
|
92
|
-
}
|
|
93
|
-
.comparison-mode {
|
|
94
|
-
background: #fff3cd;
|
|
95
|
-
color: #856404;
|
|
96
|
-
padding: 8px 12px;
|
|
97
|
-
border-radius: 4px;
|
|
98
|
-
display: inline-block;
|
|
99
|
-
margin-top: 10px;
|
|
100
|
-
font-weight: 500;
|
|
101
|
-
}
|
|
102
|
-
.plot-grid {
|
|
103
|
-
display: grid;
|
|
104
|
-
grid-template-columns: 1fr 1fr;
|
|
105
|
-
gap: 20px;
|
|
106
|
-
margin-bottom: 30px;
|
|
107
|
-
}
|
|
108
|
-
.plot-grid.second-row { grid-template-columns: 1fr; }
|
|
109
|
-
.plot-container {
|
|
110
|
-
background: white;
|
|
111
|
-
padding: 20px;
|
|
112
|
-
border-radius: 8px;
|
|
113
|
-
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
114
|
-
}
|
|
115
|
-
.plot-container.full-width { grid-column: 1 / -1; }
|
|
116
|
-
.plot-title { font-size: 18px; font-weight: 600; margin-bottom: 8px; color: #333; }
|
|
117
|
-
.plot-description { font-size: 14px; color: #666; margin-bottom: 15px; }
|
|
118
|
-
.plot-area {
|
|
119
|
-
display: flex;
|
|
120
|
-
justify-content: center;
|
|
121
|
-
align-items: center;
|
|
122
|
-
min-height: 300px;
|
|
123
|
-
}
|
|
124
|
-
.plot-area svg { overflow: visible; }
|
|
125
|
-
.plot-area svg g[aria-label="x-axis label"] text { font-size: 14px; }
|
|
126
|
-
.summary-stats { background: #f8f9fa; padding: 15px; border-radius: 6px; margin-top: 20px; }
|
|
127
|
-
.stats-grid {
|
|
128
|
-
display: grid;
|
|
129
|
-
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
130
|
-
gap: 10px;
|
|
131
|
-
margin-top: 10px;
|
|
132
|
-
}
|
|
133
|
-
.stat-item { background: white; padding: 10px; border-radius: 4px; text-align: center; }
|
|
134
|
-
.stat-label { font-size: 12px; color: #666; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
135
|
-
.stat-value { font-size: 18px; font-weight: 600; color: #333; margin-top: 4px; }
|
|
136
|
-
.loading { color: #666; font-style: italic; padding: 20px; text-align: center; }
|
|
137
|
-
.error { color: #d32f2f; background: #ffebee; padding: 15px; border-radius: 4px; margin: 10px 0; }
|
|
138
|
-
.ci-faster { color: #22c55e; }
|
|
139
|
-
.ci-slower { color: #ef4444; }
|
|
140
|
-
.ci-uncertain { color: #6b7280; }
|
|
141
|
-
.group-header {
|
|
142
|
-
display: flex;
|
|
143
|
-
align-items: center;
|
|
144
|
-
gap: 12px;
|
|
145
|
-
margin: 30px 0 20px;
|
|
146
|
-
padding-bottom: 10px;
|
|
147
|
-
border-bottom: 2px solid #e0e0e0;
|
|
148
|
-
}
|
|
149
|
-
.group-header h2 { margin: 0; border: none; padding: 0; }
|
|
150
|
-
.badge {
|
|
151
|
-
font-size: 12px;
|
|
152
|
-
font-weight: 600;
|
|
153
|
-
padding: 4px 10px;
|
|
154
|
-
border-radius: 12px;
|
|
155
|
-
text-transform: uppercase;
|
|
156
|
-
letter-spacing: 0.5px;
|
|
157
|
-
}
|
|
158
|
-
.badge-faster { background: #dcfce7; color: #166534; }
|
|
159
|
-
.badge-slower { background: #fee2e2; color: #991b1b; }
|
|
160
|
-
.badge-uncertain { background: #dbeafe; color: #1e40af; }
|
|
161
|
-
.version-info { font-size: 12px; color: #666; margin-top: 6px; }
|
|
162
|
-
.header-right { text-align: right; }
|
|
163
|
-
.ci-plot-container { display: inline-block; vertical-align: middle; margin-left: 8px; }
|
|
164
|
-
.ci-plot-container svg { display: block; }
|
|
165
|
-
</style>
|
|
166
|
-
</head>
|
|
167
|
-
<body>
|
|
168
|
-
<div class="header">
|
|
169
|
-
<div class="cli-args">${formatCliArgs(data.metadata.cliArgs)}</div>
|
|
170
|
-
<div class="header-right">
|
|
171
|
-
<div class="metadata">Generated: ${formatDateWithTimezone(new Date().toISOString())}</div>
|
|
172
|
-
${versionInfoHtml(data)}
|
|
173
|
-
</div>
|
|
174
|
-
</div>
|
|
175
|
-
|
|
176
|
-
${data.groups
|
|
177
|
-
.map(
|
|
178
|
-
(group, i) => `
|
|
179
|
-
<div id="group-${i}">
|
|
180
|
-
${
|
|
181
|
-
group.benchmarks.length > 0
|
|
182
|
-
? `
|
|
183
|
-
<div class="group-header">
|
|
184
|
-
<h2>${group.name}</h2>
|
|
185
|
-
${comparisonBadge(group, i)}
|
|
186
|
-
</div>
|
|
187
|
-
|
|
188
|
-
<div class="plot-grid">
|
|
189
|
-
<div class="plot-container">
|
|
190
|
-
<div class="plot-title">Time per Sample</div>
|
|
191
|
-
<div class="plot-description">Execution time for each sample in collection order</div>
|
|
192
|
-
<div id="sample-timeseries-${i}" class="plot-area">
|
|
193
|
-
<div class="loading">Loading time series...</div>
|
|
194
|
-
</div>
|
|
195
|
-
</div>
|
|
196
|
-
|
|
197
|
-
<div class="plot-container">
|
|
198
|
-
<div class="plot-title">Time Distribution</div>
|
|
199
|
-
<div class="plot-description">Frequency distribution of execution times</div>
|
|
200
|
-
<div id="histogram-${i}" class="plot-area">
|
|
201
|
-
<div class="loading">Loading histogram...</div>
|
|
202
|
-
</div>
|
|
203
|
-
</div>
|
|
204
|
-
</div>
|
|
205
|
-
|
|
206
|
-
<div id="stats-${i}"></div>
|
|
207
|
-
`
|
|
208
|
-
: '<div class="error">No benchmark data available for this group</div>'
|
|
209
|
-
}
|
|
210
|
-
</div>
|
|
211
|
-
`,
|
|
212
|
-
)
|
|
213
|
-
.join("")}
|
|
214
|
-
|
|
215
|
-
<script type="importmap">
|
|
216
|
-
{
|
|
217
|
-
"imports": {
|
|
218
|
-
"d3": "https://cdn.jsdelivr.net/npm/d3@7/+esm",
|
|
219
|
-
"@observablehq/plot": "https://cdn.jsdelivr.net/npm/@observablehq/plot@0.6/+esm"
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
</script>
|
|
223
|
-
<script type="module">
|
|
224
|
-
import { renderPlots } from "./plots.js";
|
|
225
|
-
const benchmarkData = ${JSON.stringify(data, null, 2)};
|
|
226
|
-
renderPlots(benchmarkData);
|
|
227
|
-
</script>
|
|
228
|
-
</body>
|
|
229
|
-
</html>`;
|
|
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
|
-
}
|