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,159 @@
|
|
|
1
|
+
import type { ChildProcess } from "node:child_process";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import type { Browser } from "playwright";
|
|
5
|
+
import { chromium } from "playwright";
|
|
6
|
+
import { afterAll, beforeAll, expect, test } from "vitest";
|
|
7
|
+
|
|
8
|
+
const binPath = path.resolve(import.meta.dirname!, "../../bin/benchforge");
|
|
9
|
+
const examplePath = path.resolve(
|
|
10
|
+
import.meta.dirname!,
|
|
11
|
+
"../../examples/simple-cli.ts",
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
let proc: ChildProcess;
|
|
15
|
+
let port: number;
|
|
16
|
+
let browser: Browser;
|
|
17
|
+
|
|
18
|
+
test("live viewer: summary tab shows stats", {
|
|
19
|
+
timeout: 30_000,
|
|
20
|
+
}, async () => {
|
|
21
|
+
const consoleErrors: string[] = [];
|
|
22
|
+
const page = await browser.newPage();
|
|
23
|
+
try {
|
|
24
|
+
page.on("console", msg => {
|
|
25
|
+
if (msg.type() === "error" && !msg.text().includes("WebGL"))
|
|
26
|
+
consoleErrors.push(msg.text());
|
|
27
|
+
});
|
|
28
|
+
await page.goto(`http://localhost:${port}`, { waitUntil: "networkidle" });
|
|
29
|
+
|
|
30
|
+
const summaryPanel = page.locator("#summary-panel");
|
|
31
|
+
const panel = summaryPanel.locator(".section-panel").first();
|
|
32
|
+
await panel.waitFor({ state: "visible", timeout: 15_000 });
|
|
33
|
+
const statRows = await summaryPanel.locator(".stat-row").count();
|
|
34
|
+
expect(statRows).toBeGreaterThan(0);
|
|
35
|
+
} finally {
|
|
36
|
+
await page.close();
|
|
37
|
+
}
|
|
38
|
+
expect(consoleErrors).toEqual([]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("live viewer: samples tab shows chart SVG", {
|
|
42
|
+
timeout: 30_000,
|
|
43
|
+
}, async () => {
|
|
44
|
+
const consoleErrors: string[] = [];
|
|
45
|
+
const page = await browser.newPage();
|
|
46
|
+
try {
|
|
47
|
+
page.on("console", msg => {
|
|
48
|
+
if (msg.type() === "error" && !msg.text().includes("WebGL"))
|
|
49
|
+
consoleErrors.push(msg.text());
|
|
50
|
+
});
|
|
51
|
+
await page.goto(`http://localhost:${port}`, { waitUntil: "networkidle" });
|
|
52
|
+
|
|
53
|
+
// Wait for summary to load (samples tab becomes enabled)
|
|
54
|
+
await page
|
|
55
|
+
.locator("#summary-panel .section-panel")
|
|
56
|
+
.first()
|
|
57
|
+
.waitFor({ state: "visible", timeout: 15_000 });
|
|
58
|
+
|
|
59
|
+
await page.locator("#tab-samples").click();
|
|
60
|
+
|
|
61
|
+
const samplesPanel = page.locator("#samples-panel");
|
|
62
|
+
const svg = samplesPanel.locator("svg").first();
|
|
63
|
+
await svg.waitFor({ state: "visible", timeout: 15_000 });
|
|
64
|
+
const childCount = await svg
|
|
65
|
+
.locator("path, rect, circle, line, text")
|
|
66
|
+
.count();
|
|
67
|
+
expect(childCount).toBeGreaterThan(0);
|
|
68
|
+
} finally {
|
|
69
|
+
await page.close();
|
|
70
|
+
}
|
|
71
|
+
expect(consoleErrors).toEqual([]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("live viewer: allocation tab has speedscope content", {
|
|
75
|
+
timeout: 30_000,
|
|
76
|
+
}, async () => {
|
|
77
|
+
const page = await browser.newPage();
|
|
78
|
+
try {
|
|
79
|
+
await page.goto(`http://localhost:${port}`, { waitUntil: "networkidle" });
|
|
80
|
+
|
|
81
|
+
await page.locator("#tab-flamechart").click();
|
|
82
|
+
const frame = page.frameLocator("#speedscope-iframe");
|
|
83
|
+
await frame
|
|
84
|
+
.locator("body *")
|
|
85
|
+
.first()
|
|
86
|
+
.waitFor({ state: "visible", timeout: 15_000 });
|
|
87
|
+
} finally {
|
|
88
|
+
await page.close();
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("live viewer: timing tab has speedscope content", {
|
|
93
|
+
timeout: 30_000,
|
|
94
|
+
}, async () => {
|
|
95
|
+
const page = await browser.newPage();
|
|
96
|
+
try {
|
|
97
|
+
await page.goto(`http://localhost:${port}`, { waitUntil: "networkidle" });
|
|
98
|
+
|
|
99
|
+
await page.locator("#tab-time-flamechart").click();
|
|
100
|
+
const frame = page.frameLocator("#time-speedscope-iframe");
|
|
101
|
+
await frame
|
|
102
|
+
.locator("body *")
|
|
103
|
+
.first()
|
|
104
|
+
.waitFor({ state: "visible", timeout: 15_000 });
|
|
105
|
+
} finally {
|
|
106
|
+
await page.close();
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
beforeAll(async () => {
|
|
111
|
+
const args = [
|
|
112
|
+
examplePath,
|
|
113
|
+
"--view-serve",
|
|
114
|
+
"--alloc",
|
|
115
|
+
"--profile",
|
|
116
|
+
"--iterations",
|
|
117
|
+
"3",
|
|
118
|
+
"--warmup",
|
|
119
|
+
"0",
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
proc = spawn(binPath, args, {
|
|
123
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Parse port from stdout line like "Viewer: http://localhost:3939"
|
|
127
|
+
const portP = new Promise<number>((resolve, reject) => {
|
|
128
|
+
let stdout = "";
|
|
129
|
+
proc.stdout!.on("data", (chunk: Buffer) => {
|
|
130
|
+
stdout += chunk.toString();
|
|
131
|
+
const match = stdout.match(/Viewer: http:\/\/localhost:(\d+)/);
|
|
132
|
+
if (match) resolve(Number(match[1]));
|
|
133
|
+
});
|
|
134
|
+
proc.on("error", reject);
|
|
135
|
+
proc.on("exit", code => {
|
|
136
|
+
if (!port)
|
|
137
|
+
reject(
|
|
138
|
+
new Error(
|
|
139
|
+
`Process exited (${code}) before viewer started.\nstdout: ${stdout}`,
|
|
140
|
+
),
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
setTimeout(
|
|
144
|
+
() =>
|
|
145
|
+
reject(new Error(`Timed out waiting for viewer.\nstdout: ${stdout}`)),
|
|
146
|
+
60_000,
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
[port, browser] = await Promise.all([
|
|
151
|
+
portP,
|
|
152
|
+
chromium.launch({ headless: true }),
|
|
153
|
+
]);
|
|
154
|
+
}, 90_000);
|
|
155
|
+
|
|
156
|
+
afterAll(async () => {
|
|
157
|
+
await browser?.close();
|
|
158
|
+
proc?.kill();
|
|
159
|
+
});
|
|
@@ -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
|
+
}
|