benchforge 0.1.9 → 0.1.11
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 +40 -6
- package/dist/{BenchRunner-CSKN9zPy.d.mts → BenchRunner-BzyUfiyB.d.mts} +32 -8
- package/dist/{BrowserHeapSampler-DCeL42RE.mjs → BrowserHeapSampler-B6asLKWQ.mjs} +57 -57
- package/dist/BrowserHeapSampler-B6asLKWQ.mjs.map +1 -0
- package/dist/{GcStats-ByEovUi1.mjs → GcStats-wX7Xyblu.mjs} +15 -15
- package/dist/GcStats-wX7Xyblu.mjs.map +1 -0
- package/dist/HeapSampler-B8dtKHn1.mjs.map +1 -1
- package/dist/{TimingUtils-ClclVQ7E.mjs → TimingUtils-DwOwkc8G.mjs} +225 -225
- package/dist/TimingUtils-DwOwkc8G.mjs.map +1 -0
- package/dist/bin/benchforge.mjs +1 -1
- package/dist/browser/index.js +210 -210
- package/dist/index.d.mts +102 -46
- package/dist/index.mjs +3 -3
- package/dist/runners/WorkerScript.d.mts +1 -1
- package/dist/runners/WorkerScript.mjs +66 -66
- package/dist/runners/WorkerScript.mjs.map +1 -1
- package/dist/{src-Cf_LXwlp.mjs → src-B-DDaCa9.mjs} +1225 -990
- package/dist/src-B-DDaCa9.mjs.map +1 -0
- package/package.json +2 -1
- package/src/BenchMatrix.ts +125 -125
- package/src/BenchmarkReport.ts +50 -45
- package/src/HtmlDataPrep.ts +21 -21
- package/src/PermutationTest.ts +24 -24
- package/src/StandardSections.ts +45 -45
- package/src/StatisticalUtils.ts +60 -61
- package/src/browser/BrowserGcStats.ts +5 -5
- package/src/browser/BrowserHeapSampler.ts +63 -63
- package/src/cli/CliArgs.ts +6 -3
- package/src/cli/FilterBenchmarks.ts +5 -5
- package/src/cli/RunBenchCLI.ts +526 -498
- package/src/export/JsonExport.ts +10 -10
- package/src/export/PerfettoExport.ts +74 -74
- package/src/export/SpeedscopeExport.ts +202 -0
- package/src/heap-sample/HeapSampleReport.ts +143 -70
- package/src/heap-sample/HeapSampler.ts +55 -12
- package/src/heap-sample/ResolvedProfile.ts +89 -0
- package/src/html/HtmlReport.ts +33 -33
- package/src/html/HtmlTemplate.ts +67 -67
- package/src/html/browser/CIPlot.ts +50 -50
- package/src/html/browser/HistogramKde.ts +13 -13
- package/src/html/browser/LegendUtils.ts +48 -48
- package/src/html/browser/RenderPlots.ts +98 -98
- package/src/html/browser/SampleTimeSeries.ts +79 -79
- package/src/index.ts +6 -0
- package/src/matrix/MatrixFilter.ts +6 -6
- package/src/matrix/MatrixReport.ts +96 -96
- package/src/matrix/VariantLoader.ts +5 -5
- package/src/runners/AdaptiveWrapper.ts +151 -151
- package/src/runners/BasicRunner.ts +175 -175
- package/src/runners/BenchRunner.ts +8 -8
- package/src/runners/GcStats.ts +22 -22
- package/src/runners/RunnerOrchestrator.ts +168 -168
- package/src/runners/WorkerScript.ts +96 -96
- package/src/table-util/Formatters.ts +41 -36
- package/src/table-util/TableReport.ts +122 -122
- package/src/table-util/test/TableValueExtractor.ts +9 -9
- package/src/test/AdaptiveStatistics.integration.ts +7 -39
- package/src/test/HeapAttribution.test.ts +51 -0
- package/src/test/RunBenchCLI.test.ts +18 -18
- package/src/test/TestUtils.ts +24 -24
- package/src/tests/BenchMatrix.test.ts +12 -12
- package/src/tests/MatrixFilter.test.ts +15 -15
- package/dist/BrowserHeapSampler-DCeL42RE.mjs.map +0 -1
- package/dist/GcStats-ByEovUi1.mjs.map +0 -1
- package/dist/TimingUtils-ClclVQ7E.mjs.map +0 -1
- package/dist/src-Cf_LXwlp.mjs.map +0 -1
|
@@ -8,23 +8,6 @@ import type {
|
|
|
8
8
|
TimeSeriesPoint,
|
|
9
9
|
} from "./Types.ts";
|
|
10
10
|
|
|
11
|
-
const OPT_STATUS_NAMES: Record<number, string> = {
|
|
12
|
-
1: "interpreted",
|
|
13
|
-
129: "sparkplug",
|
|
14
|
-
17: "turbofan",
|
|
15
|
-
33: "maglev",
|
|
16
|
-
49: "turbofan+maglev",
|
|
17
|
-
32769: "optimized",
|
|
18
|
-
};
|
|
19
|
-
const OPT_TIER_COLORS: Record<string, string> = {
|
|
20
|
-
turbofan: "#22c55e",
|
|
21
|
-
optimized: "#22c55e",
|
|
22
|
-
"turbofan+maglev": "#22c55e",
|
|
23
|
-
maglev: "#eab308",
|
|
24
|
-
sparkplug: "#f97316",
|
|
25
|
-
interpreted: "#dc3545",
|
|
26
|
-
};
|
|
27
|
-
|
|
28
11
|
interface SampleData {
|
|
29
12
|
benchmark: string;
|
|
30
13
|
sample: number;
|
|
@@ -49,6 +32,23 @@ interface PlotContext {
|
|
|
49
32
|
benchmarks: string[];
|
|
50
33
|
}
|
|
51
34
|
|
|
35
|
+
const OPT_STATUS_NAMES: Record<number, string> = {
|
|
36
|
+
1: "interpreted",
|
|
37
|
+
129: "sparkplug",
|
|
38
|
+
17: "turbofan",
|
|
39
|
+
33: "maglev",
|
|
40
|
+
49: "turbofan+maglev",
|
|
41
|
+
32769: "optimized",
|
|
42
|
+
};
|
|
43
|
+
const OPT_TIER_COLORS: Record<string, string> = {
|
|
44
|
+
turbofan: "#22c55e",
|
|
45
|
+
optimized: "#22c55e",
|
|
46
|
+
"turbofan+maglev": "#22c55e",
|
|
47
|
+
maglev: "#eab308",
|
|
48
|
+
sparkplug: "#f97316",
|
|
49
|
+
interpreted: "#dc3545",
|
|
50
|
+
};
|
|
51
|
+
|
|
52
52
|
/** Create sample time series showing each sample in order */
|
|
53
53
|
export function createSampleTimeSeries(
|
|
54
54
|
timeSeries: TimeSeriesPoint[],
|
|
@@ -146,68 +146,6 @@ function buildPlotContext(timeSeries: TimeSeriesPoint[]): PlotContext {
|
|
|
146
146
|
};
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
function buildSampleData(
|
|
150
|
-
timeSeries: TimeSeriesPoint[],
|
|
151
|
-
benchmarks: string[],
|
|
152
|
-
): Omit<SampleData, "displayValue">[] {
|
|
153
|
-
const result: Omit<SampleData, "displayValue">[] = [];
|
|
154
|
-
for (const benchmark of benchmarks) {
|
|
155
|
-
const isBaseline = benchmark.includes("(baseline)");
|
|
156
|
-
for (const d of timeSeries.filter(t => t.benchmark === benchmark)) {
|
|
157
|
-
const optTier =
|
|
158
|
-
d.optStatus !== undefined
|
|
159
|
-
? OPT_STATUS_NAMES[d.optStatus] || "unknown"
|
|
160
|
-
: null;
|
|
161
|
-
result.push({
|
|
162
|
-
benchmark,
|
|
163
|
-
sample: d.iteration,
|
|
164
|
-
value: d.value,
|
|
165
|
-
isBaseline,
|
|
166
|
-
isWarmup: d.isWarmup || false,
|
|
167
|
-
optTier,
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
return result;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/** Pick display unit (ns/us/ms) based on average value magnitude */
|
|
175
|
-
function getTimeUnit(values: number[]) {
|
|
176
|
-
const avg = d3.mean(values)!;
|
|
177
|
-
const fmt0 = (d: number) => d3.format(",.0f")(d);
|
|
178
|
-
const fmt1 = (d: number) => d3.format(",.1f")(d);
|
|
179
|
-
if (avg < 0.001)
|
|
180
|
-
return {
|
|
181
|
-
unitSuffix: "ns",
|
|
182
|
-
convertValue: (ms: number) => ms * 1e6,
|
|
183
|
-
formatValue: fmt0,
|
|
184
|
-
};
|
|
185
|
-
if (avg < 1)
|
|
186
|
-
return {
|
|
187
|
-
unitSuffix: "μs",
|
|
188
|
-
convertValue: (ms: number) => ms * 1e3,
|
|
189
|
-
formatValue: fmt1,
|
|
190
|
-
};
|
|
191
|
-
return {
|
|
192
|
-
unitSuffix: "ms",
|
|
193
|
-
convertValue: (ms: number) => ms,
|
|
194
|
-
formatValue: fmt1,
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/** Compute Y axis range with padding, snapping yMin to a round number */
|
|
199
|
-
function computeYRange(values: number[]) {
|
|
200
|
-
const dataMin = d3.min(values)!;
|
|
201
|
-
const dataMax = d3.max(values)!;
|
|
202
|
-
const dataRange = dataMax - dataMin;
|
|
203
|
-
const padding = dataRange * 0.15;
|
|
204
|
-
let yMin = dataMin - padding;
|
|
205
|
-
const magnitude = 10 ** Math.floor(Math.log10(Math.abs(yMin)));
|
|
206
|
-
yMin = Math.floor(yMin / magnitude) * magnitude;
|
|
207
|
-
if (dataMin > 0 && yMin < 0) yMin = 0;
|
|
208
|
-
return { yMin, yMax: dataMax + dataRange * 0.05 };
|
|
209
|
-
}
|
|
210
|
-
|
|
211
149
|
/** Scale heap byte values into the plot's Y coordinate range */
|
|
212
150
|
function prepareHeapData(heapSeries: HeapPoint[], yMin: number, yMax: number) {
|
|
213
151
|
if (heapSeries.length === 0) return [];
|
|
@@ -387,3 +325,65 @@ function buildLegendItems(
|
|
|
387
325
|
}
|
|
388
326
|
return items;
|
|
389
327
|
}
|
|
328
|
+
|
|
329
|
+
function buildSampleData(
|
|
330
|
+
timeSeries: TimeSeriesPoint[],
|
|
331
|
+
benchmarks: string[],
|
|
332
|
+
): Omit<SampleData, "displayValue">[] {
|
|
333
|
+
const result: Omit<SampleData, "displayValue">[] = [];
|
|
334
|
+
for (const benchmark of benchmarks) {
|
|
335
|
+
const isBaseline = benchmark.includes("(baseline)");
|
|
336
|
+
for (const d of timeSeries.filter(t => t.benchmark === benchmark)) {
|
|
337
|
+
const optTier =
|
|
338
|
+
d.optStatus !== undefined
|
|
339
|
+
? OPT_STATUS_NAMES[d.optStatus] || "unknown"
|
|
340
|
+
: null;
|
|
341
|
+
result.push({
|
|
342
|
+
benchmark,
|
|
343
|
+
sample: d.iteration,
|
|
344
|
+
value: d.value,
|
|
345
|
+
isBaseline,
|
|
346
|
+
isWarmup: d.isWarmup || false,
|
|
347
|
+
optTier,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return result;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Pick display unit (ns/us/ms) based on average value magnitude */
|
|
355
|
+
function getTimeUnit(values: number[]) {
|
|
356
|
+
const avg = d3.mean(values)!;
|
|
357
|
+
const fmt0 = (d: number) => d3.format(",.0f")(d);
|
|
358
|
+
const fmt1 = (d: number) => d3.format(",.1f")(d);
|
|
359
|
+
if (avg < 0.001)
|
|
360
|
+
return {
|
|
361
|
+
unitSuffix: "ns",
|
|
362
|
+
convertValue: (ms: number) => ms * 1e6,
|
|
363
|
+
formatValue: fmt0,
|
|
364
|
+
};
|
|
365
|
+
if (avg < 1)
|
|
366
|
+
return {
|
|
367
|
+
unitSuffix: "μs",
|
|
368
|
+
convertValue: (ms: number) => ms * 1e3,
|
|
369
|
+
formatValue: fmt1,
|
|
370
|
+
};
|
|
371
|
+
return {
|
|
372
|
+
unitSuffix: "ms",
|
|
373
|
+
convertValue: (ms: number) => ms,
|
|
374
|
+
formatValue: fmt1,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** Compute Y axis range with padding, snapping yMin to a round number */
|
|
379
|
+
function computeYRange(values: number[]) {
|
|
380
|
+
const dataMin = d3.min(values)!;
|
|
381
|
+
const dataMax = d3.max(values)!;
|
|
382
|
+
const dataRange = dataMax - dataMin;
|
|
383
|
+
const padding = dataRange * 0.15;
|
|
384
|
+
let yMin = dataMin - padding;
|
|
385
|
+
const magnitude = 10 ** Math.floor(Math.log10(Math.abs(yMin)));
|
|
386
|
+
yMin = Math.floor(yMin / magnitude) * magnitude;
|
|
387
|
+
if (dataMin > 0 && yMin < 0) yMin = 0;
|
|
388
|
+
return { yMin, yMax: dataMax + dataRange * 0.05 };
|
|
389
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -44,6 +44,12 @@ export {
|
|
|
44
44
|
} from "./cli/RunBenchCLI.ts";
|
|
45
45
|
export * from "./export/JsonFormat.ts";
|
|
46
46
|
export { exportPerfettoTrace } from "./export/PerfettoExport.ts";
|
|
47
|
+
export {
|
|
48
|
+
exportAndLaunchSpeedscope,
|
|
49
|
+
exportSpeedscope,
|
|
50
|
+
heapProfileToSpeedscope,
|
|
51
|
+
launchSpeedscope,
|
|
52
|
+
} from "./export/SpeedscopeExport.ts";
|
|
47
53
|
export type { GitVersion } from "./GitUtils.ts";
|
|
48
54
|
export {
|
|
49
55
|
formatDateWithTimezone,
|
|
@@ -8,6 +8,12 @@ export interface MatrixFilter {
|
|
|
8
8
|
variant?: string;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
/** Filtered matrix with explicit case and variant lists */
|
|
12
|
+
export interface FilteredMatrix<T = unknown> extends BenchMatrix<T> {
|
|
13
|
+
filteredCases?: string[];
|
|
14
|
+
filteredVariants?: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
11
17
|
/** Parse filter string: "case/variant", "case/", "/variant", or "case" */
|
|
12
18
|
export function parseMatrixFilter(filter: string): MatrixFilter {
|
|
13
19
|
if (filter.includes("/")) {
|
|
@@ -20,12 +26,6 @@ export function parseMatrixFilter(filter: string): MatrixFilter {
|
|
|
20
26
|
return { case: filter };
|
|
21
27
|
}
|
|
22
28
|
|
|
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
29
|
/** Apply filter to a matrix, merging with existing filters via intersection */
|
|
30
30
|
export async function filterMatrix<T>(
|
|
31
31
|
matrix: FilteredMatrix<T>,
|
|
@@ -43,6 +43,39 @@ interface MatrixReportRow extends Record<string, unknown> {
|
|
|
43
43
|
diffCI?: DifferenceCI;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/** GC statistics columns - derived from gcStatsSection for consistency */
|
|
47
|
+
export const gcStatsColumns: ExtraColumn[] = gcStatsSection
|
|
48
|
+
.columns()[0]
|
|
49
|
+
.columns.map(col => ({
|
|
50
|
+
key: col.key as string,
|
|
51
|
+
title: col.title,
|
|
52
|
+
groupTitle: "GC",
|
|
53
|
+
extract: (r: CaseResult) =>
|
|
54
|
+
gcStatsSection.extract(r.measured)[col.key as keyof GcStatsInfo],
|
|
55
|
+
formatter: (v: unknown) => col.formatter?.(v) ?? "-",
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
/** GC pause time column */
|
|
59
|
+
export const gcPauseColumn: ExtraColumn = {
|
|
60
|
+
key: "gcPause",
|
|
61
|
+
title: "pause",
|
|
62
|
+
groupTitle: "GC",
|
|
63
|
+
extract: r => r.measured.gcStats?.gcPauseTime,
|
|
64
|
+
formatter: v => (v != null ? `${(v as number).toFixed(1)}ms` : "-"),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** Heap sampling total bytes column */
|
|
68
|
+
export const heapTotalColumn: ExtraColumn = {
|
|
69
|
+
key: "heapTotal",
|
|
70
|
+
title: "heap",
|
|
71
|
+
extract: r => {
|
|
72
|
+
const profile = r.measured.heapProfile;
|
|
73
|
+
if (!profile?.head) return undefined;
|
|
74
|
+
return totalProfileBytes(profile);
|
|
75
|
+
},
|
|
76
|
+
formatter: formatBytesOrDash,
|
|
77
|
+
};
|
|
78
|
+
|
|
46
79
|
/** Format matrix results as one table per case */
|
|
47
80
|
export function reportMatrixResults(
|
|
48
81
|
results: MatrixResults,
|
|
@@ -53,6 +86,11 @@ export function reportMatrixResults(
|
|
|
53
86
|
return [header, ...tables].join("\n\n");
|
|
54
87
|
}
|
|
55
88
|
|
|
89
|
+
/** Format bytes with fallback to "-" for missing values */
|
|
90
|
+
function formatBytesOrDash(value: unknown): string {
|
|
91
|
+
return formatBytes(value) ?? "-";
|
|
92
|
+
}
|
|
93
|
+
|
|
56
94
|
/** Build one table for each case showing all variants */
|
|
57
95
|
function buildCaseTables(
|
|
58
96
|
results: MatrixResults,
|
|
@@ -86,6 +124,20 @@ function buildCaseTable(
|
|
|
86
124
|
return `${caseTitle}\n${table}`;
|
|
87
125
|
}
|
|
88
126
|
|
|
127
|
+
/** Format case title with metadata if available */
|
|
128
|
+
function formatCaseTitle(results: MatrixResults, caseId: string): string {
|
|
129
|
+
const caseResult = results.variants[0]?.cases.find(c => c.caseId === caseId);
|
|
130
|
+
const metadata = caseResult?.metadata;
|
|
131
|
+
|
|
132
|
+
if (metadata && Object.keys(metadata).length > 0) {
|
|
133
|
+
const metaParts = Object.entries(metadata)
|
|
134
|
+
.map(([k, v]) => `${v} ${k}`)
|
|
135
|
+
.join(", ");
|
|
136
|
+
return `${caseId} (${metaParts})`;
|
|
137
|
+
}
|
|
138
|
+
return caseId;
|
|
139
|
+
}
|
|
140
|
+
|
|
89
141
|
/** Build table using ResultsMapper sections */
|
|
90
142
|
function buildSectionTable(
|
|
91
143
|
results: MatrixResults,
|
|
@@ -127,24 +179,6 @@ function buildSectionTable(
|
|
|
127
179
|
return `${caseTitle}\n${table}`;
|
|
128
180
|
}
|
|
129
181
|
|
|
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
182
|
/** Build rows for all variants for a given case */
|
|
149
183
|
function buildCaseRows(
|
|
150
184
|
results: MatrixResults,
|
|
@@ -157,35 +191,6 @@ function buildCaseRows(
|
|
|
157
191
|
});
|
|
158
192
|
}
|
|
159
193
|
|
|
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
194
|
/** Build column configuration */
|
|
190
195
|
function buildColumns(
|
|
191
196
|
hasBaseline: boolean,
|
|
@@ -231,60 +236,55 @@ function buildColumns(
|
|
|
231
236
|
return groups;
|
|
232
237
|
}
|
|
233
238
|
|
|
234
|
-
/**
|
|
235
|
-
function
|
|
236
|
-
|
|
237
|
-
|
|
239
|
+
/** Build column groups from ResultsMapper sections */
|
|
240
|
+
function buildSectionColumns(
|
|
241
|
+
sections: ResultsMapper[],
|
|
242
|
+
variantTitle: string,
|
|
243
|
+
hasBaseline: boolean,
|
|
244
|
+
): ColumnGroup<Record<string, unknown>>[] {
|
|
245
|
+
const nameCol: ColumnGroup<Record<string, unknown>> = {
|
|
246
|
+
columns: [{ key: "name", title: variantTitle }],
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const sectionColumns = sections.flatMap(s => s.columns());
|
|
250
|
+
const columnGroups = hasBaseline
|
|
251
|
+
? injectDiffColumns(sectionColumns)
|
|
252
|
+
: (sectionColumns as ColumnGroup<Record<string, unknown>>[]);
|
|
253
|
+
|
|
254
|
+
return [nameCol, ...columnGroups];
|
|
238
255
|
}
|
|
239
256
|
|
|
240
|
-
/**
|
|
241
|
-
function
|
|
242
|
-
|
|
243
|
-
|
|
257
|
+
/** Build a single row from case result */
|
|
258
|
+
function buildRow(
|
|
259
|
+
variantId: string,
|
|
260
|
+
caseResult: CaseResult,
|
|
261
|
+
extraColumns?: ExtraColumn[],
|
|
262
|
+
): MatrixReportRow {
|
|
263
|
+
const { measured, baseline } = caseResult;
|
|
264
|
+
const samples = measured.samples;
|
|
265
|
+
const time = measured.time?.avg ?? average(samples);
|
|
244
266
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
267
|
+
const row: MatrixReportRow = {
|
|
268
|
+
name: truncate(variantId, 25),
|
|
269
|
+
time,
|
|
270
|
+
samples: samples.length,
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
if (baseline) {
|
|
274
|
+
row.diffCI = bootstrapDifferenceCI(baseline.samples, samples);
|
|
250
275
|
}
|
|
251
|
-
return caseId;
|
|
252
|
-
}
|
|
253
276
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
}));
|
|
277
|
+
if (extraColumns) {
|
|
278
|
+
for (const col of extraColumns) {
|
|
279
|
+
row[col.key] = col.extract(caseResult);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
265
282
|
|
|
266
|
-
|
|
267
|
-
function formatBytesOrDash(value: unknown): string {
|
|
268
|
-
return formatBytes(value) ?? "-";
|
|
283
|
+
return row;
|
|
269
284
|
}
|
|
270
285
|
|
|
271
|
-
/**
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
};
|
|
286
|
+
/** Format diff with CI, or "baseline" marker */
|
|
287
|
+
function formatDiff(value: unknown): string | null {
|
|
288
|
+
if (!value) return null;
|
|
289
|
+
return formatDiffWithCI(value as DifferenceCI);
|
|
290
|
+
}
|
|
@@ -22,6 +22,11 @@ export async function loadVariant<T = unknown>(
|
|
|
22
22
|
return extractVariant(module, variantId, moduleUrl);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/** Get module URL for a variant in a directory */
|
|
26
|
+
export function variantModuleUrl(dirUrl: string, variantId: string): string {
|
|
27
|
+
return new URL(`${variantId}.ts`, dirUrl).href;
|
|
28
|
+
}
|
|
29
|
+
|
|
25
30
|
/** Extract variant from module exports */
|
|
26
31
|
function extractVariant<T>(
|
|
27
32
|
module: Record<string, unknown>,
|
|
@@ -39,8 +44,3 @@ function extractVariant<T>(
|
|
|
39
44
|
}
|
|
40
45
|
return { setup: setup as (data: T) => unknown, run: run as () => void };
|
|
41
46
|
}
|
|
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
|
-
}
|