benchforge 0.1.9 → 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 -260
- 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-DglX1NOn.d.mts +302 -0
- 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 +731 -522
- 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 +92 -120
- 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 -26
- 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 -48
- 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 +138 -844
- 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 +91 -126
- package/src/export/SpeedscopeTypes.ts +98 -0
- package/src/export/TimeExport.ts +115 -0
- package/src/index.ts +87 -62
- 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 +55 -53
- package/src/matrix/MatrixInlineRunner.ts +50 -0
- package/src/matrix/MatrixReport.ts +94 -254
- package/src/matrix/VariantLoader.ts +9 -9
- 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 +55 -13
- package/src/profiling/node/ResolvedProfile.ts +98 -0
- 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 +167 -287
- package/src/runners/BenchRunner.ts +27 -22
- package/src/{Benchmark.ts → runners/BenchmarkSpec.ts} +5 -6
- package/src/runners/CreateRunner.ts +5 -7
- package/src/runners/GcStats.ts +58 -61
- 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 +180 -296
- 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 +162 -178
- package/src/stats/BootstrapDifference.ts +282 -0
- package/src/{PermutationTest.ts → stats/PermutationTest.ts} +31 -40
- 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 +9 -41
- package/src/{tests → test}/BenchMatrix.test.ts +31 -28
- 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 +51 -0
- package/src/{tests → test}/MatrixFilter.test.ts +16 -16
- 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 +57 -56
- 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 +35 -30
- 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 +42 -47
- 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/BenchRunner-CSKN9zPy.d.mts +0 -225
- package/dist/BrowserHeapSampler-DCeL42RE.mjs +0 -202
- package/dist/BrowserHeapSampler-DCeL42RE.mjs.map +0 -1
- package/dist/GcStats-ByEovUi1.mjs +0 -77
- package/dist/GcStats-ByEovUi1.mjs.map +0 -1
- package/dist/HeapSampler-B8dtKHn1.mjs.map +0 -1
- package/dist/TimingUtils-ClclVQ7E.mjs +0 -597
- package/dist/TimingUtils-ClclVQ7E.mjs.map +0 -1
- package/dist/browser/index.js +0 -914
- package/dist/src-Cf_LXwlp.mjs +0 -2873
- package/dist/src-Cf_LXwlp.mjs.map +0 -1
- package/src/BenchMatrix.ts +0 -380
- package/src/BenchmarkReport.ts +0 -156
- package/src/HtmlDataPrep.ts +0 -148
- package/src/StandardSections.ts +0 -261
- package/src/StatisticalUtils.ts +0 -176
- 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/heap-sample/HeapSampleReport.ts +0 -196
- 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 -152
- 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 +9 -9
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type BenchforgeArchive,
|
|
3
|
+
migrateArchive,
|
|
4
|
+
} from "../export/ArchiveFormat.ts";
|
|
5
|
+
import type { LineCoverage } from "../export/CoverageExport.ts";
|
|
6
|
+
import type { SpeedscopeFrame } from "../export/SpeedscopeTypes.ts";
|
|
7
|
+
|
|
8
|
+
/** Discriminant for heap allocation vs CPU time profiles. */
|
|
9
|
+
export type ProfileType = "alloc" | "time";
|
|
10
|
+
|
|
11
|
+
/** Feature flags and settings received from the server or inferred from an archive. */
|
|
12
|
+
export interface ViewerConfig {
|
|
13
|
+
editorUri: string | null;
|
|
14
|
+
hasReport: boolean;
|
|
15
|
+
hasProfile: boolean;
|
|
16
|
+
hasTimeProfile: boolean;
|
|
17
|
+
hasCoverage: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Coverage data keyed by source URL. */
|
|
21
|
+
export type ViewerCoverageData = Record<string, LineCoverage[]>;
|
|
22
|
+
|
|
23
|
+
/** A single sampled profile with weights per sample stack. */
|
|
24
|
+
export interface ViewerSpeedscopeProfile {
|
|
25
|
+
type: "sampled";
|
|
26
|
+
unit: "bytes" | "microseconds";
|
|
27
|
+
samples: number[][];
|
|
28
|
+
weights: number[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Container for shared frames and one or more sampled profiles. */
|
|
32
|
+
export interface ViewerSpeedscopeFile {
|
|
33
|
+
shared: { frames: SpeedscopeFrame[] };
|
|
34
|
+
profiles: ViewerSpeedscopeProfile[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Parsed archive data, with optional fields for forward compatibility. */
|
|
38
|
+
export type ArchiveData = Partial<BenchforgeArchive>;
|
|
39
|
+
|
|
40
|
+
/** Abstraction over data sources (live server or static archive). */
|
|
41
|
+
export interface DataProvider {
|
|
42
|
+
readonly config: ViewerConfig;
|
|
43
|
+
fetchReportData(): Promise<unknown>;
|
|
44
|
+
fetchSource(url: string): Promise<string>;
|
|
45
|
+
fetchProfileData(type: ProfileType): Promise<ViewerSpeedscopeFile | null>;
|
|
46
|
+
fetchCoverageData(): Promise<ViewerCoverageData | null>;
|
|
47
|
+
// LATER once we replace the speedscope iframe with an integrated viewer,
|
|
48
|
+
// we can pass profile data directly instead of returning URLs.
|
|
49
|
+
profileUrl(type: ProfileType): string | null;
|
|
50
|
+
createArchive(): Promise<{ blob: Blob; filename: string }>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Fetches data from the live CLI viewer HTTP server. */
|
|
54
|
+
export class ServerProvider implements DataProvider {
|
|
55
|
+
private profileCache = new Map<
|
|
56
|
+
string,
|
|
57
|
+
Promise<ViewerSpeedscopeFile | null>
|
|
58
|
+
>();
|
|
59
|
+
|
|
60
|
+
readonly config: ViewerConfig;
|
|
61
|
+
constructor(config: ViewerConfig) {
|
|
62
|
+
this.config = config;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Fetch the server config and return a ready-to-use provider. */
|
|
66
|
+
static async create(): Promise<ServerProvider> {
|
|
67
|
+
const resp = await fetch("/api/config");
|
|
68
|
+
return new ServerProvider((await resp.json()) as ViewerConfig);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async fetchReportData(): Promise<unknown> {
|
|
72
|
+
const resp = await fetch("/api/report-data");
|
|
73
|
+
if (!resp.ok) throw new Error("No report data: " + resp.status);
|
|
74
|
+
return resp.json();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async fetchSource(url: string): Promise<string> {
|
|
78
|
+
const resp = await fetch("/api/source?url=" + encodeURIComponent(url));
|
|
79
|
+
if (!resp.ok) throw new Error("Source unavailable");
|
|
80
|
+
return resp.text();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Fetch a speedscope profile, caching the result by type. */
|
|
84
|
+
fetchProfileData(type: ProfileType): Promise<ViewerSpeedscopeFile | null> {
|
|
85
|
+
const url = this.profileUrl(type);
|
|
86
|
+
if (!url) return Promise.resolve(null);
|
|
87
|
+
let cached = this.profileCache.get(type);
|
|
88
|
+
if (!cached) {
|
|
89
|
+
cached = fetch(url).then(r =>
|
|
90
|
+
r.ok ? (r.json() as Promise<ViewerSpeedscopeFile>) : null,
|
|
91
|
+
);
|
|
92
|
+
this.profileCache.set(type, cached);
|
|
93
|
+
}
|
|
94
|
+
return cached;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private coverageCache?: Promise<ViewerCoverageData | null>;
|
|
98
|
+
|
|
99
|
+
fetchCoverageData(): Promise<ViewerCoverageData | null> {
|
|
100
|
+
if (!this.config.hasCoverage) return Promise.resolve(null);
|
|
101
|
+
this.coverageCache ??= fetch("/api/coverage").then(r =>
|
|
102
|
+
r.ok ? (r.json() as Promise<ViewerCoverageData>) : null,
|
|
103
|
+
);
|
|
104
|
+
return this.coverageCache;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
profileUrl(type: ProfileType): string | null {
|
|
108
|
+
if (type === "alloc") return this.config.hasProfile ? "/api/profile" : null;
|
|
109
|
+
return this.config.hasTimeProfile ? "/api/profile/time" : null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Request a `.benchforge` archive, extracting filename from Content-Disposition. */
|
|
113
|
+
async createArchive(): Promise<{ blob: Blob; filename: string }> {
|
|
114
|
+
const resp = await fetch("/api/archive", { method: "POST" });
|
|
115
|
+
if (!resp.ok) throw new Error("Archive failed");
|
|
116
|
+
const header = resp.headers.get("Content-Disposition") || "";
|
|
117
|
+
const filename =
|
|
118
|
+
header.match(/filename="?(.+?)"?$/)?.[1] ||
|
|
119
|
+
"benchforge-archive.benchforge";
|
|
120
|
+
return { blob: await resp.blob(), filename };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Serves data from an in-memory `.benchforge` archive (drag-drop or URL). */
|
|
125
|
+
export class ArchiveProvider implements DataProvider {
|
|
126
|
+
readonly config: ViewerConfig;
|
|
127
|
+
private blobUrls = new Map<string, string>();
|
|
128
|
+
private archive: ArchiveData;
|
|
129
|
+
|
|
130
|
+
constructor(archive: ArchiveData | Record<string, unknown>) {
|
|
131
|
+
this.archive = migrateArchive(archive as Record<string, unknown>);
|
|
132
|
+
this.config = {
|
|
133
|
+
editorUri: null,
|
|
134
|
+
hasReport: !!archive.report,
|
|
135
|
+
hasProfile: !!archive.allocProfile,
|
|
136
|
+
hasTimeProfile: !!archive.timeProfile,
|
|
137
|
+
hasCoverage: !!archive.coverage,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private rawProfile(type: ProfileType): unknown {
|
|
142
|
+
return type === "alloc"
|
|
143
|
+
? this.archive.allocProfile
|
|
144
|
+
: this.archive.timeProfile;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async fetchReportData(): Promise<unknown> {
|
|
148
|
+
if (!this.archive.report) throw new Error("No report data");
|
|
149
|
+
return this.archive.report;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async fetchSource(url: string): Promise<string> {
|
|
153
|
+
const source = this.archive.sources?.[url];
|
|
154
|
+
if (source === undefined) throw new Error("Source unavailable");
|
|
155
|
+
return source;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async fetchProfileData(
|
|
159
|
+
type: ProfileType,
|
|
160
|
+
): Promise<ViewerSpeedscopeFile | null> {
|
|
161
|
+
return (this.rawProfile(type) as ViewerSpeedscopeFile) ?? null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async fetchCoverageData(): Promise<ViewerCoverageData | null> {
|
|
165
|
+
return (this.archive.coverage as ViewerCoverageData) ?? null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Return a blob URL for the profile, lazily created and cached. */
|
|
169
|
+
profileUrl(type: ProfileType): string | null {
|
|
170
|
+
const data = this.rawProfile(type);
|
|
171
|
+
if (!data) return null;
|
|
172
|
+
let url = this.blobUrls.get(type);
|
|
173
|
+
if (!url) {
|
|
174
|
+
const blob = new Blob([JSON.stringify(data)], {
|
|
175
|
+
type: "application/json",
|
|
176
|
+
});
|
|
177
|
+
url = URL.createObjectURL(blob);
|
|
178
|
+
this.blobUrls.set(type, url);
|
|
179
|
+
}
|
|
180
|
+
return url;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async createArchive(): Promise<{ blob: Blob; filename: string }> {
|
|
184
|
+
const blob = new Blob([JSON.stringify(this.archive)], {
|
|
185
|
+
type: "application/json",
|
|
186
|
+
});
|
|
187
|
+
const fallback = new Date().toISOString().replace(/[:.]/g, "-");
|
|
188
|
+
const timestamp = this.archive.metadata?.timestamp || fallback;
|
|
189
|
+
return { blob, filename: `benchforge-${timestamp}.benchforge` };
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { GitVersion } from "../report/GitUtils.ts";
|
|
2
|
+
import type { PausePoint } from "../runners/MeasuredResults.ts";
|
|
3
|
+
import type {
|
|
4
|
+
CILevel,
|
|
5
|
+
DifferenceCI,
|
|
6
|
+
HistogramBin,
|
|
7
|
+
} from "../stats/StatisticalUtils.ts";
|
|
8
|
+
|
|
9
|
+
/** Top-level data structure for the HTML benchmark report. */
|
|
10
|
+
export interface ReportData {
|
|
11
|
+
groups: BenchmarkGroup[];
|
|
12
|
+
metadata: {
|
|
13
|
+
timestamp: string;
|
|
14
|
+
bencherVersion: string;
|
|
15
|
+
cliArgs?: Record<string, unknown>;
|
|
16
|
+
cliDefaults?: Record<string, unknown>;
|
|
17
|
+
gcTrackingEnabled?: boolean;
|
|
18
|
+
currentVersion?: GitVersion;
|
|
19
|
+
baselineVersion?: GitVersion;
|
|
20
|
+
environment?: {
|
|
21
|
+
node: string;
|
|
22
|
+
platform: string;
|
|
23
|
+
arch: string;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A named group of benchmarks, optionally compared against a baseline. */
|
|
29
|
+
export interface BenchmarkGroup {
|
|
30
|
+
name: string;
|
|
31
|
+
baseline?: BenchmarkEntry;
|
|
32
|
+
benchmarks: BenchmarkEntry[];
|
|
33
|
+
warnings?: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** One benchmark's raw data, statistics, and optional comparison results. */
|
|
37
|
+
export interface BenchmarkEntry {
|
|
38
|
+
name: string;
|
|
39
|
+
samples: number[];
|
|
40
|
+
warmupSamples?: number[];
|
|
41
|
+
allocationSamples?: number[];
|
|
42
|
+
heapSamples?: number[];
|
|
43
|
+
gcEvents?: GcEvent[];
|
|
44
|
+
optSamples?: number[];
|
|
45
|
+
pausePoints?: PausePoint[];
|
|
46
|
+
batchOffsets?: number[];
|
|
47
|
+
stats: BenchmarkStats;
|
|
48
|
+
heapSize?: { min: number; max: number; avg: number };
|
|
49
|
+
totalTime?: number;
|
|
50
|
+
sections?: ViewerSection[];
|
|
51
|
+
coverageSummary?: CoverageSummary;
|
|
52
|
+
heapSummary?: HeapSummary;
|
|
53
|
+
comparisonCI?: DifferenceCI;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Summary percentile statistics for a benchmark's samples. */
|
|
57
|
+
export interface BenchmarkStats {
|
|
58
|
+
min: number;
|
|
59
|
+
max: number;
|
|
60
|
+
avg: number;
|
|
61
|
+
p50: number;
|
|
62
|
+
p75: number;
|
|
63
|
+
p99: number;
|
|
64
|
+
p999: number;
|
|
65
|
+
cv?: number;
|
|
66
|
+
mad?: number;
|
|
67
|
+
outlierRate?: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** A section of related stats for the viewer (e.g., "Lines / Sec", "GC"). */
|
|
71
|
+
export interface ViewerSection {
|
|
72
|
+
title: string;
|
|
73
|
+
tabLink?: string;
|
|
74
|
+
rows: ViewerRow[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** A stat row with per-run values and optional comparison CI. */
|
|
78
|
+
export interface ViewerRow {
|
|
79
|
+
label: string;
|
|
80
|
+
entries: ViewerEntry[];
|
|
81
|
+
comparisonCI?: DifferenceCI;
|
|
82
|
+
shared?: boolean;
|
|
83
|
+
/** First comparable row with a statKind in the section. */
|
|
84
|
+
primary?: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** A single run's value for a stat. */
|
|
88
|
+
export interface ViewerEntry {
|
|
89
|
+
runName: string;
|
|
90
|
+
value: string;
|
|
91
|
+
bootstrapCI?: BootstrapCIData;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Bootstrap CI data for inline visualization. */
|
|
95
|
+
export interface BootstrapCIData {
|
|
96
|
+
estimate: number;
|
|
97
|
+
ci: [number, number];
|
|
98
|
+
histogram: HistogramBin[];
|
|
99
|
+
/** Formatted CI bounds for display (e.g., ["0.12ms", "0.15ms"]) */
|
|
100
|
+
ciLabels?: [string, string];
|
|
101
|
+
/** Block-level (between-run) or sample-level (within-run) resampling */
|
|
102
|
+
ciLevel?: CILevel;
|
|
103
|
+
/** false when batch count is too low for reliable CI */
|
|
104
|
+
ciReliable?: boolean;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Summary of coverage/call-count data. */
|
|
108
|
+
export interface CoverageSummary {
|
|
109
|
+
functionCount: number;
|
|
110
|
+
totalCalls: number;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Summary of heap allocation profile. */
|
|
114
|
+
export interface HeapSummary {
|
|
115
|
+
totalBytes: number;
|
|
116
|
+
userBytes: number;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** A garbage collection event with timing relative to the benchmark start. */
|
|
120
|
+
export interface GcEvent {
|
|
121
|
+
offset: number;
|
|
122
|
+
duration: number;
|
|
123
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { signal } from "@preact/signals";
|
|
2
|
+
import type { DataProvider } from "./Providers.ts";
|
|
3
|
+
import type { ReportData } from "./ReportData.ts";
|
|
4
|
+
|
|
5
|
+
/** Tracked state for an open source-code tab in the viewer. */
|
|
6
|
+
export interface SourceTabState {
|
|
7
|
+
id: string;
|
|
8
|
+
file: string;
|
|
9
|
+
line: number;
|
|
10
|
+
col: number;
|
|
11
|
+
generation: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** User color-scheme preference: follow OS, or force light/dark. */
|
|
15
|
+
export type ThemePreference = "system" | "light" | "dark";
|
|
16
|
+
|
|
17
|
+
/** Active data source (server or archive). */
|
|
18
|
+
export const provider = signal<DataProvider | null>(null);
|
|
19
|
+
|
|
20
|
+
/** Parsed report data from the provider. */
|
|
21
|
+
export const reportData = signal<ReportData | null>(null);
|
|
22
|
+
|
|
23
|
+
/** Currently visible tab id. */
|
|
24
|
+
export const activeTabId = signal("summary");
|
|
25
|
+
|
|
26
|
+
/** Whether sample data has been loaded for the samples tab. */
|
|
27
|
+
export const samplesLoaded = signal(false);
|
|
28
|
+
|
|
29
|
+
/** Error info when a `?url=` archive fetch fails. */
|
|
30
|
+
export const urlError = signal<{ url: string; detail: string } | null>(null);
|
|
31
|
+
|
|
32
|
+
/** Open source-code tabs. */
|
|
33
|
+
export const sourceTabs = signal<SourceTabState[]>([]);
|
|
34
|
+
|
|
35
|
+
const cookieTheme = document.cookie.match(/(?:^|; )theme=(light|dark)/);
|
|
36
|
+
|
|
37
|
+
/** User's light/dark theme preference, initialized from cookie. */
|
|
38
|
+
export const themePreference = signal<ThemePreference>(
|
|
39
|
+
(cookieTheme?.[1] as ThemePreference) ?? "system",
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
/** Pick the best default tab based on available data. */
|
|
43
|
+
export function defaultTabId(): string {
|
|
44
|
+
const config = provider.value?.config;
|
|
45
|
+
if (config?.hasReport) return "summary";
|
|
46
|
+
if (config?.hasProfile) return "flamechart";
|
|
47
|
+
if (config?.hasTimeProfile) return "time-flamechart";
|
|
48
|
+
return "summary";
|
|
49
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ThemePreference, themePreference } from "./State.ts";
|
|
2
|
+
|
|
3
|
+
/** Apply a theme preference, persisting to a cookie and updating the document. */
|
|
4
|
+
export function setTheme(pref: ThemePreference): void {
|
|
5
|
+
themePreference.value = pref;
|
|
6
|
+
if (pref === "system") {
|
|
7
|
+
delete document.documentElement.dataset.theme;
|
|
8
|
+
// biome-ignore lint/suspicious/noDocumentCookie: no alternative API for setting cookies
|
|
9
|
+
document.cookie = "theme=; max-age=0; path=/; SameSite=Lax";
|
|
10
|
+
} else {
|
|
11
|
+
document.documentElement.dataset.theme = pref;
|
|
12
|
+
// biome-ignore lint/suspicious/noDocumentCookie: no alternative API for setting cookies
|
|
13
|
+
document.cookie = `theme=${pref}; max-age=31536000; path=/; SameSite=Lax`;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { useEffect, useState } from "preact/hooks";
|
|
2
|
+
import type { DataProvider } from "../Providers.ts";
|
|
3
|
+
import {
|
|
4
|
+
type ArchiveData,
|
|
5
|
+
ArchiveProvider,
|
|
6
|
+
ServerProvider,
|
|
7
|
+
} from "../Providers.ts";
|
|
8
|
+
import {
|
|
9
|
+
activeTabId,
|
|
10
|
+
defaultTabId,
|
|
11
|
+
provider,
|
|
12
|
+
reportData,
|
|
13
|
+
samplesLoaded,
|
|
14
|
+
sourceTabs,
|
|
15
|
+
urlError,
|
|
16
|
+
} from "../State.ts";
|
|
17
|
+
import { DropZone } from "./DropZone.tsx";
|
|
18
|
+
import { Shell } from "./Shell.tsx";
|
|
19
|
+
|
|
20
|
+
/** Reset viewer state and activate the appropriate default tab for the provider. */
|
|
21
|
+
export function initViewer(p: DataProvider): void {
|
|
22
|
+
provider.value = p;
|
|
23
|
+
reportData.value = null;
|
|
24
|
+
samplesLoaded.value = false;
|
|
25
|
+
urlError.value = null;
|
|
26
|
+
sourceTabs.value = [];
|
|
27
|
+
activeTabId.value = defaultTabId();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function App() {
|
|
31
|
+
const [resolved, setResolved] = useState(false);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
resolve().then(() => setResolved(true));
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
if (provider.value) return <Shell />;
|
|
38
|
+
if (resolved) return <DropZone />;
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Try archive URL param, preloaded data, then dev server -- first match wins. */
|
|
43
|
+
async function resolve(): Promise<void> {
|
|
44
|
+
const archiveUrl = new URLSearchParams(window.location.search).get("url");
|
|
45
|
+
if (archiveUrl) {
|
|
46
|
+
try {
|
|
47
|
+
const resp = await fetch(archiveUrl);
|
|
48
|
+
if (resp.ok) {
|
|
49
|
+
initViewer(new ArchiveProvider((await resp.json()) as ArchiveData));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
urlError.value = { url: archiveUrl, detail: `(${resp.status})` };
|
|
53
|
+
} catch {
|
|
54
|
+
urlError.value = {
|
|
55
|
+
url: archiveUrl,
|
|
56
|
+
detail: "(Perhaps CORS or network error?)",
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const preloaded = (window as unknown as Record<string, unknown>)
|
|
62
|
+
.__benchforgeArchive as ArchiveData | undefined;
|
|
63
|
+
if (preloaded) {
|
|
64
|
+
initViewer(new ArchiveProvider(preloaded));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
initViewer(await ServerProvider.create());
|
|
70
|
+
} catch {
|
|
71
|
+
// No server available
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { useRef, useState } from "preact/hooks";
|
|
2
|
+
import { type ArchiveData, ArchiveProvider } from "../Providers.ts";
|
|
3
|
+
import { urlError } from "../State.ts";
|
|
4
|
+
import { initViewer } from "./App.tsx";
|
|
5
|
+
|
|
6
|
+
/** Landing page for loading `.benchforge` archive files via drag-drop or file picker. */
|
|
7
|
+
export function DropZone() {
|
|
8
|
+
const [dragOver, setDragOver] = useState(false);
|
|
9
|
+
const [error, setError] = useState<string | null>(null);
|
|
10
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
11
|
+
|
|
12
|
+
/** Parse an archive JSON file and initialize the viewer with its data. */
|
|
13
|
+
async function loadFile(file: File): Promise<void> {
|
|
14
|
+
try {
|
|
15
|
+
const text = await file.text();
|
|
16
|
+
const archive = JSON.parse(text) as ArchiveData;
|
|
17
|
+
initViewer(new ArchiveProvider(archive));
|
|
18
|
+
} catch (err) {
|
|
19
|
+
console.error("Failed to load archive:", err);
|
|
20
|
+
setError(String(err));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div
|
|
26
|
+
class={`drop-zone${dragOver ? " drag-over" : ""}`}
|
|
27
|
+
onDragOver={e => {
|
|
28
|
+
e.preventDefault();
|
|
29
|
+
setDragOver(true);
|
|
30
|
+
}}
|
|
31
|
+
onDragLeave={() => setDragOver(false)}
|
|
32
|
+
onDrop={async e => {
|
|
33
|
+
e.preventDefault();
|
|
34
|
+
setDragOver(false);
|
|
35
|
+
const file = e.dataTransfer?.files[0];
|
|
36
|
+
if (file) loadFile(file);
|
|
37
|
+
}}
|
|
38
|
+
>
|
|
39
|
+
<div class="drop-zone-content">
|
|
40
|
+
<h2>Benchforge Viewer</h2>
|
|
41
|
+
<p>
|
|
42
|
+
Drop a <code>.benchforge</code> file here to view results
|
|
43
|
+
</p>
|
|
44
|
+
<div class="drop-zone-divider">or</div>
|
|
45
|
+
<label class="drop-zone-browse">
|
|
46
|
+
Browse files
|
|
47
|
+
<input
|
|
48
|
+
ref={inputRef}
|
|
49
|
+
type="file"
|
|
50
|
+
accept=".benchforge"
|
|
51
|
+
hidden
|
|
52
|
+
onChange={() => {
|
|
53
|
+
const file = inputRef.current?.files?.[0];
|
|
54
|
+
if (file) loadFile(file);
|
|
55
|
+
}}
|
|
56
|
+
/>
|
|
57
|
+
</label>
|
|
58
|
+
{urlError.value && (
|
|
59
|
+
<p class="drop-zone-error">
|
|
60
|
+
Failed to load archive from <b>{urlError.value.url}</b>.{" "}
|
|
61
|
+
{urlError.value.detail}
|
|
62
|
+
<p>Download the file and drop it here instead.</p>
|
|
63
|
+
</p>
|
|
64
|
+
)}
|
|
65
|
+
{error && (
|
|
66
|
+
<p class="drop-zone-error">Failed to load file: {error}</p>
|
|
67
|
+
)}
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { RefObject } from "preact";
|
|
2
|
+
import { useEffect, useRef } from "preact/hooks";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Lazy-import a plot module and mount the resulting element into a div ref.
|
|
6
|
+
* Defers the ~hundreds-of-KB `@observablehq/plot` + `d3` bundle until the
|
|
7
|
+
* Samples tab is actually opened, keeping the initial viewer load small.
|
|
8
|
+
*/
|
|
9
|
+
export function useLazyPlot(
|
|
10
|
+
render: () => Promise<Element | null | undefined>,
|
|
11
|
+
deps: unknown[],
|
|
12
|
+
errorLabel?: string,
|
|
13
|
+
): RefObject<HTMLDivElement> {
|
|
14
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
render()
|
|
17
|
+
.then(el => {
|
|
18
|
+
if (!ref.current || !el) return;
|
|
19
|
+
ref.current.innerHTML = "";
|
|
20
|
+
ref.current.appendChild(el);
|
|
21
|
+
})
|
|
22
|
+
.catch(err => {
|
|
23
|
+
console.error(`${errorLabel ?? "Plot"} failed:`, err);
|
|
24
|
+
if (ref.current) {
|
|
25
|
+
const div = document.createElement("div");
|
|
26
|
+
div.className = "loading";
|
|
27
|
+
div.textContent = String(err.message ?? err);
|
|
28
|
+
ref.current.replaceChildren(div);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}, deps);
|
|
32
|
+
return ref;
|
|
33
|
+
}
|