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.
Files changed (98) hide show
  1. package/README.md +432 -0
  2. package/bin/benchforge +3 -0
  3. package/dist/bin/benchforge.mjs +9 -0
  4. package/dist/bin/benchforge.mjs.map +1 -0
  5. package/dist/browser/index.js +914 -0
  6. package/dist/index.mjs +3 -0
  7. package/dist/src-CGuaC3Wo.mjs +3676 -0
  8. package/dist/src-CGuaC3Wo.mjs.map +1 -0
  9. package/package.json +49 -0
  10. package/src/BenchMatrix.ts +380 -0
  11. package/src/Benchmark.ts +33 -0
  12. package/src/BenchmarkReport.ts +156 -0
  13. package/src/GitUtils.ts +79 -0
  14. package/src/HtmlDataPrep.ts +148 -0
  15. package/src/MeasuredResults.ts +127 -0
  16. package/src/NodeGC.ts +48 -0
  17. package/src/PermutationTest.ts +115 -0
  18. package/src/StandardSections.ts +268 -0
  19. package/src/StatisticalUtils.ts +176 -0
  20. package/src/TypeUtil.ts +8 -0
  21. package/src/bin/benchforge.ts +4 -0
  22. package/src/browser/BrowserGcStats.ts +44 -0
  23. package/src/browser/BrowserHeapSampler.ts +248 -0
  24. package/src/cli/CliArgs.ts +64 -0
  25. package/src/cli/FilterBenchmarks.ts +68 -0
  26. package/src/cli/RunBenchCLI.ts +856 -0
  27. package/src/export/JsonExport.ts +103 -0
  28. package/src/export/JsonFormat.ts +91 -0
  29. package/src/export/PerfettoExport.ts +203 -0
  30. package/src/heap-sample/HeapSampleReport.ts +196 -0
  31. package/src/heap-sample/HeapSampler.ts +78 -0
  32. package/src/html/HtmlReport.ts +131 -0
  33. package/src/html/HtmlTemplate.ts +284 -0
  34. package/src/html/Types.ts +88 -0
  35. package/src/html/browser/CIPlot.ts +287 -0
  36. package/src/html/browser/HistogramKde.ts +118 -0
  37. package/src/html/browser/LegendUtils.ts +163 -0
  38. package/src/html/browser/RenderPlots.ts +263 -0
  39. package/src/html/browser/SampleTimeSeries.ts +389 -0
  40. package/src/html/browser/Types.ts +96 -0
  41. package/src/html/browser/index.ts +1 -0
  42. package/src/html/index.ts +17 -0
  43. package/src/index.ts +92 -0
  44. package/src/matrix/CaseLoader.ts +36 -0
  45. package/src/matrix/MatrixFilter.ts +103 -0
  46. package/src/matrix/MatrixReport.ts +290 -0
  47. package/src/matrix/VariantLoader.ts +46 -0
  48. package/src/runners/AdaptiveWrapper.ts +391 -0
  49. package/src/runners/BasicRunner.ts +368 -0
  50. package/src/runners/BenchRunner.ts +60 -0
  51. package/src/runners/CreateRunner.ts +11 -0
  52. package/src/runners/GcStats.ts +107 -0
  53. package/src/runners/RunnerOrchestrator.ts +374 -0
  54. package/src/runners/RunnerUtils.ts +2 -0
  55. package/src/runners/TimingUtils.ts +13 -0
  56. package/src/runners/WorkerScript.ts +256 -0
  57. package/src/table-util/ConvergenceFormatters.ts +19 -0
  58. package/src/table-util/Formatters.ts +152 -0
  59. package/src/table-util/README.md +70 -0
  60. package/src/table-util/TableReport.ts +293 -0
  61. package/src/table-util/test/TableReport.test.ts +105 -0
  62. package/src/table-util/test/TableValueExtractor.test.ts +41 -0
  63. package/src/table-util/test/TableValueExtractor.ts +100 -0
  64. package/src/test/AdaptiveRunner.test.ts +185 -0
  65. package/src/test/AdaptiveStatistics.integration.ts +119 -0
  66. package/src/test/BenchmarkReport.test.ts +82 -0
  67. package/src/test/BrowserBench.e2e.test.ts +44 -0
  68. package/src/test/BrowserBench.test.ts +79 -0
  69. package/src/test/GcStats.test.ts +94 -0
  70. package/src/test/PermutationTest.test.ts +121 -0
  71. package/src/test/RunBenchCLI.test.ts +166 -0
  72. package/src/test/RunnerOrchestrator.test.ts +102 -0
  73. package/src/test/StatisticalUtils.test.ts +112 -0
  74. package/src/test/TestUtils.ts +93 -0
  75. package/src/test/fixtures/test-bench-script.ts +30 -0
  76. package/src/tests/AdaptiveConvergence.test.ts +177 -0
  77. package/src/tests/AdaptiveSampling.test.ts +240 -0
  78. package/src/tests/BenchMatrix.test.ts +366 -0
  79. package/src/tests/MatrixFilter.test.ts +117 -0
  80. package/src/tests/MatrixReport.test.ts +139 -0
  81. package/src/tests/RealDataValidation.test.ts +177 -0
  82. package/src/tests/fixtures/baseline/impl.ts +4 -0
  83. package/src/tests/fixtures/bevy30-samples.ts +158 -0
  84. package/src/tests/fixtures/cases/asyncCases.ts +7 -0
  85. package/src/tests/fixtures/cases/cases.ts +8 -0
  86. package/src/tests/fixtures/cases/variants/product.ts +2 -0
  87. package/src/tests/fixtures/cases/variants/sum.ts +2 -0
  88. package/src/tests/fixtures/discover/fast.ts +1 -0
  89. package/src/tests/fixtures/discover/slow.ts +4 -0
  90. package/src/tests/fixtures/invalid/bad.ts +1 -0
  91. package/src/tests/fixtures/loader/fast.ts +1 -0
  92. package/src/tests/fixtures/loader/slow.ts +4 -0
  93. package/src/tests/fixtures/loader/stateful.ts +2 -0
  94. package/src/tests/fixtures/stateful/stateful.ts +2 -0
  95. package/src/tests/fixtures/variants/extra.ts +1 -0
  96. package/src/tests/fixtures/variants/impl.ts +1 -0
  97. package/src/tests/fixtures/worker/fast.ts +1 -0
  98. package/src/tests/fixtures/worker/slow.ts +4 -0
@@ -0,0 +1,366 @@
1
+ import { expect, test } from "vitest";
2
+ import type { BenchMatrix, StatefulVariant } from "../BenchMatrix.ts";
3
+ import { isStatefulVariant, runMatrix } from "../BenchMatrix.ts";
4
+ import { loadCaseData, loadCasesModule } from "../matrix/CaseLoader.ts";
5
+ import { discoverVariants, loadVariant } from "../matrix/VariantLoader.ts";
6
+
7
+ test("inline variants, no cases", async () => {
8
+ const matrix: BenchMatrix = {
9
+ name: "Test",
10
+ variants: {
11
+ fast: () => {},
12
+ slow: () => {
13
+ let _x = 0;
14
+ for (let i = 0; i < 1000; i++) _x++;
15
+ },
16
+ },
17
+ };
18
+ const results = await runMatrix(matrix, { iterations: 10 });
19
+ expect(results.name).toBe("Test");
20
+ expect(results.variants).toHaveLength(2);
21
+ expect(results.variants.map(v => v.id).sort()).toEqual(["fast", "slow"]);
22
+ for (const variant of results.variants) {
23
+ expect(variant.cases).toHaveLength(1);
24
+ expect(variant.cases[0].caseId).toBe("default");
25
+ expect(variant.cases[0].measured.samples.length).toBeGreaterThan(0);
26
+ }
27
+ });
28
+
29
+ test("inline variants with cases", async () => {
30
+ const matrix: BenchMatrix<string> = {
31
+ name: "Test",
32
+ variants: {
33
+ upper: (s: string) => s.toUpperCase(),
34
+ lower: (s: string) => s.toLowerCase(),
35
+ },
36
+ cases: ["Hello", "World"],
37
+ };
38
+ const results = await runMatrix(matrix, { iterations: 10 });
39
+ expect(results.variants).toHaveLength(2);
40
+ for (const variant of results.variants) {
41
+ expect(variant.cases).toHaveLength(2);
42
+ expect(variant.cases.map(c => c.caseId)).toEqual(["Hello", "World"]);
43
+ }
44
+ });
45
+
46
+ test("stateful variant", async () => {
47
+ const stateful = {
48
+ setup: (id: string) => ({ prepared: id.toUpperCase() }),
49
+ run: (state: { prepared: string }) => state.prepared.toLowerCase(),
50
+ };
51
+ const matrix: BenchMatrix<string> = {
52
+ name: "Test",
53
+ variants: { stateful },
54
+ cases: ["a", "b"],
55
+ };
56
+ const results = await runMatrix(matrix, { iterations: 10 });
57
+ expect(results.variants).toHaveLength(1);
58
+ expect(results.variants[0].id).toBe("stateful");
59
+ expect(results.variants[0].cases).toHaveLength(2);
60
+ });
61
+
62
+ test("async setup in stateful variant", async () => {
63
+ const asyncSetup = {
64
+ setup: async (n: string) => {
65
+ await Promise.resolve();
66
+ return { value: Number(n) * 2 };
67
+ },
68
+ run: (state: { value: number }) => state.value + 1,
69
+ };
70
+ const matrix: BenchMatrix<string> = {
71
+ name: "AsyncTest",
72
+ variants: { asyncSetup },
73
+ cases: ["1", "2"],
74
+ };
75
+ const results = await runMatrix(matrix, { iterations: 10 });
76
+ expect(results.variants).toHaveLength(1);
77
+ expect(results.variants[0].cases).toHaveLength(2);
78
+ });
79
+
80
+ test("isStatefulVariant type guard", () => {
81
+ const fn = () => {};
82
+ const stateful: StatefulVariant = { setup: () => ({}), run: () => {} };
83
+
84
+ expect(isStatefulVariant(fn)).toBe(false);
85
+ expect(isStatefulVariant(stateful)).toBe(true);
86
+ });
87
+
88
+ test("error when no variants provided", async () => {
89
+ const matrix: BenchMatrix = { name: "Empty" };
90
+ await expect(runMatrix(matrix)).rejects.toThrow(
91
+ "requires either 'variants' or 'variantDir'",
92
+ );
93
+ });
94
+
95
+ const discoverUrl = `file://${import.meta.dirname}/fixtures/discover/`;
96
+
97
+ test("discoverVariants finds .ts files", async () => {
98
+ const variants = await discoverVariants(discoverUrl);
99
+ expect(variants.sort()).toEqual(["fast", "slow"]);
100
+ });
101
+
102
+ test("loadVariant loads stateless variant", async () => {
103
+ const variant = await loadVariant(discoverUrl, "fast");
104
+ expect(typeof variant).toBe("function");
105
+ });
106
+
107
+ test("loadVariant loads stateful variant", async () => {
108
+ const statefulUrl = `file://${import.meta.dirname}/fixtures/stateful/`;
109
+ const variant = await loadVariant(statefulUrl, "stateful");
110
+ expect(typeof variant).toBe("object");
111
+ expect(typeof (variant as any).setup).toBe("function");
112
+ expect(typeof (variant as any).run).toBe("function");
113
+ });
114
+
115
+ test("loadVariant throws when run is missing", async () => {
116
+ const invalidUrl = `file://${import.meta.dirname}/fixtures/invalid/`;
117
+ await expect(loadVariant(invalidUrl, "bad")).rejects.toThrow(
118
+ "must export 'run'",
119
+ );
120
+ });
121
+
122
+ const workerFixturesUrl = `file://${import.meta.dirname}/fixtures/worker/`;
123
+
124
+ test("runMatrix with variantDir discovers and runs variants", async () => {
125
+ const matrix: BenchMatrix = {
126
+ name: "DirTest",
127
+ variantDir: workerFixturesUrl,
128
+ cases: ["a"],
129
+ };
130
+ const results = await runMatrix(matrix, { iterations: 5 });
131
+ expect(results.name).toBe("DirTest");
132
+ const variantIds = results.variants.map(v => v.id).sort();
133
+ expect(variantIds).toEqual(["fast", "slow"]);
134
+ });
135
+
136
+ test("runMatrix with variantDir runs each variant in isolated worker", async () => {
137
+ const matrix: BenchMatrix = {
138
+ name: "IsolationTest",
139
+ variantDir: workerFixturesUrl,
140
+ cases: ["test"],
141
+ };
142
+ const results = await runMatrix(matrix, { iterations: 3 });
143
+ expect(results.variants).toHaveLength(2);
144
+ for (const variant of results.variants) {
145
+ expect(variant.cases).toHaveLength(1);
146
+ expect(variant.cases[0].measured.samples.length).toBeGreaterThan(0);
147
+ }
148
+ });
149
+
150
+ const casesFixturesUrl = `file://${import.meta.dirname}/fixtures/cases`;
151
+ const casesModuleUrl = `${casesFixturesUrl}/cases.ts`;
152
+ const asyncCasesUrl = `${casesFixturesUrl}/asyncCases.ts`;
153
+ const casesVariantDirUrl = `${casesFixturesUrl}/variants/`;
154
+
155
+ test("loadCasesModule loads cases array", async () => {
156
+ const mod = await loadCasesModule(casesModuleUrl);
157
+ expect(mod.cases).toEqual(["small", "large"]);
158
+ expect(typeof mod.loadCase).toBe("function");
159
+ });
160
+
161
+ test("loadCasesModule throws when cases is missing", async () => {
162
+ const badModuleUrl = `${casesFixturesUrl}/variants/sum.ts`; // has run but no cases
163
+ await expect(loadCasesModule(badModuleUrl)).rejects.toThrow(
164
+ "must export 'cases' array",
165
+ );
166
+ });
167
+
168
+ test("loadCaseData calls loadCase when available", async () => {
169
+ const mod = await loadCasesModule<number[]>(casesModuleUrl);
170
+ const small = await loadCaseData(mod, "small");
171
+ expect(small.data).toEqual([1, 2, 3]);
172
+ expect(small.metadata?.size).toBe(3);
173
+
174
+ const large = await loadCaseData(mod, "large");
175
+ expect(large.data).toHaveLength(100);
176
+ expect(large.metadata?.size).toBe(100);
177
+ });
178
+
179
+ test("loadCaseData returns caseId when no loadCase", async () => {
180
+ const result = await loadCaseData(undefined, "myCase");
181
+ expect(result.data).toBe("myCase");
182
+ expect(result.metadata).toBeUndefined();
183
+ });
184
+
185
+ test("loadCaseData handles async loadCase", async () => {
186
+ const mod = await loadCasesModule<string>(asyncCasesUrl);
187
+ const loaded = await loadCaseData(mod, "alpha");
188
+ expect(loaded.data).toBe("ALPHA");
189
+ expect(loaded.metadata?.original).toBe("alpha");
190
+ });
191
+
192
+ test("inline variants with casesModule", async () => {
193
+ const matrix: BenchMatrix<number[]> = {
194
+ name: "InlineCasesModule",
195
+ variants: {
196
+ sum: (arr: number[]) => arr.reduce((a, b) => a + b, 0),
197
+ length: (arr: number[]) => arr.length,
198
+ },
199
+ casesModule: casesModuleUrl,
200
+ };
201
+ const results = await runMatrix(matrix, { iterations: 5 });
202
+ expect(results.variants).toHaveLength(2);
203
+ expect(results.variants[0].cases).toHaveLength(2);
204
+ const cases = results.variants[0].cases;
205
+ expect(cases.map(c => c.caseId)).toEqual(["small", "large"]);
206
+ expect(cases[0].metadata?.size).toBe(3);
207
+ expect(cases[1].metadata?.size).toBe(100);
208
+ });
209
+
210
+ test("inline variants with async casesModule", async () => {
211
+ const matrix: BenchMatrix<string> = {
212
+ name: "AsyncCasesModule",
213
+ variants: {
214
+ lower: (s: string) => s.toLowerCase(),
215
+ },
216
+ casesModule: asyncCasesUrl,
217
+ };
218
+ const results = await runMatrix(matrix, { iterations: 5 });
219
+ expect(results.variants).toHaveLength(1);
220
+ expect(results.variants[0].cases).toHaveLength(2);
221
+ expect(results.variants[0].cases.map(c => c.caseId)).toEqual([
222
+ "alpha",
223
+ "beta",
224
+ ]);
225
+ });
226
+
227
+ test("variantDir with casesModule in worker", async () => {
228
+ const matrix: BenchMatrix<number[]> = {
229
+ name: "WorkerCasesModule",
230
+ variantDir: casesVariantDirUrl,
231
+ casesModule: casesModuleUrl,
232
+ };
233
+ const results = await runMatrix(matrix, { iterations: 5 });
234
+ const variantIds = results.variants.map(v => v.id).sort();
235
+ expect(variantIds).toEqual(["product", "sum"]);
236
+ const sum = results.variants.find(v => v.id === "sum");
237
+ expect(sum?.cases).toHaveLength(2);
238
+ expect(sum?.cases.map(c => c.caseId)).toEqual(["small", "large"]);
239
+ });
240
+
241
+ test("error when both baselineDir and baselineVariant set", async () => {
242
+ const matrix: BenchMatrix = {
243
+ name: "Invalid",
244
+ variantDir: workerFixturesUrl,
245
+ baselineDir: workerFixturesUrl,
246
+ baselineVariant: "fast",
247
+ };
248
+ await expect(runMatrix(matrix)).rejects.toThrow(
249
+ "cannot have both 'baselineDir' and 'baselineVariant'",
250
+ );
251
+ });
252
+
253
+ test("error when inline variants use baselineDir", async () => {
254
+ const matrix: BenchMatrix = {
255
+ name: "Invalid",
256
+ variants: { fast: () => {} },
257
+ baselineDir: workerFixturesUrl,
258
+ };
259
+ await expect(runMatrix(matrix)).rejects.toThrow("cannot use 'baselineDir'");
260
+ });
261
+
262
+ test("baselineVariant with inline variants", async () => {
263
+ const matrix: BenchMatrix = {
264
+ name: "BaselineVariantTest",
265
+ variants: {
266
+ fast: () => {},
267
+ slow: () => {
268
+ let _x = 0;
269
+ for (let i = 0; i < 1000; i++) _x++;
270
+ },
271
+ },
272
+ baselineVariant: "fast",
273
+ };
274
+ const results = await runMatrix(matrix, { iterations: 20 });
275
+ expect(results.variants).toHaveLength(2);
276
+
277
+ const fastVariant = results.variants.find(v => v.id === "fast");
278
+ const slowVariant = results.variants.find(v => v.id === "slow");
279
+
280
+ expect(fastVariant?.cases[0].baseline).toBeUndefined();
281
+ expect(fastVariant?.cases[0].deltaPercent).toBeUndefined();
282
+
283
+ expect(slowVariant?.cases[0].baseline).toBeDefined();
284
+ expect(slowVariant?.cases[0].deltaPercent).toBeDefined();
285
+ expect(typeof slowVariant?.cases[0].deltaPercent).toBe("number");
286
+ });
287
+
288
+ test("baselineVariant skips when variant not found", async () => {
289
+ const matrix: BenchMatrix = {
290
+ name: "BadBaselineVariant",
291
+ variants: { fast: () => {} },
292
+ baselineVariant: "nonexistent",
293
+ };
294
+ const result = await runMatrix(matrix);
295
+ // No deltaPercent since baseline variant wasn't found
296
+ expect(result.variants[0].cases[0].deltaPercent).toBeUndefined();
297
+ });
298
+
299
+ const variantsDirUrl = `file://${import.meta.dirname}/fixtures/variants/`;
300
+ const baselineDirUrl = `file://${import.meta.dirname}/fixtures/baseline/`;
301
+
302
+ test("baselineDir comparison", async () => {
303
+ const matrix: BenchMatrix = {
304
+ name: "BaselineDirTest",
305
+ variantDir: variantsDirUrl,
306
+ baselineDir: baselineDirUrl,
307
+ cases: ["a"],
308
+ };
309
+ const results = await runMatrix(matrix, { iterations: 10 });
310
+
311
+ expect(results.variants).toHaveLength(2); // impl and extra
312
+ const implVariant = results.variants.find(v => v.id === "impl");
313
+ expect(implVariant).toBeDefined();
314
+ const caseResult = implVariant!.cases[0];
315
+ expect(caseResult.baseline).toBeDefined();
316
+ expect(caseResult.deltaPercent).toBeDefined();
317
+ expect(typeof caseResult.deltaPercent).toBe("number");
318
+ // Current impl is faster (no work), baseline is slower (loop)
319
+ // So deltaPercent should be negative (current is faster than baseline)
320
+ expect(caseResult.deltaPercent).toBeLessThan(0);
321
+ });
322
+
323
+ test("baselineDir only applies to matching variants", async () => {
324
+ // extra.ts exists in variants/ but not in baseline/
325
+ const matrix: BenchMatrix = {
326
+ name: "PartialBaselineTest",
327
+ variantDir: variantsDirUrl,
328
+ baselineDir: baselineDirUrl,
329
+ cases: ["a"],
330
+ };
331
+ const results = await runMatrix(matrix, { iterations: 10 });
332
+
333
+ const variantIds = results.variants.map(v => v.id).sort();
334
+ expect(variantIds).toEqual(["extra", "impl"]);
335
+
336
+ // impl should have baseline (matches file in baselineDir)
337
+ const implVariant = results.variants.find(v => v.id === "impl");
338
+ expect(implVariant?.cases[0].baseline).toBeDefined();
339
+
340
+ // extra should NOT have baseline (no matching file in baselineDir)
341
+ const extraVariant = results.variants.find(v => v.id === "extra");
342
+ expect(extraVariant?.cases[0].baseline).toBeUndefined();
343
+ expect(extraVariant?.cases[0].deltaPercent).toBeUndefined();
344
+ });
345
+
346
+ test("baselineVariant with variantDir", async () => {
347
+ const matrix: BenchMatrix = {
348
+ name: "BaselineVariantDirTest",
349
+ variantDir: variantsDirUrl,
350
+ baselineVariant: "impl",
351
+ cases: ["a"],
352
+ };
353
+ const results = await runMatrix(matrix, { iterations: 10 });
354
+
355
+ const variantIds = results.variants.map(v => v.id).sort();
356
+ expect(variantIds).toEqual(["extra", "impl"]);
357
+
358
+ // impl is baseline, should not have baseline/deltaPercent
359
+ const implVariant = results.variants.find(v => v.id === "impl");
360
+ expect(implVariant?.cases[0].baseline).toBeUndefined();
361
+
362
+ // extra should have baseline referencing impl
363
+ const extraVariant = results.variants.find(v => v.id === "extra");
364
+ expect(extraVariant?.cases[0].baseline).toBeDefined();
365
+ expect(extraVariant?.cases[0].deltaPercent).toBeDefined();
366
+ });
@@ -0,0 +1,117 @@
1
+ import { expect, test } from "vitest";
2
+ import type { BenchMatrix } from "../BenchMatrix.ts";
3
+ import { filterMatrix, parseMatrixFilter } from "../matrix/MatrixFilter.ts";
4
+
5
+ test("parseMatrixFilter: case/variant", () => {
6
+ expect(parseMatrixFilter("bevy/link")).toEqual({
7
+ case: "bevy",
8
+ variant: "link",
9
+ });
10
+ });
11
+
12
+ test("parseMatrixFilter: case/", () => {
13
+ expect(parseMatrixFilter("bevy/")).toEqual({
14
+ case: "bevy",
15
+ variant: undefined,
16
+ });
17
+ });
18
+
19
+ test("parseMatrixFilter: /variant", () => {
20
+ expect(parseMatrixFilter("/link")).toEqual({
21
+ case: undefined,
22
+ variant: "link",
23
+ });
24
+ });
25
+
26
+ test("parseMatrixFilter: case only (no slash)", () => {
27
+ expect(parseMatrixFilter("bevy")).toEqual({ case: "bevy" });
28
+ });
29
+
30
+ test("parseMatrixFilter: empty parts", () => {
31
+ expect(parseMatrixFilter("/")).toEqual({
32
+ case: undefined,
33
+ variant: undefined,
34
+ });
35
+ });
36
+
37
+ const inlineMatrix: BenchMatrix<string> = {
38
+ name: "Test",
39
+ variants: {
40
+ fast: (s: string) => s.toUpperCase(),
41
+ slow: (s: string) => s.toLowerCase(),
42
+ medium: (s: string) => s,
43
+ },
44
+ cases: ["small", "large", "bevy_env_map"],
45
+ };
46
+
47
+ test("filterMatrix: no filter returns original", async () => {
48
+ const result = await filterMatrix(inlineMatrix, undefined);
49
+ expect(result).toBe(inlineMatrix);
50
+ });
51
+
52
+ test("filterMatrix: case filter only", async () => {
53
+ const result = await filterMatrix(inlineMatrix, { case: "bevy" });
54
+ expect(result.filteredCases).toEqual(["bevy_env_map"]);
55
+ expect(result.filteredVariants).toBeUndefined();
56
+ });
57
+
58
+ test("filterMatrix: variant filter only", async () => {
59
+ const result = await filterMatrix(inlineMatrix, { variant: "fast" });
60
+ expect(result.filteredVariants).toEqual(["fast"]);
61
+ expect(result.filteredCases).toBeUndefined();
62
+ });
63
+
64
+ test("filterMatrix: case and variant filter", async () => {
65
+ const result = await filterMatrix(inlineMatrix, {
66
+ case: "small",
67
+ variant: "slow",
68
+ });
69
+ expect(result.filteredCases).toEqual(["small"]);
70
+ expect(result.filteredVariants).toEqual(["slow"]);
71
+ });
72
+
73
+ test("filterMatrix: case-insensitive matching", async () => {
74
+ const result = await filterMatrix(inlineMatrix, {
75
+ case: "SMALL",
76
+ variant: "FAST",
77
+ });
78
+ expect(result.filteredCases).toEqual(["small"]);
79
+ expect(result.filteredVariants).toEqual(["fast"]);
80
+ });
81
+
82
+ test("filterMatrix: substring matching", async () => {
83
+ const result = await filterMatrix(inlineMatrix, { case: "env" });
84
+ expect(result.filteredCases).toEqual(["bevy_env_map"]);
85
+ });
86
+
87
+ test("filterMatrix: no matching cases throws", async () => {
88
+ await expect(
89
+ filterMatrix(inlineMatrix, { case: "nonexistent" }),
90
+ ).rejects.toThrow('No cases match filter: "nonexistent"');
91
+ });
92
+
93
+ test("filterMatrix: no matching variants throws", async () => {
94
+ await expect(
95
+ filterMatrix(inlineMatrix, { variant: "nonexistent" }),
96
+ ).rejects.toThrow('No variants match filter: "nonexistent"');
97
+ });
98
+
99
+ test("filterMatrix: multiple matching cases", async () => {
100
+ const result = await filterMatrix(inlineMatrix, { case: "l" }); // matches small, large
101
+ expect(result.filteredCases).toEqual(["small", "large"]);
102
+ });
103
+
104
+ test("filterMatrix: multiple matching variants", async () => {
105
+ const result = await filterMatrix(inlineMatrix, { variant: "s" }); // matches fast, slow
106
+ expect(result.filteredVariants?.sort()).toEqual(["fast", "slow"]);
107
+ });
108
+
109
+ const noExplicitCases: BenchMatrix = {
110
+ name: "NoCase",
111
+ variants: { fast: () => {} },
112
+ };
113
+
114
+ test("filterMatrix: implicit default case returns default", async () => {
115
+ const result = await filterMatrix(noExplicitCases, { case: "default" });
116
+ expect(result.filteredCases).toEqual(["default"]);
117
+ });
@@ -0,0 +1,139 @@
1
+ import { expect, test } from "vitest";
2
+ import type { CaseResult, MatrixResults } from "../BenchMatrix.ts";
3
+ import { reportMatrixResults } from "../matrix/MatrixReport.ts";
4
+
5
+ /** Create simple measured results for testing */
6
+ function mockMeasured(avg: number, count = 10): CaseResult["measured"] {
7
+ const samples = Array(count).fill(avg);
8
+ return {
9
+ name: "test",
10
+ samples,
11
+ time: { avg, min: avg, max: avg, p50: avg, p75: avg, p99: avg, p999: avg },
12
+ };
13
+ }
14
+
15
+ /** Create a mock matrix result */
16
+ function mockResults(
17
+ name: string,
18
+ variants: Array<{
19
+ id: string;
20
+ cases: Array<{
21
+ caseId: string;
22
+ mean: number;
23
+ metadata?: Record<string, unknown>;
24
+ }>;
25
+ }>,
26
+ ): MatrixResults {
27
+ const mapped = variants.map(v => ({
28
+ id: v.id,
29
+ cases: v.cases.map(c => ({
30
+ caseId: c.caseId,
31
+ measured: mockMeasured(c.mean),
32
+ metadata: c.metadata,
33
+ })),
34
+ }));
35
+ return { name, variants: mapped };
36
+ }
37
+
38
+ test("reportMatrixResults: basic output includes matrix name", () => {
39
+ const results = mockResults("TestMatrix", [
40
+ { id: "fast", cases: [{ caseId: "a", mean: 1.0 }] },
41
+ ]);
42
+ const report = reportMatrixResults(results);
43
+ expect(report).toContain("Matrix: TestMatrix");
44
+ });
45
+
46
+ test("reportMatrixResults: outputs one table per case", () => {
47
+ const results = mockResults("MultiCase", [
48
+ {
49
+ id: "fast",
50
+ cases: [
51
+ { caseId: "case1", mean: 1.0 },
52
+ { caseId: "case2", mean: 2.0 },
53
+ ],
54
+ },
55
+ {
56
+ id: "slow",
57
+ cases: [
58
+ { caseId: "case1", mean: 10.0 },
59
+ { caseId: "case2", mean: 20.0 },
60
+ ],
61
+ },
62
+ ]);
63
+ const report = reportMatrixResults(results);
64
+ expect(report).toContain("case1");
65
+ expect(report).toContain("case2");
66
+ expect(report).toContain("fast");
67
+ expect(report).toContain("slow");
68
+ });
69
+
70
+ test("reportMatrixResults: includes metadata in case title", () => {
71
+ const results = mockResults("WithMetadata", [
72
+ {
73
+ id: "fast",
74
+ cases: [{ caseId: "bevy_env_map", mean: 1.0, metadata: { LOC: 1200 } }],
75
+ },
76
+ ]);
77
+ const report = reportMatrixResults(results);
78
+ expect(report).toContain("bevy_env_map (1200 LOC)");
79
+ });
80
+
81
+ test("reportMatrixResults: formats time in ms", () => {
82
+ const results = mockResults("TimeFormat", [
83
+ { id: "variant", cases: [{ caseId: "test", mean: 2.34 }] },
84
+ ]);
85
+ const report = reportMatrixResults(results);
86
+ expect(report).toContain("2.34ms");
87
+ });
88
+
89
+ test("reportMatrixResults: formats time in seconds for large values", () => {
90
+ const results = mockResults("TimeFormat", [
91
+ { id: "variant", cases: [{ caseId: "test", mean: 1500 }] },
92
+ ]);
93
+ const report = reportMatrixResults(results);
94
+ expect(report).toContain("1.50s");
95
+ });
96
+
97
+ test("reportMatrixResults: shows variant column", () => {
98
+ const results = mockResults("VarCol", [
99
+ { id: "my_variant", cases: [{ caseId: "test", mean: 1.0 }] },
100
+ ]);
101
+ const report = reportMatrixResults(results);
102
+ expect(report).toContain("variant");
103
+ expect(report).toContain("my_variant");
104
+ });
105
+
106
+ test("reportMatrixResults: truncates long variant names", () => {
107
+ const results = mockResults("TruncTest", [
108
+ {
109
+ id: "this_is_a_very_long_variant_name_that_should_be_truncated",
110
+ cases: [{ caseId: "test", mean: 1.0 }],
111
+ },
112
+ ]);
113
+ const report = reportMatrixResults(results);
114
+ // 25 char limit => 22 chars + "..."
115
+ expect(report).toContain("this_is_a_very_long_va...");
116
+ });
117
+
118
+ test("reportMatrixResults: empty variants returns header only", () => {
119
+ const results: MatrixResults = { name: "Empty", variants: [] };
120
+ const report = reportMatrixResults(results);
121
+ expect(report).toBe("Matrix: Empty");
122
+ });
123
+
124
+ test("reportMatrixResults: with baseline shows diff percentage", () => {
125
+ const baseline = mockMeasured(2.0, 20);
126
+ const measured = mockMeasured(1.0, 20);
127
+ const results: MatrixResults = {
128
+ name: "WithBaseline",
129
+ variants: [
130
+ {
131
+ id: "current",
132
+ cases: [{ caseId: "test", measured, baseline, deltaPercent: -50 }],
133
+ },
134
+ ],
135
+ };
136
+ const report = reportMatrixResults(results);
137
+ // Should contain diff percentage inline (not as separate header)
138
+ expect(report).toContain("-50.0%");
139
+ });