@benchsdk/runner 0.2.0 → 0.5.2
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/README.md +30 -5
- package/dist/index.cjs +927 -159
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +210 -21
- package/dist/index.d.ts +210 -21
- package/dist/index.js +914 -157
- package/dist/index.js.map +1 -1
- package/package.json +10 -8
package/dist/index.js
CHANGED
|
@@ -3,67 +3,231 @@ var TaskError = class extends Error {
|
|
|
3
3
|
code;
|
|
4
4
|
data;
|
|
5
5
|
steps;
|
|
6
|
+
step;
|
|
7
|
+
timeoutMs;
|
|
6
8
|
constructor(message, opts) {
|
|
7
9
|
super(message);
|
|
8
10
|
this.name = "TaskError";
|
|
9
11
|
this.code = opts?.code;
|
|
10
12
|
this.data = opts?.data;
|
|
11
13
|
this.steps = opts?.steps;
|
|
14
|
+
this.step = opts?.step;
|
|
15
|
+
this.timeoutMs = opts?.timeoutMs;
|
|
16
|
+
}
|
|
17
|
+
toString() {
|
|
18
|
+
let s = `[${this.name}${this.code ? ` (${this.code})` : ""}] ${this.message}`;
|
|
19
|
+
if (this.step) {
|
|
20
|
+
s += `
|
|
21
|
+
step: ${this.step}`;
|
|
22
|
+
}
|
|
23
|
+
if (this.timeoutMs !== void 0) {
|
|
24
|
+
s += `
|
|
25
|
+
timeoutMs: ${this.timeoutMs}`;
|
|
26
|
+
}
|
|
27
|
+
if (this.data && Object.keys(this.data).length > 0) {
|
|
28
|
+
s += `
|
|
29
|
+
data: ${JSON.stringify(this.data, null, 2)}`;
|
|
30
|
+
}
|
|
31
|
+
return s;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
var BenchmarkConfigError = class _BenchmarkConfigError extends Error {
|
|
35
|
+
issues;
|
|
36
|
+
constructor(issues) {
|
|
37
|
+
super(_BenchmarkConfigError.formatIssues(issues));
|
|
38
|
+
this.name = "BenchmarkConfigError";
|
|
39
|
+
this.issues = issues;
|
|
40
|
+
}
|
|
41
|
+
static formatIssues(issues) {
|
|
42
|
+
const lines = issues.map((i) => ` - ${i.field}: ${i.message}`);
|
|
43
|
+
return `Invalid benchmark config:
|
|
44
|
+
${lines.join("\n")}
|
|
45
|
+
|
|
46
|
+
Fix the fields above and try again.`;
|
|
12
47
|
}
|
|
13
48
|
};
|
|
14
|
-
function
|
|
15
|
-
if (value
|
|
16
|
-
|
|
17
|
-
throw new Error(`${field} must be an integer >= 1 (got ${value})`);
|
|
49
|
+
function assertFiniteNumber(value, field) {
|
|
50
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
51
|
+
throw new Error(`${field} must be a finite number (got ${value})`);
|
|
18
52
|
}
|
|
53
|
+
return value;
|
|
19
54
|
}
|
|
20
|
-
function
|
|
21
|
-
if (
|
|
22
|
-
throw new Error(
|
|
55
|
+
function assertNonEmptyString(value, field) {
|
|
56
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
57
|
+
throw new Error(`${field} must be a non-empty string`);
|
|
23
58
|
}
|
|
24
|
-
|
|
25
|
-
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
function assertOnlyAllowedKeys(value, allowed, field) {
|
|
62
|
+
for (const key of Object.keys(value)) {
|
|
63
|
+
if (!allowed.includes(key)) {
|
|
64
|
+
throw new Error(`${field} contains unexpected key: '${key}'`);
|
|
65
|
+
}
|
|
26
66
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
67
|
+
}
|
|
68
|
+
function validateBenchmarkScoringConfig(scoring, display) {
|
|
69
|
+
if (!Array.isArray(scoring.metrics) || scoring.metrics.length === 0) {
|
|
70
|
+
throw new Error("scoring.metrics must be a non-empty array");
|
|
71
|
+
}
|
|
72
|
+
if (scoring.success !== void 0) {
|
|
73
|
+
const requireData = scoring.success.requireData;
|
|
74
|
+
if (requireData === null || typeof requireData !== "object" || Array.isArray(requireData)) {
|
|
75
|
+
throw new Error("scoring.success.requireData must be a plain object");
|
|
30
76
|
}
|
|
31
|
-
if (
|
|
32
|
-
throw new Error("
|
|
77
|
+
if (Object.keys(requireData).length === 0) {
|
|
78
|
+
throw new Error("scoring.success.requireData must declare at least one data field");
|
|
33
79
|
}
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
if (
|
|
37
|
-
throw new Error(
|
|
80
|
+
for (const [key, value] of Object.entries(requireData)) {
|
|
81
|
+
const type = typeof value;
|
|
82
|
+
if (type !== "string" && type !== "number" && type !== "boolean") {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`scoring.success.requireData.${key} must be a string, number, or boolean (got ${type})`
|
|
85
|
+
);
|
|
38
86
|
}
|
|
39
|
-
|
|
40
|
-
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const seen = /* @__PURE__ */ new Set();
|
|
90
|
+
let totalWeight = 0;
|
|
91
|
+
for (let i = 0; i < scoring.metrics.length; i++) {
|
|
92
|
+
const metric = scoring.metrics[i];
|
|
93
|
+
if (metric === null || typeof metric !== "object" || Array.isArray(metric)) {
|
|
94
|
+
throw new Error(`scoring.metrics[${i}] must be an object`);
|
|
95
|
+
}
|
|
96
|
+
const key = metric.key;
|
|
97
|
+
if (typeof key !== "string" || key.trim() === "") {
|
|
98
|
+
throw new Error(`scoring.metrics[${i}].key must be a non-empty string`);
|
|
99
|
+
}
|
|
100
|
+
if (seen.has(key)) {
|
|
101
|
+
throw new Error(`duplicate scoring metric key: ${key}`);
|
|
102
|
+
}
|
|
103
|
+
seen.add(key);
|
|
104
|
+
const displayMetrics = Array.isArray(display?.metrics) ? display.metrics : void 0;
|
|
105
|
+
const displayMetric = displayMetrics?.find((m) => m?.key === key);
|
|
106
|
+
if (metric.unit !== void 0) {
|
|
107
|
+
if (typeof metric.unit !== "string") {
|
|
108
|
+
throw new Error(`scoring.metrics[${i}].unit must be a string`);
|
|
41
109
|
}
|
|
42
|
-
|
|
43
|
-
|
|
110
|
+
if (displayMetric?.unit !== void 0 && metric.unit !== displayMetric.unit) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
`scoring.metrics[${i}].unit '${metric.unit}' conflicts with display.metrics[${i}].unit '${displayMetric.unit}' for key '${key}'`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
assertFiniteNumber(metric.ceiling, `scoring.metrics[${i}].ceiling`);
|
|
117
|
+
if (metric.floor !== void 0) {
|
|
118
|
+
assertFiniteNumber(metric.floor, `scoring.metrics[${i}].floor`);
|
|
119
|
+
}
|
|
120
|
+
if (metric.weights === null || typeof metric.weights !== "object" || Array.isArray(metric.weights)) {
|
|
121
|
+
throw new Error(`scoring.metrics[${i}].weights must be an object`);
|
|
122
|
+
}
|
|
123
|
+
const median = assertFiniteNumber(metric.weights.median, `scoring.metrics[${i}].weights.median`);
|
|
124
|
+
const p95 = assertFiniteNumber(metric.weights.p95, `scoring.metrics[${i}].weights.p95`);
|
|
125
|
+
const p99 = assertFiniteNumber(metric.weights.p99, `scoring.metrics[${i}].weights.p99`);
|
|
126
|
+
if (median < 0 || p95 < 0 || p99 < 0) {
|
|
127
|
+
throw new Error(`scoring.metrics[${i}].weights must be non-negative`);
|
|
128
|
+
}
|
|
129
|
+
totalWeight += median + p95 + p99;
|
|
130
|
+
if (metric.trim !== void 0) {
|
|
131
|
+
assertFiniteNumber(metric.trim, `scoring.metrics[${i}].trim`);
|
|
44
132
|
}
|
|
45
133
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (config.staggerDelayMs !== void 0 && (!Number.isFinite(config.staggerDelayMs) || config.staggerDelayMs < 0)) {
|
|
49
|
-
throw new Error(`staggerDelayMs must be a number >= 0 (got ${config.staggerDelayMs})`);
|
|
134
|
+
if (Math.abs(totalWeight - 1) > 0.01) {
|
|
135
|
+
throw new Error(`scoring metric weights must sum to 1.0 (got ${totalWeight.toFixed(3)})`);
|
|
50
136
|
}
|
|
51
|
-
|
|
52
|
-
|
|
137
|
+
}
|
|
138
|
+
function validateBenchmarkDisplayConfig(display) {
|
|
139
|
+
if (typeof display !== "object" || display === null || Array.isArray(display)) {
|
|
140
|
+
throw new Error("display must be an object");
|
|
53
141
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
142
|
+
assertOnlyAllowedKeys(display, ["metrics", "steps", "overview"], "display");
|
|
143
|
+
const displayMetricKeys = /* @__PURE__ */ new Set();
|
|
144
|
+
if (display.metrics !== void 0) {
|
|
145
|
+
if (!Array.isArray(display.metrics)) {
|
|
146
|
+
throw new Error("display.metrics must be an array");
|
|
147
|
+
}
|
|
148
|
+
for (let i = 0; i < display.metrics.length; i++) {
|
|
149
|
+
const metric = display.metrics[i];
|
|
150
|
+
if (metric === null || typeof metric !== "object" || Array.isArray(metric)) {
|
|
151
|
+
throw new Error(`display.metrics[${i}] must be an object`);
|
|
152
|
+
}
|
|
153
|
+
assertOnlyAllowedKeys(metric, ["key", "label", "unit", "direction", "decimals", "order"], `display.metrics[${i}]`);
|
|
154
|
+
const key = assertNonEmptyString(metric.key, `display.metrics[${i}].key`);
|
|
155
|
+
if (displayMetricKeys.has(key)) {
|
|
156
|
+
throw new Error(`duplicate display metric key: ${key}`);
|
|
58
157
|
}
|
|
59
|
-
|
|
60
|
-
|
|
158
|
+
displayMetricKeys.add(key);
|
|
159
|
+
assertNonEmptyString(metric.label, `display.metrics[${i}].label`);
|
|
160
|
+
if (metric.unit !== void 0 && typeof metric.unit !== "string") {
|
|
161
|
+
throw new Error(`display.metrics[${i}].unit must be a string`);
|
|
61
162
|
}
|
|
62
|
-
if (
|
|
63
|
-
throw new Error(`
|
|
163
|
+
if (metric.direction !== void 0 && metric.direction !== "higher-better" && metric.direction !== "lower-better") {
|
|
164
|
+
throw new Error(`display.metrics[${i}].direction must be 'higher-better' or 'lower-better'`);
|
|
165
|
+
}
|
|
166
|
+
if (metric.decimals !== void 0 && (!Number.isInteger(metric.decimals) || metric.decimals < 0)) {
|
|
167
|
+
throw new Error(`display.metrics[${i}].decimals must be a non-negative integer`);
|
|
168
|
+
}
|
|
169
|
+
if (metric.order !== void 0 && (!Number.isInteger(metric.order) || metric.order < 0)) {
|
|
170
|
+
throw new Error(`display.metrics[${i}].order must be a non-negative integer`);
|
|
64
171
|
}
|
|
65
172
|
}
|
|
66
173
|
}
|
|
174
|
+
if (display.steps !== void 0) {
|
|
175
|
+
if (!Array.isArray(display.steps)) {
|
|
176
|
+
throw new Error("display.steps must be an array");
|
|
177
|
+
}
|
|
178
|
+
const seenStepKeys = /* @__PURE__ */ new Set();
|
|
179
|
+
for (let i = 0; i < display.steps.length; i++) {
|
|
180
|
+
const step = display.steps[i];
|
|
181
|
+
if (step === null || typeof step !== "object" || Array.isArray(step)) {
|
|
182
|
+
throw new Error(`display.steps[${i}] must be an object`);
|
|
183
|
+
}
|
|
184
|
+
assertOnlyAllowedKeys(step, ["key", "label", "order"], `display.steps[${i}]`);
|
|
185
|
+
const key = assertNonEmptyString(step.key, `display.steps[${i}].key`);
|
|
186
|
+
if (seenStepKeys.has(key)) {
|
|
187
|
+
throw new Error(`duplicate display step key: ${key}`);
|
|
188
|
+
}
|
|
189
|
+
seenStepKeys.add(key);
|
|
190
|
+
assertNonEmptyString(step.label, `display.steps[${i}].label`);
|
|
191
|
+
if (step.order !== void 0 && (!Number.isInteger(step.order) || step.order < 0)) {
|
|
192
|
+
throw new Error(`display.steps[${i}].order must be a non-negative integer`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (display.overview !== void 0) {
|
|
197
|
+
if (typeof display.overview !== "object" || display.overview === null || Array.isArray(display.overview)) {
|
|
198
|
+
throw new Error("display.overview must be an object");
|
|
199
|
+
}
|
|
200
|
+
assertOnlyAllowedKeys(display.overview, ["defaultMetric", "defaultLayout"], "display.overview");
|
|
201
|
+
const { defaultMetric, defaultLayout } = display.overview;
|
|
202
|
+
if (defaultMetric !== void 0) {
|
|
203
|
+
const metric = assertNonEmptyString(defaultMetric, "display.overview.defaultMetric");
|
|
204
|
+
const validDefaultMetrics = new Set(displayMetricKeys);
|
|
205
|
+
validDefaultMetrics.add("compositeScore");
|
|
206
|
+
validDefaultMetrics.add("task");
|
|
207
|
+
if (display.metrics !== void 0 && !validDefaultMetrics.has(metric)) {
|
|
208
|
+
throw new Error(`display.overview.defaultMetric '${metric}' is not declared in display.metrics and is not a known default (compositeScore, task)`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (defaultLayout !== void 0 && !["ranking", "cards", "chart", "leaderboard"].includes(defaultLayout)) {
|
|
212
|
+
throw new Error("display.overview.defaultLayout must be 'ranking', 'cards', 'chart', or 'leaderboard'");
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function defineBenchmarkConfig(config) {
|
|
217
|
+
const issues = validateBenchmarkConfig(config);
|
|
218
|
+
if (config.scoring !== void 0) {
|
|
219
|
+
try {
|
|
220
|
+
validateBenchmarkScoringConfig(config.scoring, config.display);
|
|
221
|
+
} catch (error) {
|
|
222
|
+
issues.push({
|
|
223
|
+
field: "scoring",
|
|
224
|
+
message: error instanceof Error ? error.message : String(error)
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (issues.length > 0) {
|
|
229
|
+
throw new BenchmarkConfigError(issues);
|
|
230
|
+
}
|
|
67
231
|
return config;
|
|
68
232
|
}
|
|
69
233
|
function defineTask(task) {
|
|
@@ -72,6 +236,136 @@ function defineTask(task) {
|
|
|
72
236
|
}
|
|
73
237
|
return task;
|
|
74
238
|
}
|
|
239
|
+
function validateBenchmarkConfig(config) {
|
|
240
|
+
const issues = [];
|
|
241
|
+
if (!config.benchmarkSlug || typeof config.benchmarkSlug !== "string") {
|
|
242
|
+
issues.push({ field: "benchmarkSlug", message: "is required" });
|
|
243
|
+
}
|
|
244
|
+
if (!config.benchmarkName || typeof config.benchmarkName !== "string") {
|
|
245
|
+
issues.push({ field: "benchmarkName", message: "is required" });
|
|
246
|
+
}
|
|
247
|
+
if (!Array.isArray(config.participants) || config.participants.length === 0) {
|
|
248
|
+
issues.push({ field: "participants", message: "must be a non-empty array" });
|
|
249
|
+
} else {
|
|
250
|
+
const seenParticipants = /* @__PURE__ */ new Set();
|
|
251
|
+
for (let i = 0; i < config.participants.length; i++) {
|
|
252
|
+
const p = config.participants[i];
|
|
253
|
+
if (p === null || typeof p !== "object" || Array.isArray(p)) {
|
|
254
|
+
issues.push({ field: `participants[${i}]`, message: "must be an object" });
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
const participant = p;
|
|
258
|
+
if (typeof participant.name !== "string" || participant.name.trim() === "") {
|
|
259
|
+
issues.push({ field: `participants[${i}].name`, message: "must be a non-empty string" });
|
|
260
|
+
} else if (seenParticipants.has(participant.name)) {
|
|
261
|
+
issues.push({ field: `participants[${i}].name`, message: `duplicate participant name: ${participant.name}` });
|
|
262
|
+
} else {
|
|
263
|
+
seenParticipants.add(participant.name);
|
|
264
|
+
}
|
|
265
|
+
if (participant.requiredEnvVars !== void 0 && (!Array.isArray(participant.requiredEnvVars) || !participant.requiredEnvVars.every((v) => typeof v === "string"))) {
|
|
266
|
+
issues.push({ field: `participants[${i}].requiredEnvVars`, message: "must be an array of strings" });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (config.phases !== void 0) {
|
|
271
|
+
if (config.iterations !== void 0) {
|
|
272
|
+
issues.push({ field: "iterations", message: "phases and iterations are mutually exclusive" });
|
|
273
|
+
}
|
|
274
|
+
if (!Array.isArray(config.phases) || config.phases.length === 0) {
|
|
275
|
+
issues.push({ field: "phases", message: "must be a non-empty array" });
|
|
276
|
+
} else {
|
|
277
|
+
const seen = /* @__PURE__ */ new Set();
|
|
278
|
+
for (let i = 0; i < config.phases.length; i++) {
|
|
279
|
+
const phase = config.phases[i];
|
|
280
|
+
if (phase === null || typeof phase !== "object" || Array.isArray(phase)) {
|
|
281
|
+
issues.push({ field: `phases[${i}]`, message: "must be an object" });
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
const phaseObj = phase;
|
|
285
|
+
if (typeof phaseObj.name !== "string" || phaseObj.name.trim() === "") {
|
|
286
|
+
issues.push({ field: `phases[${i}]`, message: "must have a non-empty string name" });
|
|
287
|
+
} else {
|
|
288
|
+
const name = phaseObj.name;
|
|
289
|
+
if (seen.has(name)) {
|
|
290
|
+
issues.push({ field: `phases['${name}']`, message: `duplicate phase name: ${name}` });
|
|
291
|
+
}
|
|
292
|
+
seen.add(name);
|
|
293
|
+
const iterations = phaseObj.iterations;
|
|
294
|
+
if (typeof iterations !== "number" || !Number.isInteger(iterations) || iterations < 1) {
|
|
295
|
+
issues.push({ field: `phases['${name}'].iterations`, message: `must be an integer >= 1 (got ${iterations})` });
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (config.iterations !== void 0 && (!Number.isInteger(config.iterations) || config.iterations < 1)) {
|
|
302
|
+
issues.push({ field: "iterations", message: `must be an integer >= 1 (got ${config.iterations})` });
|
|
303
|
+
}
|
|
304
|
+
if (config.concurrency !== void 0 && (!Number.isInteger(config.concurrency) || config.concurrency < 1)) {
|
|
305
|
+
issues.push({ field: "concurrency", message: `must be an integer >= 1 (got ${config.concurrency})` });
|
|
306
|
+
}
|
|
307
|
+
if (config.staggerDelayMs !== void 0 && (!Number.isFinite(config.staggerDelayMs) || config.staggerDelayMs < 0)) {
|
|
308
|
+
issues.push({ field: "staggerDelayMs", message: `must be a number >= 0 (got ${config.staggerDelayMs})` });
|
|
309
|
+
}
|
|
310
|
+
if (config.groupBy !== void 0 && config.groupBy !== "participant" && config.groupBy !== "round") {
|
|
311
|
+
issues.push({ field: "groupBy", message: `must be 'participant' or 'round' (got ${config.groupBy})` });
|
|
312
|
+
}
|
|
313
|
+
if (config.shapes !== void 0) {
|
|
314
|
+
if (typeof config.shapes !== "object" || config.shapes === null || Array.isArray(config.shapes)) {
|
|
315
|
+
issues.push({ field: "shapes", message: "must be a plain object" });
|
|
316
|
+
} else {
|
|
317
|
+
for (const [shapeName, shape] of Object.entries(config.shapes)) {
|
|
318
|
+
if (shape === null || typeof shape !== "object" || Array.isArray(shape)) {
|
|
319
|
+
issues.push({ field: `shapes['${shapeName}']`, message: "must be an object" });
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
const shapeObj = shape;
|
|
323
|
+
const slug = shapeObj.slug;
|
|
324
|
+
if (typeof slug !== "string" || slug === "" || !/^[a-z0-9][a-z0-9-]*$/.test(slug)) {
|
|
325
|
+
issues.push({ field: `shapes['${shapeName}'].slug`, message: `needs a lowercase slug (got ${JSON.stringify(slug)})` });
|
|
326
|
+
}
|
|
327
|
+
const name = shapeObj.name;
|
|
328
|
+
if (name !== void 0 && (typeof name !== "string" || name.trim() === "")) {
|
|
329
|
+
issues.push({ field: `shapes['${shapeName}'].name`, message: "must be a non-empty string" });
|
|
330
|
+
}
|
|
331
|
+
const staggerDelayMs = shapeObj.staggerDelayMs;
|
|
332
|
+
if (staggerDelayMs !== void 0 && (typeof staggerDelayMs !== "number" || !Number.isFinite(staggerDelayMs) || staggerDelayMs < 0)) {
|
|
333
|
+
issues.push({ field: `shapes['${shapeName}'].staggerDelayMs`, message: `must be a number >= 0 (got ${staggerDelayMs})` });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (config.dimensions !== void 0) {
|
|
339
|
+
if (config.dimensions === null || typeof config.dimensions !== "object" || Array.isArray(config.dimensions)) {
|
|
340
|
+
issues.push({ field: "dimensions", message: "must be a plain object" });
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (config.customCliFlags !== void 0) {
|
|
344
|
+
if (!Array.isArray(config.customCliFlags) || !config.customCliFlags.every((f) => typeof f === "string" && f.startsWith("--"))) {
|
|
345
|
+
issues.push({ field: "customCliFlags", message: 'must be an array of strings starting with "--"' });
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (config.display !== void 0) {
|
|
349
|
+
try {
|
|
350
|
+
validateBenchmarkDisplayConfig(config.display);
|
|
351
|
+
} catch (error) {
|
|
352
|
+
issues.push({
|
|
353
|
+
field: "display",
|
|
354
|
+
message: error instanceof Error ? error.message : String(error)
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (config.display?.overview?.defaultMetric === "compositeScore" && config.scoring === void 0 && config.onScore === void 0) {
|
|
359
|
+
issues.push({
|
|
360
|
+
field: "display.overview.defaultMetric",
|
|
361
|
+
message: "cannot be 'compositeScore' without config.scoring or config.onScore"
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
return issues;
|
|
365
|
+
}
|
|
366
|
+
function defineOnComplete(onComplete) {
|
|
367
|
+
return onComplete;
|
|
368
|
+
}
|
|
75
369
|
|
|
76
370
|
// src/no-available-participants.ts
|
|
77
371
|
var NoAvailableParticipantsError = class extends Error {
|
|
@@ -88,12 +382,15 @@ var NoAvailableParticipantsError = class extends Error {
|
|
|
88
382
|
// src/runner.ts
|
|
89
383
|
import { execSync } from "child_process";
|
|
90
384
|
import os from "os";
|
|
385
|
+
import { createBenchmarkClient, BenchmarkApiError } from "@benchsdk/api";
|
|
386
|
+
import { resolveAuth } from "@benchsdk/cli";
|
|
91
387
|
import {
|
|
92
388
|
BenchmarkReporter,
|
|
93
|
-
|
|
389
|
+
createSystemMetricsCollector,
|
|
94
390
|
filterParticipantsByEnv,
|
|
391
|
+
runWorker,
|
|
95
392
|
selectParticipants
|
|
96
|
-
} from "@benchsdk/
|
|
393
|
+
} from "@benchsdk/worker";
|
|
97
394
|
|
|
98
395
|
// src/scoring.ts
|
|
99
396
|
function isFiniteNumber(v) {
|
|
@@ -149,40 +446,121 @@ var higherIsBetter = (name, opts) => ({
|
|
|
149
446
|
...opts,
|
|
150
447
|
higherIsBetter: true
|
|
151
448
|
});
|
|
152
|
-
|
|
449
|
+
var WEIGHT_SUM_TOLERANCE = 0.01;
|
|
450
|
+
var ScoringSpecError = class extends Error {
|
|
451
|
+
constructor(message) {
|
|
452
|
+
super(message);
|
|
453
|
+
this.name = "ScoringSpecError";
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
function validateScoringSpec(spec) {
|
|
457
|
+
const totalWeight = spec.metrics.reduce(
|
|
458
|
+
(sum, m) => sum + m.weights.median + m.weights.p95 + m.weights.p99,
|
|
459
|
+
0
|
|
460
|
+
);
|
|
461
|
+
if (Math.abs(totalWeight - 1) > WEIGHT_SUM_TOLERANCE) {
|
|
462
|
+
const breakdown = spec.metrics.map((m) => `${m.name}=${(m.weights.median + m.weights.p95 + m.weights.p99).toFixed(3)}`).join(", ");
|
|
463
|
+
throw new ScoringSpecError(
|
|
464
|
+
`Scoring spec weights sum to ${totalWeight.toFixed(3)}, expected 1.0 (\xB1${WEIGHT_SUM_TOLERANCE}). Each metric's weights.median + weights.p95 + weights.p99, summed across every declared metric, must total 1.0 for compositeScore to stay a meaningful 0-100 scale. Per-metric totals: ${breakdown || "(no metrics declared)"}`
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
function groupRecordsByKey(records, key) {
|
|
469
|
+
const groups = /* @__PURE__ */ new Map();
|
|
470
|
+
for (const record of records) {
|
|
471
|
+
const raw = record.data?.[key];
|
|
472
|
+
const value = raw === void 0 ? void 0 : raw;
|
|
473
|
+
const mapKey = value === void 0 ? "__undefined__" : JSON.stringify(value);
|
|
474
|
+
let group = groups.get(mapKey);
|
|
475
|
+
if (!group) {
|
|
476
|
+
group = { value, records: [] };
|
|
477
|
+
groups.set(mapKey, group);
|
|
478
|
+
}
|
|
479
|
+
group.records.push(record);
|
|
480
|
+
}
|
|
481
|
+
return Array.from(groups.values());
|
|
482
|
+
}
|
|
483
|
+
function scoreGroup(records, spec, baseDimensions, groupKey, groupValue, provider, unitByMetric) {
|
|
153
484
|
const successFilter = spec.success ?? ((r) => r.status === "success");
|
|
154
|
-
const
|
|
485
|
+
const passing = records.filter(successFilter);
|
|
486
|
+
const successRate = records.length === 0 ? 0 : passing.length / records.length;
|
|
487
|
+
const skipped = records.length === 0;
|
|
488
|
+
let metricScoresSum = 0;
|
|
489
|
+
const metrics = [];
|
|
490
|
+
for (const metric of spec.metrics) {
|
|
491
|
+
const samples = collectSamples(metric, passing);
|
|
492
|
+
if (samples.length === 0) {
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
const { median, p95, p99 } = computeStats(samples, metric.trim ?? 0.05);
|
|
496
|
+
const metricScore = metric.weights.median * scoreStat(median, metric) + metric.weights.p95 * scoreStat(p95, metric) + metric.weights.p99 * scoreStat(p99, metric);
|
|
497
|
+
metricScoresSum += metricScore;
|
|
498
|
+
const unitKey = typeof metric.value === "string" ? metric.value : metric.name;
|
|
499
|
+
const unit = metric.unit ?? unitByMetric.get(unitKey) ?? "";
|
|
500
|
+
metrics.push({ name: metric.name, unit, median, p95, p99 });
|
|
501
|
+
}
|
|
502
|
+
const compositeScore = successRate === 0 ? 0 : Math.round(metricScoresSum * successRate * 100) / 100;
|
|
503
|
+
const dimensions = toJsonObject({
|
|
504
|
+
...baseDimensions,
|
|
505
|
+
...groupKey !== void 0 && groupValue !== void 0 ? { [groupKey]: groupValue } : {}
|
|
506
|
+
});
|
|
507
|
+
return {
|
|
508
|
+
provider,
|
|
509
|
+
dimensions,
|
|
510
|
+
metrics,
|
|
511
|
+
compositeScore,
|
|
512
|
+
successRate,
|
|
513
|
+
skipped
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
function score(outcome, spec, displayMetrics) {
|
|
517
|
+
validateScoringSpec(spec);
|
|
518
|
+
const baseDimensions = toJsonObject(spec.dimensions ?? {});
|
|
519
|
+
const unitByMetric = new Map(displayMetrics?.map((m) => [m.key, m.unit]));
|
|
155
520
|
const results = [];
|
|
156
521
|
for (const { participant, records } of outcome.participants) {
|
|
157
|
-
const
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const metrics = [];
|
|
162
|
-
for (const metric of spec.metrics) {
|
|
163
|
-
const samples = collectSamples(metric, passing);
|
|
164
|
-
if (samples.length === 0) {
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
const { median, p95, p99 } = computeStats(samples, metric.trim ?? 0.05);
|
|
168
|
-
const metricScore = metric.weights.median * scoreStat(median, metric) + metric.weights.p95 * scoreStat(p95, metric) + metric.weights.p99 * scoreStat(p99, metric);
|
|
169
|
-
metricScoresSum += metricScore;
|
|
170
|
-
metrics.push({ name: metric.name, unit: metric.unit, median, p95, p99 });
|
|
171
|
-
}
|
|
172
|
-
const compositeScore = successRate === 0 ? 0 : Math.round(metricScoresSum * successRate * 100) / 100;
|
|
173
|
-
results.push({
|
|
174
|
-
provider: participant,
|
|
175
|
-
dimensions,
|
|
176
|
-
metrics,
|
|
177
|
-
compositeScore,
|
|
178
|
-
successRate,
|
|
179
|
-
skipped
|
|
180
|
-
});
|
|
522
|
+
const groups = spec.groupBy && records.length > 0 ? [{ value: void 0, records }, ...groupRecordsByKey(records, spec.groupBy)] : [{ value: void 0, records }];
|
|
523
|
+
for (const group of groups) {
|
|
524
|
+
results.push(scoreGroup(group.records, spec, baseDimensions, spec.groupBy, group.value, participant, unitByMetric));
|
|
525
|
+
}
|
|
181
526
|
}
|
|
182
527
|
return results;
|
|
183
528
|
}
|
|
529
|
+
function scoringConfigToSpec(config, dimensions, display) {
|
|
530
|
+
const success = config.success;
|
|
531
|
+
const unitByMetric = new Map(display?.metrics?.map((m) => [m.key, m.unit]));
|
|
532
|
+
return {
|
|
533
|
+
...dimensions ? { dimensions: toJsonObject(dimensions) } : {},
|
|
534
|
+
...config.groupBy ? { groupBy: config.groupBy } : {},
|
|
535
|
+
...success ? {
|
|
536
|
+
success: (record) => record.status === "success" && Object.entries(success.requireData).every(([key, value]) => record.data?.[key] === value)
|
|
537
|
+
} : {},
|
|
538
|
+
metrics: config.metrics.map((metric) => ({
|
|
539
|
+
name: metric.key,
|
|
540
|
+
value: metric.key,
|
|
541
|
+
unit: (display?.metrics === void 0 ? metric.unit : unitByMetric.get(metric.key) ?? metric.unit) ?? "",
|
|
542
|
+
ceiling: metric.ceiling,
|
|
543
|
+
floor: metric.floor,
|
|
544
|
+
higherIsBetter: metric.higherIsBetter,
|
|
545
|
+
weights: metric.weights,
|
|
546
|
+
trim: metric.trim
|
|
547
|
+
}))
|
|
548
|
+
};
|
|
549
|
+
}
|
|
184
550
|
|
|
185
551
|
// src/log-buffer.ts
|
|
552
|
+
var LOG_LEVEL_ORDER = ["debug", "info", "warn", "error"];
|
|
553
|
+
function isLogOptions(value) {
|
|
554
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
555
|
+
const o = value;
|
|
556
|
+
const keys = Object.keys(o);
|
|
557
|
+
if (keys.length === 0) return false;
|
|
558
|
+
if (!keys.every((k) => k === "level" || k === "meta")) return false;
|
|
559
|
+
if (o.level !== void 0 && (typeof o.level !== "string" || !LOG_LEVEL_ORDER.includes(o.level))) {
|
|
560
|
+
return false;
|
|
561
|
+
}
|
|
562
|
+
return true;
|
|
563
|
+
}
|
|
186
564
|
var LogBuffer = class {
|
|
187
565
|
lines = [];
|
|
188
566
|
step(taskIndex, stepName, outcome) {
|
|
@@ -199,9 +577,15 @@ var LogBuffer = class {
|
|
|
199
577
|
}
|
|
200
578
|
}
|
|
201
579
|
/** Appends a free-form narration line (backs the task context's `log`). */
|
|
202
|
-
line(message,
|
|
203
|
-
const
|
|
204
|
-
|
|
580
|
+
line(message, metaOrOptions) {
|
|
581
|
+
const opts = isLogOptions(metaOrOptions) ? { level: metaOrOptions.level ?? "info", meta: metaOrOptions.meta } : { level: "info", meta: metaOrOptions };
|
|
582
|
+
const suffix = opts.meta && Object.keys(opts.meta).length > 0 ? ` ${JSON.stringify(opts.meta)}` : "";
|
|
583
|
+
const taskMatch = message.match(/^(\[task \d+\])\s*(.*)$/);
|
|
584
|
+
if (taskMatch) {
|
|
585
|
+
this.lines.push(`${(/* @__PURE__ */ new Date()).toISOString()} ${taskMatch[1]} [${opts.level}] ${taskMatch[2]}${suffix}`);
|
|
586
|
+
} else {
|
|
587
|
+
this.lines.push(`${(/* @__PURE__ */ new Date()).toISOString()} [${opts.level}] ${message}${suffix}`);
|
|
588
|
+
}
|
|
205
589
|
}
|
|
206
590
|
isEmpty() {
|
|
207
591
|
return this.lines.length === 0;
|
|
@@ -215,7 +599,6 @@ function indent(text, prefix = "") {
|
|
|
215
599
|
}
|
|
216
600
|
|
|
217
601
|
// src/runner.ts
|
|
218
|
-
var DEFAULT_PLATFORM_URL = "https://platform.computesdk.com";
|
|
219
602
|
function isEnvNoIngest() {
|
|
220
603
|
const v = process.env.BENCHSDK_NO_INGEST;
|
|
221
604
|
return v === "1" || v?.toLowerCase() === "true";
|
|
@@ -223,15 +606,68 @@ function isEnvNoIngest() {
|
|
|
223
606
|
function sleep(ms) {
|
|
224
607
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
225
608
|
}
|
|
609
|
+
function runWithConcurrency(fns, limit) {
|
|
610
|
+
if (limit >= fns.length || fns.length === 0) {
|
|
611
|
+
return Promise.all(fns.map((fn) => fn()));
|
|
612
|
+
}
|
|
613
|
+
const results = new Array(fns.length);
|
|
614
|
+
let running = 0;
|
|
615
|
+
let completed = 0;
|
|
616
|
+
let nextIndex = 0;
|
|
617
|
+
return new Promise((resolve2, reject) => {
|
|
618
|
+
const runNext = () => {
|
|
619
|
+
if (completed === fns.length) {
|
|
620
|
+
resolve2(results);
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
while (running < limit && nextIndex < fns.length) {
|
|
624
|
+
const index = nextIndex++;
|
|
625
|
+
running++;
|
|
626
|
+
fns[index]().then(
|
|
627
|
+
(value) => {
|
|
628
|
+
results[index] = value;
|
|
629
|
+
running--;
|
|
630
|
+
completed++;
|
|
631
|
+
runNext();
|
|
632
|
+
},
|
|
633
|
+
(error) => reject(error)
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
};
|
|
637
|
+
runNext();
|
|
638
|
+
});
|
|
639
|
+
}
|
|
226
640
|
function getErrorCode(error) {
|
|
227
|
-
|
|
641
|
+
if (error instanceof Error && "code" in error && typeof error.code === "string" && error.code) {
|
|
642
|
+
return error.code;
|
|
643
|
+
}
|
|
644
|
+
if (error instanceof Error && error.name) return error.name;
|
|
645
|
+
return "ERROR";
|
|
646
|
+
}
|
|
647
|
+
function isTaskError(error) {
|
|
648
|
+
return error instanceof Error && (error instanceof TaskError || error.name === "TaskError");
|
|
228
649
|
}
|
|
229
|
-
|
|
650
|
+
var STEP_OUTCOME_KEYS = /* @__PURE__ */ new Set(["stdout", "stderr", "error", "exitCode", "code", "signal", "pid"]);
|
|
651
|
+
function isStepOutcome(value) {
|
|
652
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
653
|
+
const o = value;
|
|
654
|
+
const keys = Object.keys(o);
|
|
655
|
+
if (keys.length === 0) return false;
|
|
656
|
+
if (!keys.every((k) => STEP_OUTCOME_KEYS.has(k))) return false;
|
|
657
|
+
return typeof o.stdout === "string" || typeof o.stderr === "string" || typeof o.error === "string";
|
|
658
|
+
}
|
|
659
|
+
function withTimeout(promise, { stepName, timeoutMs, participantSlug }) {
|
|
230
660
|
return new Promise((resolve2, reject) => {
|
|
231
|
-
const timer = setTimeout(
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
661
|
+
const timer = setTimeout(() => {
|
|
662
|
+
const participant = participantSlug ? ` for participant "${participantSlug}"` : "";
|
|
663
|
+
reject(
|
|
664
|
+
new TaskError(`Step "${stepName}" timed out after ${timeoutMs}ms${participant}`, {
|
|
665
|
+
code: "step_timeout",
|
|
666
|
+
step: stepName,
|
|
667
|
+
timeoutMs
|
|
668
|
+
})
|
|
669
|
+
);
|
|
670
|
+
}, timeoutMs);
|
|
235
671
|
promise.then(
|
|
236
672
|
(value) => {
|
|
237
673
|
clearTimeout(timer);
|
|
@@ -244,20 +680,23 @@ function withTimeout(promise, ms, name) {
|
|
|
244
680
|
);
|
|
245
681
|
});
|
|
246
682
|
}
|
|
247
|
-
async function runStepInvocations(name, fn, options) {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
683
|
+
async function runStepInvocations(name, fn, options, participantSlug) {
|
|
684
|
+
if (options?.concurrency !== void 0) {
|
|
685
|
+
console.warn(`[benchsdk] step "${name}" option "concurrency" is deprecated; use "parallelInvocations"`);
|
|
686
|
+
}
|
|
687
|
+
const requestedParallelism = options?.parallelInvocations ?? options?.concurrency;
|
|
688
|
+
if (requestedParallelism !== void 0 && (!Number.isInteger(requestedParallelism) || requestedParallelism < 1)) {
|
|
689
|
+
throw new Error(`step "${name}" parallelInvocations must be an integer >= 1 (got ${requestedParallelism})`);
|
|
251
690
|
}
|
|
252
691
|
const timeoutMs = options?.timeoutMs;
|
|
253
692
|
if (timeoutMs !== void 0 && (!Number.isFinite(timeoutMs) || timeoutMs < 0)) {
|
|
254
693
|
throw new Error(`step "${name}" timeoutMs must be a number >= 0 (got ${timeoutMs})`);
|
|
255
694
|
}
|
|
256
|
-
const count =
|
|
695
|
+
const count = requestedParallelism ?? 1;
|
|
257
696
|
const invocations = Array.from({ length: count }, () => {
|
|
258
697
|
const promise = Promise.resolve().then(() => fn());
|
|
259
698
|
if (timeoutMs === void 0) return promise;
|
|
260
|
-
return withTimeout(promise, timeoutMs,
|
|
699
|
+
return withTimeout(promise, { stepName: name, timeoutMs, participantSlug });
|
|
261
700
|
});
|
|
262
701
|
if (count === 1) {
|
|
263
702
|
return invocations[0];
|
|
@@ -275,18 +714,20 @@ async function runStepInvocations(name, fn, options) {
|
|
|
275
714
|
if (firstError !== void 0) throw firstError;
|
|
276
715
|
return results;
|
|
277
716
|
}
|
|
278
|
-
async function runStepWithClient(clientStep, name, fn, options) {
|
|
279
|
-
const { concurrency:
|
|
717
|
+
async function runStepWithClient(clientStep, name, fn, options, participantSlug) {
|
|
718
|
+
const { parallelInvocations: runnerParallelism, concurrency: deprecatedConcurrency, timeoutMs, ...clientOptions } = options ?? {};
|
|
280
719
|
const clientStepOptions = {
|
|
281
720
|
...clientOptions,
|
|
282
721
|
timeoutMs,
|
|
283
|
-
stepConcurrency:
|
|
722
|
+
stepConcurrency: runnerParallelism ?? deprecatedConcurrency
|
|
284
723
|
};
|
|
285
|
-
const result = await clientStep(name, () => runStepInvocations(name, fn, options), clientStepOptions);
|
|
724
|
+
const result = await clientStep(name, () => runStepInvocations(name, fn, options, participantSlug), clientStepOptions);
|
|
286
725
|
return result;
|
|
287
726
|
}
|
|
288
|
-
function parseCliArgs(argv) {
|
|
727
|
+
function parseCliArgs(argv, allowedCustomFlags) {
|
|
289
728
|
const args = {};
|
|
729
|
+
const unknown = [];
|
|
730
|
+
const allowed = new Set(allowedCustomFlags ?? []);
|
|
290
731
|
const readValue = (raw, i) => {
|
|
291
732
|
const eq = raw.indexOf("=");
|
|
292
733
|
if (eq !== -1) return { value: raw.slice(eq + 1), nextIndex: i };
|
|
@@ -378,22 +819,46 @@ function parseCliArgs(argv) {
|
|
|
378
819
|
case "--dry-run":
|
|
379
820
|
args.noIngest = true;
|
|
380
821
|
break;
|
|
381
|
-
default:
|
|
822
|
+
default: {
|
|
823
|
+
if (allowed.has(name)) {
|
|
824
|
+
if (!arg.includes("=")) {
|
|
825
|
+
const next = argv[i + 1];
|
|
826
|
+
if (next && !next.startsWith("-")) {
|
|
827
|
+
i++;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
} else {
|
|
831
|
+
unknown.push(name);
|
|
832
|
+
if (!arg.includes("=")) {
|
|
833
|
+
const next = argv[i + 1];
|
|
834
|
+
if (next && !next.startsWith("-")) {
|
|
835
|
+
i++;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
382
839
|
break;
|
|
840
|
+
}
|
|
383
841
|
}
|
|
384
842
|
}
|
|
843
|
+
if (unknown.length > 0) {
|
|
844
|
+
throw new Error(`Unknown flag(s): ${unknown.join(", ")}`);
|
|
845
|
+
}
|
|
385
846
|
if (!args.noIngest && isEnvNoIngest()) {
|
|
386
847
|
args.noIngest = true;
|
|
387
848
|
}
|
|
388
849
|
return args;
|
|
389
850
|
}
|
|
390
851
|
function mergeConfig(config, args) {
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
852
|
+
const phases = config.phases;
|
|
853
|
+
const unevenPhases = phases !== void 0 && phases.some((p) => p.iterations !== phases[0].iterations);
|
|
854
|
+
if (unevenPhases && args.iterations !== void 0) {
|
|
855
|
+
console.warn("--iterations is ignored because this benchmark sizes its phases individually.");
|
|
394
856
|
}
|
|
857
|
+
const phaseIterations = phases !== void 0 && !unevenPhases ? args.iterations : void 0;
|
|
858
|
+
const phaseTotal = phases !== void 0 ? phaseIterations !== void 0 ? phaseIterations * phases.length : phases.reduce((sum, p) => sum + p.iterations, 0) : void 0;
|
|
395
859
|
const resolved = {
|
|
396
860
|
iterations: phaseTotal ?? args.iterations ?? config.iterations ?? 1,
|
|
861
|
+
phaseIterations,
|
|
397
862
|
concurrency: args.concurrency ?? config.concurrency ?? 1,
|
|
398
863
|
staggerDelayMs: args.staggerDelayMs ?? config.staggerDelayMs ?? 0,
|
|
399
864
|
groupBy: args.groupBy ?? config.groupBy ?? "participant",
|
|
@@ -407,13 +872,16 @@ function mergeConfig(config, args) {
|
|
|
407
872
|
}
|
|
408
873
|
return resolved;
|
|
409
874
|
}
|
|
410
|
-
function buildSchedule(config,
|
|
875
|
+
function buildSchedule(config, resolved, task) {
|
|
411
876
|
if (config.phases?.length) {
|
|
412
877
|
return config.phases.flatMap(
|
|
413
|
-
(phase) => Array.from({ length: phase.iterations }, () => ({
|
|
878
|
+
(phase) => Array.from({ length: resolved.phaseIterations ?? phase.iterations }, () => ({
|
|
879
|
+
phase: phase.name,
|
|
880
|
+
task
|
|
881
|
+
}))
|
|
414
882
|
);
|
|
415
883
|
}
|
|
416
|
-
return Array.from({ length: iterations }, () => ({ phase: void 0, task }));
|
|
884
|
+
return Array.from({ length: resolved.iterations }, () => ({ phase: void 0, task }));
|
|
417
885
|
}
|
|
418
886
|
function defaultOnResult(record, meta) {
|
|
419
887
|
const n = record.taskIndex + 1;
|
|
@@ -421,21 +889,10 @@ function defaultOnResult(record, meta) {
|
|
|
421
889
|
const data = record.data && Object.keys(record.data).length > 0 ? ` ${JSON.stringify(record.data)}` : "";
|
|
422
890
|
console.log(` [${meta.participant}] Task ${n}/${meta.iterations}: success${data}`);
|
|
423
891
|
} else {
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
}
|
|
427
|
-
function resolvePlatform() {
|
|
428
|
-
const root = (process.env.BENCHMARKS_PLATFORM_URL || DEFAULT_PLATFORM_URL).replace(/\/+$/, "");
|
|
429
|
-
const apiKey = process.env.BENCHMARKS_PLATFORM_API_KEY;
|
|
430
|
-
if (!apiKey) {
|
|
431
|
-
throw new Error(
|
|
432
|
-
"BENCHMARKS_PLATFORM_API_KEY is required. Create an org-scoped API key in your organization settings on the platform and set it in your .env."
|
|
433
|
-
);
|
|
892
|
+
const detail = record.data?.errorMessage ?? record.data?.error;
|
|
893
|
+
const suffix = typeof detail === "string" && detail.length > 0 ? `: ${detail}` : "";
|
|
894
|
+
console.log(` [${meta.participant}] Task ${n}/${meta.iterations}: FAILED \u2014 ${record.errorCode ?? "unknown error"}${suffix}`);
|
|
434
895
|
}
|
|
435
|
-
return {
|
|
436
|
-
baseUrl: `${root}/api/v1`,
|
|
437
|
-
apiKey
|
|
438
|
-
};
|
|
439
896
|
}
|
|
440
897
|
function resolveShape(config, shapeName) {
|
|
441
898
|
if (!shapeName) return void 0;
|
|
@@ -475,23 +932,74 @@ function resolveParticipants(config, resolved) {
|
|
|
475
932
|
if (available.length === 0) throw new NoAvailableParticipantsError(skipped);
|
|
476
933
|
return available;
|
|
477
934
|
}
|
|
478
|
-
|
|
479
|
-
const
|
|
935
|
+
function resolveTriggerSource(env = process.env) {
|
|
936
|
+
const source = env.BENCH_TRIGGER_SOURCE?.trim();
|
|
937
|
+
if (source) return source;
|
|
938
|
+
return env.GITHUB_EVENT_NAME?.trim() || "manual";
|
|
939
|
+
}
|
|
940
|
+
function triggerToJson(env = process.env) {
|
|
941
|
+
const requestedBy = env.BENCH_TRIGGER_REQUESTED_BY?.trim();
|
|
942
|
+
const requestId = env.BENCH_TRIGGER_REQUEST_ID?.trim();
|
|
943
|
+
const event = env.GITHUB_EVENT_NAME?.trim();
|
|
944
|
+
return {
|
|
945
|
+
source: resolveTriggerSource(env),
|
|
946
|
+
...event ? { event } : {},
|
|
947
|
+
...requestedBy ? { requestedBy } : {},
|
|
948
|
+
...requestId ? { requestId } : {}
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
function runConfigToJson(config, resolved, participants, env = process.env) {
|
|
952
|
+
const phases = config.phases?.map((phase) => ({
|
|
953
|
+
name: phase.name,
|
|
954
|
+
iterations: resolved.phaseIterations ?? phase.iterations
|
|
955
|
+
}));
|
|
956
|
+
const runConfig = {
|
|
957
|
+
benchmarkSlug: config.benchmarkSlug,
|
|
958
|
+
benchmarkName: config.benchmarkName,
|
|
959
|
+
...resolved.phaseIterations !== void 0 ? { phaseIterations: resolved.phaseIterations } : {},
|
|
960
|
+
...phases ? { phases } : {},
|
|
961
|
+
...!config.phases ? { iterations: resolved.iterations } : {},
|
|
962
|
+
concurrency: resolved.concurrency,
|
|
963
|
+
staggerDelayMs: resolved.staggerDelayMs,
|
|
964
|
+
groupBy: resolved.groupBy,
|
|
965
|
+
...config.dimensions ? { dimensions: config.dimensions } : {},
|
|
966
|
+
...config.scoring ? { scoring: config.scoring } : {},
|
|
967
|
+
participants,
|
|
968
|
+
trigger: triggerToJson(env)
|
|
969
|
+
};
|
|
970
|
+
return JSON.parse(JSON.stringify(runConfig));
|
|
971
|
+
}
|
|
972
|
+
async function runBenchmark(fileConfig, task, argv = [], options = {}) {
|
|
973
|
+
const args = parseCliArgs(argv, fileConfig.customCliFlags);
|
|
480
974
|
const noIngest = args.noIngest ?? isEnvNoIngest();
|
|
481
975
|
const shaped = applyShape(fileConfig, resolveShape(fileConfig, args.shape));
|
|
482
976
|
const config = applyIdentityOverrides(shaped, args);
|
|
483
977
|
const resolved = mergeConfig(config, args);
|
|
484
|
-
const available = resolveParticipants(config, resolved);
|
|
485
978
|
let baseUrl = "";
|
|
486
979
|
let apiKey = "";
|
|
980
|
+
let token;
|
|
981
|
+
let orgSlug;
|
|
982
|
+
let orgId;
|
|
487
983
|
let client = null;
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
984
|
+
const auth = noIngest ? await resolveAuth({ baseUrl: options.baseUrl, apiKey: options.apiKey }).catch(() => null) : await resolveAuth({ baseUrl: options.baseUrl, apiKey: options.apiKey });
|
|
985
|
+
if (auth) {
|
|
986
|
+
baseUrl = auth.apiBaseUrl;
|
|
987
|
+
apiKey = auth.apiKey ?? "";
|
|
988
|
+
token = auth.token;
|
|
989
|
+
orgSlug = auth.orgSlug;
|
|
990
|
+
orgId = auth.orgId;
|
|
991
|
+
client = createBenchmarkClient({
|
|
992
|
+
baseUrl: auth.apiBaseUrl,
|
|
993
|
+
apiKey: auth.apiKey,
|
|
994
|
+
token: auth.token,
|
|
995
|
+
orgSlug: auth.orgSlug,
|
|
996
|
+
orgId: auth.orgId
|
|
997
|
+
});
|
|
491
998
|
}
|
|
492
|
-
const
|
|
999
|
+
const available = resolveParticipants(config, resolved);
|
|
1000
|
+
const schedule = buildSchedule(config, resolved, task);
|
|
493
1001
|
const totalTasks = schedule.length;
|
|
494
|
-
const concurrencyLabel =
|
|
1002
|
+
const concurrencyLabel = String(resolved.concurrency);
|
|
495
1003
|
console.log(`${config.benchmarkName} (self-contained)`);
|
|
496
1004
|
console.log(`Date: ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
497
1005
|
if (noIngest) {
|
|
@@ -508,14 +1016,32 @@ async function runBenchmark(fileConfig, task, argv = []) {
|
|
|
508
1016
|
runId = "no-ingest";
|
|
509
1017
|
dashboardUrl = "";
|
|
510
1018
|
} else {
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
1019
|
+
{
|
|
1020
|
+
const benchmarkConfig = {
|
|
1021
|
+
...config.scoring ? { scoring: config.scoring } : {},
|
|
1022
|
+
...config.display ? { display: config.display } : {}
|
|
1023
|
+
};
|
|
1024
|
+
let initializeTarget = identityIsOurs;
|
|
1025
|
+
if (!initializeTarget) {
|
|
1026
|
+
try {
|
|
1027
|
+
await client.getBenchmark(config.benchmarkSlug);
|
|
1028
|
+
} catch (error) {
|
|
1029
|
+
if (!(error instanceof BenchmarkApiError && error.status === 404)) throw error;
|
|
1030
|
+
initializeTarget = true;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
const upsertInput = {};
|
|
1034
|
+
if (initializeTarget) {
|
|
1035
|
+
upsertInput.name = config.benchmarkName;
|
|
1036
|
+
if (Object.keys(benchmarkConfig).length > 0) upsertInput.config = benchmarkConfig;
|
|
1037
|
+
}
|
|
1038
|
+
await client.upsertBenchmark(config.benchmarkSlug, upsertInput);
|
|
515
1039
|
}
|
|
1040
|
+
const runConfig = client ? runConfigToJson(config, resolved, available.map((p) => p.name)) : {};
|
|
516
1041
|
if (args.runKey) {
|
|
517
1042
|
const { run: run2, organizationSlug } = await client.createRun(config.benchmarkSlug, {
|
|
518
|
-
runKey: args.runKey
|
|
1043
|
+
runKey: args.runKey,
|
|
1044
|
+
config: runConfig
|
|
519
1045
|
});
|
|
520
1046
|
runId = run2.id;
|
|
521
1047
|
dashboardUrl = dashboardUrlFor(baseUrl, organizationSlug, config.benchmarkSlug, run2.id);
|
|
@@ -529,7 +1055,8 @@ async function runBenchmark(fileConfig, task, argv = []) {
|
|
|
529
1055
|
const { run: run2, organizationSlug } = await client.createRun(config.benchmarkSlug, {
|
|
530
1056
|
totalTasks,
|
|
531
1057
|
workerCount: 1,
|
|
532
|
-
participants: available.map((p) => p.name)
|
|
1058
|
+
participants: available.map((p) => p.name),
|
|
1059
|
+
config: runConfig
|
|
533
1060
|
});
|
|
534
1061
|
runId = run2.id;
|
|
535
1062
|
dashboardUrl = dashboardUrlFor(baseUrl, organizationSlug, config.benchmarkSlug, run2.id);
|
|
@@ -541,9 +1068,9 @@ async function runBenchmark(fileConfig, task, argv = []) {
|
|
|
541
1068
|
const onResult = defaultOnResult;
|
|
542
1069
|
let participantRecords;
|
|
543
1070
|
if (resolved.groupBy === "round") {
|
|
544
|
-
participantRecords = await runGroupedByRound(config, schedule, available, resolved, client, runId, baseUrl, apiKey, onResult, noIngest);
|
|
1071
|
+
participantRecords = await runGroupedByRound(config, schedule, available, resolved, client, runId, baseUrl, apiKey, token, orgSlug, orgId, onResult, noIngest);
|
|
545
1072
|
} else {
|
|
546
|
-
participantRecords = await runGroupedByParticipant(config, schedule, available, resolved, client, runId, onResult);
|
|
1073
|
+
participantRecords = await runGroupedByParticipant(config, schedule, available, resolved, client, runId, onResult, noIngest);
|
|
547
1074
|
}
|
|
548
1075
|
console.log(`All done. ${noIngest ? "No platform run created." : `View at: ${dashboardUrl}`}`);
|
|
549
1076
|
const outcome = {
|
|
@@ -552,20 +1079,25 @@ async function runBenchmark(fileConfig, task, argv = []) {
|
|
|
552
1079
|
participants: participantRecords,
|
|
553
1080
|
config: resolved
|
|
554
1081
|
};
|
|
555
|
-
if (client && config.onScore) {
|
|
1082
|
+
if (!noIngest && client && (config.onScore || config.scoring)) {
|
|
556
1083
|
try {
|
|
557
|
-
const spec = await config.onScore(lowerIsBetter, higherIsBetter);
|
|
558
|
-
const scored = score(outcome, spec);
|
|
1084
|
+
const spec = config.onScore ? await config.onScore(lowerIsBetter, higherIsBetter) : scoringConfigToSpec(config.scoring, config.dimensions, config.display);
|
|
1085
|
+
const scored = score(outcome, spec, config.display?.metrics);
|
|
559
1086
|
const run2 = {
|
|
560
1087
|
gitSha: process.env.GITHUB_SHA ?? getGitSha(),
|
|
561
1088
|
gitRef: process.env.GITHUB_REF_NAME ?? process.env.GITHUB_REF ?? getGitRef(),
|
|
562
|
-
triggeredBy:
|
|
1089
|
+
triggeredBy: resolveTriggerSource(),
|
|
563
1090
|
nodeVersion: process.version,
|
|
564
1091
|
platform: os.platform(),
|
|
565
1092
|
arch: os.arch()
|
|
566
1093
|
};
|
|
567
|
-
await client.submitRunSummary(config.benchmarkSlug, runId, {
|
|
1094
|
+
await client.submitRunSummary(config.benchmarkSlug, runId, {
|
|
1095
|
+
run: run2,
|
|
1096
|
+
results: scored,
|
|
1097
|
+
...config.scoring ? { scoring: config.scoring } : {}
|
|
1098
|
+
});
|
|
568
1099
|
} catch (err) {
|
|
1100
|
+
if (err instanceof ScoringSpecError) throw err;
|
|
569
1101
|
const message = err instanceof Error ? err.message : String(err);
|
|
570
1102
|
console.warn(`[benchsdk-runner] failed to submit run summary: ${message}`);
|
|
571
1103
|
}
|
|
@@ -573,6 +1105,22 @@ async function runBenchmark(fileConfig, task, argv = []) {
|
|
|
573
1105
|
if (config.onComplete) await config.onComplete(outcome);
|
|
574
1106
|
return outcome;
|
|
575
1107
|
}
|
|
1108
|
+
async function runBenchmarkWorker(options) {
|
|
1109
|
+
const config = defineBenchmarkConfig({
|
|
1110
|
+
benchmarkSlug: options.benchmarkSlug,
|
|
1111
|
+
benchmarkName: options.benchmarkName ?? options.benchmarkSlug,
|
|
1112
|
+
participants: [options.participant],
|
|
1113
|
+
iterations: options.iterations ?? 1,
|
|
1114
|
+
concurrency: options.concurrency ?? 1,
|
|
1115
|
+
staggerDelayMs: options.staggerDelayMs ?? 0,
|
|
1116
|
+
groupBy: options.groupBy ?? "participant",
|
|
1117
|
+
defaultProviders: [options.participant.name]
|
|
1118
|
+
});
|
|
1119
|
+
const argv = [];
|
|
1120
|
+
if (options.runKey) argv.push("--run-key", options.runKey);
|
|
1121
|
+
if (options.noIngest) argv.push("--dry-run");
|
|
1122
|
+
return runBenchmark(config, options.task, argv);
|
|
1123
|
+
}
|
|
576
1124
|
function getGitSha() {
|
|
577
1125
|
if (process.env.GITHUB_SHA) return process.env.GITHUB_SHA;
|
|
578
1126
|
try {
|
|
@@ -590,13 +1138,13 @@ function getGitRef() {
|
|
|
590
1138
|
return void 0;
|
|
591
1139
|
}
|
|
592
1140
|
}
|
|
593
|
-
async function runGroupedByParticipant(config, schedule, available, resolved, client, runId, onResult) {
|
|
1141
|
+
async function runGroupedByParticipant(config, schedule, available, resolved, client, runId, onResult, noIngest) {
|
|
594
1142
|
const participantRecords = [];
|
|
595
1143
|
for (const participant of available) {
|
|
596
1144
|
console.log(`${"=".repeat(70)}`);
|
|
597
1145
|
console.log(` Participant: ${participant.name}`);
|
|
598
1146
|
console.log("=".repeat(70));
|
|
599
|
-
if (!client) {
|
|
1147
|
+
if (noIngest || !client) {
|
|
600
1148
|
const records = [];
|
|
601
1149
|
let rampStartMs2;
|
|
602
1150
|
let nextIndex = 0;
|
|
@@ -628,7 +1176,7 @@ async function runGroupedByParticipant(config, schedule, available, resolved, cl
|
|
|
628
1176
|
}
|
|
629
1177
|
let rampStartMs;
|
|
630
1178
|
await client.planWorkers(config.benchmarkSlug, runId, participant.name);
|
|
631
|
-
const result = await
|
|
1179
|
+
const result = await runWorker(client, {
|
|
632
1180
|
benchmarkSlug: config.benchmarkSlug,
|
|
633
1181
|
runId,
|
|
634
1182
|
participantSlug: participant.name,
|
|
@@ -641,16 +1189,21 @@ async function runGroupedByParticipant(config, schedule, available, resolved, cl
|
|
|
641
1189
|
if (waitMs > 0) await sleep(waitMs);
|
|
642
1190
|
}
|
|
643
1191
|
const slot = schedule[scheduleIndex];
|
|
644
|
-
const taskResult = await slot.task({
|
|
645
|
-
participant,
|
|
646
|
-
taskIndex: scheduleIndex,
|
|
647
|
-
phase: slot.phase,
|
|
648
|
-
step: (name, fn, options) => runStepWithClient(ctx.step, name, fn, options),
|
|
649
|
-
measure: ctx.measure,
|
|
650
|
-
log: ctx.log
|
|
651
|
-
});
|
|
652
1192
|
if (slot.phase) ctx.measure({ phase: slot.phase });
|
|
653
|
-
|
|
1193
|
+
try {
|
|
1194
|
+
const taskResult = await slot.task({
|
|
1195
|
+
participant,
|
|
1196
|
+
taskIndex: scheduleIndex,
|
|
1197
|
+
phase: slot.phase,
|
|
1198
|
+
step: (name, fn, options) => runStepWithClient(ctx.step, name, fn, options, participant.name),
|
|
1199
|
+
measure: ctx.measure,
|
|
1200
|
+
log: ctx.log
|
|
1201
|
+
});
|
|
1202
|
+
return taskResult?.data;
|
|
1203
|
+
} catch (error) {
|
|
1204
|
+
if (isTaskError(error) && error.data) ctx.measure(error.data);
|
|
1205
|
+
throw error;
|
|
1206
|
+
}
|
|
654
1207
|
},
|
|
655
1208
|
onResult: (record) => onResult(record, { iterations: schedule.length, participant: participant.name })
|
|
656
1209
|
});
|
|
@@ -666,14 +1219,17 @@ async function runGroupedByParticipant(config, schedule, available, resolved, cl
|
|
|
666
1219
|
}
|
|
667
1220
|
return participantRecords;
|
|
668
1221
|
}
|
|
669
|
-
async function runGroupedByRound(config, schedule, available, resolved, client, runId, baseUrl, apiKey, onResult, noIngest = false) {
|
|
1222
|
+
async function runGroupedByRound(config, schedule, available, resolved, client, runId, baseUrl, apiKey, token, orgSlug, orgId, onResult, noIngest = false) {
|
|
670
1223
|
const reporters = /* @__PURE__ */ new Map();
|
|
671
1224
|
const logBuffers = /* @__PURE__ */ new Map();
|
|
672
1225
|
const failed = /* @__PURE__ */ new Map();
|
|
673
1226
|
const recordsByParticipant = /* @__PURE__ */ new Map();
|
|
1227
|
+
let metricsCollector;
|
|
1228
|
+
const metricsSamples = [];
|
|
674
1229
|
for (const participant of available) {
|
|
675
1230
|
logBuffers.set(participant.name, new LogBuffer());
|
|
676
1231
|
failed.set(participant.name, false);
|
|
1232
|
+
recordsByParticipant.set(participant.name, []);
|
|
677
1233
|
if (noIngest || !client) {
|
|
678
1234
|
reporters.set(participant.name, null);
|
|
679
1235
|
continue;
|
|
@@ -687,6 +1243,9 @@ async function runGroupedByRound(config, schedule, available, resolved, client,
|
|
|
687
1243
|
reporter = await BenchmarkReporter.claim({
|
|
688
1244
|
baseUrl,
|
|
689
1245
|
apiKey,
|
|
1246
|
+
token,
|
|
1247
|
+
orgSlug,
|
|
1248
|
+
orgId,
|
|
690
1249
|
benchmarkSlug: config.benchmarkSlug,
|
|
691
1250
|
runId,
|
|
692
1251
|
participantSlug: participant.name,
|
|
@@ -700,6 +1259,10 @@ async function runGroupedByRound(config, schedule, available, resolved, client,
|
|
|
700
1259
|
console.warn(` ${participant.name}: could not claim a platform worker \u2014 running without platform reporting.`);
|
|
701
1260
|
}
|
|
702
1261
|
reporters.set(participant.name, reporter);
|
|
1262
|
+
if (reporter && !metricsCollector) {
|
|
1263
|
+
metricsCollector = createSystemMetricsCollector();
|
|
1264
|
+
metricsSamples.push(await metricsCollector.sample());
|
|
1265
|
+
}
|
|
703
1266
|
}
|
|
704
1267
|
console.log(`Interleaving ${available.length} participant(s), ${schedule.length} round(s) each.
|
|
705
1268
|
`);
|
|
@@ -708,7 +1271,7 @@ async function runGroupedByRound(config, schedule, available, resolved, client,
|
|
|
708
1271
|
if (resolved.staggerDelayMs > 0 && i > 0) {
|
|
709
1272
|
await sleep(resolved.staggerDelayMs);
|
|
710
1273
|
}
|
|
711
|
-
|
|
1274
|
+
const roundFns = available.map((participant) => async () => {
|
|
712
1275
|
const reporter = reporters.get(participant.name) ?? null;
|
|
713
1276
|
const logBuffer = logBuffers.get(participant.name);
|
|
714
1277
|
const record = await runTaskRecord(
|
|
@@ -722,9 +1285,6 @@ async function runGroupedByRound(config, schedule, available, resolved, client,
|
|
|
722
1285
|
if (record.status !== "success") failed.set(participant.name, true);
|
|
723
1286
|
onResult(record, { iterations: schedule.length, participant: participant.name });
|
|
724
1287
|
reporter?.recordResult(record);
|
|
725
|
-
if (!recordsByParticipant.has(participant.name)) {
|
|
726
|
-
recordsByParticipant.set(participant.name, []);
|
|
727
|
-
}
|
|
728
1288
|
const participantRecords = recordsByParticipant.get(participant.name);
|
|
729
1289
|
participantRecords.push(record);
|
|
730
1290
|
if (reporter) {
|
|
@@ -736,7 +1296,22 @@ async function runGroupedByRound(config, schedule, available, resolved, client,
|
|
|
736
1296
|
});
|
|
737
1297
|
await reporter.heartbeat();
|
|
738
1298
|
}
|
|
739
|
-
}
|
|
1299
|
+
});
|
|
1300
|
+
await runWithConcurrency(roundFns, resolved.concurrency);
|
|
1301
|
+
if (metricsCollector) metricsSamples.push(await metricsCollector.sample());
|
|
1302
|
+
}
|
|
1303
|
+
if (metricsCollector) metricsSamples.push(await metricsCollector.sample());
|
|
1304
|
+
metricsCollector?.stop();
|
|
1305
|
+
const metricsReporter = available.map((p) => reporters.get(p.name)).find((r) => Boolean(r));
|
|
1306
|
+
if (metricsReporter && metricsSamples.length > 0) {
|
|
1307
|
+
await metricsReporter.uploadArtifact({
|
|
1308
|
+
kind: "system-metrics",
|
|
1309
|
+
contentType: "application/x-ndjson",
|
|
1310
|
+
name: "metrics.jsonl",
|
|
1311
|
+
metadata: { scope: "shared-process", participants: available.map((p) => p.name) },
|
|
1312
|
+
body: metricsSamples.map((sample) => JSON.stringify(sample)).join("\n") + "\n"
|
|
1313
|
+
}).catch(() => {
|
|
1314
|
+
});
|
|
740
1315
|
}
|
|
741
1316
|
for (const participant of available) {
|
|
742
1317
|
const reporter = reporters.get(participant.name) ?? null;
|
|
@@ -777,17 +1352,19 @@ async function runTaskRecord(task, participant, scheduleIndex, taskIndex, phase,
|
|
|
777
1352
|
completedAt: new Date(stepStartedAtMs).toISOString(),
|
|
778
1353
|
latencyMs: 0
|
|
779
1354
|
};
|
|
780
|
-
|
|
1355
|
+
const requestedParallelism = options?.parallelInvocations ?? options?.concurrency;
|
|
1356
|
+
if (requestedParallelism !== void 0) stepRecord.concurrency = requestedParallelism;
|
|
781
1357
|
if (options?.timeoutMs !== void 0) stepRecord.timeoutMs = options.timeoutMs;
|
|
782
1358
|
const previousStep = activeStep;
|
|
783
1359
|
activeStep = stepRecord;
|
|
784
1360
|
try {
|
|
785
|
-
const result2 = await runStepInvocations(name, fn, options);
|
|
786
|
-
|
|
1361
|
+
const result2 = await runStepInvocations(name, fn, options, participant.name);
|
|
1362
|
+
const outcome = options?.captureOutput !== false && !Array.isArray(result2) && isStepOutcome(result2) ? result2 : {};
|
|
1363
|
+
logBuffer.step(taskIndex, name, outcome);
|
|
787
1364
|
return result2;
|
|
788
1365
|
} catch (error) {
|
|
789
1366
|
stepRecord.status = "error";
|
|
790
|
-
stepRecord.errorCode = error
|
|
1367
|
+
stepRecord.errorCode = isTaskError(error) ? error.code ?? error.name : getErrorCode(error);
|
|
791
1368
|
logBuffer.step(taskIndex, name, { error: error instanceof Error ? error.message : String(error) });
|
|
792
1369
|
throw error;
|
|
793
1370
|
} finally {
|
|
@@ -804,8 +1381,8 @@ async function runTaskRecord(task, participant, scheduleIndex, taskIndex, phase,
|
|
|
804
1381
|
Object.assign(taskMeasures, data);
|
|
805
1382
|
}
|
|
806
1383
|
},
|
|
807
|
-
log(message,
|
|
808
|
-
logBuffer.line(`[task ${taskIndex}] ${message}`,
|
|
1384
|
+
log(message, metaOrOptions) {
|
|
1385
|
+
logBuffer.line(`[task ${taskIndex}] ${message}`, metaOrOptions);
|
|
809
1386
|
}
|
|
810
1387
|
};
|
|
811
1388
|
let result = void 0;
|
|
@@ -814,7 +1391,7 @@ async function runTaskRecord(task, participant, scheduleIndex, taskIndex, phase,
|
|
|
814
1391
|
record.data = mergeData({ ...taskMeasures, ...result?.data ?? {} }, phase);
|
|
815
1392
|
} catch (error) {
|
|
816
1393
|
record.status = "error";
|
|
817
|
-
if (error
|
|
1394
|
+
if (isTaskError(error)) {
|
|
818
1395
|
record.errorCode = error.code ?? error.name;
|
|
819
1396
|
record.data = mergeData({ ...taskMeasures, ...error.data ?? {} }, phase);
|
|
820
1397
|
if (error.steps?.length) frameworkSteps.push(...error.steps);
|
|
@@ -850,16 +1427,145 @@ async function runTaskRecord(task, participant, scheduleIndex, taskIndex, phase,
|
|
|
850
1427
|
// src/cli.ts
|
|
851
1428
|
import { resolve } from "path";
|
|
852
1429
|
import { pathToFileURL } from "url";
|
|
853
|
-
|
|
1430
|
+
import { run as runPlatformCli, resolveAuth as resolveAuth2 } from "@benchsdk/cli";
|
|
1431
|
+
import { createBenchmarkClient as createBenchmarkClient2 } from "@benchsdk/api";
|
|
1432
|
+
import { filterParticipantsByEnv as filterParticipantsByEnv2, selectParticipants as selectParticipants2 } from "@benchsdk/worker";
|
|
1433
|
+
var USAGE = 'Usage:\n bench run <file.bench.ts> [--shape name] [--provider a,b] [--run-key key]\n [--benchmark slug] [--name "My benchmark"]\n [--iterations N] [--concurrency N] [--stagger-delay-ms N] [--group-by participant|round]\n [--no-ingest | --dry-run] [--check]\n bench check <file.bench.ts> [--base-url <url>] [--api-key <key>]';
|
|
854
1434
|
function isBenchmarkConfig(value) {
|
|
855
1435
|
if (typeof value !== "object" || value === null) return false;
|
|
856
1436
|
const candidate = value;
|
|
857
1437
|
return typeof candidate.benchmarkSlug === "string" && Array.isArray(candidate.participants);
|
|
858
1438
|
}
|
|
1439
|
+
function shiftFlag(argv, name) {
|
|
1440
|
+
const prefix = `--${name}`;
|
|
1441
|
+
const prefixEq = `${prefix}=`;
|
|
1442
|
+
const result = [];
|
|
1443
|
+
let value;
|
|
1444
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1445
|
+
const arg = argv[i];
|
|
1446
|
+
if (arg === prefix) {
|
|
1447
|
+
const next = argv[++i];
|
|
1448
|
+
if (!next || next.startsWith("--")) throw new Error(USAGE);
|
|
1449
|
+
value = next;
|
|
1450
|
+
continue;
|
|
1451
|
+
}
|
|
1452
|
+
if (arg.startsWith(prefixEq)) {
|
|
1453
|
+
const eqValue = arg.slice(prefixEq.length);
|
|
1454
|
+
if (!eqValue) throw new Error(USAGE);
|
|
1455
|
+
value = eqValue;
|
|
1456
|
+
continue;
|
|
1457
|
+
}
|
|
1458
|
+
result.push(arg);
|
|
1459
|
+
}
|
|
1460
|
+
return { value, argv: result };
|
|
1461
|
+
}
|
|
1462
|
+
function shiftPlatformFlags(flags) {
|
|
1463
|
+
const { value: baseUrl, argv: withoutBaseUrl } = shiftFlag(flags, "base-url");
|
|
1464
|
+
const { value: apiKey, argv: rest } = shiftFlag(withoutBaseUrl, "api-key");
|
|
1465
|
+
return { baseUrl, apiKey, flags: rest };
|
|
1466
|
+
}
|
|
1467
|
+
async function runCheck(argv) {
|
|
1468
|
+
const [command, ...rest] = argv;
|
|
1469
|
+
const [file, ...flags] = rest;
|
|
1470
|
+
if (command !== "check" || !file || file.startsWith("-")) throw new Error(USAGE);
|
|
1471
|
+
const mod = await import(pathToFileURL(resolve(process.cwd(), file)).href);
|
|
1472
|
+
const config = mod.config;
|
|
1473
|
+
const task = mod.task ?? mod.default;
|
|
1474
|
+
if (!isBenchmarkConfig(config)) {
|
|
1475
|
+
throw new Error(`${file} must export a \`config\` created with defineBenchmarkConfig (with participants).`);
|
|
1476
|
+
}
|
|
1477
|
+
if (typeof task !== "function") {
|
|
1478
|
+
throw new Error(`${file} must export a \`task\` created with defineTask.`);
|
|
1479
|
+
}
|
|
1480
|
+
const cfg = config;
|
|
1481
|
+
const configIssues = validateBenchmarkConfig(cfg);
|
|
1482
|
+
if (configIssues.length > 0) {
|
|
1483
|
+
throw new BenchmarkConfigError(configIssues);
|
|
1484
|
+
}
|
|
1485
|
+
const { baseUrl, apiKey, flags: runnerFlags } = shiftPlatformFlags(flags);
|
|
1486
|
+
const parsed = parseCliArgs(runnerFlags, cfg.customCliFlags ?? []);
|
|
1487
|
+
resolveShape(cfg, parsed.shape);
|
|
1488
|
+
const dryRun = parsed.noIngest ?? false;
|
|
1489
|
+
let client;
|
|
1490
|
+
let apiOk = dryRun;
|
|
1491
|
+
let auth = null;
|
|
1492
|
+
if (!dryRun) {
|
|
1493
|
+
try {
|
|
1494
|
+
auth = await resolveAuth2({ baseUrl, apiKey });
|
|
1495
|
+
client = createBenchmarkClient2({
|
|
1496
|
+
baseUrl: auth.apiBaseUrl,
|
|
1497
|
+
apiKey: auth.apiKey,
|
|
1498
|
+
token: auth.token,
|
|
1499
|
+
orgSlug: auth.orgSlug,
|
|
1500
|
+
orgId: auth.orgId
|
|
1501
|
+
});
|
|
1502
|
+
await client.listBenchmarks({ limit: 1 });
|
|
1503
|
+
apiOk = true;
|
|
1504
|
+
} catch (err) {
|
|
1505
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1506
|
+
console.warn(`[benchsdk] API connectivity check failed: ${message}`);
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
const effectiveProviderNames = parsed.providers ?? cfg.defaultProviders;
|
|
1510
|
+
let selected;
|
|
1511
|
+
try {
|
|
1512
|
+
selected = selectParticipants2(cfg.participants, effectiveProviderNames);
|
|
1513
|
+
} catch (err) {
|
|
1514
|
+
throw new Error(`Participant selection failed: ${err instanceof Error ? err.message : err}`);
|
|
1515
|
+
}
|
|
1516
|
+
const { available, skipped } = filterParticipantsByEnv2(selected);
|
|
1517
|
+
let scoringOk = true;
|
|
1518
|
+
if (cfg.onScore) {
|
|
1519
|
+
try {
|
|
1520
|
+
const spec = await cfg.onScore(lowerIsBetter, higherIsBetter);
|
|
1521
|
+
validateScoringSpec(spec);
|
|
1522
|
+
} catch (err) {
|
|
1523
|
+
scoringOk = false;
|
|
1524
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1525
|
+
console.warn(`[benchsdk] Scoring validation failed: ${message}`);
|
|
1526
|
+
}
|
|
1527
|
+
} else if (cfg.scoring) {
|
|
1528
|
+
try {
|
|
1529
|
+
const spec = scoringConfigToSpec(cfg.scoring, cfg.dimensions);
|
|
1530
|
+
validateScoringSpec(spec);
|
|
1531
|
+
} catch (err) {
|
|
1532
|
+
scoringOk = false;
|
|
1533
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1534
|
+
console.warn(`[benchsdk] Scoring validation failed: ${message}`);
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
const missingPlatformAuth = dryRun ? [] : auth ? [] : [["BENCHMARKS_PLATFORM_API_KEY or BENCHMARKS_PLATFORM_TOKEN", void 0]];
|
|
1538
|
+
for (const [name] of missingPlatformAuth) {
|
|
1539
|
+
console.warn(`[benchsdk] ${name} is not set`);
|
|
1540
|
+
}
|
|
1541
|
+
const report = {
|
|
1542
|
+
file,
|
|
1543
|
+
benchmarkSlug: cfg.benchmarkSlug,
|
|
1544
|
+
apiOk,
|
|
1545
|
+
envOk: dryRun || missingPlatformAuth.length === 0,
|
|
1546
|
+
participants: {
|
|
1547
|
+
requested: selected.map((p) => p.name),
|
|
1548
|
+
available: available.map((p) => p.name),
|
|
1549
|
+
skipped: skipped.map((s) => ({ name: s.name, missing: s.missing }))
|
|
1550
|
+
},
|
|
1551
|
+
scoringOk: cfg.scoring || cfg.onScore ? scoringOk : void 0
|
|
1552
|
+
};
|
|
1553
|
+
console.log(JSON.stringify(report, null, 2));
|
|
1554
|
+
const authFailure = !dryRun && missingPlatformAuth.length > 0;
|
|
1555
|
+
if (!apiOk || available.length === 0 || scoringOk === false || authFailure) {
|
|
1556
|
+
throw new Error("Benchmark check failed. See warnings above for details.");
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
859
1559
|
async function runBenchmarkFile(argv) {
|
|
860
1560
|
const [command, ...rest] = argv;
|
|
861
1561
|
const [file, ...flags] = rest;
|
|
862
1562
|
if (command !== "run" || !file || file.startsWith("-")) throw new Error(USAGE);
|
|
1563
|
+
const check = flags.includes("--check") || flags.includes("--validate");
|
|
1564
|
+
if (check) {
|
|
1565
|
+
const checkFlags = flags.filter((f) => f !== "--check" && f !== "--validate");
|
|
1566
|
+
return runCheck(["check", file, ...checkFlags]);
|
|
1567
|
+
}
|
|
1568
|
+
const { baseUrl, apiKey, flags: runnerFlags } = shiftPlatformFlags(flags);
|
|
863
1569
|
const mod = await import(pathToFileURL(resolve(process.cwd(), file)).href);
|
|
864
1570
|
const config = mod.config;
|
|
865
1571
|
const task = mod.task ?? mod.default;
|
|
@@ -869,33 +1575,84 @@ async function runBenchmarkFile(argv) {
|
|
|
869
1575
|
if (typeof task !== "function") {
|
|
870
1576
|
throw new Error(`${file} must export a \`task\` created with defineTask.`);
|
|
871
1577
|
}
|
|
872
|
-
|
|
1578
|
+
const envFlags = [];
|
|
1579
|
+
if (process.env.BENCHMARK_SLUG) {
|
|
1580
|
+
envFlags.push("--benchmark", process.env.BENCHMARK_SLUG);
|
|
1581
|
+
}
|
|
1582
|
+
if (process.env.BENCHMARK_NAME) {
|
|
1583
|
+
envFlags.push("--name", process.env.BENCHMARK_NAME);
|
|
1584
|
+
}
|
|
1585
|
+
await runBenchmark(
|
|
1586
|
+
config,
|
|
1587
|
+
task,
|
|
1588
|
+
[...envFlags, ...runnerFlags],
|
|
1589
|
+
{ baseUrl, apiKey }
|
|
1590
|
+
);
|
|
873
1591
|
}
|
|
874
1592
|
async function run(argv) {
|
|
1593
|
+
const [command, ...rest] = argv;
|
|
875
1594
|
try {
|
|
876
|
-
|
|
1595
|
+
if (command === "run") {
|
|
1596
|
+
await runBenchmarkFile(argv);
|
|
1597
|
+
} else if (command === "check") {
|
|
1598
|
+
await runCheck(argv);
|
|
1599
|
+
} else {
|
|
1600
|
+
return runPlatformCli(argv);
|
|
1601
|
+
}
|
|
877
1602
|
process.exit(0);
|
|
878
1603
|
} catch (err) {
|
|
879
1604
|
if (err instanceof NoAvailableParticipantsError) {
|
|
880
1605
|
console.log(err.message);
|
|
881
1606
|
process.exit(0);
|
|
882
1607
|
}
|
|
883
|
-
console.error("Benchmark failed:", err
|
|
1608
|
+
console.error("Benchmark failed:", String(err));
|
|
884
1609
|
process.exit(1);
|
|
885
1610
|
}
|
|
886
1611
|
}
|
|
1612
|
+
|
|
1613
|
+
// src/index.ts
|
|
1614
|
+
import { resolveAuth as resolveAuth3, createApiClient, AuthError } from "@benchsdk/cli";
|
|
1615
|
+
import {
|
|
1616
|
+
runWorker as runWorker2,
|
|
1617
|
+
BenchmarkReporter as BenchmarkReporter2,
|
|
1618
|
+
claimBenchmarkReporter,
|
|
1619
|
+
createSystemMetricsCollector as createSystemMetricsCollector2,
|
|
1620
|
+
filterParticipantsByEnv as filterParticipantsByEnv3,
|
|
1621
|
+
selectParticipants as selectParticipants3
|
|
1622
|
+
} from "@benchsdk/worker";
|
|
1623
|
+
import { BenchmarkApiError as BenchmarkApiError2, createBenchmarkClient as createBenchmarkClient3 } from "@benchsdk/api";
|
|
1624
|
+
var BENCHSDK_RUNNER_VERSION = "0.5.2";
|
|
887
1625
|
export {
|
|
1626
|
+
AuthError,
|
|
1627
|
+
BENCHSDK_RUNNER_VERSION,
|
|
1628
|
+
BenchmarkApiError2 as BenchmarkApiError,
|
|
1629
|
+
BenchmarkConfigError,
|
|
1630
|
+
BenchmarkReporter2 as BenchmarkReporter,
|
|
888
1631
|
NoAvailableParticipantsError,
|
|
1632
|
+
ScoringSpecError,
|
|
889
1633
|
TaskError,
|
|
1634
|
+
claimBenchmarkReporter,
|
|
1635
|
+
createApiClient,
|
|
1636
|
+
createBenchmarkClient3 as createBenchmarkClient,
|
|
1637
|
+
createSystemMetricsCollector2 as createSystemMetricsCollector,
|
|
890
1638
|
defineBenchmarkConfig,
|
|
1639
|
+
defineOnComplete,
|
|
891
1640
|
defineTask,
|
|
1641
|
+
filterParticipantsByEnv3 as filterParticipantsByEnv,
|
|
892
1642
|
higherIsBetter,
|
|
893
1643
|
lowerIsBetter,
|
|
894
1644
|
mergeConfig,
|
|
895
1645
|
parseCliArgs,
|
|
1646
|
+
resolveAuth3 as resolveAuth,
|
|
896
1647
|
run,
|
|
897
1648
|
runBenchmark,
|
|
898
1649
|
runBenchmarkFile,
|
|
899
|
-
|
|
1650
|
+
runBenchmarkWorker,
|
|
1651
|
+
runWorker2 as runWorker,
|
|
1652
|
+
score,
|
|
1653
|
+
scoringConfigToSpec,
|
|
1654
|
+
selectParticipants3 as selectParticipants,
|
|
1655
|
+
validateBenchmarkConfig,
|
|
1656
|
+
validateScoringSpec
|
|
900
1657
|
};
|
|
901
1658
|
//# sourceMappingURL=index.js.map
|