modestbench 0.8.0 → 0.9.1
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/CHANGELOG.md +22 -0
- package/README.md +37 -4
- package/dist/adapters/types.d.cts +1 -1
- package/dist/adapters/types.d.cts.map +1 -1
- package/dist/adapters/types.d.ts +1 -1
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/cli/commands/baseline.cjs +8 -8
- package/dist/cli/commands/baseline.cjs.map +1 -1
- package/dist/cli/commands/baseline.js +8 -8
- package/dist/cli/commands/baseline.js.map +1 -1
- package/dist/cli/commands/init.cjs +2 -2
- package/dist/cli/commands/init.cjs.map +1 -1
- package/dist/cli/commands/init.js +2 -2
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/run.cjs +12 -8
- package/dist/cli/commands/run.cjs.map +1 -1
- package/dist/cli/commands/run.d.cts +1 -13
- package/dist/cli/commands/run.d.cts.map +1 -1
- package/dist/cli/commands/run.d.ts +1 -13
- package/dist/cli/commands/run.d.ts.map +1 -1
- package/dist/cli/commands/run.js +11 -7
- package/dist/cli/commands/run.js.map +1 -1
- package/dist/cli/commands/test.cjs +1 -1
- package/dist/cli/commands/test.cjs.map +1 -1
- package/dist/cli/commands/test.js +1 -1
- package/dist/cli/commands/test.js.map +1 -1
- package/dist/cli/index.cjs +655 -870
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.d.cts +27 -17
- package/dist/cli/index.d.cts.map +1 -1
- package/dist/cli/index.d.ts +27 -17
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +657 -869
- package/dist/cli/index.js.map +1 -1
- package/dist/{core → config}/benchmark-schema.cjs +1 -1
- package/dist/config/benchmark-schema.cjs.map +1 -0
- package/dist/config/benchmark-schema.d.cts +913 -0
- package/dist/config/benchmark-schema.d.cts.map +1 -0
- package/dist/config/benchmark-schema.d.ts +913 -0
- package/dist/config/benchmark-schema.d.ts.map +1 -0
- package/dist/{core → config}/benchmark-schema.js +1 -1
- package/dist/config/benchmark-schema.js.map +1 -0
- package/dist/config/budget-schema.cjs +1 -1
- package/dist/config/budget-schema.cjs.map +1 -1
- package/dist/config/budget-schema.js +1 -1
- package/dist/config/budget-schema.js.map +1 -1
- package/dist/config/schema.cjs +188 -105
- package/dist/config/schema.cjs.map +1 -1
- package/dist/config/schema.d.cts +208 -80
- package/dist/config/schema.d.cts.map +1 -1
- package/dist/config/schema.d.ts +208 -80
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +187 -104
- package/dist/config/schema.js.map +1 -1
- package/dist/core/engine.cjs +50 -45
- package/dist/core/engine.cjs.map +1 -1
- package/dist/core/engine.d.cts.map +1 -1
- package/dist/core/engine.d.ts.map +1 -1
- package/dist/core/engine.js +50 -45
- package/dist/core/engine.js.map +1 -1
- package/dist/core/engines/accurate-engine.cjs +1 -1
- package/dist/core/engines/accurate-engine.cjs.map +1 -1
- package/dist/core/engines/accurate-engine.d.cts.map +1 -1
- package/dist/core/engines/accurate-engine.d.ts.map +1 -1
- package/dist/core/engines/accurate-engine.js +1 -1
- package/dist/core/engines/accurate-engine.js.map +1 -1
- package/dist/core/output-path-resolver.cjs +15 -1
- package/dist/core/output-path-resolver.cjs.map +1 -1
- package/dist/core/output-path-resolver.d.cts +8 -0
- package/dist/core/output-path-resolver.d.cts.map +1 -1
- package/dist/core/output-path-resolver.d.ts +8 -0
- package/dist/core/output-path-resolver.d.ts.map +1 -1
- package/dist/core/output-path-resolver.js +13 -0
- package/dist/core/output-path-resolver.js.map +1 -1
- package/dist/formatters/history/compare.cjs +6 -6
- package/dist/formatters/history/compare.cjs.map +1 -1
- package/dist/formatters/history/compare.js +6 -6
- package/dist/formatters/history/compare.js.map +1 -1
- package/dist/formatters/history/show.cjs +2 -2
- package/dist/formatters/history/show.cjs.map +1 -1
- package/dist/formatters/history/show.js +2 -2
- package/dist/formatters/history/show.js.map +1 -1
- package/dist/formatters/history/trends.cjs +1 -1
- package/dist/formatters/history/trends.cjs.map +1 -1
- package/dist/formatters/history/trends.js +1 -1
- package/dist/formatters/history/trends.js.map +1 -1
- package/dist/reporters/human.cjs +3 -3
- package/dist/reporters/human.cjs.map +1 -1
- package/dist/reporters/human.d.cts.map +1 -1
- package/dist/reporters/human.d.ts.map +1 -1
- package/dist/reporters/human.js +3 -3
- package/dist/reporters/human.js.map +1 -1
- package/dist/reporters/json.cjs +1 -1
- package/dist/reporters/json.cjs.map +1 -1
- package/dist/reporters/json.js +1 -1
- package/dist/reporters/json.js.map +1 -1
- package/dist/reporters/nyan.cjs +1 -1
- package/dist/reporters/nyan.cjs.map +1 -1
- package/dist/reporters/nyan.js +1 -1
- package/dist/reporters/nyan.js.map +1 -1
- package/dist/schema/modestbench-config.schema.json +94 -87
- package/dist/services/budget-evaluator.cjs +10 -8
- package/dist/services/budget-evaluator.cjs.map +1 -1
- package/dist/services/budget-evaluator.d.cts +2 -2
- package/dist/services/budget-evaluator.d.cts.map +1 -1
- package/dist/services/budget-evaluator.d.ts +2 -2
- package/dist/services/budget-evaluator.d.ts.map +1 -1
- package/dist/services/budget-evaluator.js +10 -8
- package/dist/services/budget-evaluator.js.map +1 -1
- package/dist/services/budget-resolver.cjs +214 -0
- package/dist/services/budget-resolver.cjs.map +1 -0
- package/dist/services/budget-resolver.d.cts +98 -0
- package/dist/services/budget-resolver.d.cts.map +1 -0
- package/dist/services/budget-resolver.d.ts +98 -0
- package/dist/services/budget-resolver.d.ts.map +1 -0
- package/dist/services/budget-resolver.js +203 -0
- package/dist/services/budget-resolver.js.map +1 -0
- package/dist/services/config-manager.cjs +2 -2
- package/dist/services/config-manager.cjs.map +1 -1
- package/dist/services/config-manager.js +2 -2
- package/dist/services/config-manager.js.map +1 -1
- package/dist/services/file-loader.cjs +1 -1
- package/dist/services/file-loader.cjs.map +1 -1
- package/dist/services/file-loader.js +1 -1
- package/dist/services/file-loader.js.map +1 -1
- package/dist/services/reporter-registry.cjs +8 -8
- package/dist/services/reporter-registry.cjs.map +1 -1
- package/dist/services/reporter-registry.js +8 -8
- package/dist/services/reporter-registry.js.map +1 -1
- package/dist/types/budgets.d.cts +31 -0
- package/dist/types/budgets.d.cts.map +1 -1
- package/dist/types/budgets.d.ts +31 -0
- package/dist/types/budgets.d.ts.map +1 -1
- package/dist/types/core.cjs.map +1 -1
- package/dist/types/core.d.cts +28 -75
- package/dist/types/core.d.cts.map +1 -1
- package/dist/types/core.d.ts +28 -75
- package/dist/types/core.d.ts.map +1 -1
- package/dist/types/core.js.map +1 -1
- package/package.json +24 -15
- package/src/adapters/types.ts +1 -1
- package/src/cli/commands/baseline.ts +8 -8
- package/src/cli/commands/init.ts +2 -2
- package/src/cli/commands/run.ts +18 -6
- package/src/cli/commands/test.ts +1 -1
- package/src/cli/index.ts +806 -942
- package/src/{core → config}/benchmark-schema.ts +1 -1
- package/src/config/budget-schema.ts +1 -1
- package/src/config/schema.ts +379 -302
- package/src/core/engine.ts +74 -69
- package/src/core/engines/accurate-engine.ts +1 -1
- package/src/core/output-path-resolver.ts +14 -0
- package/src/formatters/history/compare.ts +6 -6
- package/src/formatters/history/show.ts +2 -2
- package/src/formatters/history/trends.ts +1 -1
- package/src/reporters/human.ts +5 -3
- package/src/reporters/json.ts +1 -1
- package/src/reporters/nyan.ts +1 -1
- package/src/services/budget-evaluator.ts +15 -11
- package/src/services/budget-resolver.ts +254 -0
- package/src/services/config-manager.ts +2 -2
- package/src/services/file-loader.ts +1 -1
- package/src/services/reporter-registry.ts +8 -8
- package/src/types/budgets.ts +38 -0
- package/src/types/core.ts +64 -99
- package/dist/core/benchmark-schema.cjs.map +0 -1
- package/dist/core/benchmark-schema.d.cts +0 -139
- package/dist/core/benchmark-schema.d.cts.map +0 -1
- package/dist/core/benchmark-schema.d.ts +0 -139
- package/dist/core/benchmark-schema.d.ts.map +0 -1
- package/dist/core/benchmark-schema.js.map +0 -1
package/src/core/engine.ts
CHANGED
|
@@ -16,7 +16,6 @@ import type {
|
|
|
16
16
|
BenchmarkRun,
|
|
17
17
|
BenchmarkSuite,
|
|
18
18
|
BenchmarkTask,
|
|
19
|
-
Budget,
|
|
20
19
|
BudgetSummary,
|
|
21
20
|
CiInfo,
|
|
22
21
|
ConfigurationManager,
|
|
@@ -354,93 +353,99 @@ export abstract class ModestBenchEngine implements BenchmarkEngine {
|
|
|
354
353
|
// Evaluate budgets if configured
|
|
355
354
|
let budgetSummary: BudgetSummary | undefined;
|
|
356
355
|
|
|
357
|
-
if (config.budgets
|
|
358
|
-
const
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
356
|
+
if (config.budgets) {
|
|
357
|
+
const budgets = config.budgets;
|
|
358
|
+
const hasBudgets =
|
|
359
|
+
Object.keys(budgets.exact).length > 0 || budgets.patterns.length > 0;
|
|
360
|
+
|
|
361
|
+
if (hasBudgets) {
|
|
362
|
+
const evaluator = new BudgetEvaluator();
|
|
363
|
+
const baselineStorage = new BaselineStorageService(process.cwd());
|
|
364
|
+
|
|
365
|
+
// Collect task results
|
|
366
|
+
const taskResults = new Map<TaskId, TaskResult>();
|
|
367
|
+
|
|
368
|
+
for (const file of fileResults) {
|
|
369
|
+
for (const suite of file.suites) {
|
|
370
|
+
for (const task of suite.tasks) {
|
|
371
|
+
if (!task.error) {
|
|
372
|
+
// file.filePath is already relative to cwd
|
|
373
|
+
const taskId = createTaskId(
|
|
374
|
+
file.filePath,
|
|
375
|
+
suite.name,
|
|
376
|
+
task.name,
|
|
377
|
+
);
|
|
378
|
+
taskResults.set(taskId, task);
|
|
379
|
+
}
|
|
375
380
|
}
|
|
376
381
|
}
|
|
377
382
|
}
|
|
378
|
-
}
|
|
379
383
|
|
|
380
|
-
|
|
381
|
-
|
|
384
|
+
// Load baseline data if needed for relative budgets
|
|
385
|
+
let baselineData: Map<TaskId, BaselineSummaryData> | undefined;
|
|
382
386
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
+
// Check if any budgets use relative thresholds
|
|
388
|
+
const hasRelativeBudgets =
|
|
389
|
+
Object.values(budgets.exact).some((budget) => budget.relative) ||
|
|
390
|
+
budgets.patterns.some((pattern) => pattern.budget.relative);
|
|
387
391
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
392
|
+
if (hasRelativeBudgets) {
|
|
393
|
+
const baselineName =
|
|
394
|
+
config.baseline || (await baselineStorage.getDefault());
|
|
391
395
|
|
|
392
|
-
|
|
393
|
-
|
|
396
|
+
if (baselineName) {
|
|
397
|
+
const baseline = await baselineStorage.getBaseline(baselineName);
|
|
394
398
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
399
|
+
if (baseline) {
|
|
400
|
+
// Cast keys to TaskId since they come from validated baseline storage
|
|
401
|
+
baselineData = new Map(
|
|
402
|
+
Object.entries(baseline.summary) as [
|
|
403
|
+
TaskId,
|
|
404
|
+
BaselineSummaryData,
|
|
405
|
+
][],
|
|
406
|
+
);
|
|
407
|
+
} else {
|
|
408
|
+
console.warn(
|
|
409
|
+
`Warning: Baseline "${baselineName}" not found. Relative budgets will be skipped.`,
|
|
410
|
+
);
|
|
411
|
+
}
|
|
403
412
|
} else {
|
|
404
413
|
console.warn(
|
|
405
|
-
|
|
414
|
+
'Warning: Relative budgets configured but no baseline specified. Relative budgets will be skipped.',
|
|
406
415
|
);
|
|
407
416
|
}
|
|
408
|
-
} else {
|
|
409
|
-
console.warn(
|
|
410
|
-
'Warning: Relative budgets configured but no baseline specified. Relative budgets will be skipped.',
|
|
411
|
-
);
|
|
412
417
|
}
|
|
413
|
-
}
|
|
414
418
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
419
|
+
// Evaluate budgets
|
|
420
|
+
budgetSummary = evaluator.evaluateRun(
|
|
421
|
+
budgets,
|
|
422
|
+
taskResults,
|
|
423
|
+
baselineData,
|
|
424
|
+
);
|
|
421
425
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
+
// Notify reporters of budget results
|
|
427
|
+
for (const reporter of reporters) {
|
|
428
|
+
if (reporter.onBudgetResult) {
|
|
429
|
+
await reporter.onBudgetResult(budgetSummary);
|
|
430
|
+
}
|
|
426
431
|
}
|
|
427
|
-
}
|
|
428
432
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
433
|
+
// Handle budget failures based on budgetMode
|
|
434
|
+
if (budgetSummary.failed > 0) {
|
|
435
|
+
const mode = config.budgetMode || 'fail';
|
|
432
436
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
437
|
+
if (mode === 'fail') {
|
|
438
|
+
throw new BudgetExceededError(
|
|
439
|
+
`${budgetSummary.failed} of ${budgetSummary.total} budget(s) exceeded`,
|
|
440
|
+
budgetSummary,
|
|
441
|
+
);
|
|
442
|
+
} else if (mode === 'warn') {
|
|
443
|
+
console.warn(
|
|
444
|
+
`Warning: ${budgetSummary.failed} of ${budgetSummary.total} budget(s) exceeded`,
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
// mode === 'report': just include in output, don't fail
|
|
442
448
|
}
|
|
443
|
-
// mode === 'report': just include in output, don't fail
|
|
444
449
|
}
|
|
445
450
|
}
|
|
446
451
|
|
|
@@ -38,7 +38,7 @@ export class AccurateEngine extends ModestBenchEngine {
|
|
|
38
38
|
* Maximum iterations per round to prevent overwhelming Node.js test runner
|
|
39
39
|
* and excessive memory usage
|
|
40
40
|
*/
|
|
41
|
-
private static readonly MAX_ITERATIONS_PER_ROUND =
|
|
41
|
+
private static readonly MAX_ITERATIONS_PER_ROUND = 10_000;
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
44
|
* Maximum iterations per round for async functions (much lower due to
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import { extname, isAbsolute, join, resolve } from 'node:path';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Generates a timestamped filename for benchmark output files.
|
|
5
|
+
*
|
|
6
|
+
* @param extension - File extension without the dot (e.g., 'json', 'csv')
|
|
7
|
+
* @returns Filename in format `benchmarks-YYYY-MM-DD-HH-MM-SS.{extension}` (UTC
|
|
8
|
+
* time)
|
|
9
|
+
*/
|
|
10
|
+
export const generateTimestampedFilename = (extension: string): string => {
|
|
11
|
+
const now = new Date();
|
|
12
|
+
const pad = (n: number) => n.toString().padStart(2, '0');
|
|
13
|
+
const timestamp = `${now.getUTCFullYear()}-${pad(now.getUTCMonth() + 1)}-${pad(now.getUTCDate())}-${pad(now.getUTCHours())}-${pad(now.getUTCMinutes())}-${pad(now.getUTCSeconds())}`;
|
|
14
|
+
return `benchmarks-${timestamp}.${extension}`;
|
|
15
|
+
};
|
|
16
|
+
|
|
3
17
|
/**
|
|
4
18
|
* Resolves the final output path for a reporter
|
|
5
19
|
*
|
|
@@ -51,8 +51,8 @@ export class HistoryCompareFormatter implements HistoryFormatter<CompareResult>
|
|
|
51
51
|
lines.push('');
|
|
52
52
|
|
|
53
53
|
for (const comparison of data.tasksInBoth) {
|
|
54
|
-
const mean1 = comparison.run1!.mean /
|
|
55
|
-
const mean2 = comparison.run2!.mean /
|
|
54
|
+
const mean1 = comparison.run1!.mean / 1_000_000; // Convert to ms
|
|
55
|
+
const mean2 = comparison.run2!.mean / 1_000_000;
|
|
56
56
|
const changeSign = comparison.percentChange >= 0 ? '+' : '';
|
|
57
57
|
const changeStr = `${changeSign}${comparison.percentChange.toFixed(1)}%`;
|
|
58
58
|
|
|
@@ -73,8 +73,8 @@ export class HistoryCompareFormatter implements HistoryFormatter<CompareResult>
|
|
|
73
73
|
);
|
|
74
74
|
|
|
75
75
|
// Min - highlight higher number
|
|
76
|
-
const min1 = comparison.run1!.min /
|
|
77
|
-
const min2 = comparison.run2!.min /
|
|
76
|
+
const min1 = comparison.run1!.min / 1_000_000;
|
|
77
|
+
const min2 = comparison.run2!.min / 1_000_000;
|
|
78
78
|
const minHigher = min2 > min1;
|
|
79
79
|
const min1Str = minHigher
|
|
80
80
|
? colorize('magenta', `${min1.toFixed(3)}ms`)
|
|
@@ -87,8 +87,8 @@ export class HistoryCompareFormatter implements HistoryFormatter<CompareResult>
|
|
|
87
87
|
);
|
|
88
88
|
|
|
89
89
|
// Max - highlight higher number
|
|
90
|
-
const max1 = comparison.run1!.max /
|
|
91
|
-
const max2 = comparison.run2!.max /
|
|
90
|
+
const max1 = comparison.run1!.max / 1_000_000;
|
|
91
|
+
const max2 = comparison.run2!.max / 1_000_000;
|
|
92
92
|
const maxHigher = max2 > max1;
|
|
93
93
|
const max1Str = maxHigher
|
|
94
94
|
? colorize('magenta', `${max1.toFixed(3)}ms`)
|
|
@@ -148,8 +148,8 @@ const formatTime = (ns: number): string => {
|
|
|
148
148
|
if (ns < 1000) {
|
|
149
149
|
return `${ns.toFixed(2)}ns`;
|
|
150
150
|
}
|
|
151
|
-
if (ns <
|
|
151
|
+
if (ns < 1_000_000) {
|
|
152
152
|
return `${(ns / 1000).toFixed(3)}µs`;
|
|
153
153
|
}
|
|
154
|
-
return `${(ns /
|
|
154
|
+
return `${(ns / 1_000_000).toFixed(3)}ms`;
|
|
155
155
|
};
|
package/src/reporters/human.ts
CHANGED
|
@@ -234,7 +234,7 @@ export class HumanReporter extends BaseReporter {
|
|
|
234
234
|
);
|
|
235
235
|
}
|
|
236
236
|
this.printLine(
|
|
237
|
-
`${this.colorize('cyan', ansiChars.approx + ' Duration:')} ${this.colorize('brightWhite', BaseReporter.formatDuration(duration *
|
|
237
|
+
`${this.colorize('cyan', ansiChars.approx + ' Duration:')} ${this.colorize('brightWhite', BaseReporter.formatDuration(duration * 1_000_000))}`,
|
|
238
238
|
);
|
|
239
239
|
this.printLine();
|
|
240
240
|
|
|
@@ -463,7 +463,7 @@ export class HumanReporter extends BaseReporter {
|
|
|
463
463
|
});
|
|
464
464
|
|
|
465
465
|
const durationStr = BaseReporter.formatDuration(
|
|
466
|
-
result.duration *
|
|
466
|
+
result.duration * 1_000_000,
|
|
467
467
|
);
|
|
468
468
|
|
|
469
469
|
// Display suite setup failure
|
|
@@ -487,7 +487,9 @@ export class HumanReporter extends BaseReporter {
|
|
|
487
487
|
const passed = result.tasks.filter((t) => !t.error && !t.aborted).length;
|
|
488
488
|
const failed = result.tasks.filter((t) => t.error).length;
|
|
489
489
|
const aborted = result.tasks.filter((t) => t.aborted).length;
|
|
490
|
-
const durationStr = BaseReporter.formatDuration(
|
|
490
|
+
const durationStr = BaseReporter.formatDuration(
|
|
491
|
+
result.duration * 1_000_000,
|
|
492
|
+
); // ms to ns
|
|
491
493
|
|
|
492
494
|
// Build summary parts
|
|
493
495
|
const parts: string[] = [];
|
package/src/reporters/json.ts
CHANGED
|
@@ -90,7 +90,7 @@ export class JsonReporter extends BaseReporter {
|
|
|
90
90
|
super('json', options);
|
|
91
91
|
|
|
92
92
|
this.outputPath = options.outputPath;
|
|
93
|
-
this.prettyPrint = options.prettyPrint ??
|
|
93
|
+
this.prettyPrint = options.prettyPrint ?? false;
|
|
94
94
|
this.includeStatistics = options.includeStatistics ?? true;
|
|
95
95
|
this.includeMetadata = options.includeMetadata ?? true;
|
|
96
96
|
}
|
package/src/reporters/nyan.ts
CHANGED
|
@@ -345,7 +345,7 @@ export class NyanReporter extends BaseReporter {
|
|
|
345
345
|
*/
|
|
346
346
|
private printEpilogue(run: BenchmarkRun): void {
|
|
347
347
|
const duration = Date.now() - this.startTime;
|
|
348
|
-
const durationStr = BaseReporter.formatDuration(duration *
|
|
348
|
+
const durationStr = BaseReporter.formatDuration(duration * 1_000_000);
|
|
349
349
|
|
|
350
350
|
console.log();
|
|
351
351
|
console.log(
|
|
@@ -4,10 +4,13 @@ import type {
|
|
|
4
4
|
BudgetResult,
|
|
5
5
|
BudgetSummary,
|
|
6
6
|
BudgetViolation,
|
|
7
|
+
ResolvedBudgets,
|
|
7
8
|
TaskId,
|
|
8
9
|
TaskResult,
|
|
9
10
|
} from '../types/core.js';
|
|
10
11
|
|
|
12
|
+
import { resolveBudget } from './budget-resolver.js';
|
|
13
|
+
|
|
11
14
|
/**
|
|
12
15
|
* Service for evaluating performance budgets
|
|
13
16
|
*
|
|
@@ -34,10 +37,10 @@ export class BudgetEvaluator {
|
|
|
34
37
|
* Format time in nanoseconds to human-readable string
|
|
35
38
|
*/
|
|
36
39
|
private static formatTime(this: void, nanoseconds: number): string {
|
|
37
|
-
if (nanoseconds <
|
|
40
|
+
if (nanoseconds < 1000) {
|
|
38
41
|
return `${nanoseconds.toFixed(0)}ns`;
|
|
39
42
|
} else if (nanoseconds < 1_000_000) {
|
|
40
|
-
return `${(nanoseconds /
|
|
43
|
+
return `${(nanoseconds / 1000).toFixed(2)}μs`;
|
|
41
44
|
} else if (nanoseconds < 1_000_000_000) {
|
|
42
45
|
return `${(nanoseconds / 1_000_000).toFixed(2)}ms`;
|
|
43
46
|
} else {
|
|
@@ -49,30 +52,31 @@ export class BudgetEvaluator {
|
|
|
49
52
|
* Evaluate budgets for an entire benchmark run
|
|
50
53
|
*/
|
|
51
54
|
evaluateRun(
|
|
52
|
-
budgets:
|
|
55
|
+
budgets: ResolvedBudgets,
|
|
53
56
|
taskResults: Map<TaskId, TaskResult>,
|
|
54
57
|
baselineData?: Map<TaskId, BaselineSummaryData>,
|
|
55
58
|
): BudgetSummary {
|
|
56
59
|
const results: BudgetResult[] = [];
|
|
57
60
|
|
|
58
|
-
for (const [taskId,
|
|
59
|
-
const
|
|
61
|
+
for (const [taskId, taskResult] of taskResults) {
|
|
62
|
+
const budget = resolveBudget(taskId, budgets);
|
|
60
63
|
|
|
61
|
-
// Skip if no
|
|
62
|
-
if (!
|
|
64
|
+
// Skip if no budget matches this task
|
|
65
|
+
if (!budget) {
|
|
63
66
|
continue;
|
|
64
67
|
}
|
|
65
68
|
|
|
66
|
-
// Skip relative
|
|
67
|
-
|
|
69
|
+
// Skip if budget has ONLY relative thresholds and no baseline data
|
|
70
|
+
// (absolute budgets can still be evaluated without baseline)
|
|
71
|
+
if (budget.relative && !budget.absolute && !baselineData) {
|
|
68
72
|
continue;
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
const budgetResult = this.evaluateTask(
|
|
72
|
-
taskId
|
|
76
|
+
taskId,
|
|
73
77
|
budget,
|
|
74
78
|
taskResult,
|
|
75
|
-
baselineData?.get(taskId
|
|
79
|
+
baselineData?.get(taskId),
|
|
76
80
|
);
|
|
77
81
|
|
|
78
82
|
results.push(budgetResult);
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Budget resolution service for matching budgets to tasks
|
|
3
|
+
*
|
|
4
|
+
* This module provides pattern-based budget resolution using:
|
|
5
|
+
*
|
|
6
|
+
* - Minimatch glob patterns for file matching
|
|
7
|
+
* - Simple `*` wildcards for suite/task matching
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { minimatch } from 'minimatch';
|
|
13
|
+
|
|
14
|
+
import type {
|
|
15
|
+
Budget,
|
|
16
|
+
BudgetPattern,
|
|
17
|
+
ResolvedBudgets,
|
|
18
|
+
TaskId,
|
|
19
|
+
} from '../types/core.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Check if a glob pattern contains wildcards
|
|
23
|
+
*
|
|
24
|
+
* @param pattern - The pattern to check
|
|
25
|
+
* @returns True if the pattern contains glob metacharacters
|
|
26
|
+
*/
|
|
27
|
+
export const isGlobPattern = (pattern: string): boolean => {
|
|
28
|
+
return /[*?[\]]/.test(pattern);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Check if a file path matches a glob pattern
|
|
33
|
+
*
|
|
34
|
+
* @param pattern - Minimatch glob pattern
|
|
35
|
+
* @param filePath - File path to match against
|
|
36
|
+
* @returns True if the pattern matches the file path
|
|
37
|
+
*/
|
|
38
|
+
export const matchesFile = (pattern: string, filePath: string): boolean => {
|
|
39
|
+
// Exact match fast path
|
|
40
|
+
if (pattern === filePath) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return minimatch(filePath, pattern, { matchBase: true });
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check if a suite or task name matches a pattern
|
|
49
|
+
*
|
|
50
|
+
* @param pattern - Either an exact name or `*` for wildcard
|
|
51
|
+
* @param value - The value to match against
|
|
52
|
+
* @returns True if the pattern matches the value
|
|
53
|
+
*/
|
|
54
|
+
export const matchesSuiteOrTask = (pattern: string, value: string): boolean => {
|
|
55
|
+
return pattern === '*' || pattern === value;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Calculate specificity score for a budget pattern
|
|
60
|
+
*
|
|
61
|
+
* Higher scores indicate more specific patterns. Scoring:
|
|
62
|
+
*
|
|
63
|
+
* - File: +2 for exact match, +1 for glob with specific parts, +0 for `**\/*`
|
|
64
|
+
* - Suite: +1 for exact match, +0 for `*`
|
|
65
|
+
* - Task: +1 for exact match, +0 for `*`
|
|
66
|
+
*
|
|
67
|
+
* @param pattern - The budget pattern to score
|
|
68
|
+
* @returns Specificity score (0-4)
|
|
69
|
+
*/
|
|
70
|
+
export const calculateSpecificity = (
|
|
71
|
+
pattern: Pick<BudgetPattern, 'filePattern' | 'suitePattern' | 'taskPattern'>,
|
|
72
|
+
): number => {
|
|
73
|
+
let score = 0;
|
|
74
|
+
|
|
75
|
+
// File specificity
|
|
76
|
+
if (!isGlobPattern(pattern.filePattern)) {
|
|
77
|
+
// Exact file match
|
|
78
|
+
score += 2;
|
|
79
|
+
} else if (pattern.filePattern !== '**/*' && pattern.filePattern !== '*') {
|
|
80
|
+
// Glob with some specificity (e.g., "**/api/**/*.bench.js")
|
|
81
|
+
score += 1;
|
|
82
|
+
}
|
|
83
|
+
// else: fully generic glob like "**/*" gets +0
|
|
84
|
+
|
|
85
|
+
// Suite specificity
|
|
86
|
+
if (pattern.suitePattern !== '*') {
|
|
87
|
+
score += 1;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Task specificity
|
|
91
|
+
if (pattern.taskPattern !== '*') {
|
|
92
|
+
score += 1;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return score;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Parse a TaskId into its components
|
|
100
|
+
*
|
|
101
|
+
* TaskIds have the format: `{filePath}/{suiteName}/{taskName}` The file path
|
|
102
|
+
* can contain slashes, so we parse from the end.
|
|
103
|
+
*
|
|
104
|
+
* @param taskId - The TaskId to parse
|
|
105
|
+
* @returns Object with file, suite, and task components
|
|
106
|
+
*/
|
|
107
|
+
export const parseTaskId = (
|
|
108
|
+
taskId: TaskId,
|
|
109
|
+
): { file: string; suite: string; task: string } => {
|
|
110
|
+
const str = taskId as string;
|
|
111
|
+
const lastSlash = str.lastIndexOf('/');
|
|
112
|
+
const secondLastSlash = str.lastIndexOf('/', lastSlash - 1);
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
file: str.substring(0, secondLastSlash),
|
|
116
|
+
suite: str.substring(secondLastSlash + 1, lastSlash),
|
|
117
|
+
task: str.substring(lastSlash + 1),
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Deep merge two budget objects
|
|
123
|
+
*
|
|
124
|
+
* More specific (second) budget values override less specific (first) values.
|
|
125
|
+
* Merges at the absolute/relative level.
|
|
126
|
+
*
|
|
127
|
+
* @param base - Base budget (less specific)
|
|
128
|
+
* @param override - Override budget (more specific)
|
|
129
|
+
* @returns Merged budget
|
|
130
|
+
*/
|
|
131
|
+
export const mergeBudgets = (base: Budget, override: Budget): Budget => {
|
|
132
|
+
return {
|
|
133
|
+
...(base.absolute || override.absolute
|
|
134
|
+
? {
|
|
135
|
+
absolute: {
|
|
136
|
+
...base.absolute,
|
|
137
|
+
...override.absolute,
|
|
138
|
+
},
|
|
139
|
+
}
|
|
140
|
+
: {}),
|
|
141
|
+
...(base.relative || override.relative
|
|
142
|
+
? {
|
|
143
|
+
relative: {
|
|
144
|
+
...base.relative,
|
|
145
|
+
...override.relative,
|
|
146
|
+
},
|
|
147
|
+
}
|
|
148
|
+
: {}),
|
|
149
|
+
};
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Resolve the appropriate budget for a task
|
|
154
|
+
*
|
|
155
|
+
* Resolution order:
|
|
156
|
+
*
|
|
157
|
+
* 1. Check exact match in `exact` map (highest priority)
|
|
158
|
+
* 2. Find all matching patterns
|
|
159
|
+
* 3. Sort by specificity (ascending)
|
|
160
|
+
* 4. Merge matched budgets (more specific overrides less specific)
|
|
161
|
+
*
|
|
162
|
+
* @param taskId - The task identifier to resolve a budget for
|
|
163
|
+
* @param budgets - The resolved budgets structure
|
|
164
|
+
* @returns The resolved budget, or undefined if no budget matches
|
|
165
|
+
*/
|
|
166
|
+
export const resolveBudget = (
|
|
167
|
+
taskId: TaskId,
|
|
168
|
+
budgets: ResolvedBudgets,
|
|
169
|
+
): Budget | undefined => {
|
|
170
|
+
// Fast path: exact match
|
|
171
|
+
const exactMatch = budgets.exact[taskId as string];
|
|
172
|
+
if (exactMatch) {
|
|
173
|
+
// Still need to check patterns and merge if there are less-specific matches
|
|
174
|
+
const parsed = parseTaskId(taskId);
|
|
175
|
+
const matchingPatterns = budgets.patterns.filter(
|
|
176
|
+
(p) =>
|
|
177
|
+
matchesFile(p.filePattern, parsed.file) &&
|
|
178
|
+
matchesSuiteOrTask(p.suitePattern, parsed.suite) &&
|
|
179
|
+
matchesSuiteOrTask(p.taskPattern, parsed.task),
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
if (matchingPatterns.length === 0) {
|
|
183
|
+
return exactMatch;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Sort by specificity ascending (least specific first, so more specific can override)
|
|
187
|
+
const sorted = [...matchingPatterns].sort(
|
|
188
|
+
(a, b) => a.specificity - b.specificity,
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
// Merge all patterns, then apply exact match last
|
|
192
|
+
let merged = sorted[0]!.budget;
|
|
193
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
194
|
+
merged = mergeBudgets(merged, sorted[i]!.budget);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Exact match has highest priority
|
|
198
|
+
return mergeBudgets(merged, exactMatch);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Find all matching patterns
|
|
202
|
+
const parsed = parseTaskId(taskId);
|
|
203
|
+
const matchingPatterns = budgets.patterns.filter(
|
|
204
|
+
(p) =>
|
|
205
|
+
matchesFile(p.filePattern, parsed.file) &&
|
|
206
|
+
matchesSuiteOrTask(p.suitePattern, parsed.suite) &&
|
|
207
|
+
matchesSuiteOrTask(p.taskPattern, parsed.task),
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
if (matchingPatterns.length === 0) {
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Sort by specificity ascending (least specific first)
|
|
215
|
+
const sorted = [...matchingPatterns].sort(
|
|
216
|
+
(a, b) => a.specificity - b.specificity,
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
// Merge all matches (more specific overrides less specific)
|
|
220
|
+
let merged = sorted[0]!.budget;
|
|
221
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
222
|
+
merged = mergeBudgets(merged, sorted[i]!.budget);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return merged;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Create a BudgetPattern from its components
|
|
230
|
+
*
|
|
231
|
+
* @param filePattern - Glob pattern for file matching
|
|
232
|
+
* @param suitePattern - Suite name or `*` for wildcard
|
|
233
|
+
* @param taskPattern - Task name or `*` for wildcard
|
|
234
|
+
* @param budget - The budget to apply
|
|
235
|
+
* @returns A BudgetPattern with computed specificity
|
|
236
|
+
*/
|
|
237
|
+
export const createBudgetPattern = (
|
|
238
|
+
filePattern: string,
|
|
239
|
+
suitePattern: string,
|
|
240
|
+
taskPattern: string,
|
|
241
|
+
budget: Budget,
|
|
242
|
+
): BudgetPattern => {
|
|
243
|
+
return {
|
|
244
|
+
budget,
|
|
245
|
+
filePattern,
|
|
246
|
+
specificity: calculateSpecificity({
|
|
247
|
+
filePattern,
|
|
248
|
+
suitePattern,
|
|
249
|
+
taskPattern,
|
|
250
|
+
}),
|
|
251
|
+
suitePattern,
|
|
252
|
+
taskPattern,
|
|
253
|
+
};
|
|
254
|
+
};
|
|
@@ -63,7 +63,7 @@ const DEFAULT_CONFIG: ModestBenchConfig = {
|
|
|
63
63
|
tags: [],
|
|
64
64
|
thresholds: {},
|
|
65
65
|
time: 1000, // 1 second minimum for tinybench to gather samples
|
|
66
|
-
timeout:
|
|
66
|
+
timeout: 30_000, // 30 seconds
|
|
67
67
|
verbose: false, // No verbose output by default
|
|
68
68
|
warmup: 30, // Light warmup by default - enough for basic JIT optimization
|
|
69
69
|
};
|
|
@@ -291,7 +291,7 @@ export class ModestBenchConfigurationManager implements ConfigurationManager {
|
|
|
291
291
|
}
|
|
292
292
|
|
|
293
293
|
// Warn about potentially long runtime
|
|
294
|
-
if (validConfig.iterations > 1000 && validConfig.time >
|
|
294
|
+
if (validConfig.iterations > 1000 && validConfig.time > 60_000) {
|
|
295
295
|
warnings.push({
|
|
296
296
|
code: 'LONG_RUNTIME_WARNING',
|
|
297
297
|
file: 'configuration',
|
|
@@ -18,11 +18,11 @@ import type {
|
|
|
18
18
|
ValidationWarning,
|
|
19
19
|
} from '../types/index.js';
|
|
20
20
|
|
|
21
|
+
import { benchmarkFileSchema } from '../config/benchmark-schema.js';
|
|
21
22
|
import {
|
|
22
23
|
BENCHMARK_FILE_EXTENSIONS,
|
|
23
24
|
BENCHMARK_FILE_PATTERN,
|
|
24
25
|
} from '../constants.js';
|
|
25
|
-
import { benchmarkFileSchema } from '../core/benchmark-schema.js';
|
|
26
26
|
import {
|
|
27
27
|
FileDiscoveryError,
|
|
28
28
|
FileLoadError,
|