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
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { createServer, type Server } from "node:http";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { Browser } from "playwright";
|
|
4
|
+
import { chromium } from "playwright";
|
|
5
|
+
import sirv from "sirv";
|
|
6
|
+
import { afterAll, beforeAll, expect, test } from "vitest";
|
|
7
|
+
|
|
8
|
+
const viewerDir = path.resolve(import.meta.dirname!, "../../dist/viewer");
|
|
9
|
+
const archivePath = path.resolve(
|
|
10
|
+
import.meta.dirname!,
|
|
11
|
+
"../../examples/simple-cli.benchforge",
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
let server: Server;
|
|
15
|
+
let port: number;
|
|
16
|
+
let browser: Browser;
|
|
17
|
+
|
|
18
|
+
test("static viewer: drop zone appears on load", {
|
|
19
|
+
timeout: 30_000,
|
|
20
|
+
}, async () => {
|
|
21
|
+
const page = await browser.newPage();
|
|
22
|
+
try {
|
|
23
|
+
await page.goto(`http://localhost:${port}`, { waitUntil: "networkidle" });
|
|
24
|
+
await page.locator(".drop-zone").waitFor({ state: "visible" });
|
|
25
|
+
} finally {
|
|
26
|
+
await page.close();
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("static viewer: archive upload shows summary with stats", {
|
|
31
|
+
timeout: 30_000,
|
|
32
|
+
}, async () => {
|
|
33
|
+
const consoleErrors: string[] = [];
|
|
34
|
+
const page = await browser.newPage();
|
|
35
|
+
try {
|
|
36
|
+
page.on("console", msg => {
|
|
37
|
+
if (msg.type() === "error" && !msg.text().includes("WebGL"))
|
|
38
|
+
consoleErrors.push(msg.text());
|
|
39
|
+
});
|
|
40
|
+
await page.goto(`http://localhost:${port}`, { waitUntil: "networkidle" });
|
|
41
|
+
|
|
42
|
+
const fileInput = page.locator('.drop-zone input[type="file"]');
|
|
43
|
+
await fileInput.setInputFiles(archivePath);
|
|
44
|
+
|
|
45
|
+
await page
|
|
46
|
+
.locator(".drop-zone")
|
|
47
|
+
.waitFor({ state: "detached", timeout: 15_000 });
|
|
48
|
+
|
|
49
|
+
const summaryPanel = page.locator("#summary-panel");
|
|
50
|
+
const stats = summaryPanel.locator(".section-panel").first();
|
|
51
|
+
await stats.waitFor({ state: "visible", timeout: 15_000 });
|
|
52
|
+
const statRows = await summaryPanel.locator(".stat-row").count();
|
|
53
|
+
expect(statRows).toBeGreaterThan(0);
|
|
54
|
+
} finally {
|
|
55
|
+
await page.close();
|
|
56
|
+
}
|
|
57
|
+
expect(consoleErrors).toEqual([]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("static viewer: tab navigation after archive upload", {
|
|
61
|
+
timeout: 30_000,
|
|
62
|
+
}, async () => {
|
|
63
|
+
const consoleErrors: string[] = [];
|
|
64
|
+
const page = await browser.newPage();
|
|
65
|
+
try {
|
|
66
|
+
page.on("console", msg => {
|
|
67
|
+
if (msg.type() === "error" && !msg.text().includes("WebGL"))
|
|
68
|
+
consoleErrors.push(msg.text());
|
|
69
|
+
});
|
|
70
|
+
await page.goto(`http://localhost:${port}`, { waitUntil: "networkidle" });
|
|
71
|
+
|
|
72
|
+
const fileInput = page.locator('.drop-zone input[type="file"]');
|
|
73
|
+
await fileInput.setInputFiles(archivePath);
|
|
74
|
+
await page
|
|
75
|
+
.locator(".drop-zone")
|
|
76
|
+
.waitFor({ state: "detached", timeout: 15_000 });
|
|
77
|
+
|
|
78
|
+
// Wait for summary to load
|
|
79
|
+
const summaryPanel = page.locator("#summary-panel");
|
|
80
|
+
await summaryPanel
|
|
81
|
+
.locator(".section-panel")
|
|
82
|
+
.first()
|
|
83
|
+
.waitFor({ state: "visible", timeout: 15_000 });
|
|
84
|
+
|
|
85
|
+
// Samples tab
|
|
86
|
+
await page.locator("#tab-samples").click();
|
|
87
|
+
const samplesPanel = page.locator("#samples-panel");
|
|
88
|
+
await samplesPanel
|
|
89
|
+
.locator("svg")
|
|
90
|
+
.first()
|
|
91
|
+
.waitFor({ state: "visible", timeout: 15_000 });
|
|
92
|
+
|
|
93
|
+
// Allocation tab
|
|
94
|
+
await page.locator("#tab-flamechart").click();
|
|
95
|
+
const allocFrame = page.frameLocator("#speedscope-iframe");
|
|
96
|
+
await allocFrame
|
|
97
|
+
.locator("body *")
|
|
98
|
+
.first()
|
|
99
|
+
.waitFor({ state: "visible", timeout: 15_000 });
|
|
100
|
+
|
|
101
|
+
// Back to Summary
|
|
102
|
+
await page.locator("#tab-summary").click();
|
|
103
|
+
await summaryPanel
|
|
104
|
+
.locator(".section-panel")
|
|
105
|
+
.first()
|
|
106
|
+
.waitFor({ state: "visible" });
|
|
107
|
+
} finally {
|
|
108
|
+
await page.close();
|
|
109
|
+
}
|
|
110
|
+
expect(consoleErrors).toEqual([]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
beforeAll(async () => {
|
|
114
|
+
const assets = sirv(viewerDir, { single: true });
|
|
115
|
+
server = createServer((req, res) => {
|
|
116
|
+
assets(req, res, () => {
|
|
117
|
+
res.statusCode = 404;
|
|
118
|
+
res.end("Not found");
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
const portP = new Promise<number>((resolve, reject) => {
|
|
122
|
+
server.listen(0, "127.0.0.1", () => {
|
|
123
|
+
const addr = server.address();
|
|
124
|
+
if (typeof addr === "object" && addr) resolve(addr.port);
|
|
125
|
+
else reject(new Error("Failed to get server address"));
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
[port, browser] = await Promise.all([
|
|
129
|
+
portP,
|
|
130
|
+
chromium.launch({ headless: true }),
|
|
131
|
+
]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
afterAll(async () => {
|
|
135
|
+
await browser?.close();
|
|
136
|
+
server?.close();
|
|
137
|
+
});
|
|
@@ -155,4 +155,6 @@ export const bevy30SamplesMs = [
|
|
|
155
155
|
] as number[];
|
|
156
156
|
|
|
157
157
|
/** Get samples in nanoseconds */
|
|
158
|
-
export const bevy30SamplesNs = bevy30SamplesMs.map(
|
|
158
|
+
export const bevy30SamplesNs: number[] = bevy30SamplesMs.map(
|
|
159
|
+
s => s * 1_000_000,
|
|
160
|
+
);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Test cases module with async loadCase */
|
|
2
|
+
export const cases: string[] = ["alpha", "beta"];
|
|
3
|
+
|
|
4
|
+
export async function loadCase(
|
|
5
|
+
id: string,
|
|
6
|
+
): Promise<{ data: string; metadata: { original: string } }> {
|
|
7
|
+
await Promise.resolve(); // simulate async
|
|
8
|
+
return { data: id.toUpperCase(), metadata: { original: id } };
|
|
9
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/** Test cases module for Phase 3 testing */
|
|
2
|
-
export const cases = ["small", "large"];
|
|
2
|
+
export const cases: string[] = ["small", "large"];
|
|
3
3
|
|
|
4
|
-
export function loadCase(id: string) {
|
|
4
|
+
export function loadCase(id: string): {
|
|
5
|
+
data: number[];
|
|
6
|
+
metadata: { size: number };
|
|
7
|
+
} {
|
|
5
8
|
const data =
|
|
6
9
|
id === "small" ? [1, 2, 3] : Array.from({ length: 100 }, (_, i) => i);
|
|
7
10
|
return { data, metadata: { size: data.length } };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const run = (): void => {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const notRun = (): void => {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const run = (): void => {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const run = (): void => {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const run = (): void => {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const run = (): void => {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Format ISO date as local time with UTC: "Jan 9, 2026, 3:45 PM (2026-01-09T23:45:00Z)" */
|
|
2
|
+
export function formatDateWithTimezone(isoDate: string): string {
|
|
3
|
+
const date = new Date(isoDate);
|
|
4
|
+
const local = date.toLocaleString("en-US", {
|
|
5
|
+
month: "short",
|
|
6
|
+
day: "numeric",
|
|
7
|
+
year: "numeric",
|
|
8
|
+
hour: "numeric",
|
|
9
|
+
minute: "2-digit",
|
|
10
|
+
});
|
|
11
|
+
const utc = date.toISOString().replace(".000Z", "Z");
|
|
12
|
+
return `${local} (${utc})`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Format relative time: "5m ago", "2h ago", "yesterday", "3 days ago" */
|
|
16
|
+
export function formatRelativeTime(isoDate: string): string {
|
|
17
|
+
const date = new Date(isoDate);
|
|
18
|
+
const now = new Date();
|
|
19
|
+
const diffMs = now.getTime() - date.getTime();
|
|
20
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
21
|
+
const diffHours = Math.floor(diffMs / 3600000);
|
|
22
|
+
const diffDays = Math.floor(diffMs / 86400000);
|
|
23
|
+
|
|
24
|
+
if (diffMins < 1) return "just now";
|
|
25
|
+
if (diffMins < 60) return `${diffMins}m ago`;
|
|
26
|
+
if (diffHours < 24) return `${diffHours}h ago`;
|
|
27
|
+
if (diffDays === 1) return "yesterday";
|
|
28
|
+
if (diffDays < 30) return `${diffDays} days ago`;
|
|
29
|
+
return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
30
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Escape a string for safe insertion into HTML. */
|
|
2
|
+
export function escapeHtml(s: string): string {
|
|
3
|
+
const el = document.createElement("div");
|
|
4
|
+
el.textContent = s;
|
|
5
|
+
return el.innerHTML;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Infer a Shiki language id from a file extension. */
|
|
9
|
+
export function guessLang(file: string): string {
|
|
10
|
+
if (file.endsWith(".ts") || file.endsWith(".tsx")) return "typescript";
|
|
11
|
+
if (file.endsWith(".css")) return "css";
|
|
12
|
+
if (file.endsWith(".html")) return "html";
|
|
13
|
+
return "javascript";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Extract the pathname from a URL, returning the input unchanged if it isn't a valid URL. */
|
|
17
|
+
export function filePathFromUrl(url: string): string {
|
|
18
|
+
try {
|
|
19
|
+
return new URL(url).pathname;
|
|
20
|
+
} catch {
|
|
21
|
+
return url;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { ViewerCoverageData, ViewerSpeedscopeFile } from "./Providers.ts";
|
|
2
|
+
|
|
3
|
+
/** Per-line profiling metrics for source gutter display. */
|
|
4
|
+
export interface LineGutterData {
|
|
5
|
+
allocBytes: Map<number, number>;
|
|
6
|
+
selfTimeUs: Map<number, number>;
|
|
7
|
+
callCounts: Map<number, number>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Aggregate per-line profiling data for a source file from speedscope profiles. */
|
|
11
|
+
export function computeLineData(
|
|
12
|
+
file: string,
|
|
13
|
+
allocProfile: ViewerSpeedscopeFile | null,
|
|
14
|
+
timeProfile: ViewerSpeedscopeFile | null,
|
|
15
|
+
coverage: ViewerCoverageData | null,
|
|
16
|
+
): LineGutterData {
|
|
17
|
+
return {
|
|
18
|
+
allocBytes: aggregateSelf(file, allocProfile),
|
|
19
|
+
selfTimeUs: aggregateSelf(file, timeProfile),
|
|
20
|
+
callCounts: extractCallCounts(file, coverage),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Format byte count for gutter display, scaling to KB/MB as appropriate. */
|
|
25
|
+
export function formatGutterBytes(bytes: number | undefined): string {
|
|
26
|
+
if (!bytes) return "";
|
|
27
|
+
return formatDecimalBytes(bytes);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Format bytes using decimal (SI) units: KB = 1000, MB = 1e6, GB = 1e9. */
|
|
31
|
+
export function formatDecimalBytes(bytes: number): string {
|
|
32
|
+
if (bytes >= 1e9) return (bytes / 1e9).toFixed(1) + " GB";
|
|
33
|
+
if (bytes >= 1e6) return (bytes / 1e6).toFixed(1) + " MB";
|
|
34
|
+
if (bytes >= 1e3) return (bytes / 1e3).toFixed(1) + " KB";
|
|
35
|
+
return bytes + " B";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Format microsecond duration for gutter display, scaling to ms/s as appropriate. */
|
|
39
|
+
export function formatGutterTime(us: number | undefined): string {
|
|
40
|
+
if (!us) return "";
|
|
41
|
+
if (us >= 1_000_000) return (us / 1_000_000).toFixed(1) + " s";
|
|
42
|
+
if (us >= 1_000) return (us / 1_000).toFixed(1) + " ms";
|
|
43
|
+
return us.toFixed(0) + " us";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Format large counts with K/M suffixes (e.g. 1234567 ==> "1.2M"). */
|
|
47
|
+
export function formatCount(n: number): string {
|
|
48
|
+
if (n >= 1e6) return (n / 1e6).toFixed(1) + "M";
|
|
49
|
+
if (n >= 1e3) return (n / 1e3).toFixed(1) + "K";
|
|
50
|
+
return n.toLocaleString();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Format a call count for gutter display, scaling to K/M as appropriate. */
|
|
54
|
+
export function formatGutterCount(count: number | undefined): string {
|
|
55
|
+
if (!count) return "";
|
|
56
|
+
return formatCount(count);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Accumulate weight for the deepest (self) frame matching `file` in each sample. */
|
|
60
|
+
function aggregateSelf(
|
|
61
|
+
file: string,
|
|
62
|
+
profile: ViewerSpeedscopeFile | null,
|
|
63
|
+
): Map<number, number> {
|
|
64
|
+
const result = new Map<number, number>();
|
|
65
|
+
if (!profile) return result;
|
|
66
|
+
|
|
67
|
+
const { frames } = profile.shared;
|
|
68
|
+
|
|
69
|
+
const fileFrames = new Map<number, number>(); // frameIndex -> line
|
|
70
|
+
frames.forEach((frame, i) => {
|
|
71
|
+
if (frame.line && frame.file && fileMatches(frame.file, file))
|
|
72
|
+
fileFrames.set(i, frame.line);
|
|
73
|
+
});
|
|
74
|
+
if (fileFrames.size === 0) return result;
|
|
75
|
+
|
|
76
|
+
for (const p of profile.profiles) {
|
|
77
|
+
for (let i = 0; i < p.samples.length; i++) {
|
|
78
|
+
const leaf = p.samples[i].at(-1)!;
|
|
79
|
+
const line = fileFrames.get(leaf);
|
|
80
|
+
if (line !== undefined)
|
|
81
|
+
result.set(line, (result.get(line) || 0) + p.weights[i]);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Extract per-function call counts from coverage data for a file. */
|
|
88
|
+
function extractCallCounts(
|
|
89
|
+
file: string,
|
|
90
|
+
coverage: ViewerCoverageData | null,
|
|
91
|
+
): Map<number, number> {
|
|
92
|
+
const result = new Map<number, number>();
|
|
93
|
+
if (!coverage) return result;
|
|
94
|
+
|
|
95
|
+
const entries = coverage[file] ?? findCoverageEntries(file, coverage);
|
|
96
|
+
if (!entries) return result;
|
|
97
|
+
|
|
98
|
+
for (const entry of entries) {
|
|
99
|
+
if (entry.count <= 0) continue;
|
|
100
|
+
const prev = result.get(entry.startLine) || 0;
|
|
101
|
+
if (entry.count > prev) result.set(entry.startLine, entry.count);
|
|
102
|
+
}
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Check if a frame's file URL matches the target file path. */
|
|
107
|
+
function fileMatches(frameFile: string, target: string): boolean {
|
|
108
|
+
if (frameFile === target) return true;
|
|
109
|
+
// Frame files may be full URLs while target is a bare path, or vice versa
|
|
110
|
+
try {
|
|
111
|
+
if (new URL(frameFile).pathname === target) return true;
|
|
112
|
+
} catch {}
|
|
113
|
+
return frameFile.endsWith(target) || target.endsWith(frameFile);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Find coverage entries by URL matching when exact key lookup fails. */
|
|
117
|
+
function findCoverageEntries(file: string, coverage: ViewerCoverageData) {
|
|
118
|
+
const matchingUrl = Object.keys(coverage).find(url => fileMatches(url, file));
|
|
119
|
+
return matchingUrl ? coverage[matchingUrl] : undefined;
|
|
120
|
+
}
|
|
@@ -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
|
+
}
|