modestbench 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -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/run.cjs +93 -49
- package/dist/cli/commands/run.cjs.map +1 -1
- package/dist/cli/commands/run.d.cts +1 -0
- package/dist/cli/commands/run.d.cts.map +1 -1
- package/dist/cli/commands/run.d.ts +1 -0
- package/dist/cli/commands/run.d.ts.map +1 -1
- package/dist/cli/commands/run.js +95 -51
- package/dist/cli/commands/run.js.map +1 -1
- package/dist/cli/index.cjs +7 -1
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.d.cts.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +7 -1
- 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/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/constants.cjs +2 -0
- package/dist/constants.cjs.map +1 -1
- package/dist/constants.d.cts +2 -0
- package/dist/constants.d.cts.map +1 -1
- package/dist/constants.d.ts +2 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +2 -0
- package/dist/constants.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/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/errors/index.cjs +3 -1
- package/dist/errors/index.cjs.map +1 -1
- package/dist/errors/index.d.cts +1 -1
- package/dist/errors/index.d.cts.map +1 -1
- package/dist/errors/index.d.ts +1 -1
- package/dist/errors/index.d.ts.map +1 -1
- package/dist/errors/index.js +1 -1
- package/dist/errors/index.js.map +1 -1
- package/dist/errors/reporter.cjs +45 -1
- package/dist/errors/reporter.cjs.map +1 -1
- package/dist/errors/reporter.d.cts +32 -0
- package/dist/errors/reporter.d.cts.map +1 -1
- package/dist/errors/reporter.d.ts +32 -0
- package/dist/errors/reporter.d.ts.map +1 -1
- package/dist/errors/reporter.js +42 -0
- package/dist/errors/reporter.js.map +1 -1
- package/dist/index.cjs +16 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.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/schema/modestbench-config.schema.json +94 -87
- package/dist/services/budget-evaluator.cjs +8 -6
- 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 +8 -6
- 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/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-loader.cjs +281 -0
- package/dist/services/reporter-loader.cjs.map +1 -0
- package/dist/services/reporter-loader.d.cts +67 -0
- package/dist/services/reporter-loader.d.cts.map +1 -0
- package/dist/services/reporter-loader.d.ts +67 -0
- package/dist/services/reporter-loader.d.ts.map +1 -0
- package/dist/services/reporter-loader.js +241 -0
- package/dist/services/reporter-loader.js.map +1 -0
- 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/dist/types/index.cjs.map +1 -1
- package/dist/types/index.d.cts +1 -0
- package/dist/types/index.d.cts.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/plugin.cjs +9 -0
- package/dist/types/plugin.cjs.map +1 -0
- package/dist/types/plugin.d.cts +179 -0
- package/dist/types/plugin.d.cts.map +1 -0
- package/dist/types/plugin.d.ts +179 -0
- package/dist/types/plugin.d.ts.map +1 -0
- package/dist/types/plugin.js +8 -0
- package/dist/types/plugin.js.map +1 -0
- package/dist/utils/package.cjs +66 -5
- package/dist/utils/package.cjs.map +1 -1
- package/dist/utils/package.d.cts +6 -0
- package/dist/utils/package.d.cts.map +1 -1
- package/dist/utils/package.d.ts +6 -0
- package/dist/utils/package.d.ts.map +1 -1
- package/dist/utils/package.js +31 -1
- package/dist/utils/package.js.map +1 -1
- package/dist/utils/reporter-utils.cjs +90 -0
- package/dist/utils/reporter-utils.cjs.map +1 -0
- package/dist/utils/reporter-utils.d.cts +42 -0
- package/dist/utils/reporter-utils.d.cts.map +1 -0
- package/dist/utils/reporter-utils.d.ts +42 -0
- package/dist/utils/reporter-utils.d.ts.map +1 -0
- package/dist/utils/reporter-utils.js +83 -0
- package/dist/utils/reporter-utils.js.map +1 -0
- package/package.json +20 -9
- package/src/adapters/types.ts +1 -1
- package/src/cli/commands/run.ts +140 -69
- package/src/cli/index.ts +8 -1
- package/src/{core → config}/benchmark-schema.ts +1 -1
- package/src/config/schema.ts +379 -302
- package/src/constants.ts +2 -0
- package/src/core/engine.ts +74 -69
- package/src/core/output-path-resolver.ts +14 -0
- package/src/errors/index.ts +2 -0
- package/src/errors/reporter.ts +55 -0
- package/src/index.ts +19 -1
- package/src/reporters/json.ts +1 -1
- package/src/services/budget-evaluator.ts +13 -9
- package/src/services/budget-resolver.ts +254 -0
- package/src/services/file-loader.ts +1 -1
- package/src/services/reporter-loader.ts +323 -0
- package/src/types/budgets.ts +38 -0
- package/src/types/core.ts +64 -99
- package/src/types/index.ts +3 -0
- package/src/types/plugin.ts +197 -0
- package/src/utils/package.ts +32 -1
- package/src/utils/reporter-utils.ts +85 -0
- 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/constants.ts
CHANGED
|
@@ -110,6 +110,8 @@ export const ErrorCodes = {
|
|
|
110
110
|
|
|
111
111
|
//#region reporter-errors
|
|
112
112
|
REPORTER_ALREADY_REGISTERED: 'ERR_MB_REPORTER_ALREADY_REGISTERED',
|
|
113
|
+
REPORTER_INVALID: 'ERR_MB_REPORTER_INVALID',
|
|
114
|
+
REPORTER_LOAD_FAILED: 'ERR_MB_REPORTER_LOAD_FAILED',
|
|
113
115
|
REPORTER_OUTPUT_FAILED: 'ERR_MB_REPORTER_OUTPUT_FAILED',
|
|
114
116
|
REPORTER_UNKNOWN: 'ERR_MB_REPORTER_UNKNOWN',
|
|
115
117
|
//#endregion
|
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
|
|
|
@@ -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
|
*
|
package/src/errors/index.ts
CHANGED
package/src/errors/reporter.ts
CHANGED
|
@@ -16,6 +16,26 @@ export class ReporterAlreadyRegisteredError extends ModestBenchError {
|
|
|
16
16
|
readonly code = 'ERR_MB_REPORTER_ALREADY_REGISTERED';
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Reporter load failed
|
|
21
|
+
*
|
|
22
|
+
* Thrown when a reporter module cannot be loaded (file not found, syntax error,
|
|
23
|
+
* invalid module format, etc.).
|
|
24
|
+
*/
|
|
25
|
+
export class ReporterLoadError extends ModestBenchError {
|
|
26
|
+
readonly code = 'ERR_MB_REPORTER_LOAD_FAILED';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The specifier (file path or package name) that failed to load
|
|
30
|
+
*/
|
|
31
|
+
readonly specifier: string;
|
|
32
|
+
|
|
33
|
+
constructor(message: string, specifier: string, options?: ErrorOptions) {
|
|
34
|
+
super(`Failed to load reporter "${specifier}": ${message}`, options);
|
|
35
|
+
this.specifier = specifier;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
19
39
|
/**
|
|
20
40
|
* Reporter output failed
|
|
21
41
|
*
|
|
@@ -25,6 +45,41 @@ export class ReporterOutputError extends ModestBenchError {
|
|
|
25
45
|
readonly code = 'ERR_MB_REPORTER_OUTPUT_FAILED';
|
|
26
46
|
}
|
|
27
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Reporter validation failed
|
|
50
|
+
*
|
|
51
|
+
* Thrown when a loaded module does not implement the required Reporter
|
|
52
|
+
* interface methods.
|
|
53
|
+
*/
|
|
54
|
+
export class ReporterValidationError extends ModestBenchError {
|
|
55
|
+
readonly code = 'ERR_MB_REPORTER_INVALID';
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The methods that are missing from the reporter
|
|
59
|
+
*/
|
|
60
|
+
readonly missingMethods: string[];
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The specifier (file path or package name) of the invalid reporter
|
|
64
|
+
*/
|
|
65
|
+
readonly specifier: string;
|
|
66
|
+
|
|
67
|
+
constructor(
|
|
68
|
+
message: string,
|
|
69
|
+
specifier: string,
|
|
70
|
+
missingMethods: string[] = [],
|
|
71
|
+
options?: ErrorOptions,
|
|
72
|
+
) {
|
|
73
|
+
const methodsInfo =
|
|
74
|
+
missingMethods.length > 0
|
|
75
|
+
? ` Missing required methods: ${missingMethods.join(', ')}.`
|
|
76
|
+
: '';
|
|
77
|
+
super(`Invalid reporter "${specifier}": ${message}${methodsInfo}`, options);
|
|
78
|
+
this.specifier = specifier;
|
|
79
|
+
this.missingMethods = missingMethods;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
28
83
|
/**
|
|
29
84
|
* Unknown reporter
|
|
30
85
|
*
|
package/src/index.ts
CHANGED
|
@@ -41,6 +41,15 @@ export { parseProfile } from './services/profiler/profile-parser.js';
|
|
|
41
41
|
|
|
42
42
|
export { runWithProfiling } from './services/profiler/profile-runner.js';
|
|
43
43
|
export { ModestBenchProgressManager } from './services/progress-manager.js';
|
|
44
|
+
// Reporter plugin loader
|
|
45
|
+
export {
|
|
46
|
+
createReporterContext,
|
|
47
|
+
isBuiltInReporter,
|
|
48
|
+
isFilePath,
|
|
49
|
+
loadReporter,
|
|
50
|
+
PLUGIN_API_VERSION,
|
|
51
|
+
} from './services/reporter-loader.js';
|
|
52
|
+
|
|
44
53
|
export {
|
|
45
54
|
BaseReporter,
|
|
46
55
|
CompositeReporter,
|
|
@@ -51,4 +60,13 @@ export {
|
|
|
51
60
|
export * from './types/index.js';
|
|
52
61
|
|
|
53
62
|
// Utilities
|
|
54
|
-
export { findPackageRoot } from './utils/package.js';
|
|
63
|
+
export { findPackageRoot, getPackageVersion } from './utils/package.js';
|
|
64
|
+
|
|
65
|
+
// Reporter utilities (for plugin authors)
|
|
66
|
+
export {
|
|
67
|
+
formatBytes,
|
|
68
|
+
formatDuration,
|
|
69
|
+
formatOpsPerSecond,
|
|
70
|
+
formatPercentage,
|
|
71
|
+
reporterUtils,
|
|
72
|
+
} from './utils/reporter-utils.js';
|
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
|
}
|
|
@@ -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
|
*
|
|
@@ -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
|
+
};
|
|
@@ -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,
|