benchforge 0.1.8 → 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.
Files changed (68) hide show
  1. package/README.md +69 -42
  2. package/dist/{BenchRunner-CSKN9zPy.d.mts → BenchRunner-BzyUfiyB.d.mts} +32 -8
  3. package/dist/{BrowserHeapSampler-DCeL42RE.mjs → BrowserHeapSampler-B6asLKWQ.mjs} +57 -57
  4. package/dist/BrowserHeapSampler-B6asLKWQ.mjs.map +1 -0
  5. package/dist/{GcStats-ByEovUi1.mjs → GcStats-wX7Xyblu.mjs} +15 -15
  6. package/dist/GcStats-wX7Xyblu.mjs.map +1 -0
  7. package/dist/HeapSampler-B8dtKHn1.mjs.map +1 -1
  8. package/dist/{TimingUtils-ClclVQ7E.mjs → TimingUtils-DwOwkc8G.mjs} +225 -225
  9. package/dist/TimingUtils-DwOwkc8G.mjs.map +1 -0
  10. package/dist/bin/benchforge.mjs +1 -1
  11. package/dist/browser/index.js +210 -210
  12. package/dist/index.d.mts +106 -48
  13. package/dist/index.mjs +3 -3
  14. package/dist/runners/WorkerScript.d.mts +1 -1
  15. package/dist/runners/WorkerScript.mjs +66 -66
  16. package/dist/runners/WorkerScript.mjs.map +1 -1
  17. package/dist/{src-HfimYuW_.mjs → src-B-DDaCa9.mjs} +1250 -991
  18. package/dist/src-B-DDaCa9.mjs.map +1 -0
  19. package/package.json +4 -3
  20. package/src/BenchMatrix.ts +125 -125
  21. package/src/BenchmarkReport.ts +50 -45
  22. package/src/HtmlDataPrep.ts +21 -21
  23. package/src/PermutationTest.ts +24 -24
  24. package/src/StandardSections.ts +45 -45
  25. package/src/StatisticalUtils.ts +60 -61
  26. package/src/browser/BrowserGcStats.ts +5 -5
  27. package/src/browser/BrowserHeapSampler.ts +63 -63
  28. package/src/cli/CliArgs.ts +20 -6
  29. package/src/cli/FilterBenchmarks.ts +5 -5
  30. package/src/cli/RunBenchCLI.ts +533 -476
  31. package/src/export/JsonExport.ts +10 -10
  32. package/src/export/PerfettoExport.ts +74 -74
  33. package/src/export/SpeedscopeExport.ts +202 -0
  34. package/src/heap-sample/HeapSampleReport.ts +143 -70
  35. package/src/heap-sample/HeapSampler.ts +55 -12
  36. package/src/heap-sample/ResolvedProfile.ts +89 -0
  37. package/src/html/HtmlReport.ts +33 -33
  38. package/src/html/HtmlTemplate.ts +67 -67
  39. package/src/html/browser/CIPlot.ts +50 -50
  40. package/src/html/browser/HistogramKde.ts +13 -13
  41. package/src/html/browser/LegendUtils.ts +48 -48
  42. package/src/html/browser/RenderPlots.ts +98 -98
  43. package/src/html/browser/SampleTimeSeries.ts +79 -79
  44. package/src/index.ts +6 -0
  45. package/src/matrix/MatrixFilter.ts +6 -6
  46. package/src/matrix/MatrixReport.ts +96 -96
  47. package/src/matrix/VariantLoader.ts +5 -5
  48. package/src/runners/AdaptiveWrapper.ts +151 -151
  49. package/src/runners/BasicRunner.ts +175 -175
  50. package/src/runners/BenchRunner.ts +8 -8
  51. package/src/runners/GcStats.ts +22 -22
  52. package/src/runners/RunnerOrchestrator.ts +168 -168
  53. package/src/runners/WorkerScript.ts +96 -96
  54. package/src/table-util/Formatters.ts +41 -36
  55. package/src/table-util/TableReport.ts +122 -122
  56. package/src/table-util/test/TableValueExtractor.ts +9 -9
  57. package/src/test/AdaptiveStatistics.integration.ts +7 -39
  58. package/src/test/HeapAttribution.test.ts +51 -0
  59. package/src/test/RunBenchCLI.test.ts +36 -11
  60. package/src/test/TestUtils.ts +24 -24
  61. package/src/test/fixtures/fn-export-bench.ts +3 -0
  62. package/src/test/fixtures/suite-export-bench.ts +16 -0
  63. package/src/tests/BenchMatrix.test.ts +12 -12
  64. package/src/tests/MatrixFilter.test.ts +15 -15
  65. package/dist/BrowserHeapSampler-DCeL42RE.mjs.map +0 -1
  66. package/dist/GcStats-ByEovUi1.mjs.map +0 -1
  67. package/dist/TimingUtils-ClclVQ7E.mjs.map +0 -1
  68. package/dist/src-HfimYuW_.mjs.map +0 -1
@@ -74,23 +74,6 @@ export function diffPercentBenchmark(main: unknown, base: unknown): string {
74
74
  return coloredPercent(diff, base, false); // negative is good for benchmarks
75
75
  }
76
76
 
77
- /** Format fraction as colored +/- percentage */
78
- function coloredPercent(
79
- numerator: number,
80
- denominator: number,
81
- positiveIsGreen = true,
82
- ): string {
83
- const fraction = numerator / denominator;
84
- if (Number.isNaN(fraction) || !Number.isFinite(fraction)) {
85
- return " ";
86
- }
87
- const positive = fraction >= 0;
88
- const sign = positive ? "+" : "-";
89
- const percentStr = `${sign}${percent(fraction)}`;
90
- const isGood = positive === positiveIsGreen;
91
- return isGood ? green(percentStr) : red(percentStr);
92
- }
93
-
94
77
  /** Format memory size in KB with appropriate units */
95
78
  export function memoryKB(kb: unknown): string | null {
96
79
  if (typeof kb !== "number") return null;
@@ -98,14 +81,19 @@ export function memoryKB(kb: unknown): string | null {
98
81
  return `${(kb / 1024).toFixed(1)}MB`;
99
82
  }
100
83
 
101
- /** Format bytes with appropriate units (B, KB, MB, GB) */
102
- export function formatBytes(bytes: unknown): string | null {
84
+ /** Format bytes with appropriate units (B, KB, MB, GB).
85
+ * Use `space: true` for human-readable console output (`1.5 KB`). */
86
+ export function formatBytes(
87
+ bytes: unknown,
88
+ opts?: { space?: boolean },
89
+ ): string | null {
103
90
  if (typeof bytes !== "number") return null;
104
- if (bytes < 1024) return `${bytes.toFixed(0)}B`;
105
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
91
+ const s = opts?.space ? " " : "";
92
+ if (bytes < 1024) return `${bytes.toFixed(0)}${s}B`;
93
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}${s}KB`;
106
94
  if (bytes < 1024 * 1024 * 1024)
107
- return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
108
- return `${(bytes / 1024 / 1024 / 1024).toFixed(1)}GB`;
95
+ return `${(bytes / 1024 / 1024).toFixed(1)}${s}MB`;
96
+ return `${(bytes / 1024 / 1024 / 1024).toFixed(1)}${s}GB`;
109
97
  }
110
98
 
111
99
  /** Format percentage difference with confidence interval */
@@ -123,9 +111,31 @@ export function formatDiffWithCIHigherIsBetter(value: unknown): string | null {
123
111
  return colorByDirection(diffCIText(-percent, [-ci[1], -ci[0]]), direction);
124
112
  }
125
113
 
126
- /** @return formatted "pct [lo, hi]" text for a diff with CI */
127
- function diffCIText(pct: number, ci: [number, number]): string {
128
- return `${formatBound(pct)} [${formatBound(ci[0])}, ${formatBound(ci[1])}]`;
114
+ /** @return truncated string with ellipsis if over maxLen */
115
+ export function truncate(str: string, maxLen = 30): string {
116
+ return str.length > maxLen ? str.slice(0, maxLen - 3) + "..." : str;
117
+ }
118
+
119
+ /** Format fraction as colored +/- percentage */
120
+ function coloredPercent(
121
+ numerator: number,
122
+ denominator: number,
123
+ positiveIsGreen = true,
124
+ ): string {
125
+ const fraction = numerator / denominator;
126
+ if (Number.isNaN(fraction) || !Number.isFinite(fraction)) {
127
+ return " ";
128
+ }
129
+ const positive = fraction >= 0;
130
+ const sign = positive ? "+" : "-";
131
+ const percentStr = `${sign}${percent(fraction)}`;
132
+ const isGood = positive === positiveIsGreen;
133
+ return isGood ? green(percentStr) : red(percentStr);
134
+ }
135
+
136
+ /** @return true if value is a DifferenceCI object */
137
+ function isDifferenceCI(x: unknown): x is DifferenceCI {
138
+ return typeof x === "object" && x !== null && "ci" in x && "direction" in x;
129
139
  }
130
140
 
131
141
  /** @return text colored green for faster, red for slower */
@@ -135,18 +145,13 @@ function colorByDirection(text: string, direction: CIDirection): string {
135
145
  return text;
136
146
  }
137
147
 
148
+ /** @return formatted "pct [lo, hi]" text for a diff with CI */
149
+ function diffCIText(pct: number, ci: [number, number]): string {
150
+ return `${formatBound(pct)} [${formatBound(ci[0])}, ${formatBound(ci[1])}]`;
151
+ }
152
+
138
153
  /** @return signed percentage string (e.g. "+1.2%", "-3.4%") */
139
154
  function formatBound(v: number): string {
140
155
  const sign = v >= 0 ? "+" : "";
141
156
  return `${sign}${v.toFixed(1)}%`;
142
157
  }
143
-
144
- /** @return true if value is a DifferenceCI object */
145
- function isDifferenceCI(x: unknown): x is DifferenceCI {
146
- return typeof x === "object" && x !== null && "ci" in x && "direction" in x;
147
- }
148
-
149
- /** @return truncated string with ellipsis if over maxLen */
150
- export function truncate(str: string, maxLen = 30): string {
151
- return str.length > maxLen ? str.slice(0, maxLen - 3) + "..." : str;
152
- }
@@ -3,9 +3,6 @@ import type { Alignment, SpanningCellConfig, TableUserConfig } from "table";
3
3
  import { table } from "table";
4
4
  import { diffPercent } from "./Formatters.ts";
5
5
 
6
- const isTest = process.env.NODE_ENV === "test" || process.env.VITEST === "true";
7
- const { bold } = isTest ? { bold: (str: string) => str } : pico;
8
-
9
6
  /** Related table columns */
10
7
  export interface ColumnGroup<T> {
11
8
  groupTitle?: string;
@@ -20,6 +17,19 @@ export interface Column<T> extends ColumnFormat<T> {
20
17
  diffKey?: undefined;
21
18
  }
22
19
 
20
+ /** Table headers and configuration */
21
+ export interface TableSetup {
22
+ headerRows: string[][];
23
+ config: TableUserConfig;
24
+ }
25
+
26
+ /** Data rows with optional baseline */
27
+ export interface ResultGroup<T extends Record<string, any>> {
28
+ results: T[];
29
+
30
+ baseline?: T;
31
+ }
32
+
23
33
  /** Comparison column against baseline */
24
34
  interface DiffColumn<T> extends ColumnFormat<T> {
25
35
  diffFormatter?: (value: unknown, baseline: unknown) => string | null;
@@ -38,18 +48,19 @@ interface ColumnFormat<T> {
38
48
  width?: number;
39
49
  }
40
50
 
41
- /** Table headers and configuration */
42
- export interface TableSetup {
43
- headerRows: string[][];
44
- config: TableUserConfig;
51
+ interface Lines {
52
+ drawHorizontalLine: (index: number, size: number) => boolean;
53
+ drawVerticalLine: (index: number, size: number) => boolean;
45
54
  }
46
55
 
47
- /** Data rows with optional baseline */
48
- export interface ResultGroup<T extends Record<string, any>> {
49
- results: T[];
56
+ const isTest = process.env.NODE_ENV === "test" || process.env.VITEST === "true";
57
+ const { bold } = isTest ? { bold: (str: string) => str } : pico;
50
58
 
51
- baseline?: T;
52
- }
59
+ // Regex to strip ANSI escape codes (ESC [ ... m sequences)
60
+ const ansiEscapeRegex = new RegExp(
61
+ String.fromCharCode(27) + "\\[[0-9;]*m",
62
+ "g",
63
+ );
53
64
 
54
65
  /** Build formatted table with column groups and baselines */
55
66
  export function buildTable<T extends Record<string, any>>(
@@ -61,73 +72,6 @@ export function buildTable<T extends Record<string, any>>(
61
72
  return createTable(columnGroups, allRecords);
62
73
  }
63
74
 
64
- /** Convert columns and records to formatted table */
65
- function createTable<T extends Record<string, any>>(
66
- groups: ColumnGroup<T>[],
67
- records: T[],
68
- ): string {
69
- const dataRows = toRows(records, groups);
70
- const { headerRows, config } = setup(groups, dataRows);
71
- const allRows = [...headerRows, ...dataRows];
72
- return table(allRows, config);
73
- }
74
-
75
- /** Create header rows with group titles */
76
- function createGroupHeaders<T>(
77
- groups: ColumnGroup<T>[],
78
- numColumns: number,
79
- ): string[][] {
80
- if (!groups.some(g => g.groupTitle)) return [];
81
-
82
- const sectionRow = groups.flatMap(g => {
83
- const title = g.groupTitle ? [bold(g.groupTitle)] : [];
84
- return padWithBlanks(title, g.columns.length);
85
- });
86
- const blankRow = padWithBlanks([], numColumns);
87
- return [sectionRow, blankRow];
88
- }
89
-
90
- interface Lines {
91
- drawHorizontalLine: (index: number, size: number) => boolean;
92
- drawVerticalLine: (index: number, size: number) => boolean;
93
- }
94
-
95
- /** @return draw functions for horizontal/vertical table borders */
96
- function createLines<T>(groups: ColumnGroup<T>[]): Lines {
97
- const { sectionBorders, headerBottom } = calcBorders(groups);
98
-
99
- function drawVerticalLine(index: number, size: number): boolean {
100
- return index === 0 || index === size || sectionBorders.includes(index);
101
- }
102
- function drawHorizontalLine(index: number, size: number): boolean {
103
- return index === 0 || index === size || index === headerBottom;
104
- }
105
- return { drawHorizontalLine, drawVerticalLine };
106
- }
107
-
108
- /** @return spanning cell configs for group title headers */
109
- function createSectionSpans<T>(groups: ColumnGroup<T>[]): SpanningCellConfig[] {
110
- let col = 0;
111
- const alignment: Alignment = "center";
112
- return groups.map(g => {
113
- const colSpan = g.columns.length;
114
- const span = { row: 0, col, colSpan, alignment };
115
- col += colSpan;
116
- return span;
117
- });
118
- }
119
-
120
- /** @return bolded column title strings */
121
- function getTitles<T>(groups: ColumnGroup<T>[]): string[] {
122
- return groups.flatMap(g => g.columns.map(c => bold(c.title || " ")));
123
- }
124
-
125
- /** @return array padded with blank strings to the given length */
126
- function padWithBlanks(arr: string[], length: number): string[] {
127
- if (arr.length >= length) return arr;
128
- return [...arr, ...Array(length - arr.length).fill(" ")];
129
- }
130
-
131
75
  /** Convert records to string arrays for table */
132
76
  export function toRows<T extends Record<string, any>>(
133
77
  records: T[],
@@ -145,28 +89,6 @@ export function toRows<T extends Record<string, any>>(
145
89
  return rawRows.map(row => row.map(cell => cell ?? " "));
146
90
  }
147
91
 
148
- /** Add comparison values for diff columns */
149
- function addComparisons<T extends Record<string, any>>(
150
- groups: ColumnGroup<T>[],
151
- mainRecord: T,
152
- baselineRecord: T,
153
- ): T {
154
- const diffColumns = groups.flatMap(g => g.columns).filter(col => col.diffKey);
155
- const updatedMain = { ...mainRecord };
156
-
157
- for (const col of diffColumns) {
158
- const dcol = col as DiffColumn<T>;
159
- const diffKey = dcol.diffKey;
160
- const mainValue = mainRecord[diffKey];
161
- const baselineValue = baselineRecord[diffKey];
162
- const diffFormat = dcol.diffFormatter ?? diffPercent;
163
- const diffStr = diffFormat(mainValue, baselineValue);
164
- (updatedMain as any)[col.key] = diffStr;
165
- }
166
-
167
- return updatedMain;
168
- }
169
-
170
92
  /** Flatten groups with spacing */
171
93
  function flattenGroups<T extends Record<string, any>>(
172
94
  columnGroups: ColumnGroup<T>[],
@@ -181,6 +103,17 @@ function flattenGroups<T extends Record<string, any>>(
181
103
  });
182
104
  }
183
105
 
106
+ /** Convert columns and records to formatted table */
107
+ function createTable<T extends Record<string, any>>(
108
+ groups: ColumnGroup<T>[],
109
+ records: T[],
110
+ ): string {
111
+ const dataRows = toRows(records, groups);
112
+ const { headerRows, config } = setup(groups, dataRows);
113
+ const allRows = [...headerRows, ...dataRows];
114
+ return table(allRows, config);
115
+ }
116
+
184
117
  /** Process results with baseline comparisons */
185
118
  function addBaseline<T extends Record<string, any>>(
186
119
  columnGroups: ColumnGroup<T>[],
@@ -203,22 +136,6 @@ function addBaseline<T extends Record<string, any>>(
203
136
  return [...diffResults, markedBaseline];
204
137
  }
205
138
 
206
- /** Calculate vertical lines between sections and header bottom position */
207
- function calcBorders<T>(groups: ColumnGroup<T>[]): {
208
- sectionBorders: number[];
209
- headerBottom: number;
210
- } {
211
- if (groups.length === 0) return { sectionBorders: [], headerBottom: 1 };
212
-
213
- const sectionBorders: number[] = [];
214
- let border = 0;
215
- for (const g of groups) {
216
- border += g.columns.length;
217
- sectionBorders.push(border);
218
- }
219
- return { sectionBorders, headerBottom: 3 };
220
- }
221
-
222
139
  /** Create headers and table configuration */
223
140
  function setup<T>(groups: ColumnGroup<T>[], dataRows: string[][]): TableSetup {
224
141
  const titles = getTitles(groups);
@@ -237,6 +154,60 @@ function setup<T>(groups: ColumnGroup<T>[], dataRows: string[][]): TableSetup {
237
154
  return { headerRows, config };
238
155
  }
239
156
 
157
+ /** Add comparison values for diff columns */
158
+ function addComparisons<T extends Record<string, any>>(
159
+ groups: ColumnGroup<T>[],
160
+ mainRecord: T,
161
+ baselineRecord: T,
162
+ ): T {
163
+ const diffColumns = groups.flatMap(g => g.columns).filter(col => col.diffKey);
164
+ const updatedMain = { ...mainRecord };
165
+
166
+ for (const col of diffColumns) {
167
+ const dcol = col as DiffColumn<T>;
168
+ const diffKey = dcol.diffKey;
169
+ const mainValue = mainRecord[diffKey];
170
+ const baselineValue = baselineRecord[diffKey];
171
+ const diffFormat = dcol.diffFormatter ?? diffPercent;
172
+ const diffStr = diffFormat(mainValue, baselineValue);
173
+ (updatedMain as any)[col.key] = diffStr;
174
+ }
175
+
176
+ return updatedMain;
177
+ }
178
+
179
+ /** @return bolded column title strings */
180
+ function getTitles<T>(groups: ColumnGroup<T>[]): string[] {
181
+ return groups.flatMap(g => g.columns.map(c => bold(c.title || " ")));
182
+ }
183
+
184
+ /** Create header rows with group titles */
185
+ function createGroupHeaders<T>(
186
+ groups: ColumnGroup<T>[],
187
+ numColumns: number,
188
+ ): string[][] {
189
+ if (!groups.some(g => g.groupTitle)) return [];
190
+
191
+ const sectionRow = groups.flatMap(g => {
192
+ const title = g.groupTitle ? [bold(g.groupTitle)] : [];
193
+ return padWithBlanks(title, g.columns.length);
194
+ });
195
+ const blankRow = padWithBlanks([], numColumns);
196
+ return [sectionRow, blankRow];
197
+ }
198
+
199
+ /** @return spanning cell configs for group title headers */
200
+ function createSectionSpans<T>(groups: ColumnGroup<T>[]): SpanningCellConfig[] {
201
+ let col = 0;
202
+ const alignment: Alignment = "center";
203
+ return groups.map(g => {
204
+ const colSpan = g.columns.length;
205
+ const span = { row: 0, col, colSpan, alignment };
206
+ col += colSpan;
207
+ return span;
208
+ });
209
+ }
210
+
240
211
  /** Calculate column widths based on content, including group titles */
241
212
  function calcColumnWidths<T>(
242
213
  groups: ColumnGroup<T>[],
@@ -279,11 +250,24 @@ function calcColumnWidths<T>(
279
250
  );
280
251
  }
281
252
 
282
- // Regex to strip ANSI escape codes (ESC [ ... m sequences)
283
- const ansiEscapeRegex = new RegExp(
284
- String.fromCharCode(27) + "\\[[0-9;]*m",
285
- "g",
286
- );
253
+ /** @return draw functions for horizontal/vertical table borders */
254
+ function createLines<T>(groups: ColumnGroup<T>[]): Lines {
255
+ const { sectionBorders, headerBottom } = calcBorders(groups);
256
+
257
+ function drawVerticalLine(index: number, size: number): boolean {
258
+ return index === 0 || index === size || sectionBorders.includes(index);
259
+ }
260
+ function drawHorizontalLine(index: number, size: number): boolean {
261
+ return index === 0 || index === size || index === headerBottom;
262
+ }
263
+ return { drawHorizontalLine, drawVerticalLine };
264
+ }
265
+
266
+ /** @return array padded with blank strings to the given length */
267
+ function padWithBlanks(arr: string[], length: number): string[] {
268
+ if (arr.length >= length) return arr;
269
+ return [...arr, ...Array(length - arr.length).fill(" ")];
270
+ }
287
271
 
288
272
  /** Get visible length of a cell value (strips ANSI escape codes) */
289
273
  function cellWidth(value: unknown): number {
@@ -291,3 +275,19 @@ function cellWidth(value: unknown): number {
291
275
  const str = String(value);
292
276
  return str.replace(ansiEscapeRegex, "").length;
293
277
  }
278
+
279
+ /** Calculate vertical lines between sections and header bottom position */
280
+ function calcBorders<T>(groups: ColumnGroup<T>[]): {
281
+ sectionBorders: number[];
282
+ headerBottom: number;
283
+ } {
284
+ if (groups.length === 0) return { sectionBorders: [], headerBottom: 1 };
285
+
286
+ const sectionBorders: number[] = [];
287
+ let border = 0;
288
+ for (const g of groups) {
289
+ border += g.columns.length;
290
+ sectionBorders.push(border);
291
+ }
292
+ return { sectionBorders, headerBottom: 3 };
293
+ }
@@ -55,15 +55,6 @@ function getGroupColumnIndex(
55
55
  );
56
56
  }
57
57
 
58
- /** Count total columns in groups before the target group index. */
59
- function countColumnsBeforeGroup(
60
- headers: string[],
61
- groupIndex: number,
62
- ): number {
63
- const perGroup = headers.map(col => splitColumns(col).length);
64
- return perGroup.slice(0, groupIndex).reduce((sum, n) => sum + n, 0);
65
- }
66
-
67
58
  /** Find a column's position index in a header line. */
68
59
  function getColumnIndex(
69
60
  header: string | undefined,
@@ -91,6 +82,15 @@ function splitColumnGroups(line: string): string[] {
91
82
  return line.split("│").map(col => col.trim());
92
83
  }
93
84
 
85
+ /** Count total columns in groups before the target group index. */
86
+ function countColumnsBeforeGroup(
87
+ headers: string[],
88
+ groupIndex: number,
89
+ ): number {
90
+ const perGroup = headers.map(col => splitColumns(col).length);
91
+ return perGroup.slice(0, groupIndex).reduce((sum, n) => sum + n, 0);
92
+ }
93
+
94
94
  /** Split on 2+ whitespace or '│' borders, so single-space titles like "L1 miss" survive. */
95
95
  function splitColumns(line: string): string[] {
96
96
  return line
@@ -1,9 +1,9 @@
1
- import { expect, test } from "vitest";
1
+ import { expect } from "vitest";
2
2
  import type { BenchSuite } from "../Benchmark.ts";
3
3
  import type { BenchmarkReport } from "../BenchmarkReport.ts";
4
- import { parseBenchArgs, runBenchmarks } from "../cli/RunBenchCLI.ts";
4
+ import { parseBenchArgs } from "../cli/RunBenchCLI.ts";
5
5
 
6
- const statisticalSuite: BenchSuite = {
6
+ const _statisticalSuite: BenchSuite = {
7
7
  name: "Statistical Test Suite",
8
8
  groups: [
9
9
  {
@@ -31,7 +31,7 @@ const statisticalSuite: BenchSuite = {
31
31
  ],
32
32
  };
33
33
 
34
- const gcSuite: BenchSuite = {
34
+ const _gcSuite: BenchSuite = {
35
35
  name: "GC Test Suite",
36
36
  groups: [
37
37
  {
@@ -52,7 +52,7 @@ const gcSuite: BenchSuite = {
52
52
  ],
53
53
  };
54
54
 
55
- function parseAdaptiveArgs() {
55
+ function _parseAdaptiveArgs() {
56
56
  return parseBenchArgs((yargs: any) =>
57
57
  yargs
58
58
  .option("adaptive", { type: "boolean", default: true })
@@ -61,7 +61,7 @@ function parseAdaptiveArgs() {
61
61
  );
62
62
  }
63
63
 
64
- function verifyStatisticalMetrics(report: BenchmarkReport): void {
64
+ function _verifyStatisticalMetrics(report: BenchmarkReport): void {
65
65
  const { time, convergence } = report.measuredResults;
66
66
  expect(time).toBeDefined();
67
67
  expect(time?.p25).toBeDefined();
@@ -77,7 +77,7 @@ function verifyStatisticalMetrics(report: BenchmarkReport): void {
77
77
  expect(convergence?.reason).toBeDefined();
78
78
  }
79
79
 
80
- function verifyPercentileOrdering(report: BenchmarkReport): void {
80
+ function _verifyPercentileOrdering(report: BenchmarkReport): void {
81
81
  const t = report.measuredResults.time;
82
82
  if (t?.p25 && t?.p50 && t?.p75 && t?.p95) {
83
83
  expect(t.p25).toBeLessThanOrEqual(t.p50);
@@ -85,35 +85,3 @@ function verifyPercentileOrdering(report: BenchmarkReport): void {
85
85
  expect(t.p75).toBeLessThanOrEqual(t.p95);
86
86
  }
87
87
  }
88
-
89
- test("adaptive mode reports statistical metrics correctly", async () => {
90
- const results = await runBenchmarks(statisticalSuite, parseAdaptiveArgs());
91
- expect(results).toHaveLength(1);
92
- expect(results[0].reports).toHaveLength(2);
93
-
94
- for (const report of results[0].reports) {
95
- verifyStatisticalMetrics(report);
96
- verifyPercentileOrdering(report);
97
- }
98
-
99
- const reports = results[0].reports;
100
- const stableCV = reports.find(r => r.name === "stable-benchmark")
101
- ?.measuredResults.time?.cv;
102
- const variableCV = reports.find(r => r.name === "variable-benchmark")
103
- ?.measuredResults.time?.cv;
104
- if (stableCV && variableCV) {
105
- expect(variableCV).toBeGreaterThanOrEqual(stableCV);
106
- }
107
- }, 20000);
108
-
109
- test("adaptive mode handles GC-heavy workload", async () => {
110
- const results = await runBenchmarks(gcSuite, parseAdaptiveArgs());
111
- expect(results).toHaveLength(1);
112
- expect(results[0].reports).toHaveLength(1);
113
-
114
- const gcResult = results[0].reports[0].measuredResults;
115
- expect(gcResult.convergence).toBeDefined();
116
- expect(gcResult.time?.outlierRate).toBeDefined();
117
- expect(gcResult.time?.cv).toBeDefined();
118
- expect(gcResult.time?.mad).toBeGreaterThanOrEqual(0);
119
- }, 20000);
@@ -0,0 +1,51 @@
1
+ import { expect, test } from "vitest";
2
+ import {
3
+ aggregateSites,
4
+ type HeapSite,
5
+ } from "../heap-sample/HeapSampleReport.ts";
6
+
7
+ test("unknown column does not merge distinct functions on same line", () => {
8
+ const sites: HeapSite[] = [
9
+ { fn: "Foo", url: "test.ts", line: 10, col: undefined, bytes: 100 },
10
+ { fn: "Bar", url: "test.ts", line: 10, col: undefined, bytes: 200 },
11
+ ];
12
+ const aggregated = aggregateSites(sites);
13
+ expect(aggregated).toHaveLength(2);
14
+ });
15
+
16
+ test("same column merges regardless of function name", () => {
17
+ const sites: HeapSite[] = [
18
+ { fn: "Foo", url: "test.ts", line: 10, col: 5, bytes: 100 },
19
+ { fn: "Foo", url: "test.ts", line: 10, col: 5, bytes: 200 },
20
+ ];
21
+ const aggregated = aggregateSites(sites);
22
+ expect(aggregated).toHaveLength(1);
23
+ expect(aggregated[0].bytes).toBe(300);
24
+ });
25
+
26
+ test("aggregation preserves distinct caller stacks", () => {
27
+ const stackA = [
28
+ { fn: "root", url: "a.ts", line: 1, col: 0 },
29
+ { fn: "foo", url: "a.ts", line: 10, col: 0 },
30
+ { fn: "alloc", url: "a.ts", line: 20, col: 5 },
31
+ ];
32
+ const stackB = [
33
+ { fn: "root", url: "a.ts", line: 1, col: 0 },
34
+ { fn: "bar", url: "a.ts", line: 15, col: 0 },
35
+ { fn: "alloc", url: "a.ts", line: 20, col: 5 },
36
+ ];
37
+ const sites: HeapSite[] = [
38
+ { fn: "alloc", url: "a.ts", line: 20, col: 5, bytes: 800, stack: stackA },
39
+ { fn: "alloc", url: "a.ts", line: 20, col: 5, bytes: 200, stack: stackB },
40
+ ];
41
+ const aggregated = aggregateSites(sites);
42
+
43
+ expect(aggregated).toHaveLength(1);
44
+ expect(aggregated[0].bytes).toBe(1000);
45
+ expect(aggregated[0].callers).toHaveLength(2);
46
+ // Primary stack should be the highest-bytes path (foo)
47
+ expect(aggregated[0].stack![1].fn).toBe("foo");
48
+ // Callers sorted by bytes descending
49
+ expect(aggregated[0].callers![0].bytes).toBe(800);
50
+ expect(aggregated[0].callers![1].bytes).toBe(200);
51
+ });
@@ -49,6 +49,24 @@ const suiteWithSetup: BenchSuite = {
49
49
  ],
50
50
  };
51
51
 
52
+ /** Execute test fixture script and return output */
53
+ function executeTestScript(args = ""): string {
54
+ const script = path.join(
55
+ import.meta.dirname!,
56
+ "fixtures/test-bench-script.ts",
57
+ );
58
+ return execSync(`node --expose-gc --allow-natives-syntax ${script} ${args}`, {
59
+ encoding: "utf8",
60
+ });
61
+ }
62
+
63
+ /** Run a fixture file via bin/benchforge and return output */
64
+ function executeBenchforgeFile(file: string, args = ""): string {
65
+ const bin = path.join(import.meta.dirname!, "../../bin/benchforge");
66
+ const fixture = path.join(import.meta.dirname!, "fixtures", file);
67
+ return execSync(`${bin} ${fixture} ${args}`, { encoding: "utf8" });
68
+ }
69
+
52
70
  test("runs all benchmarks", { timeout: 30000 }, async () => {
53
71
  const output = await runBenchCLITest(testSuite, "--time 0.1");
54
72
 
@@ -87,17 +105,6 @@ test("filter preserves suite structure", () => {
87
105
  expect(filtered.groups[1].benchmarks).toHaveLength(0);
88
106
  });
89
107
 
90
- /** Execute test fixture script and return output */
91
- function executeTestScript(args = ""): string {
92
- const script = path.join(
93
- import.meta.dirname!,
94
- "fixtures/test-bench-script.ts",
95
- );
96
- return execSync(`node --expose-gc --allow-natives-syntax ${script} ${args}`, {
97
- encoding: "utf8",
98
- });
99
- }
100
-
101
108
  test("e2e: runs user script", { timeout: 30000 }, () => {
102
109
  const output = executeTestScript("--time 0.1");
103
110
 
@@ -164,3 +171,21 @@ test(
164
171
  expect(output).toContain("mean");
165
172
  },
166
173
  );
174
+
175
+ test("file mode: BenchSuite export", { timeout: 30000 }, () => {
176
+ const output = executeBenchforgeFile(
177
+ "suite-export-bench.ts",
178
+ "--iterations 5",
179
+ );
180
+
181
+ expect(output).toContain("plus");
182
+ expect(output).toContain("multiply");
183
+ expect(output).toContain("runs");
184
+ });
185
+
186
+ test("file mode: function export", { timeout: 30000 }, () => {
187
+ const output = executeBenchforgeFile("fn-export-bench.ts", "--iterations 5");
188
+
189
+ expect(output).toContain("fn-export-bench");
190
+ expect(output).toContain("runs");
191
+ });