benchforge 0.1.11 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +99 -294
- package/bin/benchforge +1 -2
- package/dist/AnalyzeArchive-8NCJhmhS.mjs +145 -0
- package/dist/AnalyzeArchive-8NCJhmhS.mjs.map +1 -0
- package/dist/BenchMatrix-BZVrBB_h.mjs +1050 -0
- package/dist/BenchMatrix-BZVrBB_h.mjs.map +1 -0
- package/dist/{BenchRunner-BzyUfiyB.d.mts → BenchRunner-DglX1NOn.d.mts} +119 -66
- package/dist/CoverageSampler-D5T9DRqe.mjs +27 -0
- package/dist/CoverageSampler-D5T9DRqe.mjs.map +1 -0
- package/dist/Formatters-BWj3d4sv.mjs +95 -0
- package/dist/Formatters-BWj3d4sv.mjs.map +1 -0
- package/dist/{HeapSampler-B8dtKHn1.mjs → HeapSampler-Dq-hpXem.mjs} +4 -4
- package/dist/HeapSampler-Dq-hpXem.mjs.map +1 -0
- package/dist/RunBenchCLI-C17DrJz8.mjs +3075 -0
- package/dist/RunBenchCLI-C17DrJz8.mjs.map +1 -0
- package/dist/StatisticalUtils-BD92crgM.mjs +255 -0
- package/dist/StatisticalUtils-BD92crgM.mjs.map +1 -0
- package/dist/TimeSampler-Ds8n7l2B.mjs +29 -0
- package/dist/TimeSampler-Ds8n7l2B.mjs.map +1 -0
- package/dist/ViewerServer-BJhdnxlN.mjs +639 -0
- package/dist/ViewerServer-BJhdnxlN.mjs.map +1 -0
- package/dist/ViewerServer-CuMNdNBz.mjs +2 -0
- package/dist/bin/benchforge.mjs +4 -5
- package/dist/bin/benchforge.mjs.map +1 -1
- package/dist/index.d.mts +711 -558
- package/dist/index.mjs +98 -3
- package/dist/index.mjs.map +1 -0
- package/dist/runners/WorkerScript.d.mts +12 -4
- package/dist/runners/WorkerScript.mjs +77 -105
- package/dist/runners/WorkerScript.mjs.map +1 -1
- package/dist/viewer/assets/CIPlot-BkOvMoMa.js +1 -0
- package/dist/viewer/assets/HistogramKde-CmSyUFY0.js +1 -0
- package/dist/viewer/assets/LegendUtils-BJpbn_jr.js +55 -0
- package/dist/viewer/assets/SampleTimeSeries-C4VBhXr3.js +1 -0
- package/dist/viewer/assets/index-Br9bp_cX.js +153 -0
- package/dist/viewer/assets/index-NzXXe_CC.css +1 -0
- package/dist/viewer/index.html +19 -0
- package/dist/viewer/speedscope/LICENSE +21 -0
- package/dist/viewer/speedscope/SourceCodePro-Regular.ttf-ILST5JV6.woff2 +0 -0
- package/dist/viewer/speedscope/favicon-16x16-V2DMIAZS.js +2 -0
- package/dist/viewer/speedscope/favicon-16x16-V2DMIAZS.js.map +7 -0
- package/dist/viewer/speedscope/favicon-16x16-VSI62OPJ.png +0 -0
- package/dist/viewer/speedscope/favicon-32x32-3EB2YCUY.png +0 -0
- package/dist/viewer/speedscope/favicon-32x32-THY3JDJL.js +2 -0
- package/dist/viewer/speedscope/favicon-32x32-THY3JDJL.js.map +7 -0
- package/dist/viewer/speedscope/favicon-FOKUP5Y5.ico +0 -0
- package/dist/viewer/speedscope/favicon-M34RF7BI.js +2 -0
- package/dist/viewer/speedscope/favicon-M34RF7BI.js.map +7 -0
- package/dist/viewer/speedscope/file-format-schema.json +274 -0
- package/dist/viewer/speedscope/index.html +19 -0
- package/dist/viewer/speedscope/jfrview_bg-BLJXNNQB.wasm +0 -0
- package/dist/viewer/speedscope/perf-vertx-stacks-01-collapsed-all-ZNUIGAJL.txt +199 -0
- package/dist/viewer/speedscope/release.txt +3 -0
- package/dist/viewer/speedscope/source-code-pro.LICENSE.md +93 -0
- package/dist/viewer/speedscope/speedscope-GHPHNKXC.css +2 -0
- package/dist/viewer/speedscope/speedscope-GHPHNKXC.css.map +7 -0
- package/dist/viewer/speedscope/speedscope-QZFMJ7VP.js +212 -0
- package/dist/viewer/speedscope/speedscope-QZFMJ7VP.js.map +7 -0
- package/package.json +52 -27
- package/src/bin/benchforge.ts +2 -2
- package/src/cli/AnalyzeArchive.ts +232 -0
- package/src/cli/BrowserBench.ts +322 -0
- package/src/cli/CliArgs.ts +164 -51
- package/src/cli/CliExport.ts +179 -0
- package/src/cli/CliOptions.ts +147 -0
- package/src/cli/CliReport.ts +197 -0
- package/src/cli/FilterBenchmarks.ts +18 -30
- package/src/cli/RunBenchCLI.ts +132 -866
- package/src/cli/SuiteRunner.ts +160 -0
- package/src/cli/ViewerServer.ts +282 -0
- package/src/export/AllocExport.ts +121 -0
- package/src/export/ArchiveExport.ts +146 -0
- package/src/export/ArchiveFormat.ts +50 -0
- package/src/export/CoverageExport.ts +148 -0
- package/src/export/EditorUri.ts +10 -0
- package/src/export/PerfettoExport.ts +64 -99
- package/src/export/SpeedscopeTypes.ts +98 -0
- package/src/export/TimeExport.ts +115 -0
- package/src/index.ts +86 -67
- package/src/matrix/BenchMatrix.ts +230 -0
- package/src/matrix/CaseLoader.ts +8 -6
- package/src/matrix/MatrixDirRunner.ts +153 -0
- package/src/matrix/MatrixFilter.ts +49 -47
- package/src/matrix/MatrixInlineRunner.ts +50 -0
- package/src/matrix/MatrixReport.ts +90 -250
- package/src/matrix/VariantLoader.ts +5 -5
- package/src/profiling/browser/BenchLoop.ts +51 -0
- package/src/profiling/browser/BrowserCDP.ts +133 -0
- package/src/profiling/browser/BrowserGcStats.ts +33 -0
- package/src/profiling/browser/BrowserProfiler.ts +160 -0
- package/src/profiling/browser/CdpClient.ts +82 -0
- package/src/profiling/browser/CdpPage.ts +138 -0
- package/src/profiling/browser/ChromeLauncher.ts +158 -0
- package/src/profiling/browser/ChromeTraceEvent.ts +28 -0
- package/src/profiling/browser/PageLoadMode.ts +61 -0
- package/src/profiling/node/CoverageSampler.ts +27 -0
- package/src/profiling/node/CoverageTypes.ts +23 -0
- package/src/profiling/node/HeapSampleReport.ts +261 -0
- package/src/{heap-sample → profiling/node}/HeapSampler.ts +1 -2
- package/src/{heap-sample → profiling/node}/ResolvedProfile.ts +18 -9
- package/src/profiling/node/TimeSampler.ts +57 -0
- package/src/report/BenchmarkReport.ts +146 -0
- package/src/report/Colors.ts +9 -0
- package/src/report/Formatters.ts +110 -0
- package/src/report/GcSections.ts +151 -0
- package/src/{GitUtils.ts → report/GitUtils.ts} +18 -19
- package/src/report/HtmlReport.ts +223 -0
- package/src/report/ParseStats.ts +73 -0
- package/src/report/StandardSections.ts +147 -0
- package/src/report/ViewerSections.ts +286 -0
- package/src/report/text/TableReport.ts +253 -0
- package/src/report/text/TextReport.ts +123 -0
- package/src/runners/AdaptiveWrapper.ts +116 -236
- package/src/runners/BenchRunner.ts +20 -15
- package/src/{Benchmark.ts → runners/BenchmarkSpec.ts} +5 -6
- package/src/runners/CreateRunner.ts +5 -7
- package/src/runners/GcStats.ts +47 -50
- package/src/{MeasuredResults.ts → runners/MeasuredResults.ts} +43 -37
- package/src/runners/MergeBatches.ts +123 -0
- package/src/{NodeGC.ts → runners/NodeGC.ts} +2 -3
- package/src/runners/RunnerOrchestrator.ts +127 -243
- package/src/runners/RunnerUtils.ts +75 -1
- package/src/runners/SampleStats.ts +100 -0
- package/src/runners/TimingRunner.ts +244 -0
- package/src/runners/TimingUtils.ts +3 -2
- package/src/runners/WorkerScript.ts +135 -151
- package/src/stats/BootstrapDifference.ts +282 -0
- package/src/{PermutationTest.ts → stats/PermutationTest.ts} +8 -17
- package/src/stats/StatisticalUtils.ts +445 -0
- package/src/{tests → test}/AdaptiveConvergence.test.ts +10 -10
- package/src/test/AdaptiveRunner.test.ts +39 -41
- package/src/{tests → test}/AdaptiveSampling.test.ts +9 -9
- package/src/test/AdaptiveStatistics.integration.ts +2 -2
- package/src/{tests → test}/BenchMatrix.test.ts +19 -16
- package/src/test/BenchmarkReport.test.ts +63 -13
- package/src/test/BrowserBench.e2e.test.ts +186 -17
- package/src/test/BrowserBench.test.ts +10 -5
- package/src/test/BuildTimeSection.test.ts +130 -0
- package/src/test/CapSamples.test.ts +82 -0
- package/src/test/CoverageExport.test.ts +115 -0
- package/src/test/CoverageSampler.test.ts +33 -0
- package/src/test/HeapAttribution.test.ts +14 -14
- package/src/{tests → test}/MatrixFilter.test.ts +1 -1
- package/src/{tests → test}/MatrixReport.test.ts +1 -1
- package/src/test/PermutationTest.test.ts +1 -1
- package/src/{tests → test}/RealDataValidation.test.ts +6 -6
- package/src/test/RunBenchCLI.test.ts +39 -38
- package/src/test/RunnerOrchestrator.test.ts +12 -12
- package/src/test/StatisticalUtils.test.ts +48 -12
- package/src/{table-util/test → test}/TableReport.test.ts +2 -2
- package/src/test/TestUtils.ts +12 -7
- package/src/test/TimeExport.test.ts +139 -0
- package/src/test/TimeSampler.test.ts +37 -0
- package/src/test/ViewerLive.e2e.test.ts +159 -0
- package/src/test/ViewerStatic.static.e2e.test.ts +137 -0
- package/src/{tests → test}/fixtures/baseline/impl.ts +1 -1
- package/src/{tests → test}/fixtures/bevy30-samples.ts +3 -1
- package/src/test/fixtures/cases/asyncCases.ts +9 -0
- package/src/{tests → test}/fixtures/cases/cases.ts +5 -2
- package/src/test/fixtures/cases/variants/product.ts +2 -0
- package/src/test/fixtures/cases/variants/sum.ts +2 -0
- package/src/test/fixtures/discover/fast.ts +1 -0
- package/src/{tests → test}/fixtures/discover/slow.ts +1 -1
- package/src/test/fixtures/invalid/bad.ts +1 -0
- package/src/test/fixtures/loader/fast.ts +1 -0
- package/src/{tests → test}/fixtures/loader/slow.ts +1 -1
- package/src/test/fixtures/loader/stateful.ts +2 -0
- package/src/test/fixtures/stateful/stateful.ts +2 -0
- package/src/test/fixtures/variants/extra.ts +1 -0
- package/src/test/fixtures/variants/impl.ts +1 -0
- package/src/test/fixtures/worker/fast.ts +1 -0
- package/src/{tests → test}/fixtures/worker/slow.ts +1 -1
- package/src/viewer/DateFormat.ts +30 -0
- package/src/viewer/Helpers.ts +23 -0
- package/src/viewer/LineData.ts +120 -0
- package/src/viewer/Providers.ts +191 -0
- package/src/viewer/ReportData.ts +123 -0
- package/src/viewer/State.ts +49 -0
- package/src/viewer/Theme.ts +15 -0
- package/src/viewer/components/App.tsx +73 -0
- package/src/viewer/components/DropZone.tsx +71 -0
- package/src/viewer/components/LazyPlot.ts +33 -0
- package/src/viewer/components/SamplesPanel.tsx +214 -0
- package/src/viewer/components/Shell.tsx +26 -0
- package/src/viewer/components/SourcePanel.tsx +216 -0
- package/src/viewer/components/SummaryPanel.tsx +332 -0
- package/src/viewer/components/TabBar.tsx +131 -0
- package/src/viewer/components/TabContent.tsx +46 -0
- package/src/viewer/components/ThemeToggle.tsx +50 -0
- package/src/viewer/index.html +20 -0
- package/src/viewer/main.tsx +4 -0
- package/src/viewer/plots/CIPlot.ts +313 -0
- package/src/{html/browser → viewer/plots}/HistogramKde.ts +33 -38
- package/src/viewer/plots/LegendUtils.ts +134 -0
- package/src/viewer/plots/PlotTypes.ts +85 -0
- package/src/viewer/plots/RenderPlots.ts +230 -0
- package/src/viewer/plots/SampleTimeSeries.ts +306 -0
- package/src/viewer/plots/SvgHelpers.ts +136 -0
- package/src/viewer/plots/TimeSeriesMarks.ts +319 -0
- package/src/viewer/report.css +427 -0
- package/src/viewer/shell.css +357 -0
- package/src/viewer/tsconfig.json +11 -0
- package/dist/BrowserHeapSampler-B6asLKWQ.mjs +0 -202
- package/dist/BrowserHeapSampler-B6asLKWQ.mjs.map +0 -1
- package/dist/GcStats-wX7Xyblu.mjs +0 -77
- package/dist/GcStats-wX7Xyblu.mjs.map +0 -1
- package/dist/HeapSampler-B8dtKHn1.mjs.map +0 -1
- package/dist/TimingUtils-DwOwkc8G.mjs +0 -597
- package/dist/TimingUtils-DwOwkc8G.mjs.map +0 -1
- package/dist/browser/index.js +0 -914
- package/dist/src-B-DDaCa9.mjs +0 -3108
- package/dist/src-B-DDaCa9.mjs.map +0 -1
- package/src/BenchMatrix.ts +0 -380
- package/src/BenchmarkReport.ts +0 -161
- package/src/HtmlDataPrep.ts +0 -148
- package/src/StandardSections.ts +0 -261
- package/src/StatisticalUtils.ts +0 -175
- package/src/TypeUtil.ts +0 -8
- package/src/browser/BrowserGcStats.ts +0 -44
- package/src/browser/BrowserHeapSampler.ts +0 -271
- package/src/export/JsonExport.ts +0 -103
- package/src/export/JsonFormat.ts +0 -91
- package/src/export/SpeedscopeExport.ts +0 -202
- package/src/heap-sample/HeapSampleReport.ts +0 -269
- package/src/html/HtmlReport.ts +0 -131
- package/src/html/HtmlTemplate.ts +0 -284
- package/src/html/Types.ts +0 -88
- package/src/html/browser/CIPlot.ts +0 -287
- package/src/html/browser/LegendUtils.ts +0 -163
- package/src/html/browser/RenderPlots.ts +0 -263
- package/src/html/browser/SampleTimeSeries.ts +0 -389
- package/src/html/browser/Types.ts +0 -96
- package/src/html/browser/index.ts +0 -1
- package/src/html/index.ts +0 -17
- package/src/runners/BasicRunner.ts +0 -364
- package/src/table-util/ConvergenceFormatters.ts +0 -19
- package/src/table-util/Formatters.ts +0 -157
- package/src/table-util/README.md +0 -70
- package/src/table-util/TableReport.ts +0 -293
- package/src/tests/fixtures/cases/asyncCases.ts +0 -7
- package/src/tests/fixtures/cases/variants/product.ts +0 -2
- package/src/tests/fixtures/cases/variants/sum.ts +0 -2
- package/src/tests/fixtures/discover/fast.ts +0 -1
- package/src/tests/fixtures/invalid/bad.ts +0 -1
- package/src/tests/fixtures/loader/fast.ts +0 -1
- package/src/tests/fixtures/loader/stateful.ts +0 -2
- package/src/tests/fixtures/stateful/stateful.ts +0 -2
- package/src/tests/fixtures/variants/extra.ts +0 -1
- package/src/tests/fixtures/variants/impl.ts +0 -1
- package/src/tests/fixtures/worker/fast.ts +0 -1
- /package/src/{table-util/test → test}/TableValueExtractor.test.ts +0 -0
- /package/src/{table-util/test → test}/TableValueExtractor.ts +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { expect, test } from "vitest";
|
|
2
|
-
import type { BenchmarkSpec } from "../
|
|
2
|
+
import type { BenchmarkSpec } from "../runners/BenchmarkSpec.ts";
|
|
3
3
|
import { runBenchmark } from "../runners/RunnerOrchestrator.ts";
|
|
4
4
|
|
|
5
5
|
/** lightweight function for testing worker communication */
|
|
@@ -9,15 +9,15 @@ function simpleTestFunction(): number {
|
|
|
9
9
|
return sum;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
test("
|
|
12
|
+
test("TimingRunner runs benchmark in worker mode", async () => {
|
|
13
13
|
const spec: BenchmarkSpec = {
|
|
14
|
-
name: "
|
|
14
|
+
name: "timing-worker-test",
|
|
15
15
|
fn: simpleTestFunction,
|
|
16
16
|
};
|
|
17
17
|
|
|
18
18
|
const results = await runBenchmark({
|
|
19
19
|
spec,
|
|
20
|
-
runner: "
|
|
20
|
+
runner: "timing",
|
|
21
21
|
options: {
|
|
22
22
|
maxTime: 5,
|
|
23
23
|
maxIterations: 50,
|
|
@@ -28,7 +28,7 @@ test("BasicRunner runs benchmark in worker mode", async () => {
|
|
|
28
28
|
expect(results).toHaveLength(1);
|
|
29
29
|
const result = results[0];
|
|
30
30
|
|
|
31
|
-
expect(result.name).toBe("
|
|
31
|
+
expect(result.name).toBe("timing-worker-test");
|
|
32
32
|
expect(result.samples.length).toBeGreaterThan(0);
|
|
33
33
|
expect(result.samples.length).toBeLessThanOrEqual(500);
|
|
34
34
|
expect(result.time.min).toBeGreaterThan(0);
|
|
@@ -38,15 +38,15 @@ test("BasicRunner runs benchmark in worker mode", async () => {
|
|
|
38
38
|
expect(result.time.p99).toBeGreaterThan(0);
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
test("
|
|
41
|
+
test("TimingRunner runs benchmark in non-worker mode", async () => {
|
|
42
42
|
const spec: BenchmarkSpec = {
|
|
43
|
-
name: "
|
|
43
|
+
name: "timing-test",
|
|
44
44
|
fn: simpleTestFunction,
|
|
45
45
|
};
|
|
46
46
|
|
|
47
47
|
const results = await runBenchmark({
|
|
48
48
|
spec,
|
|
49
|
-
runner: "
|
|
49
|
+
runner: "timing",
|
|
50
50
|
options: {
|
|
51
51
|
maxTime: 5,
|
|
52
52
|
maxIterations: 50,
|
|
@@ -57,12 +57,12 @@ test("BasicRunner runs benchmark in non-worker mode", async () => {
|
|
|
57
57
|
expect(results).toHaveLength(1);
|
|
58
58
|
const result = results[0];
|
|
59
59
|
|
|
60
|
-
expect(result.name).toBe("
|
|
60
|
+
expect(result.name).toBe("timing-test");
|
|
61
61
|
expect(result.samples.length).toBeGreaterThan(0);
|
|
62
62
|
expect(result.time.p50).toBeGreaterThan(0);
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
-
test("
|
|
65
|
+
test("TimingRunner with parameterized benchmark", async () => {
|
|
66
66
|
const spec: BenchmarkSpec<number> = {
|
|
67
67
|
name: "parameterized-test",
|
|
68
68
|
fn: (n: number) => {
|
|
@@ -74,7 +74,7 @@ test("BasicRunner with parameterized benchmark", async () => {
|
|
|
74
74
|
|
|
75
75
|
const results = await runBenchmark({
|
|
76
76
|
spec,
|
|
77
|
-
runner: "
|
|
77
|
+
runner: "timing",
|
|
78
78
|
options: { maxTime: 5, maxIterations: 20 },
|
|
79
79
|
useWorker: false,
|
|
80
80
|
params: 100,
|
|
@@ -94,7 +94,7 @@ test("RunnerOrchestrator propagates errors from worker", async () => {
|
|
|
94
94
|
|
|
95
95
|
const promise = runBenchmark({
|
|
96
96
|
spec,
|
|
97
|
-
runner: "
|
|
97
|
+
runner: "timing",
|
|
98
98
|
options: { maxTime: 1, maxIterations: 1 },
|
|
99
99
|
useWorker: true,
|
|
100
100
|
});
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { expect, test } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
blockDifferenceCI,
|
|
4
|
+
sampleDifferenceCI,
|
|
5
|
+
} from "../stats/BootstrapDifference.ts";
|
|
2
6
|
import {
|
|
3
7
|
average,
|
|
4
|
-
|
|
5
|
-
bootstrapMedian,
|
|
8
|
+
blockBootstrap,
|
|
6
9
|
coefficientOfVariation,
|
|
7
10
|
findOutliers,
|
|
11
|
+
median,
|
|
8
12
|
medianAbsoluteDeviation,
|
|
9
13
|
percentile,
|
|
14
|
+
sampleBootstrap,
|
|
10
15
|
standardDeviation,
|
|
11
|
-
} from "../StatisticalUtils.ts";
|
|
16
|
+
} from "../stats/StatisticalUtils.ts";
|
|
12
17
|
import { assertValid, getSampleData } from "./TestUtils.ts";
|
|
13
18
|
|
|
14
19
|
test("calculates mean correctly", () => {
|
|
@@ -68,11 +73,21 @@ test("identifies outliers in mixed data", () => {
|
|
|
68
73
|
expect(outliers.indices).toContain(51);
|
|
69
74
|
});
|
|
70
75
|
|
|
71
|
-
test("
|
|
76
|
+
test("sampleBootstrap estimates median with CI", () => {
|
|
77
|
+
const stable = getSampleData(400, 450);
|
|
78
|
+
const result = sampleBootstrap(stable, median, { resamples: 1000 });
|
|
79
|
+
expect(result.ciLevel).toBe("sample");
|
|
80
|
+
expect(result.ci[0]).toBeLessThanOrEqual(result.estimate);
|
|
81
|
+
expect(result.ci[1]).toBeGreaterThanOrEqual(result.estimate);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("blockBootstrap estimates median with confidence intervals", () => {
|
|
72
85
|
const stable = getSampleData(400, 450);
|
|
73
86
|
const actual = percentile(stable, 0.5);
|
|
74
|
-
const
|
|
87
|
+
const blocks = Array.from({ length: 5 }, (_, i) => i * 10);
|
|
88
|
+
const result = blockBootstrap(stable, blocks, median, { resamples: 1000 });
|
|
75
89
|
|
|
90
|
+
expect(result.ciLevel).toBe("block");
|
|
76
91
|
expect(result.estimate).toBeCloseTo(actual, 1);
|
|
77
92
|
expect(result.ci[0]).toBeLessThanOrEqual(result.estimate);
|
|
78
93
|
expect(result.ci[1]).toBeGreaterThanOrEqual(result.estimate);
|
|
@@ -80,32 +95,53 @@ test("bootstrap estimates median with confidence intervals", () => {
|
|
|
80
95
|
expect(result.samples).toHaveLength(1000);
|
|
81
96
|
});
|
|
82
97
|
|
|
83
|
-
test("
|
|
98
|
+
test("sampleDifferenceCI detects improvement", () => {
|
|
99
|
+
const baseline = getSampleData(0, 100);
|
|
100
|
+
const improved = baseline.map(v => v * 0.8);
|
|
101
|
+
const result = sampleDifferenceCI(baseline, improved, median, {
|
|
102
|
+
resamples: 1000,
|
|
103
|
+
});
|
|
104
|
+
expect(result.ciLevel).toBe("sample");
|
|
105
|
+
expect(result.percent).toBeCloseTo(-20, 0);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("blockDifferenceCI detects improvement", () => {
|
|
84
109
|
const baseline = getSampleData(0, 100);
|
|
85
110
|
const improved = baseline.map(v => v * 0.8);
|
|
86
|
-
const
|
|
111
|
+
const blocks = Array.from({ length: 10 }, (_, i) => i * 10);
|
|
112
|
+
const result = blockDifferenceCI(baseline, blocks, improved, median, {
|
|
113
|
+
resamples: 1000,
|
|
114
|
+
});
|
|
87
115
|
|
|
116
|
+
expect(result.ciLevel).toBe("block");
|
|
88
117
|
expect(result.percent).toBeCloseTo(-20, 0);
|
|
89
118
|
expect(result.ci[1]).toBeLessThan(0);
|
|
90
119
|
expect(result.direction).toBe("faster");
|
|
91
120
|
});
|
|
92
121
|
|
|
93
|
-
test("
|
|
122
|
+
test("blockDifferenceCI detects regression", () => {
|
|
94
123
|
const baseline = getSampleData(0, 100);
|
|
95
124
|
const slower = baseline.map(v => v * 1.2);
|
|
96
|
-
const
|
|
125
|
+
const blocks = Array.from({ length: 10 }, (_, i) => i * 10);
|
|
126
|
+
const result = blockDifferenceCI(baseline, blocks, slower, median, {
|
|
127
|
+
resamples: 1000,
|
|
128
|
+
});
|
|
97
129
|
|
|
130
|
+
expect(result.ciLevel).toBe("block");
|
|
98
131
|
expect(result.percent).toBeCloseTo(20, 0);
|
|
99
132
|
expect(result.ci[0]).toBeGreaterThan(0);
|
|
100
133
|
expect(result.direction).toBe("slower");
|
|
101
134
|
});
|
|
102
135
|
|
|
103
|
-
test("
|
|
136
|
+
test("blockDifferenceCI shows uncertainty for noise", () => {
|
|
104
137
|
const baseline = getSampleData(0, 100);
|
|
105
138
|
const noisy = baseline.map(v => v + (Math.random() - 0.5) * 2);
|
|
106
|
-
const
|
|
139
|
+
const blocks = Array.from({ length: 10 }, (_, i) => i * 10);
|
|
140
|
+
const result = blockDifferenceCI(baseline, blocks, noisy, median, {
|
|
141
|
+
resamples: 1000,
|
|
142
|
+
});
|
|
107
143
|
|
|
108
|
-
|
|
144
|
+
expect(result.ciLevel).toBe("block");
|
|
109
145
|
expect(result.ci[0]).toBeLessThanOrEqual(0);
|
|
110
146
|
expect(result.ci[1]).toBeGreaterThanOrEqual(0);
|
|
111
147
|
expect(result.direction).toBe("uncertain");
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { expect, test } from "vitest";
|
|
2
|
-
import { integer } from "../Formatters.ts";
|
|
2
|
+
import { integer } from "../report/Formatters.ts";
|
|
3
3
|
import {
|
|
4
4
|
buildTable,
|
|
5
5
|
type ColumnGroup,
|
|
6
6
|
type ResultGroup,
|
|
7
|
-
} from "../TableReport.ts";
|
|
7
|
+
} from "../report/text/TableReport.ts";
|
|
8
8
|
|
|
9
9
|
interface TestRecord {
|
|
10
10
|
name: string;
|
package/src/test/TestUtils.ts
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
|
-
import type { BenchSuite } from "../Benchmark.ts";
|
|
2
|
-
import type { BenchmarkReport } from "../BenchmarkReport.ts";
|
|
3
1
|
import type { Configure, DefaultCliArgs } from "../cli/CliArgs.ts";
|
|
4
2
|
import { parseCliArgs } from "../cli/CliArgs.ts";
|
|
5
|
-
import { defaultReport
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
3
|
+
import { defaultReport } from "../cli/CliReport.ts";
|
|
4
|
+
import { runBenchmarks } from "../cli/SuiteRunner.ts";
|
|
5
|
+
import type { BenchmarkReport } from "../report/BenchmarkReport.ts";
|
|
6
|
+
import type { BenchSuite } from "../runners/BenchmarkSpec.ts";
|
|
7
|
+
import type { MeasuredResults } from "../runners/MeasuredResults.ts";
|
|
8
|
+
import { average, percentile } from "../stats/StatisticalUtils.ts";
|
|
9
|
+
import { bevy30SamplesMs } from "./fixtures/bevy30-samples.ts";
|
|
9
10
|
|
|
10
11
|
/** Validation helpers for statistical tests */
|
|
11
|
-
export const assertValid
|
|
12
|
+
export const assertValid: {
|
|
13
|
+
pValue: (value: number) => void;
|
|
14
|
+
percentileOrder: (p25: number, p50: number, p75: number, p99: number) => void;
|
|
15
|
+
significance: (level: string) => void;
|
|
16
|
+
} = {
|
|
12
17
|
pValue: (value: number) => {
|
|
13
18
|
if (value < 0 || value > 1) {
|
|
14
19
|
throw new Error(`Expected p-value between 0 and 1, got ${value}`);
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { expect, test } from "vitest";
|
|
2
|
+
import { timeProfileToSpeedscope } from "../export/TimeExport.ts";
|
|
3
|
+
import type { TimeProfile } from "../profiling/node/TimeSampler.ts";
|
|
4
|
+
|
|
5
|
+
/** Build a minimal TimeProfile for testing */
|
|
6
|
+
function mockProfile(): TimeProfile {
|
|
7
|
+
return {
|
|
8
|
+
nodes: [
|
|
9
|
+
{
|
|
10
|
+
id: 1,
|
|
11
|
+
callFrame: { functionName: "", url: "", lineNumber: -1 },
|
|
12
|
+
children: [2],
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
id: 2,
|
|
16
|
+
callFrame: {
|
|
17
|
+
functionName: "main",
|
|
18
|
+
url: "file:///app.ts",
|
|
19
|
+
lineNumber: 9,
|
|
20
|
+
columnNumber: 0,
|
|
21
|
+
},
|
|
22
|
+
children: [3],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: 3,
|
|
26
|
+
callFrame: {
|
|
27
|
+
functionName: "compute",
|
|
28
|
+
url: "file:///app.ts",
|
|
29
|
+
lineNumber: 19,
|
|
30
|
+
columnNumber: 4,
|
|
31
|
+
},
|
|
32
|
+
hitCount: 5,
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
startTime: 0,
|
|
36
|
+
endTime: 5000,
|
|
37
|
+
samples: [3, 3, 2, 3, 3],
|
|
38
|
+
timeDeltas: [1000, 1000, 1000, 1000, 1000],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
test("converts TimeProfile to valid SpeedScope format", () => {
|
|
43
|
+
const profile = mockProfile();
|
|
44
|
+
const file = timeProfileToSpeedscope("test-bench", profile);
|
|
45
|
+
|
|
46
|
+
expect(file.$schema).toBe(
|
|
47
|
+
"https://www.speedscope.app/file-format-schema.json",
|
|
48
|
+
);
|
|
49
|
+
expect(file.exporter).toBe("benchforge");
|
|
50
|
+
expect(file.profiles).toHaveLength(1);
|
|
51
|
+
|
|
52
|
+
const p = file.profiles[0];
|
|
53
|
+
expect(p.type).toBe("sampled");
|
|
54
|
+
expect(p.name).toBe("test-bench");
|
|
55
|
+
expect(p.unit).toBe("microseconds");
|
|
56
|
+
expect(p.samples).toHaveLength(5);
|
|
57
|
+
expect(p.weights).toEqual([1000, 1000, 1000, 1000, 1000]);
|
|
58
|
+
expect(p.endValue).toBe(5000);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("resolves stacks from leaf to root (root-first order)", () => {
|
|
62
|
+
const profile = mockProfile();
|
|
63
|
+
const file = timeProfileToSpeedscope("test", profile);
|
|
64
|
+
|
|
65
|
+
const p = file.profiles[0];
|
|
66
|
+
const frames = file.shared.frames;
|
|
67
|
+
|
|
68
|
+
// Sample at node 3 (compute) should have stack: [main, compute]
|
|
69
|
+
const deepStack = p.samples[0];
|
|
70
|
+
expect(deepStack).toHaveLength(2); // root is skipped
|
|
71
|
+
expect(frames[deepStack[0]].name).toBe("main");
|
|
72
|
+
expect(frames[deepStack[1]].name).toBe("compute");
|
|
73
|
+
|
|
74
|
+
// Sample at node 2 (main) should have stack: [main]
|
|
75
|
+
const shallowStack = p.samples[2];
|
|
76
|
+
expect(shallowStack).toHaveLength(1);
|
|
77
|
+
expect(frames[shallowStack[0]].name).toBe("main");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("deduplicates shared frames", () => {
|
|
81
|
+
const profile = mockProfile();
|
|
82
|
+
const file = timeProfileToSpeedscope("test", profile);
|
|
83
|
+
|
|
84
|
+
// "main" and "compute" — only 2 unique frames
|
|
85
|
+
expect(file.shared.frames).toHaveLength(2);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("handles empty samples gracefully", () => {
|
|
89
|
+
const profile: TimeProfile = {
|
|
90
|
+
nodes: [
|
|
91
|
+
{ id: 1, callFrame: { functionName: "", url: "", lineNumber: -1 } },
|
|
92
|
+
],
|
|
93
|
+
startTime: 0,
|
|
94
|
+
endTime: 0,
|
|
95
|
+
samples: [],
|
|
96
|
+
timeDeltas: [],
|
|
97
|
+
};
|
|
98
|
+
const file = timeProfileToSpeedscope("empty", profile);
|
|
99
|
+
|
|
100
|
+
expect(file.profiles[0].samples).toHaveLength(0);
|
|
101
|
+
expect(file.profiles[0].weights).toHaveLength(0);
|
|
102
|
+
expect(file.profiles[0].endValue).toBe(0);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("converts 0-indexed V8 lines to 1-indexed", () => {
|
|
106
|
+
const profile = mockProfile();
|
|
107
|
+
const file = timeProfileToSpeedscope("test", profile);
|
|
108
|
+
|
|
109
|
+
const mainFrame = file.shared.frames.find(f => f.name === "main")!;
|
|
110
|
+
expect(mainFrame.line).toBe(10); // lineNumber 9 -> line 10
|
|
111
|
+
expect(mainFrame.col).toBe(1); // columnNumber 0 -> col 1
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("anonymous functions get location hint in name", () => {
|
|
115
|
+
const profile: TimeProfile = {
|
|
116
|
+
nodes: [
|
|
117
|
+
{
|
|
118
|
+
id: 1,
|
|
119
|
+
callFrame: { functionName: "", url: "", lineNumber: -1 },
|
|
120
|
+
children: [2],
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
id: 2,
|
|
124
|
+
callFrame: {
|
|
125
|
+
functionName: "",
|
|
126
|
+
url: "file:///lib/utils.ts",
|
|
127
|
+
lineNumber: 41,
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
startTime: 0,
|
|
132
|
+
endTime: 1000,
|
|
133
|
+
samples: [2],
|
|
134
|
+
timeDeltas: [1000],
|
|
135
|
+
};
|
|
136
|
+
const file = timeProfileToSpeedscope("test", profile);
|
|
137
|
+
|
|
138
|
+
expect(file.shared.frames[0].name).toBe("(anonymous utils.ts:42)");
|
|
139
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { expect, test } from "vitest";
|
|
2
|
+
import { withTimeProfiling } from "../profiling/node/TimeSampler.ts";
|
|
3
|
+
|
|
4
|
+
test("withTimeProfiling returns valid V8 CPU profile", async () => {
|
|
5
|
+
// Burn some CPU to produce samples
|
|
6
|
+
const { result, profile } = await withTimeProfiling({}, () => {
|
|
7
|
+
let sum = 0;
|
|
8
|
+
for (let i = 0; i < 1e6; i++) sum += Math.sqrt(i);
|
|
9
|
+
return sum;
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
expect(result).toBeGreaterThan(0);
|
|
13
|
+
expect(profile.nodes.length).toBeGreaterThan(0);
|
|
14
|
+
expect(profile.startTime).toBeLessThan(profile.endTime);
|
|
15
|
+
expect(profile.samples).toBeDefined();
|
|
16
|
+
expect(profile.timeDeltas).toBeDefined();
|
|
17
|
+
expect(profile.samples!.length).toBe(profile.timeDeltas!.length);
|
|
18
|
+
|
|
19
|
+
// Verify node structure
|
|
20
|
+
const node = profile.nodes[0];
|
|
21
|
+
expect(node).toHaveProperty("id");
|
|
22
|
+
expect(node).toHaveProperty("callFrame");
|
|
23
|
+
expect(node.callFrame).toHaveProperty("functionName");
|
|
24
|
+
expect(node.callFrame).toHaveProperty("url");
|
|
25
|
+
expect(node.callFrame).toHaveProperty("lineNumber");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("withTimeProfiling respects custom interval", async () => {
|
|
29
|
+
const { profile } = await withTimeProfiling({ interval: 100 }, () => {
|
|
30
|
+
let sum = 0;
|
|
31
|
+
for (let i = 0; i < 1e6; i++) sum += Math.sqrt(i);
|
|
32
|
+
return sum;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Finer interval should produce more samples for the same work
|
|
36
|
+
expect(profile.samples!.length).toBeGreaterThan(0);
|
|
37
|
+
});
|
|
@@ -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
|
+
});
|