benchforge 0.1.0
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/README.md +432 -0
- package/bin/benchforge +3 -0
- package/dist/bin/benchforge.mjs +9 -0
- package/dist/bin/benchforge.mjs.map +1 -0
- package/dist/browser/index.js +914 -0
- package/dist/index.mjs +3 -0
- package/dist/src-CGuaC3Wo.mjs +3676 -0
- package/dist/src-CGuaC3Wo.mjs.map +1 -0
- package/package.json +49 -0
- package/src/BenchMatrix.ts +380 -0
- package/src/Benchmark.ts +33 -0
- package/src/BenchmarkReport.ts +156 -0
- package/src/GitUtils.ts +79 -0
- package/src/HtmlDataPrep.ts +148 -0
- package/src/MeasuredResults.ts +127 -0
- package/src/NodeGC.ts +48 -0
- package/src/PermutationTest.ts +115 -0
- package/src/StandardSections.ts +268 -0
- package/src/StatisticalUtils.ts +176 -0
- package/src/TypeUtil.ts +8 -0
- package/src/bin/benchforge.ts +4 -0
- package/src/browser/BrowserGcStats.ts +44 -0
- package/src/browser/BrowserHeapSampler.ts +248 -0
- package/src/cli/CliArgs.ts +64 -0
- package/src/cli/FilterBenchmarks.ts +68 -0
- package/src/cli/RunBenchCLI.ts +856 -0
- package/src/export/JsonExport.ts +103 -0
- package/src/export/JsonFormat.ts +91 -0
- package/src/export/PerfettoExport.ts +203 -0
- package/src/heap-sample/HeapSampleReport.ts +196 -0
- package/src/heap-sample/HeapSampler.ts +78 -0
- package/src/html/HtmlReport.ts +131 -0
- package/src/html/HtmlTemplate.ts +284 -0
- package/src/html/Types.ts +88 -0
- package/src/html/browser/CIPlot.ts +287 -0
- package/src/html/browser/HistogramKde.ts +118 -0
- package/src/html/browser/LegendUtils.ts +163 -0
- package/src/html/browser/RenderPlots.ts +263 -0
- package/src/html/browser/SampleTimeSeries.ts +389 -0
- package/src/html/browser/Types.ts +96 -0
- package/src/html/browser/index.ts +1 -0
- package/src/html/index.ts +17 -0
- package/src/index.ts +92 -0
- package/src/matrix/CaseLoader.ts +36 -0
- package/src/matrix/MatrixFilter.ts +103 -0
- package/src/matrix/MatrixReport.ts +290 -0
- package/src/matrix/VariantLoader.ts +46 -0
- package/src/runners/AdaptiveWrapper.ts +391 -0
- package/src/runners/BasicRunner.ts +368 -0
- package/src/runners/BenchRunner.ts +60 -0
- package/src/runners/CreateRunner.ts +11 -0
- package/src/runners/GcStats.ts +107 -0
- package/src/runners/RunnerOrchestrator.ts +374 -0
- package/src/runners/RunnerUtils.ts +2 -0
- package/src/runners/TimingUtils.ts +13 -0
- package/src/runners/WorkerScript.ts +256 -0
- package/src/table-util/ConvergenceFormatters.ts +19 -0
- package/src/table-util/Formatters.ts +152 -0
- package/src/table-util/README.md +70 -0
- package/src/table-util/TableReport.ts +293 -0
- package/src/table-util/test/TableReport.test.ts +105 -0
- package/src/table-util/test/TableValueExtractor.test.ts +41 -0
- package/src/table-util/test/TableValueExtractor.ts +100 -0
- package/src/test/AdaptiveRunner.test.ts +185 -0
- package/src/test/AdaptiveStatistics.integration.ts +119 -0
- package/src/test/BenchmarkReport.test.ts +82 -0
- package/src/test/BrowserBench.e2e.test.ts +44 -0
- package/src/test/BrowserBench.test.ts +79 -0
- package/src/test/GcStats.test.ts +94 -0
- package/src/test/PermutationTest.test.ts +121 -0
- package/src/test/RunBenchCLI.test.ts +166 -0
- package/src/test/RunnerOrchestrator.test.ts +102 -0
- package/src/test/StatisticalUtils.test.ts +112 -0
- package/src/test/TestUtils.ts +93 -0
- package/src/test/fixtures/test-bench-script.ts +30 -0
- package/src/tests/AdaptiveConvergence.test.ts +177 -0
- package/src/tests/AdaptiveSampling.test.ts +240 -0
- package/src/tests/BenchMatrix.test.ts +366 -0
- package/src/tests/MatrixFilter.test.ts +117 -0
- package/src/tests/MatrixReport.test.ts +139 -0
- package/src/tests/RealDataValidation.test.ts +177 -0
- package/src/tests/fixtures/baseline/impl.ts +4 -0
- package/src/tests/fixtures/bevy30-samples.ts +158 -0
- package/src/tests/fixtures/cases/asyncCases.ts +7 -0
- package/src/tests/fixtures/cases/cases.ts +8 -0
- package/src/tests/fixtures/cases/variants/product.ts +2 -0
- package/src/tests/fixtures/cases/variants/sum.ts +2 -0
- package/src/tests/fixtures/discover/fast.ts +1 -0
- package/src/tests/fixtures/discover/slow.ts +4 -0
- package/src/tests/fixtures/invalid/bad.ts +1 -0
- package/src/tests/fixtures/loader/fast.ts +1 -0
- package/src/tests/fixtures/loader/slow.ts +4 -0
- package/src/tests/fixtures/loader/stateful.ts +2 -0
- package/src/tests/fixtures/stateful/stateful.ts +2 -0
- package/src/tests/fixtures/variants/extra.ts +1 -0
- package/src/tests/fixtures/variants/impl.ts +1 -0
- package/src/tests/fixtures/worker/fast.ts +1 -0
- package/src/tests/fixtures/worker/slow.ts +4 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
export interface Sample {
|
|
2
|
+
benchmark: string;
|
|
3
|
+
value: number;
|
|
4
|
+
iteration: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface TimeSeriesPoint {
|
|
8
|
+
benchmark: string;
|
|
9
|
+
iteration: number;
|
|
10
|
+
value: number;
|
|
11
|
+
isWarmup: boolean;
|
|
12
|
+
optStatus?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface GcEvent {
|
|
16
|
+
benchmark: string;
|
|
17
|
+
sampleIndex: number;
|
|
18
|
+
duration: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface PausePoint {
|
|
22
|
+
benchmark: string;
|
|
23
|
+
sampleIndex: number;
|
|
24
|
+
durationMs: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface HeapPoint {
|
|
28
|
+
benchmark: string;
|
|
29
|
+
iteration: number;
|
|
30
|
+
value: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface BenchmarkStats {
|
|
34
|
+
min: number;
|
|
35
|
+
max: number;
|
|
36
|
+
avg: number;
|
|
37
|
+
p50: number;
|
|
38
|
+
p75: number;
|
|
39
|
+
p99: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface SectionStat {
|
|
43
|
+
groupTitle?: string;
|
|
44
|
+
label: string;
|
|
45
|
+
value: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface HistogramBin {
|
|
49
|
+
x: number;
|
|
50
|
+
count: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Bootstrap confidence interval for A/B comparison */
|
|
54
|
+
export interface ComparisonCI {
|
|
55
|
+
percent: number;
|
|
56
|
+
ci: [number, number];
|
|
57
|
+
direction: "faster" | "slower" | "uncertain";
|
|
58
|
+
histogram?: HistogramBin[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** One benchmark's raw data, statistics, and optional comparison results */
|
|
62
|
+
export interface BenchmarkEntry {
|
|
63
|
+
name: string;
|
|
64
|
+
samples: number[];
|
|
65
|
+
warmupSamples?: number[];
|
|
66
|
+
heapSamples?: number[];
|
|
67
|
+
gcEvents?: { offset: number; duration: number }[];
|
|
68
|
+
optSamples?: number[];
|
|
69
|
+
pausePoints?: { sampleIndex: number; durationMs: number }[];
|
|
70
|
+
stats: BenchmarkStats;
|
|
71
|
+
sectionStats?: SectionStat[];
|
|
72
|
+
comparisonCI?: ComparisonCI;
|
|
73
|
+
isBaseline: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface BenchmarkGroup {
|
|
77
|
+
baseline?: BenchmarkEntry;
|
|
78
|
+
benchmarks: BenchmarkEntry[];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface GitVersion {
|
|
82
|
+
hash: string;
|
|
83
|
+
date: string;
|
|
84
|
+
dirty?: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Top-level data structure for the HTML benchmark report */
|
|
88
|
+
export interface ReportData {
|
|
89
|
+
metadata: {
|
|
90
|
+
cliArgs?: Record<string, unknown>;
|
|
91
|
+
gcTrackingEnabled?: boolean;
|
|
92
|
+
currentVersion?: GitVersion;
|
|
93
|
+
baselineVersion?: GitVersion;
|
|
94
|
+
};
|
|
95
|
+
groups: BenchmarkGroup[];
|
|
96
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { renderPlots } from "./RenderPlots.ts";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { generateHtmlReport } from "./HtmlReport.ts";
|
|
2
|
+
export {
|
|
3
|
+
formatDateWithTimezone,
|
|
4
|
+
formatRelativeTime,
|
|
5
|
+
} from "./HtmlTemplate.ts";
|
|
6
|
+
export type {
|
|
7
|
+
BenchmarkData,
|
|
8
|
+
DifferenceCI,
|
|
9
|
+
FormattedStat,
|
|
10
|
+
GcEvent,
|
|
11
|
+
GitVersion,
|
|
12
|
+
GroupData,
|
|
13
|
+
HtmlReportOptions,
|
|
14
|
+
HtmlReportResult,
|
|
15
|
+
PausePoint,
|
|
16
|
+
ReportData,
|
|
17
|
+
} from "./Types.ts";
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
AnyVariant,
|
|
3
|
+
BenchMatrix,
|
|
4
|
+
CaseResult,
|
|
5
|
+
LoadedCase,
|
|
6
|
+
MatrixDefaults,
|
|
7
|
+
MatrixResults,
|
|
8
|
+
MatrixSuite,
|
|
9
|
+
RunMatrixOptions,
|
|
10
|
+
StatefulVariant,
|
|
11
|
+
Variant,
|
|
12
|
+
VariantFn,
|
|
13
|
+
VariantResult,
|
|
14
|
+
} from "./BenchMatrix.ts";
|
|
15
|
+
export { isStatefulVariant, runMatrix } from "./BenchMatrix.ts";
|
|
16
|
+
export type { BenchGroup, BenchmarkSpec, BenchSuite } from "./Benchmark.ts";
|
|
17
|
+
export type {
|
|
18
|
+
BenchmarkReport,
|
|
19
|
+
ReportColumnGroup,
|
|
20
|
+
ReportGroup,
|
|
21
|
+
ResultsMapper,
|
|
22
|
+
UnknownRecord,
|
|
23
|
+
} from "./BenchmarkReport.ts";
|
|
24
|
+
export { reportResults } from "./BenchmarkReport.ts";
|
|
25
|
+
export type { Configure, DefaultCliArgs } from "./cli/CliArgs.ts";
|
|
26
|
+
export { defaultCliArgs, parseCliArgs } from "./cli/CliArgs.ts";
|
|
27
|
+
export type { ExportOptions, MatrixExportOptions } from "./cli/RunBenchCLI.ts";
|
|
28
|
+
export {
|
|
29
|
+
benchExports,
|
|
30
|
+
cliToMatrixOptions,
|
|
31
|
+
defaultMatrixReport,
|
|
32
|
+
defaultReport,
|
|
33
|
+
exportReports,
|
|
34
|
+
hasField,
|
|
35
|
+
matrixBenchExports,
|
|
36
|
+
matrixToReportGroups,
|
|
37
|
+
parseBenchArgs,
|
|
38
|
+
printHeapReports,
|
|
39
|
+
reportOptStatus,
|
|
40
|
+
runBenchmarks,
|
|
41
|
+
runDefaultBench,
|
|
42
|
+
runDefaultMatrixBench,
|
|
43
|
+
runMatrixSuite,
|
|
44
|
+
} from "./cli/RunBenchCLI.ts";
|
|
45
|
+
export * from "./export/JsonFormat.ts";
|
|
46
|
+
export { exportPerfettoTrace } from "./export/PerfettoExport.ts";
|
|
47
|
+
export type { GitVersion } from "./GitUtils.ts";
|
|
48
|
+
export {
|
|
49
|
+
formatDateWithTimezone,
|
|
50
|
+
formatGitVersion,
|
|
51
|
+
getBaselineVersion,
|
|
52
|
+
getCurrentGitVersion,
|
|
53
|
+
} from "./GitUtils.ts";
|
|
54
|
+
export type { PrepareHtmlOptions } from "./HtmlDataPrep.ts";
|
|
55
|
+
export { prepareHtmlData } from "./HtmlDataPrep.ts";
|
|
56
|
+
export type { HtmlReportOptions, ReportData } from "./html/index.ts";
|
|
57
|
+
export { generateHtmlReport } from "./html/index.ts";
|
|
58
|
+
export type { MeasuredResults } from "./MeasuredResults.ts";
|
|
59
|
+
export type { CasesModule } from "./matrix/CaseLoader.ts";
|
|
60
|
+
export { loadCaseData, loadCasesModule } from "./matrix/CaseLoader.ts";
|
|
61
|
+
export type { FilteredMatrix, MatrixFilter } from "./matrix/MatrixFilter.ts";
|
|
62
|
+
export { filterMatrix, parseMatrixFilter } from "./matrix/MatrixFilter.ts";
|
|
63
|
+
export type {
|
|
64
|
+
ExtraColumn,
|
|
65
|
+
MatrixReportOptions,
|
|
66
|
+
} from "./matrix/MatrixReport.ts";
|
|
67
|
+
export {
|
|
68
|
+
gcPauseColumn,
|
|
69
|
+
gcStatsColumns,
|
|
70
|
+
heapTotalColumn,
|
|
71
|
+
reportMatrixResults,
|
|
72
|
+
} from "./matrix/MatrixReport.ts";
|
|
73
|
+
export type { RunnerOptions } from "./runners/BenchRunner.ts";
|
|
74
|
+
export {
|
|
75
|
+
adaptiveSection,
|
|
76
|
+
buildGenericSections,
|
|
77
|
+
cpuSection,
|
|
78
|
+
gcSection,
|
|
79
|
+
gcStatsSection,
|
|
80
|
+
optSection,
|
|
81
|
+
runsSection,
|
|
82
|
+
timeSection,
|
|
83
|
+
totalTimeSection,
|
|
84
|
+
} from "./StandardSections.ts";
|
|
85
|
+
export { average } from "./StatisticalUtils.ts";
|
|
86
|
+
export { formatConvergence } from "./table-util/ConvergenceFormatters.ts";
|
|
87
|
+
export {
|
|
88
|
+
formatBytes,
|
|
89
|
+
integer,
|
|
90
|
+
timeMs,
|
|
91
|
+
truncate,
|
|
92
|
+
} from "./table-util/Formatters.ts";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { LoadedCase } from "../BenchMatrix.ts";
|
|
2
|
+
|
|
3
|
+
/** Module that exports case definitions */
|
|
4
|
+
export interface CasesModule<T = unknown> {
|
|
5
|
+
cases: string[];
|
|
6
|
+
defaultCases?: string[]; // subset for quick runs
|
|
7
|
+
defaultVariants?: string[]; // subset for quick runs
|
|
8
|
+
loadCase?: (id: string) => LoadedCase<T> | Promise<LoadedCase<T>>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Load a cases module by URL */
|
|
12
|
+
export async function loadCasesModule<T = unknown>(
|
|
13
|
+
moduleUrl: string,
|
|
14
|
+
): Promise<CasesModule<T>> {
|
|
15
|
+
const module = await import(moduleUrl);
|
|
16
|
+
if (!Array.isArray(module.cases)) {
|
|
17
|
+
throw new Error(`Cases module at ${moduleUrl} must export 'cases' array`);
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
cases: module.cases,
|
|
21
|
+
defaultCases: module.defaultCases,
|
|
22
|
+
defaultVariants: module.defaultVariants,
|
|
23
|
+
loadCase: module.loadCase,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Load case data from a CasesModule or pass through the caseId */
|
|
28
|
+
export async function loadCaseData<T>(
|
|
29
|
+
casesModule: CasesModule<T> | undefined,
|
|
30
|
+
caseId: string,
|
|
31
|
+
): Promise<LoadedCase<T>> {
|
|
32
|
+
if (casesModule?.loadCase) {
|
|
33
|
+
return casesModule.loadCase(caseId);
|
|
34
|
+
}
|
|
35
|
+
return { data: caseId as T };
|
|
36
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { BenchMatrix } from "../BenchMatrix.ts";
|
|
2
|
+
import { loadCasesModule } from "./CaseLoader.ts";
|
|
3
|
+
import { discoverVariants } from "./VariantLoader.ts";
|
|
4
|
+
|
|
5
|
+
/** Filter for matrix case/variant selection */
|
|
6
|
+
export interface MatrixFilter {
|
|
7
|
+
case?: string;
|
|
8
|
+
variant?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Parse filter string: "case/variant", "case/", "/variant", or "case" */
|
|
12
|
+
export function parseMatrixFilter(filter: string): MatrixFilter {
|
|
13
|
+
if (filter.includes("/")) {
|
|
14
|
+
const [casePart, variantPart] = filter.split("/", 2);
|
|
15
|
+
return {
|
|
16
|
+
case: casePart || undefined,
|
|
17
|
+
variant: variantPart || undefined,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
return { case: filter };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Filtered matrix with explicit case and variant lists */
|
|
24
|
+
export interface FilteredMatrix<T = unknown> extends BenchMatrix<T> {
|
|
25
|
+
filteredCases?: string[];
|
|
26
|
+
filteredVariants?: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Apply filter to a matrix, merging with existing filters via intersection */
|
|
30
|
+
export async function filterMatrix<T>(
|
|
31
|
+
matrix: FilteredMatrix<T>,
|
|
32
|
+
filter?: MatrixFilter,
|
|
33
|
+
): Promise<FilteredMatrix<T>> {
|
|
34
|
+
if (!filter || (!filter.case && !filter.variant)) return matrix;
|
|
35
|
+
|
|
36
|
+
const caseList = await getFilteredCases(matrix, filter.case);
|
|
37
|
+
const variantList = await getFilteredVariants(matrix, filter.variant);
|
|
38
|
+
|
|
39
|
+
const filteredCases =
|
|
40
|
+
caseList && matrix.filteredCases
|
|
41
|
+
? caseList.filter(c => matrix.filteredCases!.includes(c))
|
|
42
|
+
: (caseList ?? matrix.filteredCases);
|
|
43
|
+
|
|
44
|
+
const filteredVariants =
|
|
45
|
+
variantList && matrix.filteredVariants
|
|
46
|
+
? variantList.filter(v => matrix.filteredVariants!.includes(v))
|
|
47
|
+
: (variantList ?? matrix.filteredVariants);
|
|
48
|
+
|
|
49
|
+
return { ...matrix, filteredCases, filteredVariants };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Get case IDs matching filter pattern */
|
|
53
|
+
async function getFilteredCases<T>(
|
|
54
|
+
matrix: BenchMatrix<T>,
|
|
55
|
+
casePattern?: string,
|
|
56
|
+
): Promise<string[] | undefined> {
|
|
57
|
+
if (!casePattern) return undefined;
|
|
58
|
+
|
|
59
|
+
const caseIds = matrix.casesModule
|
|
60
|
+
? (await loadCasesModule(matrix.casesModule)).cases
|
|
61
|
+
: matrix.cases;
|
|
62
|
+
if (!caseIds) return ["default"]; // implicit single case
|
|
63
|
+
|
|
64
|
+
const filtered = caseIds.filter(id => matchPattern(id, casePattern));
|
|
65
|
+
if (filtered.length === 0) {
|
|
66
|
+
throw new Error(`No cases match filter: "${casePattern}"`);
|
|
67
|
+
}
|
|
68
|
+
return filtered;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Get variant IDs matching filter pattern */
|
|
72
|
+
async function getFilteredVariants<T>(
|
|
73
|
+
matrix: BenchMatrix<T>,
|
|
74
|
+
variantPattern?: string,
|
|
75
|
+
): Promise<string[] | undefined> {
|
|
76
|
+
if (!variantPattern) return undefined;
|
|
77
|
+
|
|
78
|
+
if (matrix.variants) {
|
|
79
|
+
const ids = Object.keys(matrix.variants).filter(id =>
|
|
80
|
+
matchPattern(id, variantPattern),
|
|
81
|
+
);
|
|
82
|
+
if (ids.length === 0) {
|
|
83
|
+
throw new Error(`No variants match filter: "${variantPattern}"`);
|
|
84
|
+
}
|
|
85
|
+
return ids;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (matrix.variantDir) {
|
|
89
|
+
const allIds = await discoverVariants(matrix.variantDir);
|
|
90
|
+
const filtered = allIds.filter(id => matchPattern(id, variantPattern));
|
|
91
|
+
if (filtered.length === 0) {
|
|
92
|
+
throw new Error(`No variants match filter: "${variantPattern}"`);
|
|
93
|
+
}
|
|
94
|
+
return filtered;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
throw new Error("BenchMatrix requires 'variants' or 'variantDir'");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Match id against pattern (case-insensitive substring) */
|
|
101
|
+
function matchPattern(id: string, pattern: string): boolean {
|
|
102
|
+
return id.toLowerCase().includes(pattern.toLowerCase());
|
|
103
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import type { CaseResult, MatrixResults } from "../BenchMatrix.ts";
|
|
2
|
+
import { injectDiffColumns, type ResultsMapper } from "../BenchmarkReport.ts";
|
|
3
|
+
import { totalProfileBytes } from "../heap-sample/HeapSampleReport.ts";
|
|
4
|
+
import { type GcStatsInfo, gcStatsSection } from "../StandardSections.ts";
|
|
5
|
+
import {
|
|
6
|
+
average,
|
|
7
|
+
bootstrapDifferenceCI,
|
|
8
|
+
type DifferenceCI,
|
|
9
|
+
} from "../StatisticalUtils.ts";
|
|
10
|
+
import {
|
|
11
|
+
duration,
|
|
12
|
+
formatBytes,
|
|
13
|
+
formatDiffWithCI,
|
|
14
|
+
truncate,
|
|
15
|
+
} from "../table-util/Formatters.ts";
|
|
16
|
+
import {
|
|
17
|
+
buildTable,
|
|
18
|
+
type ColumnGroup,
|
|
19
|
+
type ResultGroup,
|
|
20
|
+
} from "../table-util/TableReport.ts";
|
|
21
|
+
|
|
22
|
+
/** Custom column definition for extra computed metrics */
|
|
23
|
+
export interface ExtraColumn {
|
|
24
|
+
key: string;
|
|
25
|
+
title: string;
|
|
26
|
+
groupTitle?: string; // optional column group header
|
|
27
|
+
extract: (caseResult: CaseResult) => unknown;
|
|
28
|
+
formatter?: (value: unknown) => string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Options for matrix report generation */
|
|
32
|
+
export interface MatrixReportOptions {
|
|
33
|
+
extraColumns?: ExtraColumn[];
|
|
34
|
+
sections?: ResultsMapper[]; // ResultsMapper sections (like BenchSuite)
|
|
35
|
+
variantTitle?: string; // custom title for the variant column (default: "variant")
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Row data for matrix report table */
|
|
39
|
+
interface MatrixReportRow extends Record<string, unknown> {
|
|
40
|
+
name: string;
|
|
41
|
+
time: number;
|
|
42
|
+
samples: number;
|
|
43
|
+
diffCI?: DifferenceCI;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Format matrix results as one table per case */
|
|
47
|
+
export function reportMatrixResults(
|
|
48
|
+
results: MatrixResults,
|
|
49
|
+
options?: MatrixReportOptions,
|
|
50
|
+
): string {
|
|
51
|
+
const tables = buildCaseTables(results, options);
|
|
52
|
+
const header = `Matrix: ${results.name}`;
|
|
53
|
+
return [header, ...tables].join("\n\n");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Build one table for each case showing all variants */
|
|
57
|
+
function buildCaseTables(
|
|
58
|
+
results: MatrixResults,
|
|
59
|
+
options?: MatrixReportOptions,
|
|
60
|
+
): string[] {
|
|
61
|
+
if (results.variants.length === 0) return [];
|
|
62
|
+
|
|
63
|
+
// Get all case IDs from first variant (all variants have same cases)
|
|
64
|
+
const caseIds = results.variants[0].cases.map(c => c.caseId);
|
|
65
|
+
return caseIds.map(caseId => buildCaseTable(results, caseId, options));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Build table for a single case showing all variants */
|
|
69
|
+
function buildCaseTable(
|
|
70
|
+
results: MatrixResults,
|
|
71
|
+
caseId: string,
|
|
72
|
+
options?: MatrixReportOptions,
|
|
73
|
+
): string {
|
|
74
|
+
const caseTitle = formatCaseTitle(results, caseId);
|
|
75
|
+
|
|
76
|
+
if (options?.sections?.length) {
|
|
77
|
+
return buildSectionTable(results, caseId, options, caseTitle);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const rows = buildCaseRows(results, caseId, options?.extraColumns);
|
|
81
|
+
const hasBaseline = rows.some(r => r.diffCI);
|
|
82
|
+
const columns = buildColumns(hasBaseline, options);
|
|
83
|
+
|
|
84
|
+
const resultGroup: ResultGroup<MatrixReportRow> = { results: rows };
|
|
85
|
+
const table = buildTable(columns, [resultGroup]);
|
|
86
|
+
return `${caseTitle}\n${table}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Build table using ResultsMapper sections */
|
|
90
|
+
function buildSectionTable(
|
|
91
|
+
results: MatrixResults,
|
|
92
|
+
caseId: string,
|
|
93
|
+
options: MatrixReportOptions,
|
|
94
|
+
caseTitle: string,
|
|
95
|
+
): string {
|
|
96
|
+
const sections = options.sections!;
|
|
97
|
+
const variantTitle = options.variantTitle ?? "name";
|
|
98
|
+
|
|
99
|
+
const rows: Record<string, unknown>[] = [];
|
|
100
|
+
let hasBaseline = false;
|
|
101
|
+
|
|
102
|
+
for (const variant of results.variants) {
|
|
103
|
+
const caseResult = variant.cases.find(c => c.caseId === caseId);
|
|
104
|
+
if (!caseResult) continue;
|
|
105
|
+
|
|
106
|
+
const row: Record<string, unknown> = { name: truncate(variant.id, 25) };
|
|
107
|
+
|
|
108
|
+
for (const section of sections) {
|
|
109
|
+
Object.assign(
|
|
110
|
+
row,
|
|
111
|
+
section.extract(caseResult.measured, caseResult.metadata),
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (caseResult.baseline) {
|
|
116
|
+
hasBaseline = true;
|
|
117
|
+
const { samples: base } = caseResult.baseline;
|
|
118
|
+
row.diffCI = bootstrapDifferenceCI(base, caseResult.measured.samples);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
rows.push(row);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const columnGroups = buildSectionColumns(sections, variantTitle, hasBaseline);
|
|
125
|
+
const resultGroup: ResultGroup<Record<string, unknown>> = { results: rows };
|
|
126
|
+
const table = buildTable(columnGroups, [resultGroup]);
|
|
127
|
+
return `${caseTitle}\n${table}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Build column groups from ResultsMapper sections */
|
|
131
|
+
function buildSectionColumns(
|
|
132
|
+
sections: ResultsMapper[],
|
|
133
|
+
variantTitle: string,
|
|
134
|
+
hasBaseline: boolean,
|
|
135
|
+
): ColumnGroup<Record<string, unknown>>[] {
|
|
136
|
+
const nameCol: ColumnGroup<Record<string, unknown>> = {
|
|
137
|
+
columns: [{ key: "name", title: variantTitle }],
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const sectionColumns = sections.flatMap(s => s.columns());
|
|
141
|
+
const columnGroups = hasBaseline
|
|
142
|
+
? injectDiffColumns(sectionColumns)
|
|
143
|
+
: (sectionColumns as ColumnGroup<Record<string, unknown>>[]);
|
|
144
|
+
|
|
145
|
+
return [nameCol, ...columnGroups];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Build rows for all variants for a given case */
|
|
149
|
+
function buildCaseRows(
|
|
150
|
+
results: MatrixResults,
|
|
151
|
+
caseId: string,
|
|
152
|
+
extraColumns?: ExtraColumn[],
|
|
153
|
+
): MatrixReportRow[] {
|
|
154
|
+
return results.variants.flatMap(variant => {
|
|
155
|
+
const caseResult = variant.cases.find(c => c.caseId === caseId);
|
|
156
|
+
return caseResult ? [buildRow(variant.id, caseResult, extraColumns)] : [];
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Build a single row from case result */
|
|
161
|
+
function buildRow(
|
|
162
|
+
variantId: string,
|
|
163
|
+
caseResult: CaseResult,
|
|
164
|
+
extraColumns?: ExtraColumn[],
|
|
165
|
+
): MatrixReportRow {
|
|
166
|
+
const { measured, baseline } = caseResult;
|
|
167
|
+
const samples = measured.samples;
|
|
168
|
+
const time = measured.time?.avg ?? average(samples);
|
|
169
|
+
|
|
170
|
+
const row: MatrixReportRow = {
|
|
171
|
+
name: truncate(variantId, 25),
|
|
172
|
+
time,
|
|
173
|
+
samples: samples.length,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
if (baseline) {
|
|
177
|
+
row.diffCI = bootstrapDifferenceCI(baseline.samples, samples);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (extraColumns) {
|
|
181
|
+
for (const col of extraColumns) {
|
|
182
|
+
row[col.key] = col.extract(caseResult);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return row;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Build column configuration */
|
|
190
|
+
function buildColumns(
|
|
191
|
+
hasBaseline: boolean,
|
|
192
|
+
options?: MatrixReportOptions,
|
|
193
|
+
): ColumnGroup<MatrixReportRow>[] {
|
|
194
|
+
const variantTitle = options?.variantTitle ?? "variant";
|
|
195
|
+
const nameCol: ColumnGroup<MatrixReportRow> = {
|
|
196
|
+
columns: [{ key: "name", title: variantTitle }],
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const ciKey = "diffCI" as keyof MatrixReportRow;
|
|
200
|
+
const diffCol = { key: ciKey, title: "Δ% CI", formatter: formatDiff };
|
|
201
|
+
const timeCol: ColumnGroup<MatrixReportRow> = {
|
|
202
|
+
columns: [
|
|
203
|
+
{ key: "time", title: "time", formatter: duration },
|
|
204
|
+
...(hasBaseline ? [diffCol] : []),
|
|
205
|
+
],
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const groups: ColumnGroup<MatrixReportRow>[] = [nameCol, timeCol];
|
|
209
|
+
|
|
210
|
+
// Add extra columns, grouped by groupTitle
|
|
211
|
+
const extraColumns = options?.extraColumns;
|
|
212
|
+
if (extraColumns?.length) {
|
|
213
|
+
const byGroup = new Map<string | undefined, ExtraColumn[]>();
|
|
214
|
+
for (const col of extraColumns) {
|
|
215
|
+
const group = byGroup.get(col.groupTitle) ?? [];
|
|
216
|
+
group.push(col);
|
|
217
|
+
byGroup.set(col.groupTitle, group);
|
|
218
|
+
}
|
|
219
|
+
for (const [groupTitle, cols] of byGroup) {
|
|
220
|
+
groups.push({
|
|
221
|
+
groupTitle,
|
|
222
|
+
columns: cols.map(col => ({
|
|
223
|
+
key: col.key as keyof MatrixReportRow,
|
|
224
|
+
title: col.title,
|
|
225
|
+
formatter: col.formatter ?? String,
|
|
226
|
+
})),
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return groups;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Format diff with CI, or "baseline" marker */
|
|
235
|
+
function formatDiff(value: unknown): string | null {
|
|
236
|
+
if (!value) return null;
|
|
237
|
+
return formatDiffWithCI(value as DifferenceCI);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Format case title with metadata if available */
|
|
241
|
+
function formatCaseTitle(results: MatrixResults, caseId: string): string {
|
|
242
|
+
const caseResult = results.variants[0]?.cases.find(c => c.caseId === caseId);
|
|
243
|
+
const metadata = caseResult?.metadata;
|
|
244
|
+
|
|
245
|
+
if (metadata && Object.keys(metadata).length > 0) {
|
|
246
|
+
const metaParts = Object.entries(metadata)
|
|
247
|
+
.map(([k, v]) => `${v} ${k}`)
|
|
248
|
+
.join(", ");
|
|
249
|
+
return `${caseId} (${metaParts})`;
|
|
250
|
+
}
|
|
251
|
+
return caseId;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** GC statistics columns - derived from gcStatsSection for consistency */
|
|
255
|
+
export const gcStatsColumns: ExtraColumn[] = gcStatsSection
|
|
256
|
+
.columns()[0]
|
|
257
|
+
.columns.map(col => ({
|
|
258
|
+
key: col.key as string,
|
|
259
|
+
title: col.title,
|
|
260
|
+
groupTitle: "GC",
|
|
261
|
+
extract: (r: CaseResult) =>
|
|
262
|
+
gcStatsSection.extract(r.measured)[col.key as keyof GcStatsInfo],
|
|
263
|
+
formatter: (v: unknown) => col.formatter?.(v) ?? "-",
|
|
264
|
+
}));
|
|
265
|
+
|
|
266
|
+
/** Format bytes with fallback to "-" for missing values */
|
|
267
|
+
function formatBytesOrDash(value: unknown): string {
|
|
268
|
+
return formatBytes(value) ?? "-";
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** GC pause time column */
|
|
272
|
+
export const gcPauseColumn: ExtraColumn = {
|
|
273
|
+
key: "gcPause",
|
|
274
|
+
title: "pause",
|
|
275
|
+
groupTitle: "GC",
|
|
276
|
+
extract: r => r.measured.gcStats?.gcPauseTime,
|
|
277
|
+
formatter: v => (v != null ? `${(v as number).toFixed(1)}ms` : "-"),
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
/** Heap sampling total bytes column */
|
|
281
|
+
export const heapTotalColumn: ExtraColumn = {
|
|
282
|
+
key: "heapTotal",
|
|
283
|
+
title: "heap",
|
|
284
|
+
extract: r => {
|
|
285
|
+
const profile = r.measured.heapProfile;
|
|
286
|
+
if (!profile?.head) return undefined;
|
|
287
|
+
return totalProfileBytes(profile);
|
|
288
|
+
},
|
|
289
|
+
formatter: formatBytesOrDash,
|
|
290
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import type { Variant } from "../BenchMatrix.ts";
|
|
4
|
+
|
|
5
|
+
/** Discover variant ids from a directory of .ts files */
|
|
6
|
+
export async function discoverVariants(dirUrl: string): Promise<string[]> {
|
|
7
|
+
const dirPath = fileURLToPath(dirUrl);
|
|
8
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
9
|
+
return entries
|
|
10
|
+
.filter(e => e.isFile() && e.name.endsWith(".ts"))
|
|
11
|
+
.map(e => e.name.slice(0, -3))
|
|
12
|
+
.sort();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Load a variant module and extract run/setup exports */
|
|
16
|
+
export async function loadVariant<T = unknown>(
|
|
17
|
+
dirUrl: string,
|
|
18
|
+
variantId: string,
|
|
19
|
+
): Promise<Variant<T>> {
|
|
20
|
+
const moduleUrl = variantModuleUrl(dirUrl, variantId);
|
|
21
|
+
const module = await import(moduleUrl);
|
|
22
|
+
return extractVariant(module, variantId, moduleUrl);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Extract variant from module exports */
|
|
26
|
+
function extractVariant<T>(
|
|
27
|
+
module: Record<string, unknown>,
|
|
28
|
+
variantId: string,
|
|
29
|
+
moduleUrl: string,
|
|
30
|
+
): Variant<T> {
|
|
31
|
+
const { setup, run } = module;
|
|
32
|
+
const loc = `Variant '${variantId}' at ${moduleUrl}`;
|
|
33
|
+
if (typeof run !== "function") {
|
|
34
|
+
throw new Error(`${loc} must export 'run'`);
|
|
35
|
+
}
|
|
36
|
+
if (setup === undefined) return run as (data: T) => void;
|
|
37
|
+
if (typeof setup !== "function") {
|
|
38
|
+
throw new Error(`${loc}: 'setup' must be a function`);
|
|
39
|
+
}
|
|
40
|
+
return { setup: setup as (data: T) => unknown, run: run as () => void };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Get module URL for a variant in a directory */
|
|
44
|
+
export function variantModuleUrl(dirUrl: string, variantId: string): string {
|
|
45
|
+
return new URL(`${variantId}.ts`, dirUrl).href;
|
|
46
|
+
}
|