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/config/schema.ts
CHANGED
|
@@ -8,11 +8,46 @@
|
|
|
8
8
|
|
|
9
9
|
import * as z from 'zod';
|
|
10
10
|
|
|
11
|
-
import type {
|
|
11
|
+
import type {
|
|
12
|
+
Budget,
|
|
13
|
+
BudgetPattern,
|
|
14
|
+
ResolvedBudgets,
|
|
15
|
+
} from '../types/budgets.js';
|
|
12
16
|
|
|
13
17
|
import { BENCHMARK_FILE_PATTERN } from '../constants.js';
|
|
18
|
+
import {
|
|
19
|
+
createBudgetPattern,
|
|
20
|
+
isGlobPattern,
|
|
21
|
+
} from '../services/budget-resolver.js';
|
|
14
22
|
import { parsePercentageString, parseTimeString } from './budget-schema.js';
|
|
15
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Schema for JSON reporter configuration options
|
|
26
|
+
*/
|
|
27
|
+
export const jsonReporterConfigSchema = z.object({
|
|
28
|
+
prettyPrint: z
|
|
29
|
+
.boolean()
|
|
30
|
+
.optional()
|
|
31
|
+
.describe('Whether to pretty-print JSON output (default: false)'),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Schema for reporter-specific configuration
|
|
36
|
+
*
|
|
37
|
+
* Allows typed configuration for known reporters while permitting unknown
|
|
38
|
+
* reporter configs via catchall.
|
|
39
|
+
*/
|
|
40
|
+
export const reporterConfigSchema = z
|
|
41
|
+
.object({
|
|
42
|
+
json: jsonReporterConfigSchema
|
|
43
|
+
.optional()
|
|
44
|
+
.describe('Configuration options for the JSON reporter'),
|
|
45
|
+
})
|
|
46
|
+
.catchall(z.unknown())
|
|
47
|
+
.describe(
|
|
48
|
+
'Configuration options specific to individual reporters, keyed by reporter name',
|
|
49
|
+
);
|
|
50
|
+
|
|
16
51
|
/**
|
|
17
52
|
* Schema for threshold configuration
|
|
18
53
|
*
|
|
@@ -58,11 +93,11 @@ const thresholdConfigSchema = z
|
|
|
58
93
|
});
|
|
59
94
|
|
|
60
95
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
96
|
+
* Input schema for budget values (before transformation)
|
|
97
|
+
*
|
|
98
|
+
* Accepts string values like "10ms" or "10%" for human-readable configuration.
|
|
64
99
|
*/
|
|
65
|
-
const
|
|
100
|
+
const budgetInputSchema = z
|
|
66
101
|
.object({
|
|
67
102
|
absolute: z
|
|
68
103
|
.object({
|
|
@@ -72,6 +107,7 @@ const budgetSchema = z
|
|
|
72
107
|
.describe('Maximum 99th percentile in nanoseconds or time string'),
|
|
73
108
|
maxTime: z
|
|
74
109
|
.union([z.number().positive(), z.string()])
|
|
110
|
+
.optional()
|
|
75
111
|
.describe(
|
|
76
112
|
'Maximum mean time in nanoseconds or time string (e.g., "10ms")',
|
|
77
113
|
),
|
|
@@ -98,324 +134,367 @@ const budgetSchema = z
|
|
|
98
134
|
.describe('Performance budget with absolute and/or relative thresholds');
|
|
99
135
|
|
|
100
136
|
/**
|
|
101
|
-
*
|
|
137
|
+
* Transform budget values (parse time/percentage strings to numbers)
|
|
102
138
|
*
|
|
103
|
-
*
|
|
104
|
-
* configuration from all sources (files, CLI args, defaults).
|
|
139
|
+
* Returns a Budget object with all string values converted to numbers.
|
|
105
140
|
*/
|
|
106
|
-
const
|
|
107
|
-
.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
excludeTags: z
|
|
142
|
-
.array(z.string())
|
|
143
|
-
.describe(
|
|
144
|
-
'Tags to exclude from benchmark execution. Benchmarks matching any of these tags will be skipped.',
|
|
145
|
-
),
|
|
146
|
-
iterations: z
|
|
147
|
-
.number()
|
|
148
|
-
.int()
|
|
149
|
-
.positive()
|
|
150
|
-
.describe(
|
|
151
|
-
'Default number of iterations to run for each benchmark task. Higher values provide more accurate statistics but take longer to execute.',
|
|
152
|
-
),
|
|
153
|
-
limitBy: z
|
|
154
|
-
.enum(['time', 'iterations', 'any', 'all'])
|
|
155
|
-
.describe(
|
|
156
|
-
'How to limit benchmark execution: "time" stops after time limit, "iterations" stops after iteration count, "any" stops at whichever comes first, "all" runs until both limits are reached',
|
|
157
|
-
),
|
|
158
|
-
metadata: z
|
|
159
|
-
.record(z.string(), z.unknown())
|
|
160
|
-
.describe(
|
|
161
|
-
'Custom metadata to attach to benchmark runs. Can include project name, version, environment details, etc.',
|
|
162
|
-
),
|
|
163
|
-
outputDir: z
|
|
164
|
-
.string()
|
|
165
|
-
.min(1)
|
|
166
|
-
.optional()
|
|
167
|
-
.describe(
|
|
168
|
-
'Directory path where benchmark results and reports will be written. If not specified, data reporters will write to stdout.',
|
|
169
|
-
),
|
|
170
|
-
pattern: z
|
|
171
|
-
.union([z.string().min(1), z.array(z.string().min(1))])
|
|
172
|
-
.describe(
|
|
173
|
-
`Glob pattern(s) for discovering benchmark files. Can be a single pattern string or array of patterns (e.g., "**/*${BENCHMARK_FILE_PATTERN}")`,
|
|
174
|
-
),
|
|
175
|
-
profile: z
|
|
176
|
-
.object({
|
|
177
|
-
exclude: z
|
|
178
|
-
.array(z.string())
|
|
179
|
-
.optional()
|
|
180
|
-
.describe('Glob patterns to exclude from profiling results'),
|
|
181
|
-
focus: z
|
|
182
|
-
.array(z.string())
|
|
183
|
-
.optional()
|
|
184
|
-
.describe(
|
|
185
|
-
'Glob patterns to focus on in profiling results. If specified, only matching files will be shown',
|
|
186
|
-
),
|
|
187
|
-
minCallCount: z
|
|
188
|
-
.number()
|
|
189
|
-
.int()
|
|
190
|
-
.nonnegative()
|
|
191
|
-
.optional()
|
|
192
|
-
.describe(
|
|
193
|
-
'Minimum number of times a function must be called to be included in results',
|
|
194
|
-
),
|
|
195
|
-
minExecutionPercent: z
|
|
196
|
-
.number()
|
|
197
|
-
.nonnegative()
|
|
198
|
-
.max(100)
|
|
199
|
-
.default(1.0)
|
|
200
|
-
.describe(
|
|
201
|
-
'Minimum execution percentage threshold for including functions in results',
|
|
202
|
-
),
|
|
203
|
-
outputFile: z
|
|
204
|
-
.string()
|
|
205
|
-
.optional()
|
|
206
|
-
.describe('Path to write profile report to file'),
|
|
207
|
-
smartDetection: z
|
|
208
|
-
.boolean()
|
|
209
|
-
.default(true)
|
|
210
|
-
.describe(
|
|
211
|
-
'Automatically detect and focus on user code, excluding node_modules and Node.js internals',
|
|
212
|
-
),
|
|
213
|
-
topN: z
|
|
214
|
-
.number()
|
|
215
|
-
.int()
|
|
216
|
-
.positive()
|
|
217
|
-
.default(25)
|
|
218
|
-
.describe('Maximum number of top functions to show in results'),
|
|
219
|
-
})
|
|
220
|
-
.optional()
|
|
221
|
-
.describe(
|
|
222
|
-
'Configuration for profile command to identify benchmark candidates',
|
|
223
|
-
),
|
|
224
|
-
quiet: z
|
|
225
|
-
.boolean()
|
|
226
|
-
.describe(
|
|
227
|
-
'Run in quiet mode with minimal console output (only errors and final results)',
|
|
228
|
-
),
|
|
229
|
-
reporterConfig: z
|
|
230
|
-
.record(z.string(), z.unknown())
|
|
231
|
-
.describe(
|
|
232
|
-
'Configuration options specific to individual reporters, keyed by reporter name',
|
|
233
|
-
),
|
|
234
|
-
reporters: z
|
|
235
|
-
.array(z.string())
|
|
236
|
-
.min(1)
|
|
237
|
-
.describe(
|
|
238
|
-
'List of reporter names to use for output. Available reporters: "human", "json", "csv"',
|
|
239
|
-
),
|
|
240
|
-
tags: z
|
|
241
|
-
.array(z.string())
|
|
242
|
-
.describe(
|
|
243
|
-
'Tags to filter which benchmarks to run. If empty, all benchmarks are included. Only benchmarks with matching tags will execute.',
|
|
244
|
-
),
|
|
245
|
-
thresholds: thresholdConfigSchema,
|
|
246
|
-
time: z
|
|
247
|
-
.number()
|
|
248
|
-
.int()
|
|
249
|
-
.positive()
|
|
250
|
-
.describe(
|
|
251
|
-
'Maximum time to spend on each benchmark task in milliseconds. Tasks will run at least until this duration or iteration count is reached, depending on limitBy setting.',
|
|
252
|
-
),
|
|
253
|
-
timeout: z
|
|
254
|
-
.number()
|
|
255
|
-
.int()
|
|
256
|
-
.positive()
|
|
257
|
-
.describe(
|
|
258
|
-
'Timeout for individual benchmark tasks in milliseconds. Tasks exceeding this duration will be terminated and marked as failed.',
|
|
259
|
-
),
|
|
260
|
-
verbose: z
|
|
261
|
-
.boolean()
|
|
262
|
-
.describe(
|
|
263
|
-
'Enable verbose output. Provides more detailed console output including progress, intermediate results, and diagnostic information',
|
|
264
|
-
),
|
|
265
|
-
warmup: z
|
|
266
|
-
.number()
|
|
267
|
-
.int()
|
|
268
|
-
.nonnegative()
|
|
269
|
-
.describe(
|
|
270
|
-
'Number of warmup iterations to run before measurement begins. Warmup helps stabilize performance by allowing JIT compilation and caching to occur.',
|
|
271
|
-
),
|
|
272
|
-
})
|
|
273
|
-
.strict()
|
|
274
|
-
.describe(
|
|
275
|
-
'ModestBench configuration for controlling benchmark discovery, execution, and reporting',
|
|
276
|
-
)
|
|
277
|
-
.meta({
|
|
278
|
-
title: 'ModestBench Configuration',
|
|
279
|
-
});
|
|
141
|
+
const transformBudgetValues = (
|
|
142
|
+
budget: z.infer<typeof budgetInputSchema>,
|
|
143
|
+
): Budget => {
|
|
144
|
+
return {
|
|
145
|
+
// Build absolute budget object if present
|
|
146
|
+
absolute: budget.absolute
|
|
147
|
+
? {
|
|
148
|
+
maxP99:
|
|
149
|
+
budget.absolute.maxP99 !== undefined
|
|
150
|
+
? typeof budget.absolute.maxP99 === 'string'
|
|
151
|
+
? parseTimeString(budget.absolute.maxP99)
|
|
152
|
+
: budget.absolute.maxP99
|
|
153
|
+
: undefined,
|
|
154
|
+
maxTime:
|
|
155
|
+
budget.absolute.maxTime !== undefined
|
|
156
|
+
? typeof budget.absolute.maxTime === 'string'
|
|
157
|
+
? parseTimeString(budget.absolute.maxTime)
|
|
158
|
+
: budget.absolute.maxTime
|
|
159
|
+
: undefined,
|
|
160
|
+
minOpsPerSec: budget.absolute.minOpsPerSec,
|
|
161
|
+
}
|
|
162
|
+
: undefined,
|
|
163
|
+
// Build relative budget object if present
|
|
164
|
+
relative: budget.relative
|
|
165
|
+
? {
|
|
166
|
+
maxRegression:
|
|
167
|
+
budget.relative.maxRegression !== undefined
|
|
168
|
+
? typeof budget.relative.maxRegression === 'string'
|
|
169
|
+
? parsePercentageString(budget.relative.maxRegression)
|
|
170
|
+
: budget.relative.maxRegression
|
|
171
|
+
: undefined,
|
|
172
|
+
}
|
|
173
|
+
: undefined,
|
|
174
|
+
};
|
|
175
|
+
};
|
|
280
176
|
|
|
281
177
|
/**
|
|
282
|
-
*
|
|
178
|
+
* Budget schema with transform for string-to-number conversion
|
|
283
179
|
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
180
|
+
* Input: Budget with string values like "10ms" or "10%" Output: Budget with
|
|
181
|
+
* numeric values only
|
|
286
182
|
*/
|
|
287
|
-
|
|
288
|
-
Partial<ModestBenchConfig>
|
|
289
|
-
> = modestBenchConfigSchema.partial();
|
|
183
|
+
const budgetSchema = budgetInputSchema.transform(transformBudgetValues);
|
|
290
184
|
|
|
291
185
|
/**
|
|
292
|
-
* Input
|
|
186
|
+
* Input schema for budgets (nested file → suite → task → budget structure)
|
|
187
|
+
* without transforms - used for JSON Schema generation.
|
|
293
188
|
*/
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
minOpsPerSec?: number;
|
|
299
|
-
};
|
|
300
|
-
relative?: {
|
|
301
|
-
maxRegression?: number | string;
|
|
302
|
-
};
|
|
303
|
-
}
|
|
189
|
+
const budgetsRawInputSchema = z.record(
|
|
190
|
+
z.string(),
|
|
191
|
+
z.record(z.string(), z.record(z.string(), budgetInputSchema)),
|
|
192
|
+
);
|
|
304
193
|
|
|
305
194
|
/**
|
|
306
|
-
*
|
|
195
|
+
* Input schema for budgets with individual budget transforms applied.
|
|
196
|
+
*
|
|
197
|
+
* Used to validate the human-readable nested format from config files.
|
|
307
198
|
*/
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
minOpsPerSec?: number;
|
|
313
|
-
};
|
|
314
|
-
relative?: {
|
|
315
|
-
maxRegression?: number;
|
|
316
|
-
};
|
|
317
|
-
}
|
|
199
|
+
const budgetsInputSchema = z.record(
|
|
200
|
+
z.string(),
|
|
201
|
+
z.record(z.string(), z.record(z.string(), budgetSchema)),
|
|
202
|
+
);
|
|
318
203
|
|
|
319
204
|
/**
|
|
320
|
-
*
|
|
205
|
+
* Check if a suite or task name is a wildcard
|
|
206
|
+
*
|
|
207
|
+
* @param name - The suite or task name
|
|
208
|
+
* @returns True if the name is a wildcard (`*`)
|
|
321
209
|
*/
|
|
322
|
-
const
|
|
323
|
-
const transformed: BudgetOutput = {};
|
|
210
|
+
const isWildcard = (name: string): boolean => name === '*';
|
|
324
211
|
|
|
325
|
-
|
|
326
|
-
|
|
212
|
+
/**
|
|
213
|
+
* Check if a budget entry contains any wildcards or glob patterns
|
|
214
|
+
*
|
|
215
|
+
* @param file - File pattern
|
|
216
|
+
* @param suite - Suite name or wildcard
|
|
217
|
+
* @param task - Task name or wildcard
|
|
218
|
+
* @returns True if any part contains wildcards
|
|
219
|
+
*/
|
|
220
|
+
const hasWildcards = (file: string, suite: string, task: string): boolean => {
|
|
221
|
+
return isGlobPattern(file) || isWildcard(suite) || isWildcard(task);
|
|
222
|
+
};
|
|
327
223
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
224
|
+
/**
|
|
225
|
+
* Transform nested budget structure to ResolvedBudgets with exact matches and
|
|
226
|
+
* patterns separated
|
|
227
|
+
*
|
|
228
|
+
* @param nested - Nested budgets structure (file → suite → task → budget)
|
|
229
|
+
* @returns ResolvedBudgets with exact matches and wildcard patterns
|
|
230
|
+
*/
|
|
231
|
+
const flattenBudgets = (
|
|
232
|
+
nested: z.infer<typeof budgetsInputSchema>,
|
|
233
|
+
): ResolvedBudgets => {
|
|
234
|
+
const exact: Record<string, Budget> = {};
|
|
235
|
+
const patterns: BudgetPattern[] = [];
|
|
332
236
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
237
|
+
for (const [file, suites] of Object.entries(nested)) {
|
|
238
|
+
for (const [suite, tasks] of Object.entries(suites)) {
|
|
239
|
+
for (const [task, budget] of Object.entries(tasks)) {
|
|
240
|
+
if (hasWildcards(file, suite, task)) {
|
|
241
|
+
// This is a pattern budget
|
|
242
|
+
patterns.push(createBudgetPattern(file, suite, task, budget));
|
|
243
|
+
} else {
|
|
244
|
+
// This is an exact match
|
|
245
|
+
const taskId = `${file}/${suite}/${task}`;
|
|
246
|
+
exact[taskId] = budget;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
345
249
|
}
|
|
346
250
|
}
|
|
347
251
|
|
|
348
|
-
|
|
349
|
-
|
|
252
|
+
// Sort patterns by specificity descending for consistent iteration order
|
|
253
|
+
patterns.sort((a, b) => b.specificity - a.specificity);
|
|
350
254
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
255
|
+
return { exact, patterns };
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Budgets schema with transform for nested-to-ResolvedBudgets conversion
|
|
260
|
+
*
|
|
261
|
+
* Input: { [file]: { [suite]: { [task]: Budget } } } Output: ResolvedBudgets {
|
|
262
|
+
* exact: { [taskId]: Budget }, patterns: BudgetPattern[] }
|
|
263
|
+
*/
|
|
264
|
+
const budgetsSchema = budgetsInputSchema.transform(flattenBudgets);
|
|
359
265
|
|
|
360
|
-
|
|
266
|
+
/**
|
|
267
|
+
* Shared configuration properties (everything except budgets)
|
|
268
|
+
*
|
|
269
|
+
* These properties are identical between the runtime schema (with transforms)
|
|
270
|
+
* and the JSON Schema generation schema (without transforms).
|
|
271
|
+
*/
|
|
272
|
+
const baseConfigProperties = {
|
|
273
|
+
$schema: z
|
|
274
|
+
.string()
|
|
275
|
+
.optional()
|
|
276
|
+
.describe(
|
|
277
|
+
'JSON Schema reference for IDE support (not used by ModestBench)',
|
|
278
|
+
),
|
|
279
|
+
bail: z.boolean().describe('Stop benchmark execution on first failure'),
|
|
280
|
+
baseline: z
|
|
281
|
+
.string()
|
|
282
|
+
.optional()
|
|
283
|
+
.describe(
|
|
284
|
+
'Name of baseline to use for relative budget comparisons. Must match a saved baseline name.',
|
|
285
|
+
),
|
|
286
|
+
budgetMode: z
|
|
287
|
+
.enum(['fail', 'warn', 'report'])
|
|
288
|
+
.optional()
|
|
289
|
+
.describe(
|
|
290
|
+
'How to handle budget violations: "fail" exits with error (default), "warn" shows warnings, "report" includes in output without failing',
|
|
291
|
+
),
|
|
292
|
+
exclude: z
|
|
293
|
+
.array(z.string())
|
|
294
|
+
.describe(
|
|
295
|
+
'Glob patterns to exclude from benchmark file discovery (e.g., "node_modules/**", ".git/**")',
|
|
296
|
+
),
|
|
297
|
+
excludeTags: z
|
|
298
|
+
.array(z.string())
|
|
299
|
+
.describe(
|
|
300
|
+
'Tags to exclude from benchmark execution. Benchmarks matching any of these tags will be skipped.',
|
|
301
|
+
),
|
|
302
|
+
iterations: z
|
|
303
|
+
.number()
|
|
304
|
+
.int()
|
|
305
|
+
.positive()
|
|
306
|
+
.describe(
|
|
307
|
+
'Default number of iterations to run for each benchmark task. Higher values provide more accurate statistics but take longer to execute.',
|
|
308
|
+
),
|
|
309
|
+
limitBy: z
|
|
310
|
+
.enum(['time', 'iterations', 'any', 'all'])
|
|
311
|
+
.describe(
|
|
312
|
+
'How to limit benchmark execution: "time" stops after time limit, "iterations" stops after iteration count, "any" stops at whichever comes first, "all" runs until both limits are reached',
|
|
313
|
+
),
|
|
314
|
+
metadata: z
|
|
315
|
+
.record(z.string(), z.unknown())
|
|
316
|
+
.describe(
|
|
317
|
+
'Custom metadata to attach to benchmark runs. Can include project name, version, environment details, etc.',
|
|
318
|
+
),
|
|
319
|
+
outputDir: z
|
|
320
|
+
.string()
|
|
321
|
+
.min(1)
|
|
322
|
+
.optional()
|
|
323
|
+
.describe(
|
|
324
|
+
'Directory path where benchmark results and reports will be written. If not specified, data reporters will write to stdout.',
|
|
325
|
+
),
|
|
326
|
+
pattern: z
|
|
327
|
+
.union([z.string().min(1), z.array(z.string().min(1))])
|
|
328
|
+
.describe(
|
|
329
|
+
`Glob pattern(s) for discovering benchmark files. Can be a single pattern string or array of patterns (e.g., "**/*${BENCHMARK_FILE_PATTERN}")`,
|
|
330
|
+
),
|
|
331
|
+
profile: z
|
|
332
|
+
.object({
|
|
333
|
+
exclude: z
|
|
334
|
+
.array(z.string())
|
|
335
|
+
.optional()
|
|
336
|
+
.describe('Glob patterns to exclude from profiling results'),
|
|
337
|
+
focus: z
|
|
338
|
+
.array(z.string())
|
|
339
|
+
.optional()
|
|
340
|
+
.describe(
|
|
341
|
+
'Glob patterns to focus on in profiling results. If specified, only matching files will be shown',
|
|
342
|
+
),
|
|
343
|
+
minCallCount: z
|
|
344
|
+
.number()
|
|
345
|
+
.int()
|
|
346
|
+
.nonnegative()
|
|
347
|
+
.optional()
|
|
348
|
+
.describe(
|
|
349
|
+
'Minimum number of times a function must be called to be included in results',
|
|
350
|
+
),
|
|
351
|
+
minExecutionPercent: z
|
|
352
|
+
.number()
|
|
353
|
+
.nonnegative()
|
|
354
|
+
.max(100)
|
|
355
|
+
.default(1.0)
|
|
356
|
+
.describe(
|
|
357
|
+
'Minimum execution percentage threshold for including functions in results',
|
|
358
|
+
),
|
|
359
|
+
outputFile: z
|
|
360
|
+
.string()
|
|
361
|
+
.optional()
|
|
362
|
+
.describe('Path to write profile report to file'),
|
|
363
|
+
smartDetection: z
|
|
364
|
+
.boolean()
|
|
365
|
+
.default(true)
|
|
366
|
+
.describe(
|
|
367
|
+
'Automatically detect and focus on user code, excluding node_modules and Node.js internals',
|
|
368
|
+
),
|
|
369
|
+
topN: z
|
|
370
|
+
.number()
|
|
371
|
+
.int()
|
|
372
|
+
.positive()
|
|
373
|
+
.default(25)
|
|
374
|
+
.describe('Maximum number of top functions to show in results'),
|
|
375
|
+
})
|
|
376
|
+
.optional()
|
|
377
|
+
.describe(
|
|
378
|
+
'Configuration for profile command to identify benchmark candidates',
|
|
379
|
+
),
|
|
380
|
+
quiet: z
|
|
381
|
+
.boolean()
|
|
382
|
+
.describe(
|
|
383
|
+
'Run in quiet mode with minimal console output (only errors and final results)',
|
|
384
|
+
),
|
|
385
|
+
reporterConfig: reporterConfigSchema,
|
|
386
|
+
reporters: z
|
|
387
|
+
.array(z.string())
|
|
388
|
+
.min(1)
|
|
389
|
+
.describe(
|
|
390
|
+
'List of reporter names to use for output. Available reporters: "human", "json", "csv"',
|
|
391
|
+
),
|
|
392
|
+
tags: z
|
|
393
|
+
.array(z.string())
|
|
394
|
+
.describe(
|
|
395
|
+
'Tags to filter which benchmarks to run. If empty, all benchmarks are included. Only benchmarks with matching tags will execute.',
|
|
396
|
+
),
|
|
397
|
+
thresholds: thresholdConfigSchema,
|
|
398
|
+
time: z
|
|
399
|
+
.number()
|
|
400
|
+
.int()
|
|
401
|
+
.positive()
|
|
402
|
+
.describe(
|
|
403
|
+
'Maximum time to spend on each benchmark task in milliseconds. Tasks will run at least until this duration or iteration count is reached, depending on limitBy setting.',
|
|
404
|
+
),
|
|
405
|
+
timeout: z
|
|
406
|
+
.number()
|
|
407
|
+
.int()
|
|
408
|
+
.positive()
|
|
409
|
+
.describe(
|
|
410
|
+
'Timeout for individual benchmark tasks in milliseconds. Tasks exceeding this duration will be terminated and marked as failed.',
|
|
411
|
+
),
|
|
412
|
+
verbose: z
|
|
413
|
+
.boolean()
|
|
414
|
+
.describe(
|
|
415
|
+
'Enable verbose output. Provides more detailed console output including progress, intermediate results, and diagnostic information',
|
|
416
|
+
),
|
|
417
|
+
warmup: z
|
|
418
|
+
.number()
|
|
419
|
+
.int()
|
|
420
|
+
.nonnegative()
|
|
421
|
+
.describe(
|
|
422
|
+
'Number of warmup iterations to run before measurement begins. Warmup helps stabilize performance by allowing JIT compilation and caching to occur.',
|
|
423
|
+
),
|
|
361
424
|
};
|
|
362
425
|
|
|
426
|
+
/** Description for the budgets field */
|
|
427
|
+
const budgetsDescription =
|
|
428
|
+
'Performance budgets organized by file → suite → task. Budgets define acceptable performance thresholds. Supports wildcards (* for suite/task, glob patterns for files).';
|
|
429
|
+
|
|
430
|
+
/** Description and metadata for the config schema */
|
|
431
|
+
const configSchemaDescription =
|
|
432
|
+
'ModestBench configuration for controlling benchmark discovery, execution, and reporting';
|
|
433
|
+
const configSchemaMeta = { title: 'ModestBench Configuration' };
|
|
434
|
+
|
|
363
435
|
/**
|
|
364
|
-
*
|
|
365
|
-
*
|
|
436
|
+
* Schema for the main ModestBench configuration
|
|
437
|
+
*
|
|
438
|
+
* This is the complete configuration schema used for validating benchmark
|
|
439
|
+
* configuration from all sources (files, CLI args, defaults).
|
|
366
440
|
*
|
|
367
|
-
*
|
|
441
|
+
* The budgets field uses transforms to:
|
|
442
|
+
*
|
|
443
|
+
* 1. Parse string values like "10ms" or "10%" to numbers
|
|
444
|
+
* 2. Separate exact matches from wildcard patterns into ResolvedBudgets
|
|
368
445
|
*/
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
446
|
+
const modestBenchConfigSchema = z
|
|
447
|
+
.object({
|
|
448
|
+
...baseConfigProperties,
|
|
449
|
+
budgets: budgetsSchema.optional().describe(budgetsDescription),
|
|
450
|
+
})
|
|
451
|
+
.strict()
|
|
452
|
+
.describe(configSchemaDescription)
|
|
453
|
+
.meta(configSchemaMeta);
|
|
375
454
|
|
|
376
|
-
|
|
455
|
+
/**
|
|
456
|
+
* Input schema for configuration (without transforms)
|
|
457
|
+
*
|
|
458
|
+
* This schema is used for JSON Schema generation. It validates the same input
|
|
459
|
+
* structure but without transforms, which can't be represented in JSON Schema
|
|
460
|
+
* format.
|
|
461
|
+
*/
|
|
462
|
+
const modestBenchConfigInputSchema = z
|
|
463
|
+
.object({
|
|
464
|
+
...baseConfigProperties,
|
|
465
|
+
budgets: budgetsRawInputSchema.optional().describe(budgetsDescription),
|
|
466
|
+
})
|
|
467
|
+
.strict()
|
|
468
|
+
.describe(configSchemaDescription)
|
|
469
|
+
.meta(configSchemaMeta);
|
|
377
470
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
}
|
|
386
|
-
}
|
|
471
|
+
/**
|
|
472
|
+
* Partial input schema for JSON Schema generation
|
|
473
|
+
*
|
|
474
|
+
* This is used for generating JSON Schema for IDE autocomplete in config files.
|
|
475
|
+
*/
|
|
476
|
+
export const partialModestBenchConfigInputSchema =
|
|
477
|
+
modestBenchConfigInputSchema.partial();
|
|
387
478
|
|
|
388
|
-
|
|
389
|
-
|
|
479
|
+
/**
|
|
480
|
+
* Validate a partial configuration object
|
|
481
|
+
*
|
|
482
|
+
* This is used for validating configuration from files or CLI args before
|
|
483
|
+
* merging with defaults.
|
|
484
|
+
*/
|
|
485
|
+
export const partialModestBenchConfigSchema: z.ZodType<
|
|
486
|
+
Partial<ModestBenchConfig>
|
|
487
|
+
> = modestBenchConfigSchema.partial();
|
|
390
488
|
|
|
391
489
|
/**
|
|
392
|
-
* Safely parse and validate a partial configuration object
|
|
393
|
-
* transformation
|
|
490
|
+
* Safely parse and validate a partial configuration object
|
|
394
491
|
*
|
|
395
492
|
* @param config - The configuration object to validate
|
|
396
493
|
* @returns A result object with either success: true and data, or success:
|
|
397
494
|
* false and error
|
|
398
495
|
*/
|
|
399
496
|
export const safeParsePartialConfig = (config: unknown) => {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
// Transform nested budgets to flat structure after validation
|
|
403
|
-
if (result.success && result.data.budgets) {
|
|
404
|
-
return {
|
|
405
|
-
...result,
|
|
406
|
-
data: {
|
|
407
|
-
...result.data,
|
|
408
|
-
budgets: transformBudgets(
|
|
409
|
-
result.data.budgets as Record<
|
|
410
|
-
string,
|
|
411
|
-
Record<string, Record<string, unknown>>
|
|
412
|
-
>,
|
|
413
|
-
),
|
|
414
|
-
},
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
return result;
|
|
497
|
+
return partialModestBenchConfigSchema.safeParse(config);
|
|
419
498
|
};
|
|
420
499
|
|
|
421
500
|
/**
|
|
@@ -426,23 +505,21 @@ export const safeParsePartialConfig = (config: unknown) => {
|
|
|
426
505
|
* false and error
|
|
427
506
|
*/
|
|
428
507
|
export const safeParseConfig = (config: unknown) => {
|
|
429
|
-
|
|
508
|
+
return modestBenchConfigSchema.safeParse(config);
|
|
509
|
+
};
|
|
430
510
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
result.data.budgets as Record<
|
|
439
|
-
string,
|
|
440
|
-
Record<string, Record<string, unknown>>
|
|
441
|
-
>,
|
|
442
|
-
),
|
|
443
|
-
},
|
|
444
|
-
};
|
|
445
|
-
}
|
|
511
|
+
/**
|
|
512
|
+
* Configuration type after parsing (output type)
|
|
513
|
+
*
|
|
514
|
+
* This is the type you get after parsing a config file - budgets are
|
|
515
|
+
* transformed to ResolvedBudgets and string values are converted to numbers.
|
|
516
|
+
*/
|
|
517
|
+
export type ModestBenchConfig = z.infer<typeof modestBenchConfigSchema>;
|
|
446
518
|
|
|
447
|
-
|
|
448
|
-
|
|
519
|
+
/**
|
|
520
|
+
* Configuration type before parsing (input type)
|
|
521
|
+
*
|
|
522
|
+
* This is the type of config files written by users - budgets are nested (file
|
|
523
|
+
* → suite → task) and values can be strings like "10ms" or "10%".
|
|
524
|
+
*/
|
|
525
|
+
export type ModestBenchConfigInput = z.input<typeof modestBenchConfigSchema>;
|