@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.cjs
CHANGED
|
@@ -30,18 +30,37 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var src_exports = {};
|
|
32
32
|
__export(src_exports, {
|
|
33
|
+
AuthError: () => import_cli4.AuthError,
|
|
34
|
+
BENCHSDK_RUNNER_VERSION: () => BENCHSDK_RUNNER_VERSION,
|
|
35
|
+
BenchmarkApiError: () => import_api3.BenchmarkApiError,
|
|
36
|
+
BenchmarkConfigError: () => BenchmarkConfigError,
|
|
37
|
+
BenchmarkReporter: () => import_worker3.BenchmarkReporter,
|
|
33
38
|
NoAvailableParticipantsError: () => NoAvailableParticipantsError,
|
|
39
|
+
ScoringSpecError: () => ScoringSpecError,
|
|
34
40
|
TaskError: () => TaskError,
|
|
41
|
+
claimBenchmarkReporter: () => import_worker3.claimBenchmarkReporter,
|
|
42
|
+
createApiClient: () => import_cli4.createApiClient,
|
|
43
|
+
createBenchmarkClient: () => import_api3.createBenchmarkClient,
|
|
44
|
+
createSystemMetricsCollector: () => import_worker3.createSystemMetricsCollector,
|
|
35
45
|
defineBenchmarkConfig: () => defineBenchmarkConfig,
|
|
46
|
+
defineOnComplete: () => defineOnComplete,
|
|
36
47
|
defineTask: () => defineTask,
|
|
48
|
+
filterParticipantsByEnv: () => import_worker3.filterParticipantsByEnv,
|
|
37
49
|
higherIsBetter: () => higherIsBetter,
|
|
38
50
|
lowerIsBetter: () => lowerIsBetter,
|
|
39
51
|
mergeConfig: () => mergeConfig,
|
|
40
52
|
parseCliArgs: () => parseCliArgs,
|
|
53
|
+
resolveAuth: () => import_cli4.resolveAuth,
|
|
41
54
|
run: () => run,
|
|
42
55
|
runBenchmark: () => runBenchmark,
|
|
43
56
|
runBenchmarkFile: () => runBenchmarkFile,
|
|
44
|
-
|
|
57
|
+
runBenchmarkWorker: () => runBenchmarkWorker,
|
|
58
|
+
runWorker: () => import_worker3.runWorker,
|
|
59
|
+
score: () => score,
|
|
60
|
+
scoringConfigToSpec: () => scoringConfigToSpec,
|
|
61
|
+
selectParticipants: () => import_worker3.selectParticipants,
|
|
62
|
+
validateBenchmarkConfig: () => validateBenchmarkConfig,
|
|
63
|
+
validateScoringSpec: () => validateScoringSpec
|
|
45
64
|
});
|
|
46
65
|
module.exports = __toCommonJS(src_exports);
|
|
47
66
|
|
|
@@ -50,67 +69,231 @@ var TaskError = class extends Error {
|
|
|
50
69
|
code;
|
|
51
70
|
data;
|
|
52
71
|
steps;
|
|
72
|
+
step;
|
|
73
|
+
timeoutMs;
|
|
53
74
|
constructor(message, opts) {
|
|
54
75
|
super(message);
|
|
55
76
|
this.name = "TaskError";
|
|
56
77
|
this.code = opts?.code;
|
|
57
78
|
this.data = opts?.data;
|
|
58
79
|
this.steps = opts?.steps;
|
|
80
|
+
this.step = opts?.step;
|
|
81
|
+
this.timeoutMs = opts?.timeoutMs;
|
|
82
|
+
}
|
|
83
|
+
toString() {
|
|
84
|
+
let s = `[${this.name}${this.code ? ` (${this.code})` : ""}] ${this.message}`;
|
|
85
|
+
if (this.step) {
|
|
86
|
+
s += `
|
|
87
|
+
step: ${this.step}`;
|
|
88
|
+
}
|
|
89
|
+
if (this.timeoutMs !== void 0) {
|
|
90
|
+
s += `
|
|
91
|
+
timeoutMs: ${this.timeoutMs}`;
|
|
92
|
+
}
|
|
93
|
+
if (this.data && Object.keys(this.data).length > 0) {
|
|
94
|
+
s += `
|
|
95
|
+
data: ${JSON.stringify(this.data, null, 2)}`;
|
|
96
|
+
}
|
|
97
|
+
return s;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
var BenchmarkConfigError = class _BenchmarkConfigError extends Error {
|
|
101
|
+
issues;
|
|
102
|
+
constructor(issues) {
|
|
103
|
+
super(_BenchmarkConfigError.formatIssues(issues));
|
|
104
|
+
this.name = "BenchmarkConfigError";
|
|
105
|
+
this.issues = issues;
|
|
106
|
+
}
|
|
107
|
+
static formatIssues(issues) {
|
|
108
|
+
const lines = issues.map((i) => ` - ${i.field}: ${i.message}`);
|
|
109
|
+
return `Invalid benchmark config:
|
|
110
|
+
${lines.join("\n")}
|
|
111
|
+
|
|
112
|
+
Fix the fields above and try again.`;
|
|
59
113
|
}
|
|
60
114
|
};
|
|
61
|
-
function
|
|
62
|
-
if (value
|
|
63
|
-
|
|
64
|
-
throw new Error(`${field} must be an integer >= 1 (got ${value})`);
|
|
115
|
+
function assertFiniteNumber(value, field) {
|
|
116
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
117
|
+
throw new Error(`${field} must be a finite number (got ${value})`);
|
|
65
118
|
}
|
|
119
|
+
return value;
|
|
66
120
|
}
|
|
67
|
-
function
|
|
68
|
-
if (
|
|
69
|
-
throw new Error(
|
|
121
|
+
function assertNonEmptyString(value, field) {
|
|
122
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
123
|
+
throw new Error(`${field} must be a non-empty string`);
|
|
70
124
|
}
|
|
71
|
-
|
|
72
|
-
|
|
125
|
+
return value;
|
|
126
|
+
}
|
|
127
|
+
function assertOnlyAllowedKeys(value, allowed, field) {
|
|
128
|
+
for (const key of Object.keys(value)) {
|
|
129
|
+
if (!allowed.includes(key)) {
|
|
130
|
+
throw new Error(`${field} contains unexpected key: '${key}'`);
|
|
131
|
+
}
|
|
73
132
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
133
|
+
}
|
|
134
|
+
function validateBenchmarkScoringConfig(scoring, display) {
|
|
135
|
+
if (!Array.isArray(scoring.metrics) || scoring.metrics.length === 0) {
|
|
136
|
+
throw new Error("scoring.metrics must be a non-empty array");
|
|
137
|
+
}
|
|
138
|
+
if (scoring.success !== void 0) {
|
|
139
|
+
const requireData = scoring.success.requireData;
|
|
140
|
+
if (requireData === null || typeof requireData !== "object" || Array.isArray(requireData)) {
|
|
141
|
+
throw new Error("scoring.success.requireData must be a plain object");
|
|
77
142
|
}
|
|
78
|
-
if (
|
|
79
|
-
throw new Error("
|
|
143
|
+
if (Object.keys(requireData).length === 0) {
|
|
144
|
+
throw new Error("scoring.success.requireData must declare at least one data field");
|
|
80
145
|
}
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
if (
|
|
84
|
-
throw new Error(
|
|
146
|
+
for (const [key, value] of Object.entries(requireData)) {
|
|
147
|
+
const type = typeof value;
|
|
148
|
+
if (type !== "string" && type !== "number" && type !== "boolean") {
|
|
149
|
+
throw new Error(
|
|
150
|
+
`scoring.success.requireData.${key} must be a string, number, or boolean (got ${type})`
|
|
151
|
+
);
|
|
85
152
|
}
|
|
86
|
-
|
|
87
|
-
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const seen = /* @__PURE__ */ new Set();
|
|
156
|
+
let totalWeight = 0;
|
|
157
|
+
for (let i = 0; i < scoring.metrics.length; i++) {
|
|
158
|
+
const metric = scoring.metrics[i];
|
|
159
|
+
if (metric === null || typeof metric !== "object" || Array.isArray(metric)) {
|
|
160
|
+
throw new Error(`scoring.metrics[${i}] must be an object`);
|
|
161
|
+
}
|
|
162
|
+
const key = metric.key;
|
|
163
|
+
if (typeof key !== "string" || key.trim() === "") {
|
|
164
|
+
throw new Error(`scoring.metrics[${i}].key must be a non-empty string`);
|
|
165
|
+
}
|
|
166
|
+
if (seen.has(key)) {
|
|
167
|
+
throw new Error(`duplicate scoring metric key: ${key}`);
|
|
168
|
+
}
|
|
169
|
+
seen.add(key);
|
|
170
|
+
const displayMetrics = Array.isArray(display?.metrics) ? display.metrics : void 0;
|
|
171
|
+
const displayMetric = displayMetrics?.find((m) => m?.key === key);
|
|
172
|
+
if (metric.unit !== void 0) {
|
|
173
|
+
if (typeof metric.unit !== "string") {
|
|
174
|
+
throw new Error(`scoring.metrics[${i}].unit must be a string`);
|
|
88
175
|
}
|
|
89
|
-
|
|
90
|
-
|
|
176
|
+
if (displayMetric?.unit !== void 0 && metric.unit !== displayMetric.unit) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`scoring.metrics[${i}].unit '${metric.unit}' conflicts with display.metrics[${i}].unit '${displayMetric.unit}' for key '${key}'`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
assertFiniteNumber(metric.ceiling, `scoring.metrics[${i}].ceiling`);
|
|
183
|
+
if (metric.floor !== void 0) {
|
|
184
|
+
assertFiniteNumber(metric.floor, `scoring.metrics[${i}].floor`);
|
|
185
|
+
}
|
|
186
|
+
if (metric.weights === null || typeof metric.weights !== "object" || Array.isArray(metric.weights)) {
|
|
187
|
+
throw new Error(`scoring.metrics[${i}].weights must be an object`);
|
|
188
|
+
}
|
|
189
|
+
const median = assertFiniteNumber(metric.weights.median, `scoring.metrics[${i}].weights.median`);
|
|
190
|
+
const p95 = assertFiniteNumber(metric.weights.p95, `scoring.metrics[${i}].weights.p95`);
|
|
191
|
+
const p99 = assertFiniteNumber(metric.weights.p99, `scoring.metrics[${i}].weights.p99`);
|
|
192
|
+
if (median < 0 || p95 < 0 || p99 < 0) {
|
|
193
|
+
throw new Error(`scoring.metrics[${i}].weights must be non-negative`);
|
|
194
|
+
}
|
|
195
|
+
totalWeight += median + p95 + p99;
|
|
196
|
+
if (metric.trim !== void 0) {
|
|
197
|
+
assertFiniteNumber(metric.trim, `scoring.metrics[${i}].trim`);
|
|
91
198
|
}
|
|
92
199
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (config.staggerDelayMs !== void 0 && (!Number.isFinite(config.staggerDelayMs) || config.staggerDelayMs < 0)) {
|
|
96
|
-
throw new Error(`staggerDelayMs must be a number >= 0 (got ${config.staggerDelayMs})`);
|
|
200
|
+
if (Math.abs(totalWeight - 1) > 0.01) {
|
|
201
|
+
throw new Error(`scoring metric weights must sum to 1.0 (got ${totalWeight.toFixed(3)})`);
|
|
97
202
|
}
|
|
98
|
-
|
|
99
|
-
|
|
203
|
+
}
|
|
204
|
+
function validateBenchmarkDisplayConfig(display) {
|
|
205
|
+
if (typeof display !== "object" || display === null || Array.isArray(display)) {
|
|
206
|
+
throw new Error("display must be an object");
|
|
100
207
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
208
|
+
assertOnlyAllowedKeys(display, ["metrics", "steps", "overview"], "display");
|
|
209
|
+
const displayMetricKeys = /* @__PURE__ */ new Set();
|
|
210
|
+
if (display.metrics !== void 0) {
|
|
211
|
+
if (!Array.isArray(display.metrics)) {
|
|
212
|
+
throw new Error("display.metrics must be an array");
|
|
213
|
+
}
|
|
214
|
+
for (let i = 0; i < display.metrics.length; i++) {
|
|
215
|
+
const metric = display.metrics[i];
|
|
216
|
+
if (metric === null || typeof metric !== "object" || Array.isArray(metric)) {
|
|
217
|
+
throw new Error(`display.metrics[${i}] must be an object`);
|
|
218
|
+
}
|
|
219
|
+
assertOnlyAllowedKeys(metric, ["key", "label", "unit", "direction", "decimals", "order"], `display.metrics[${i}]`);
|
|
220
|
+
const key = assertNonEmptyString(metric.key, `display.metrics[${i}].key`);
|
|
221
|
+
if (displayMetricKeys.has(key)) {
|
|
222
|
+
throw new Error(`duplicate display metric key: ${key}`);
|
|
105
223
|
}
|
|
106
|
-
|
|
107
|
-
|
|
224
|
+
displayMetricKeys.add(key);
|
|
225
|
+
assertNonEmptyString(metric.label, `display.metrics[${i}].label`);
|
|
226
|
+
if (metric.unit !== void 0 && typeof metric.unit !== "string") {
|
|
227
|
+
throw new Error(`display.metrics[${i}].unit must be a string`);
|
|
108
228
|
}
|
|
109
|
-
if (
|
|
110
|
-
throw new Error(`
|
|
229
|
+
if (metric.direction !== void 0 && metric.direction !== "higher-better" && metric.direction !== "lower-better") {
|
|
230
|
+
throw new Error(`display.metrics[${i}].direction must be 'higher-better' or 'lower-better'`);
|
|
231
|
+
}
|
|
232
|
+
if (metric.decimals !== void 0 && (!Number.isInteger(metric.decimals) || metric.decimals < 0)) {
|
|
233
|
+
throw new Error(`display.metrics[${i}].decimals must be a non-negative integer`);
|
|
234
|
+
}
|
|
235
|
+
if (metric.order !== void 0 && (!Number.isInteger(metric.order) || metric.order < 0)) {
|
|
236
|
+
throw new Error(`display.metrics[${i}].order must be a non-negative integer`);
|
|
111
237
|
}
|
|
112
238
|
}
|
|
113
239
|
}
|
|
240
|
+
if (display.steps !== void 0) {
|
|
241
|
+
if (!Array.isArray(display.steps)) {
|
|
242
|
+
throw new Error("display.steps must be an array");
|
|
243
|
+
}
|
|
244
|
+
const seenStepKeys = /* @__PURE__ */ new Set();
|
|
245
|
+
for (let i = 0; i < display.steps.length; i++) {
|
|
246
|
+
const step = display.steps[i];
|
|
247
|
+
if (step === null || typeof step !== "object" || Array.isArray(step)) {
|
|
248
|
+
throw new Error(`display.steps[${i}] must be an object`);
|
|
249
|
+
}
|
|
250
|
+
assertOnlyAllowedKeys(step, ["key", "label", "order"], `display.steps[${i}]`);
|
|
251
|
+
const key = assertNonEmptyString(step.key, `display.steps[${i}].key`);
|
|
252
|
+
if (seenStepKeys.has(key)) {
|
|
253
|
+
throw new Error(`duplicate display step key: ${key}`);
|
|
254
|
+
}
|
|
255
|
+
seenStepKeys.add(key);
|
|
256
|
+
assertNonEmptyString(step.label, `display.steps[${i}].label`);
|
|
257
|
+
if (step.order !== void 0 && (!Number.isInteger(step.order) || step.order < 0)) {
|
|
258
|
+
throw new Error(`display.steps[${i}].order must be a non-negative integer`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (display.overview !== void 0) {
|
|
263
|
+
if (typeof display.overview !== "object" || display.overview === null || Array.isArray(display.overview)) {
|
|
264
|
+
throw new Error("display.overview must be an object");
|
|
265
|
+
}
|
|
266
|
+
assertOnlyAllowedKeys(display.overview, ["defaultMetric", "defaultLayout"], "display.overview");
|
|
267
|
+
const { defaultMetric, defaultLayout } = display.overview;
|
|
268
|
+
if (defaultMetric !== void 0) {
|
|
269
|
+
const metric = assertNonEmptyString(defaultMetric, "display.overview.defaultMetric");
|
|
270
|
+
const validDefaultMetrics = new Set(displayMetricKeys);
|
|
271
|
+
validDefaultMetrics.add("compositeScore");
|
|
272
|
+
validDefaultMetrics.add("task");
|
|
273
|
+
if (display.metrics !== void 0 && !validDefaultMetrics.has(metric)) {
|
|
274
|
+
throw new Error(`display.overview.defaultMetric '${metric}' is not declared in display.metrics and is not a known default (compositeScore, task)`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (defaultLayout !== void 0 && !["ranking", "cards", "chart", "leaderboard"].includes(defaultLayout)) {
|
|
278
|
+
throw new Error("display.overview.defaultLayout must be 'ranking', 'cards', 'chart', or 'leaderboard'");
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
function defineBenchmarkConfig(config) {
|
|
283
|
+
const issues = validateBenchmarkConfig(config);
|
|
284
|
+
if (config.scoring !== void 0) {
|
|
285
|
+
try {
|
|
286
|
+
validateBenchmarkScoringConfig(config.scoring, config.display);
|
|
287
|
+
} catch (error) {
|
|
288
|
+
issues.push({
|
|
289
|
+
field: "scoring",
|
|
290
|
+
message: error instanceof Error ? error.message : String(error)
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (issues.length > 0) {
|
|
295
|
+
throw new BenchmarkConfigError(issues);
|
|
296
|
+
}
|
|
114
297
|
return config;
|
|
115
298
|
}
|
|
116
299
|
function defineTask(task) {
|
|
@@ -119,6 +302,136 @@ function defineTask(task) {
|
|
|
119
302
|
}
|
|
120
303
|
return task;
|
|
121
304
|
}
|
|
305
|
+
function validateBenchmarkConfig(config) {
|
|
306
|
+
const issues = [];
|
|
307
|
+
if (!config.benchmarkSlug || typeof config.benchmarkSlug !== "string") {
|
|
308
|
+
issues.push({ field: "benchmarkSlug", message: "is required" });
|
|
309
|
+
}
|
|
310
|
+
if (!config.benchmarkName || typeof config.benchmarkName !== "string") {
|
|
311
|
+
issues.push({ field: "benchmarkName", message: "is required" });
|
|
312
|
+
}
|
|
313
|
+
if (!Array.isArray(config.participants) || config.participants.length === 0) {
|
|
314
|
+
issues.push({ field: "participants", message: "must be a non-empty array" });
|
|
315
|
+
} else {
|
|
316
|
+
const seenParticipants = /* @__PURE__ */ new Set();
|
|
317
|
+
for (let i = 0; i < config.participants.length; i++) {
|
|
318
|
+
const p = config.participants[i];
|
|
319
|
+
if (p === null || typeof p !== "object" || Array.isArray(p)) {
|
|
320
|
+
issues.push({ field: `participants[${i}]`, message: "must be an object" });
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
const participant = p;
|
|
324
|
+
if (typeof participant.name !== "string" || participant.name.trim() === "") {
|
|
325
|
+
issues.push({ field: `participants[${i}].name`, message: "must be a non-empty string" });
|
|
326
|
+
} else if (seenParticipants.has(participant.name)) {
|
|
327
|
+
issues.push({ field: `participants[${i}].name`, message: `duplicate participant name: ${participant.name}` });
|
|
328
|
+
} else {
|
|
329
|
+
seenParticipants.add(participant.name);
|
|
330
|
+
}
|
|
331
|
+
if (participant.requiredEnvVars !== void 0 && (!Array.isArray(participant.requiredEnvVars) || !participant.requiredEnvVars.every((v) => typeof v === "string"))) {
|
|
332
|
+
issues.push({ field: `participants[${i}].requiredEnvVars`, message: "must be an array of strings" });
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (config.phases !== void 0) {
|
|
337
|
+
if (config.iterations !== void 0) {
|
|
338
|
+
issues.push({ field: "iterations", message: "phases and iterations are mutually exclusive" });
|
|
339
|
+
}
|
|
340
|
+
if (!Array.isArray(config.phases) || config.phases.length === 0) {
|
|
341
|
+
issues.push({ field: "phases", message: "must be a non-empty array" });
|
|
342
|
+
} else {
|
|
343
|
+
const seen = /* @__PURE__ */ new Set();
|
|
344
|
+
for (let i = 0; i < config.phases.length; i++) {
|
|
345
|
+
const phase = config.phases[i];
|
|
346
|
+
if (phase === null || typeof phase !== "object" || Array.isArray(phase)) {
|
|
347
|
+
issues.push({ field: `phases[${i}]`, message: "must be an object" });
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
const phaseObj = phase;
|
|
351
|
+
if (typeof phaseObj.name !== "string" || phaseObj.name.trim() === "") {
|
|
352
|
+
issues.push({ field: `phases[${i}]`, message: "must have a non-empty string name" });
|
|
353
|
+
} else {
|
|
354
|
+
const name = phaseObj.name;
|
|
355
|
+
if (seen.has(name)) {
|
|
356
|
+
issues.push({ field: `phases['${name}']`, message: `duplicate phase name: ${name}` });
|
|
357
|
+
}
|
|
358
|
+
seen.add(name);
|
|
359
|
+
const iterations = phaseObj.iterations;
|
|
360
|
+
if (typeof iterations !== "number" || !Number.isInteger(iterations) || iterations < 1) {
|
|
361
|
+
issues.push({ field: `phases['${name}'].iterations`, message: `must be an integer >= 1 (got ${iterations})` });
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (config.iterations !== void 0 && (!Number.isInteger(config.iterations) || config.iterations < 1)) {
|
|
368
|
+
issues.push({ field: "iterations", message: `must be an integer >= 1 (got ${config.iterations})` });
|
|
369
|
+
}
|
|
370
|
+
if (config.concurrency !== void 0 && (!Number.isInteger(config.concurrency) || config.concurrency < 1)) {
|
|
371
|
+
issues.push({ field: "concurrency", message: `must be an integer >= 1 (got ${config.concurrency})` });
|
|
372
|
+
}
|
|
373
|
+
if (config.staggerDelayMs !== void 0 && (!Number.isFinite(config.staggerDelayMs) || config.staggerDelayMs < 0)) {
|
|
374
|
+
issues.push({ field: "staggerDelayMs", message: `must be a number >= 0 (got ${config.staggerDelayMs})` });
|
|
375
|
+
}
|
|
376
|
+
if (config.groupBy !== void 0 && config.groupBy !== "participant" && config.groupBy !== "round") {
|
|
377
|
+
issues.push({ field: "groupBy", message: `must be 'participant' or 'round' (got ${config.groupBy})` });
|
|
378
|
+
}
|
|
379
|
+
if (config.shapes !== void 0) {
|
|
380
|
+
if (typeof config.shapes !== "object" || config.shapes === null || Array.isArray(config.shapes)) {
|
|
381
|
+
issues.push({ field: "shapes", message: "must be a plain object" });
|
|
382
|
+
} else {
|
|
383
|
+
for (const [shapeName, shape] of Object.entries(config.shapes)) {
|
|
384
|
+
if (shape === null || typeof shape !== "object" || Array.isArray(shape)) {
|
|
385
|
+
issues.push({ field: `shapes['${shapeName}']`, message: "must be an object" });
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
const shapeObj = shape;
|
|
389
|
+
const slug = shapeObj.slug;
|
|
390
|
+
if (typeof slug !== "string" || slug === "" || !/^[a-z0-9][a-z0-9-]*$/.test(slug)) {
|
|
391
|
+
issues.push({ field: `shapes['${shapeName}'].slug`, message: `needs a lowercase slug (got ${JSON.stringify(slug)})` });
|
|
392
|
+
}
|
|
393
|
+
const name = shapeObj.name;
|
|
394
|
+
if (name !== void 0 && (typeof name !== "string" || name.trim() === "")) {
|
|
395
|
+
issues.push({ field: `shapes['${shapeName}'].name`, message: "must be a non-empty string" });
|
|
396
|
+
}
|
|
397
|
+
const staggerDelayMs = shapeObj.staggerDelayMs;
|
|
398
|
+
if (staggerDelayMs !== void 0 && (typeof staggerDelayMs !== "number" || !Number.isFinite(staggerDelayMs) || staggerDelayMs < 0)) {
|
|
399
|
+
issues.push({ field: `shapes['${shapeName}'].staggerDelayMs`, message: `must be a number >= 0 (got ${staggerDelayMs})` });
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (config.dimensions !== void 0) {
|
|
405
|
+
if (config.dimensions === null || typeof config.dimensions !== "object" || Array.isArray(config.dimensions)) {
|
|
406
|
+
issues.push({ field: "dimensions", message: "must be a plain object" });
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
if (config.customCliFlags !== void 0) {
|
|
410
|
+
if (!Array.isArray(config.customCliFlags) || !config.customCliFlags.every((f) => typeof f === "string" && f.startsWith("--"))) {
|
|
411
|
+
issues.push({ field: "customCliFlags", message: 'must be an array of strings starting with "--"' });
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (config.display !== void 0) {
|
|
415
|
+
try {
|
|
416
|
+
validateBenchmarkDisplayConfig(config.display);
|
|
417
|
+
} catch (error) {
|
|
418
|
+
issues.push({
|
|
419
|
+
field: "display",
|
|
420
|
+
message: error instanceof Error ? error.message : String(error)
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (config.display?.overview?.defaultMetric === "compositeScore" && config.scoring === void 0 && config.onScore === void 0) {
|
|
425
|
+
issues.push({
|
|
426
|
+
field: "display.overview.defaultMetric",
|
|
427
|
+
message: "cannot be 'compositeScore' without config.scoring or config.onScore"
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
return issues;
|
|
431
|
+
}
|
|
432
|
+
function defineOnComplete(onComplete) {
|
|
433
|
+
return onComplete;
|
|
434
|
+
}
|
|
122
435
|
|
|
123
436
|
// src/no-available-participants.ts
|
|
124
437
|
var NoAvailableParticipantsError = class extends Error {
|
|
@@ -135,7 +448,9 @@ var NoAvailableParticipantsError = class extends Error {
|
|
|
135
448
|
// src/runner.ts
|
|
136
449
|
var import_node_child_process = require("child_process");
|
|
137
450
|
var import_node_os = __toESM(require("os"), 1);
|
|
138
|
-
var
|
|
451
|
+
var import_api = require("@benchsdk/api");
|
|
452
|
+
var import_cli = require("@benchsdk/cli");
|
|
453
|
+
var import_worker = require("@benchsdk/worker");
|
|
139
454
|
|
|
140
455
|
// src/scoring.ts
|
|
141
456
|
function isFiniteNumber(v) {
|
|
@@ -191,40 +506,121 @@ var higherIsBetter = (name, opts) => ({
|
|
|
191
506
|
...opts,
|
|
192
507
|
higherIsBetter: true
|
|
193
508
|
});
|
|
194
|
-
|
|
509
|
+
var WEIGHT_SUM_TOLERANCE = 0.01;
|
|
510
|
+
var ScoringSpecError = class extends Error {
|
|
511
|
+
constructor(message) {
|
|
512
|
+
super(message);
|
|
513
|
+
this.name = "ScoringSpecError";
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
function validateScoringSpec(spec) {
|
|
517
|
+
const totalWeight = spec.metrics.reduce(
|
|
518
|
+
(sum, m) => sum + m.weights.median + m.weights.p95 + m.weights.p99,
|
|
519
|
+
0
|
|
520
|
+
);
|
|
521
|
+
if (Math.abs(totalWeight - 1) > WEIGHT_SUM_TOLERANCE) {
|
|
522
|
+
const breakdown = spec.metrics.map((m) => `${m.name}=${(m.weights.median + m.weights.p95 + m.weights.p99).toFixed(3)}`).join(", ");
|
|
523
|
+
throw new ScoringSpecError(
|
|
524
|
+
`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)"}`
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
function groupRecordsByKey(records, key) {
|
|
529
|
+
const groups = /* @__PURE__ */ new Map();
|
|
530
|
+
for (const record of records) {
|
|
531
|
+
const raw = record.data?.[key];
|
|
532
|
+
const value = raw === void 0 ? void 0 : raw;
|
|
533
|
+
const mapKey = value === void 0 ? "__undefined__" : JSON.stringify(value);
|
|
534
|
+
let group = groups.get(mapKey);
|
|
535
|
+
if (!group) {
|
|
536
|
+
group = { value, records: [] };
|
|
537
|
+
groups.set(mapKey, group);
|
|
538
|
+
}
|
|
539
|
+
group.records.push(record);
|
|
540
|
+
}
|
|
541
|
+
return Array.from(groups.values());
|
|
542
|
+
}
|
|
543
|
+
function scoreGroup(records, spec, baseDimensions, groupKey, groupValue, provider, unitByMetric) {
|
|
195
544
|
const successFilter = spec.success ?? ((r) => r.status === "success");
|
|
196
|
-
const
|
|
545
|
+
const passing = records.filter(successFilter);
|
|
546
|
+
const successRate = records.length === 0 ? 0 : passing.length / records.length;
|
|
547
|
+
const skipped = records.length === 0;
|
|
548
|
+
let metricScoresSum = 0;
|
|
549
|
+
const metrics = [];
|
|
550
|
+
for (const metric of spec.metrics) {
|
|
551
|
+
const samples = collectSamples(metric, passing);
|
|
552
|
+
if (samples.length === 0) {
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
const { median, p95, p99 } = computeStats(samples, metric.trim ?? 0.05);
|
|
556
|
+
const metricScore = metric.weights.median * scoreStat(median, metric) + metric.weights.p95 * scoreStat(p95, metric) + metric.weights.p99 * scoreStat(p99, metric);
|
|
557
|
+
metricScoresSum += metricScore;
|
|
558
|
+
const unitKey = typeof metric.value === "string" ? metric.value : metric.name;
|
|
559
|
+
const unit = metric.unit ?? unitByMetric.get(unitKey) ?? "";
|
|
560
|
+
metrics.push({ name: metric.name, unit, median, p95, p99 });
|
|
561
|
+
}
|
|
562
|
+
const compositeScore = successRate === 0 ? 0 : Math.round(metricScoresSum * successRate * 100) / 100;
|
|
563
|
+
const dimensions = toJsonObject({
|
|
564
|
+
...baseDimensions,
|
|
565
|
+
...groupKey !== void 0 && groupValue !== void 0 ? { [groupKey]: groupValue } : {}
|
|
566
|
+
});
|
|
567
|
+
return {
|
|
568
|
+
provider,
|
|
569
|
+
dimensions,
|
|
570
|
+
metrics,
|
|
571
|
+
compositeScore,
|
|
572
|
+
successRate,
|
|
573
|
+
skipped
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
function score(outcome, spec, displayMetrics) {
|
|
577
|
+
validateScoringSpec(spec);
|
|
578
|
+
const baseDimensions = toJsonObject(spec.dimensions ?? {});
|
|
579
|
+
const unitByMetric = new Map(displayMetrics?.map((m) => [m.key, m.unit]));
|
|
197
580
|
const results = [];
|
|
198
581
|
for (const { participant, records } of outcome.participants) {
|
|
199
|
-
const
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const metrics = [];
|
|
204
|
-
for (const metric of spec.metrics) {
|
|
205
|
-
const samples = collectSamples(metric, passing);
|
|
206
|
-
if (samples.length === 0) {
|
|
207
|
-
continue;
|
|
208
|
-
}
|
|
209
|
-
const { median, p95, p99 } = computeStats(samples, metric.trim ?? 0.05);
|
|
210
|
-
const metricScore = metric.weights.median * scoreStat(median, metric) + metric.weights.p95 * scoreStat(p95, metric) + metric.weights.p99 * scoreStat(p99, metric);
|
|
211
|
-
metricScoresSum += metricScore;
|
|
212
|
-
metrics.push({ name: metric.name, unit: metric.unit, median, p95, p99 });
|
|
213
|
-
}
|
|
214
|
-
const compositeScore = successRate === 0 ? 0 : Math.round(metricScoresSum * successRate * 100) / 100;
|
|
215
|
-
results.push({
|
|
216
|
-
provider: participant,
|
|
217
|
-
dimensions,
|
|
218
|
-
metrics,
|
|
219
|
-
compositeScore,
|
|
220
|
-
successRate,
|
|
221
|
-
skipped
|
|
222
|
-
});
|
|
582
|
+
const groups = spec.groupBy && records.length > 0 ? [{ value: void 0, records }, ...groupRecordsByKey(records, spec.groupBy)] : [{ value: void 0, records }];
|
|
583
|
+
for (const group of groups) {
|
|
584
|
+
results.push(scoreGroup(group.records, spec, baseDimensions, spec.groupBy, group.value, participant, unitByMetric));
|
|
585
|
+
}
|
|
223
586
|
}
|
|
224
587
|
return results;
|
|
225
588
|
}
|
|
589
|
+
function scoringConfigToSpec(config, dimensions, display) {
|
|
590
|
+
const success = config.success;
|
|
591
|
+
const unitByMetric = new Map(display?.metrics?.map((m) => [m.key, m.unit]));
|
|
592
|
+
return {
|
|
593
|
+
...dimensions ? { dimensions: toJsonObject(dimensions) } : {},
|
|
594
|
+
...config.groupBy ? { groupBy: config.groupBy } : {},
|
|
595
|
+
...success ? {
|
|
596
|
+
success: (record) => record.status === "success" && Object.entries(success.requireData).every(([key, value]) => record.data?.[key] === value)
|
|
597
|
+
} : {},
|
|
598
|
+
metrics: config.metrics.map((metric) => ({
|
|
599
|
+
name: metric.key,
|
|
600
|
+
value: metric.key,
|
|
601
|
+
unit: (display?.metrics === void 0 ? metric.unit : unitByMetric.get(metric.key) ?? metric.unit) ?? "",
|
|
602
|
+
ceiling: metric.ceiling,
|
|
603
|
+
floor: metric.floor,
|
|
604
|
+
higherIsBetter: metric.higherIsBetter,
|
|
605
|
+
weights: metric.weights,
|
|
606
|
+
trim: metric.trim
|
|
607
|
+
}))
|
|
608
|
+
};
|
|
609
|
+
}
|
|
226
610
|
|
|
227
611
|
// src/log-buffer.ts
|
|
612
|
+
var LOG_LEVEL_ORDER = ["debug", "info", "warn", "error"];
|
|
613
|
+
function isLogOptions(value) {
|
|
614
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
615
|
+
const o = value;
|
|
616
|
+
const keys = Object.keys(o);
|
|
617
|
+
if (keys.length === 0) return false;
|
|
618
|
+
if (!keys.every((k) => k === "level" || k === "meta")) return false;
|
|
619
|
+
if (o.level !== void 0 && (typeof o.level !== "string" || !LOG_LEVEL_ORDER.includes(o.level))) {
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
return true;
|
|
623
|
+
}
|
|
228
624
|
var LogBuffer = class {
|
|
229
625
|
lines = [];
|
|
230
626
|
step(taskIndex, stepName, outcome) {
|
|
@@ -241,9 +637,15 @@ var LogBuffer = class {
|
|
|
241
637
|
}
|
|
242
638
|
}
|
|
243
639
|
/** Appends a free-form narration line (backs the task context's `log`). */
|
|
244
|
-
line(message,
|
|
245
|
-
const
|
|
246
|
-
|
|
640
|
+
line(message, metaOrOptions) {
|
|
641
|
+
const opts = isLogOptions(metaOrOptions) ? { level: metaOrOptions.level ?? "info", meta: metaOrOptions.meta } : { level: "info", meta: metaOrOptions };
|
|
642
|
+
const suffix = opts.meta && Object.keys(opts.meta).length > 0 ? ` ${JSON.stringify(opts.meta)}` : "";
|
|
643
|
+
const taskMatch = message.match(/^(\[task \d+\])\s*(.*)$/);
|
|
644
|
+
if (taskMatch) {
|
|
645
|
+
this.lines.push(`${(/* @__PURE__ */ new Date()).toISOString()} ${taskMatch[1]} [${opts.level}] ${taskMatch[2]}${suffix}`);
|
|
646
|
+
} else {
|
|
647
|
+
this.lines.push(`${(/* @__PURE__ */ new Date()).toISOString()} [${opts.level}] ${message}${suffix}`);
|
|
648
|
+
}
|
|
247
649
|
}
|
|
248
650
|
isEmpty() {
|
|
249
651
|
return this.lines.length === 0;
|
|
@@ -257,7 +659,6 @@ function indent(text, prefix = "") {
|
|
|
257
659
|
}
|
|
258
660
|
|
|
259
661
|
// src/runner.ts
|
|
260
|
-
var DEFAULT_PLATFORM_URL = "https://platform.computesdk.com";
|
|
261
662
|
function isEnvNoIngest() {
|
|
262
663
|
const v = process.env.BENCHSDK_NO_INGEST;
|
|
263
664
|
return v === "1" || v?.toLowerCase() === "true";
|
|
@@ -265,15 +666,68 @@ function isEnvNoIngest() {
|
|
|
265
666
|
function sleep(ms) {
|
|
266
667
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
267
668
|
}
|
|
669
|
+
function runWithConcurrency(fns, limit) {
|
|
670
|
+
if (limit >= fns.length || fns.length === 0) {
|
|
671
|
+
return Promise.all(fns.map((fn) => fn()));
|
|
672
|
+
}
|
|
673
|
+
const results = new Array(fns.length);
|
|
674
|
+
let running = 0;
|
|
675
|
+
let completed = 0;
|
|
676
|
+
let nextIndex = 0;
|
|
677
|
+
return new Promise((resolve2, reject) => {
|
|
678
|
+
const runNext = () => {
|
|
679
|
+
if (completed === fns.length) {
|
|
680
|
+
resolve2(results);
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
while (running < limit && nextIndex < fns.length) {
|
|
684
|
+
const index = nextIndex++;
|
|
685
|
+
running++;
|
|
686
|
+
fns[index]().then(
|
|
687
|
+
(value) => {
|
|
688
|
+
results[index] = value;
|
|
689
|
+
running--;
|
|
690
|
+
completed++;
|
|
691
|
+
runNext();
|
|
692
|
+
},
|
|
693
|
+
(error) => reject(error)
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
runNext();
|
|
698
|
+
});
|
|
699
|
+
}
|
|
268
700
|
function getErrorCode(error) {
|
|
269
|
-
|
|
701
|
+
if (error instanceof Error && "code" in error && typeof error.code === "string" && error.code) {
|
|
702
|
+
return error.code;
|
|
703
|
+
}
|
|
704
|
+
if (error instanceof Error && error.name) return error.name;
|
|
705
|
+
return "ERROR";
|
|
706
|
+
}
|
|
707
|
+
function isTaskError(error) {
|
|
708
|
+
return error instanceof Error && (error instanceof TaskError || error.name === "TaskError");
|
|
270
709
|
}
|
|
271
|
-
|
|
710
|
+
var STEP_OUTCOME_KEYS = /* @__PURE__ */ new Set(["stdout", "stderr", "error", "exitCode", "code", "signal", "pid"]);
|
|
711
|
+
function isStepOutcome(value) {
|
|
712
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
713
|
+
const o = value;
|
|
714
|
+
const keys = Object.keys(o);
|
|
715
|
+
if (keys.length === 0) return false;
|
|
716
|
+
if (!keys.every((k) => STEP_OUTCOME_KEYS.has(k))) return false;
|
|
717
|
+
return typeof o.stdout === "string" || typeof o.stderr === "string" || typeof o.error === "string";
|
|
718
|
+
}
|
|
719
|
+
function withTimeout(promise, { stepName, timeoutMs, participantSlug }) {
|
|
272
720
|
return new Promise((resolve2, reject) => {
|
|
273
|
-
const timer = setTimeout(
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
721
|
+
const timer = setTimeout(() => {
|
|
722
|
+
const participant = participantSlug ? ` for participant "${participantSlug}"` : "";
|
|
723
|
+
reject(
|
|
724
|
+
new TaskError(`Step "${stepName}" timed out after ${timeoutMs}ms${participant}`, {
|
|
725
|
+
code: "step_timeout",
|
|
726
|
+
step: stepName,
|
|
727
|
+
timeoutMs
|
|
728
|
+
})
|
|
729
|
+
);
|
|
730
|
+
}, timeoutMs);
|
|
277
731
|
promise.then(
|
|
278
732
|
(value) => {
|
|
279
733
|
clearTimeout(timer);
|
|
@@ -286,20 +740,23 @@ function withTimeout(promise, ms, name) {
|
|
|
286
740
|
);
|
|
287
741
|
});
|
|
288
742
|
}
|
|
289
|
-
async function runStepInvocations(name, fn, options) {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
743
|
+
async function runStepInvocations(name, fn, options, participantSlug) {
|
|
744
|
+
if (options?.concurrency !== void 0) {
|
|
745
|
+
console.warn(`[benchsdk] step "${name}" option "concurrency" is deprecated; use "parallelInvocations"`);
|
|
746
|
+
}
|
|
747
|
+
const requestedParallelism = options?.parallelInvocations ?? options?.concurrency;
|
|
748
|
+
if (requestedParallelism !== void 0 && (!Number.isInteger(requestedParallelism) || requestedParallelism < 1)) {
|
|
749
|
+
throw new Error(`step "${name}" parallelInvocations must be an integer >= 1 (got ${requestedParallelism})`);
|
|
293
750
|
}
|
|
294
751
|
const timeoutMs = options?.timeoutMs;
|
|
295
752
|
if (timeoutMs !== void 0 && (!Number.isFinite(timeoutMs) || timeoutMs < 0)) {
|
|
296
753
|
throw new Error(`step "${name}" timeoutMs must be a number >= 0 (got ${timeoutMs})`);
|
|
297
754
|
}
|
|
298
|
-
const count =
|
|
755
|
+
const count = requestedParallelism ?? 1;
|
|
299
756
|
const invocations = Array.from({ length: count }, () => {
|
|
300
757
|
const promise = Promise.resolve().then(() => fn());
|
|
301
758
|
if (timeoutMs === void 0) return promise;
|
|
302
|
-
return withTimeout(promise, timeoutMs,
|
|
759
|
+
return withTimeout(promise, { stepName: name, timeoutMs, participantSlug });
|
|
303
760
|
});
|
|
304
761
|
if (count === 1) {
|
|
305
762
|
return invocations[0];
|
|
@@ -317,18 +774,20 @@ async function runStepInvocations(name, fn, options) {
|
|
|
317
774
|
if (firstError !== void 0) throw firstError;
|
|
318
775
|
return results;
|
|
319
776
|
}
|
|
320
|
-
async function runStepWithClient(clientStep, name, fn, options) {
|
|
321
|
-
const { concurrency:
|
|
777
|
+
async function runStepWithClient(clientStep, name, fn, options, participantSlug) {
|
|
778
|
+
const { parallelInvocations: runnerParallelism, concurrency: deprecatedConcurrency, timeoutMs, ...clientOptions } = options ?? {};
|
|
322
779
|
const clientStepOptions = {
|
|
323
780
|
...clientOptions,
|
|
324
781
|
timeoutMs,
|
|
325
|
-
stepConcurrency:
|
|
782
|
+
stepConcurrency: runnerParallelism ?? deprecatedConcurrency
|
|
326
783
|
};
|
|
327
|
-
const result = await clientStep(name, () => runStepInvocations(name, fn, options), clientStepOptions);
|
|
784
|
+
const result = await clientStep(name, () => runStepInvocations(name, fn, options, participantSlug), clientStepOptions);
|
|
328
785
|
return result;
|
|
329
786
|
}
|
|
330
|
-
function parseCliArgs(argv) {
|
|
787
|
+
function parseCliArgs(argv, allowedCustomFlags) {
|
|
331
788
|
const args = {};
|
|
789
|
+
const unknown = [];
|
|
790
|
+
const allowed = new Set(allowedCustomFlags ?? []);
|
|
332
791
|
const readValue = (raw, i) => {
|
|
333
792
|
const eq = raw.indexOf("=");
|
|
334
793
|
if (eq !== -1) return { value: raw.slice(eq + 1), nextIndex: i };
|
|
@@ -420,22 +879,46 @@ function parseCliArgs(argv) {
|
|
|
420
879
|
case "--dry-run":
|
|
421
880
|
args.noIngest = true;
|
|
422
881
|
break;
|
|
423
|
-
default:
|
|
882
|
+
default: {
|
|
883
|
+
if (allowed.has(name)) {
|
|
884
|
+
if (!arg.includes("=")) {
|
|
885
|
+
const next = argv[i + 1];
|
|
886
|
+
if (next && !next.startsWith("-")) {
|
|
887
|
+
i++;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
} else {
|
|
891
|
+
unknown.push(name);
|
|
892
|
+
if (!arg.includes("=")) {
|
|
893
|
+
const next = argv[i + 1];
|
|
894
|
+
if (next && !next.startsWith("-")) {
|
|
895
|
+
i++;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
424
899
|
break;
|
|
900
|
+
}
|
|
425
901
|
}
|
|
426
902
|
}
|
|
903
|
+
if (unknown.length > 0) {
|
|
904
|
+
throw new Error(`Unknown flag(s): ${unknown.join(", ")}`);
|
|
905
|
+
}
|
|
427
906
|
if (!args.noIngest && isEnvNoIngest()) {
|
|
428
907
|
args.noIngest = true;
|
|
429
908
|
}
|
|
430
909
|
return args;
|
|
431
910
|
}
|
|
432
911
|
function mergeConfig(config, args) {
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
912
|
+
const phases = config.phases;
|
|
913
|
+
const unevenPhases = phases !== void 0 && phases.some((p) => p.iterations !== phases[0].iterations);
|
|
914
|
+
if (unevenPhases && args.iterations !== void 0) {
|
|
915
|
+
console.warn("--iterations is ignored because this benchmark sizes its phases individually.");
|
|
436
916
|
}
|
|
917
|
+
const phaseIterations = phases !== void 0 && !unevenPhases ? args.iterations : void 0;
|
|
918
|
+
const phaseTotal = phases !== void 0 ? phaseIterations !== void 0 ? phaseIterations * phases.length : phases.reduce((sum, p) => sum + p.iterations, 0) : void 0;
|
|
437
919
|
const resolved = {
|
|
438
920
|
iterations: phaseTotal ?? args.iterations ?? config.iterations ?? 1,
|
|
921
|
+
phaseIterations,
|
|
439
922
|
concurrency: args.concurrency ?? config.concurrency ?? 1,
|
|
440
923
|
staggerDelayMs: args.staggerDelayMs ?? config.staggerDelayMs ?? 0,
|
|
441
924
|
groupBy: args.groupBy ?? config.groupBy ?? "participant",
|
|
@@ -449,13 +932,16 @@ function mergeConfig(config, args) {
|
|
|
449
932
|
}
|
|
450
933
|
return resolved;
|
|
451
934
|
}
|
|
452
|
-
function buildSchedule(config,
|
|
935
|
+
function buildSchedule(config, resolved, task) {
|
|
453
936
|
if (config.phases?.length) {
|
|
454
937
|
return config.phases.flatMap(
|
|
455
|
-
(phase) => Array.from({ length: phase.iterations }, () => ({
|
|
938
|
+
(phase) => Array.from({ length: resolved.phaseIterations ?? phase.iterations }, () => ({
|
|
939
|
+
phase: phase.name,
|
|
940
|
+
task
|
|
941
|
+
}))
|
|
456
942
|
);
|
|
457
943
|
}
|
|
458
|
-
return Array.from({ length: iterations }, () => ({ phase: void 0, task }));
|
|
944
|
+
return Array.from({ length: resolved.iterations }, () => ({ phase: void 0, task }));
|
|
459
945
|
}
|
|
460
946
|
function defaultOnResult(record, meta) {
|
|
461
947
|
const n = record.taskIndex + 1;
|
|
@@ -463,21 +949,10 @@ function defaultOnResult(record, meta) {
|
|
|
463
949
|
const data = record.data && Object.keys(record.data).length > 0 ? ` ${JSON.stringify(record.data)}` : "";
|
|
464
950
|
console.log(` [${meta.participant}] Task ${n}/${meta.iterations}: success${data}`);
|
|
465
951
|
} else {
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
}
|
|
469
|
-
function resolvePlatform() {
|
|
470
|
-
const root = (process.env.BENCHMARKS_PLATFORM_URL || DEFAULT_PLATFORM_URL).replace(/\/+$/, "");
|
|
471
|
-
const apiKey = process.env.BENCHMARKS_PLATFORM_API_KEY;
|
|
472
|
-
if (!apiKey) {
|
|
473
|
-
throw new Error(
|
|
474
|
-
"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."
|
|
475
|
-
);
|
|
952
|
+
const detail = record.data?.errorMessage ?? record.data?.error;
|
|
953
|
+
const suffix = typeof detail === "string" && detail.length > 0 ? `: ${detail}` : "";
|
|
954
|
+
console.log(` [${meta.participant}] Task ${n}/${meta.iterations}: FAILED \u2014 ${record.errorCode ?? "unknown error"}${suffix}`);
|
|
476
955
|
}
|
|
477
|
-
return {
|
|
478
|
-
baseUrl: `${root}/api/v1`,
|
|
479
|
-
apiKey
|
|
480
|
-
};
|
|
481
956
|
}
|
|
482
957
|
function resolveShape(config, shapeName) {
|
|
483
958
|
if (!shapeName) return void 0;
|
|
@@ -510,30 +985,81 @@ function dashboardUrlFor(baseUrl, organizationSlug, benchmarkSlug, runId) {
|
|
|
510
985
|
return `${baseUrl.replace(/\/api\/v1\/?$/, "")}/${organizationSlug}/benchmarks/${benchmarkSlug}/runs/${runId}`;
|
|
511
986
|
}
|
|
512
987
|
function resolveParticipants(config, resolved) {
|
|
513
|
-
const { available, skipped } = (0,
|
|
988
|
+
const { available, skipped } = (0, import_worker.filterParticipantsByEnv)((0, import_worker.selectParticipants)(config.participants, resolved.providers));
|
|
514
989
|
for (const s of skipped) {
|
|
515
990
|
console.log(`Skipping ${s.name}: missing ${s.missing.join(", ")}`);
|
|
516
991
|
}
|
|
517
992
|
if (available.length === 0) throw new NoAvailableParticipantsError(skipped);
|
|
518
993
|
return available;
|
|
519
994
|
}
|
|
520
|
-
|
|
521
|
-
const
|
|
995
|
+
function resolveTriggerSource(env = process.env) {
|
|
996
|
+
const source = env.BENCH_TRIGGER_SOURCE?.trim();
|
|
997
|
+
if (source) return source;
|
|
998
|
+
return env.GITHUB_EVENT_NAME?.trim() || "manual";
|
|
999
|
+
}
|
|
1000
|
+
function triggerToJson(env = process.env) {
|
|
1001
|
+
const requestedBy = env.BENCH_TRIGGER_REQUESTED_BY?.trim();
|
|
1002
|
+
const requestId = env.BENCH_TRIGGER_REQUEST_ID?.trim();
|
|
1003
|
+
const event = env.GITHUB_EVENT_NAME?.trim();
|
|
1004
|
+
return {
|
|
1005
|
+
source: resolveTriggerSource(env),
|
|
1006
|
+
...event ? { event } : {},
|
|
1007
|
+
...requestedBy ? { requestedBy } : {},
|
|
1008
|
+
...requestId ? { requestId } : {}
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
function runConfigToJson(config, resolved, participants, env = process.env) {
|
|
1012
|
+
const phases = config.phases?.map((phase) => ({
|
|
1013
|
+
name: phase.name,
|
|
1014
|
+
iterations: resolved.phaseIterations ?? phase.iterations
|
|
1015
|
+
}));
|
|
1016
|
+
const runConfig = {
|
|
1017
|
+
benchmarkSlug: config.benchmarkSlug,
|
|
1018
|
+
benchmarkName: config.benchmarkName,
|
|
1019
|
+
...resolved.phaseIterations !== void 0 ? { phaseIterations: resolved.phaseIterations } : {},
|
|
1020
|
+
...phases ? { phases } : {},
|
|
1021
|
+
...!config.phases ? { iterations: resolved.iterations } : {},
|
|
1022
|
+
concurrency: resolved.concurrency,
|
|
1023
|
+
staggerDelayMs: resolved.staggerDelayMs,
|
|
1024
|
+
groupBy: resolved.groupBy,
|
|
1025
|
+
...config.dimensions ? { dimensions: config.dimensions } : {},
|
|
1026
|
+
...config.scoring ? { scoring: config.scoring } : {},
|
|
1027
|
+
participants,
|
|
1028
|
+
trigger: triggerToJson(env)
|
|
1029
|
+
};
|
|
1030
|
+
return JSON.parse(JSON.stringify(runConfig));
|
|
1031
|
+
}
|
|
1032
|
+
async function runBenchmark(fileConfig, task, argv = [], options = {}) {
|
|
1033
|
+
const args = parseCliArgs(argv, fileConfig.customCliFlags);
|
|
522
1034
|
const noIngest = args.noIngest ?? isEnvNoIngest();
|
|
523
1035
|
const shaped = applyShape(fileConfig, resolveShape(fileConfig, args.shape));
|
|
524
1036
|
const config = applyIdentityOverrides(shaped, args);
|
|
525
1037
|
const resolved = mergeConfig(config, args);
|
|
526
|
-
const available = resolveParticipants(config, resolved);
|
|
527
1038
|
let baseUrl = "";
|
|
528
1039
|
let apiKey = "";
|
|
1040
|
+
let token;
|
|
1041
|
+
let orgSlug;
|
|
1042
|
+
let orgId;
|
|
529
1043
|
let client = null;
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
1044
|
+
const auth = noIngest ? await (0, import_cli.resolveAuth)({ baseUrl: options.baseUrl, apiKey: options.apiKey }).catch(() => null) : await (0, import_cli.resolveAuth)({ baseUrl: options.baseUrl, apiKey: options.apiKey });
|
|
1045
|
+
if (auth) {
|
|
1046
|
+
baseUrl = auth.apiBaseUrl;
|
|
1047
|
+
apiKey = auth.apiKey ?? "";
|
|
1048
|
+
token = auth.token;
|
|
1049
|
+
orgSlug = auth.orgSlug;
|
|
1050
|
+
orgId = auth.orgId;
|
|
1051
|
+
client = (0, import_api.createBenchmarkClient)({
|
|
1052
|
+
baseUrl: auth.apiBaseUrl,
|
|
1053
|
+
apiKey: auth.apiKey,
|
|
1054
|
+
token: auth.token,
|
|
1055
|
+
orgSlug: auth.orgSlug,
|
|
1056
|
+
orgId: auth.orgId
|
|
1057
|
+
});
|
|
533
1058
|
}
|
|
534
|
-
const
|
|
1059
|
+
const available = resolveParticipants(config, resolved);
|
|
1060
|
+
const schedule = buildSchedule(config, resolved, task);
|
|
535
1061
|
const totalTasks = schedule.length;
|
|
536
|
-
const concurrencyLabel =
|
|
1062
|
+
const concurrencyLabel = String(resolved.concurrency);
|
|
537
1063
|
console.log(`${config.benchmarkName} (self-contained)`);
|
|
538
1064
|
console.log(`Date: ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
539
1065
|
if (noIngest) {
|
|
@@ -550,14 +1076,32 @@ async function runBenchmark(fileConfig, task, argv = []) {
|
|
|
550
1076
|
runId = "no-ingest";
|
|
551
1077
|
dashboardUrl = "";
|
|
552
1078
|
} else {
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
1079
|
+
{
|
|
1080
|
+
const benchmarkConfig = {
|
|
1081
|
+
...config.scoring ? { scoring: config.scoring } : {},
|
|
1082
|
+
...config.display ? { display: config.display } : {}
|
|
1083
|
+
};
|
|
1084
|
+
let initializeTarget = identityIsOurs;
|
|
1085
|
+
if (!initializeTarget) {
|
|
1086
|
+
try {
|
|
1087
|
+
await client.getBenchmark(config.benchmarkSlug);
|
|
1088
|
+
} catch (error) {
|
|
1089
|
+
if (!(error instanceof import_api.BenchmarkApiError && error.status === 404)) throw error;
|
|
1090
|
+
initializeTarget = true;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
const upsertInput = {};
|
|
1094
|
+
if (initializeTarget) {
|
|
1095
|
+
upsertInput.name = config.benchmarkName;
|
|
1096
|
+
if (Object.keys(benchmarkConfig).length > 0) upsertInput.config = benchmarkConfig;
|
|
1097
|
+
}
|
|
1098
|
+
await client.upsertBenchmark(config.benchmarkSlug, upsertInput);
|
|
557
1099
|
}
|
|
1100
|
+
const runConfig = client ? runConfigToJson(config, resolved, available.map((p) => p.name)) : {};
|
|
558
1101
|
if (args.runKey) {
|
|
559
1102
|
const { run: run2, organizationSlug } = await client.createRun(config.benchmarkSlug, {
|
|
560
|
-
runKey: args.runKey
|
|
1103
|
+
runKey: args.runKey,
|
|
1104
|
+
config: runConfig
|
|
561
1105
|
});
|
|
562
1106
|
runId = run2.id;
|
|
563
1107
|
dashboardUrl = dashboardUrlFor(baseUrl, organizationSlug, config.benchmarkSlug, run2.id);
|
|
@@ -571,7 +1115,8 @@ async function runBenchmark(fileConfig, task, argv = []) {
|
|
|
571
1115
|
const { run: run2, organizationSlug } = await client.createRun(config.benchmarkSlug, {
|
|
572
1116
|
totalTasks,
|
|
573
1117
|
workerCount: 1,
|
|
574
|
-
participants: available.map((p) => p.name)
|
|
1118
|
+
participants: available.map((p) => p.name),
|
|
1119
|
+
config: runConfig
|
|
575
1120
|
});
|
|
576
1121
|
runId = run2.id;
|
|
577
1122
|
dashboardUrl = dashboardUrlFor(baseUrl, organizationSlug, config.benchmarkSlug, run2.id);
|
|
@@ -583,9 +1128,9 @@ async function runBenchmark(fileConfig, task, argv = []) {
|
|
|
583
1128
|
const onResult = defaultOnResult;
|
|
584
1129
|
let participantRecords;
|
|
585
1130
|
if (resolved.groupBy === "round") {
|
|
586
|
-
participantRecords = await runGroupedByRound(config, schedule, available, resolved, client, runId, baseUrl, apiKey, onResult, noIngest);
|
|
1131
|
+
participantRecords = await runGroupedByRound(config, schedule, available, resolved, client, runId, baseUrl, apiKey, token, orgSlug, orgId, onResult, noIngest);
|
|
587
1132
|
} else {
|
|
588
|
-
participantRecords = await runGroupedByParticipant(config, schedule, available, resolved, client, runId, onResult);
|
|
1133
|
+
participantRecords = await runGroupedByParticipant(config, schedule, available, resolved, client, runId, onResult, noIngest);
|
|
589
1134
|
}
|
|
590
1135
|
console.log(`All done. ${noIngest ? "No platform run created." : `View at: ${dashboardUrl}`}`);
|
|
591
1136
|
const outcome = {
|
|
@@ -594,20 +1139,25 @@ async function runBenchmark(fileConfig, task, argv = []) {
|
|
|
594
1139
|
participants: participantRecords,
|
|
595
1140
|
config: resolved
|
|
596
1141
|
};
|
|
597
|
-
if (client && config.onScore) {
|
|
1142
|
+
if (!noIngest && client && (config.onScore || config.scoring)) {
|
|
598
1143
|
try {
|
|
599
|
-
const spec = await config.onScore(lowerIsBetter, higherIsBetter);
|
|
600
|
-
const scored = score(outcome, spec);
|
|
1144
|
+
const spec = config.onScore ? await config.onScore(lowerIsBetter, higherIsBetter) : scoringConfigToSpec(config.scoring, config.dimensions, config.display);
|
|
1145
|
+
const scored = score(outcome, spec, config.display?.metrics);
|
|
601
1146
|
const run2 = {
|
|
602
1147
|
gitSha: process.env.GITHUB_SHA ?? getGitSha(),
|
|
603
1148
|
gitRef: process.env.GITHUB_REF_NAME ?? process.env.GITHUB_REF ?? getGitRef(),
|
|
604
|
-
triggeredBy:
|
|
1149
|
+
triggeredBy: resolveTriggerSource(),
|
|
605
1150
|
nodeVersion: process.version,
|
|
606
1151
|
platform: import_node_os.default.platform(),
|
|
607
1152
|
arch: import_node_os.default.arch()
|
|
608
1153
|
};
|
|
609
|
-
await client.submitRunSummary(config.benchmarkSlug, runId, {
|
|
1154
|
+
await client.submitRunSummary(config.benchmarkSlug, runId, {
|
|
1155
|
+
run: run2,
|
|
1156
|
+
results: scored,
|
|
1157
|
+
...config.scoring ? { scoring: config.scoring } : {}
|
|
1158
|
+
});
|
|
610
1159
|
} catch (err) {
|
|
1160
|
+
if (err instanceof ScoringSpecError) throw err;
|
|
611
1161
|
const message = err instanceof Error ? err.message : String(err);
|
|
612
1162
|
console.warn(`[benchsdk-runner] failed to submit run summary: ${message}`);
|
|
613
1163
|
}
|
|
@@ -615,6 +1165,22 @@ async function runBenchmark(fileConfig, task, argv = []) {
|
|
|
615
1165
|
if (config.onComplete) await config.onComplete(outcome);
|
|
616
1166
|
return outcome;
|
|
617
1167
|
}
|
|
1168
|
+
async function runBenchmarkWorker(options) {
|
|
1169
|
+
const config = defineBenchmarkConfig({
|
|
1170
|
+
benchmarkSlug: options.benchmarkSlug,
|
|
1171
|
+
benchmarkName: options.benchmarkName ?? options.benchmarkSlug,
|
|
1172
|
+
participants: [options.participant],
|
|
1173
|
+
iterations: options.iterations ?? 1,
|
|
1174
|
+
concurrency: options.concurrency ?? 1,
|
|
1175
|
+
staggerDelayMs: options.staggerDelayMs ?? 0,
|
|
1176
|
+
groupBy: options.groupBy ?? "participant",
|
|
1177
|
+
defaultProviders: [options.participant.name]
|
|
1178
|
+
});
|
|
1179
|
+
const argv = [];
|
|
1180
|
+
if (options.runKey) argv.push("--run-key", options.runKey);
|
|
1181
|
+
if (options.noIngest) argv.push("--dry-run");
|
|
1182
|
+
return runBenchmark(config, options.task, argv);
|
|
1183
|
+
}
|
|
618
1184
|
function getGitSha() {
|
|
619
1185
|
if (process.env.GITHUB_SHA) return process.env.GITHUB_SHA;
|
|
620
1186
|
try {
|
|
@@ -632,13 +1198,13 @@ function getGitRef() {
|
|
|
632
1198
|
return void 0;
|
|
633
1199
|
}
|
|
634
1200
|
}
|
|
635
|
-
async function runGroupedByParticipant(config, schedule, available, resolved, client, runId, onResult) {
|
|
1201
|
+
async function runGroupedByParticipant(config, schedule, available, resolved, client, runId, onResult, noIngest) {
|
|
636
1202
|
const participantRecords = [];
|
|
637
1203
|
for (const participant of available) {
|
|
638
1204
|
console.log(`${"=".repeat(70)}`);
|
|
639
1205
|
console.log(` Participant: ${participant.name}`);
|
|
640
1206
|
console.log("=".repeat(70));
|
|
641
|
-
if (!client) {
|
|
1207
|
+
if (noIngest || !client) {
|
|
642
1208
|
const records = [];
|
|
643
1209
|
let rampStartMs2;
|
|
644
1210
|
let nextIndex = 0;
|
|
@@ -670,7 +1236,7 @@ async function runGroupedByParticipant(config, schedule, available, resolved, cl
|
|
|
670
1236
|
}
|
|
671
1237
|
let rampStartMs;
|
|
672
1238
|
await client.planWorkers(config.benchmarkSlug, runId, participant.name);
|
|
673
|
-
const result = await
|
|
1239
|
+
const result = await (0, import_worker.runWorker)(client, {
|
|
674
1240
|
benchmarkSlug: config.benchmarkSlug,
|
|
675
1241
|
runId,
|
|
676
1242
|
participantSlug: participant.name,
|
|
@@ -683,16 +1249,21 @@ async function runGroupedByParticipant(config, schedule, available, resolved, cl
|
|
|
683
1249
|
if (waitMs > 0) await sleep(waitMs);
|
|
684
1250
|
}
|
|
685
1251
|
const slot = schedule[scheduleIndex];
|
|
686
|
-
const taskResult = await slot.task({
|
|
687
|
-
participant,
|
|
688
|
-
taskIndex: scheduleIndex,
|
|
689
|
-
phase: slot.phase,
|
|
690
|
-
step: (name, fn, options) => runStepWithClient(ctx.step, name, fn, options),
|
|
691
|
-
measure: ctx.measure,
|
|
692
|
-
log: ctx.log
|
|
693
|
-
});
|
|
694
1252
|
if (slot.phase) ctx.measure({ phase: slot.phase });
|
|
695
|
-
|
|
1253
|
+
try {
|
|
1254
|
+
const taskResult = await slot.task({
|
|
1255
|
+
participant,
|
|
1256
|
+
taskIndex: scheduleIndex,
|
|
1257
|
+
phase: slot.phase,
|
|
1258
|
+
step: (name, fn, options) => runStepWithClient(ctx.step, name, fn, options, participant.name),
|
|
1259
|
+
measure: ctx.measure,
|
|
1260
|
+
log: ctx.log
|
|
1261
|
+
});
|
|
1262
|
+
return taskResult?.data;
|
|
1263
|
+
} catch (error) {
|
|
1264
|
+
if (isTaskError(error) && error.data) ctx.measure(error.data);
|
|
1265
|
+
throw error;
|
|
1266
|
+
}
|
|
696
1267
|
},
|
|
697
1268
|
onResult: (record) => onResult(record, { iterations: schedule.length, participant: participant.name })
|
|
698
1269
|
});
|
|
@@ -708,14 +1279,17 @@ async function runGroupedByParticipant(config, schedule, available, resolved, cl
|
|
|
708
1279
|
}
|
|
709
1280
|
return participantRecords;
|
|
710
1281
|
}
|
|
711
|
-
async function runGroupedByRound(config, schedule, available, resolved, client, runId, baseUrl, apiKey, onResult, noIngest = false) {
|
|
1282
|
+
async function runGroupedByRound(config, schedule, available, resolved, client, runId, baseUrl, apiKey, token, orgSlug, orgId, onResult, noIngest = false) {
|
|
712
1283
|
const reporters = /* @__PURE__ */ new Map();
|
|
713
1284
|
const logBuffers = /* @__PURE__ */ new Map();
|
|
714
1285
|
const failed = /* @__PURE__ */ new Map();
|
|
715
1286
|
const recordsByParticipant = /* @__PURE__ */ new Map();
|
|
1287
|
+
let metricsCollector;
|
|
1288
|
+
const metricsSamples = [];
|
|
716
1289
|
for (const participant of available) {
|
|
717
1290
|
logBuffers.set(participant.name, new LogBuffer());
|
|
718
1291
|
failed.set(participant.name, false);
|
|
1292
|
+
recordsByParticipant.set(participant.name, []);
|
|
719
1293
|
if (noIngest || !client) {
|
|
720
1294
|
reporters.set(participant.name, null);
|
|
721
1295
|
continue;
|
|
@@ -726,9 +1300,12 @@ async function runGroupedByRound(config, schedule, available, resolved, client,
|
|
|
726
1300
|
});
|
|
727
1301
|
let reporter = null;
|
|
728
1302
|
try {
|
|
729
|
-
reporter = await
|
|
1303
|
+
reporter = await import_worker.BenchmarkReporter.claim({
|
|
730
1304
|
baseUrl,
|
|
731
1305
|
apiKey,
|
|
1306
|
+
token,
|
|
1307
|
+
orgSlug,
|
|
1308
|
+
orgId,
|
|
732
1309
|
benchmarkSlug: config.benchmarkSlug,
|
|
733
1310
|
runId,
|
|
734
1311
|
participantSlug: participant.name,
|
|
@@ -742,6 +1319,10 @@ async function runGroupedByRound(config, schedule, available, resolved, client,
|
|
|
742
1319
|
console.warn(` ${participant.name}: could not claim a platform worker \u2014 running without platform reporting.`);
|
|
743
1320
|
}
|
|
744
1321
|
reporters.set(participant.name, reporter);
|
|
1322
|
+
if (reporter && !metricsCollector) {
|
|
1323
|
+
metricsCollector = (0, import_worker.createSystemMetricsCollector)();
|
|
1324
|
+
metricsSamples.push(await metricsCollector.sample());
|
|
1325
|
+
}
|
|
745
1326
|
}
|
|
746
1327
|
console.log(`Interleaving ${available.length} participant(s), ${schedule.length} round(s) each.
|
|
747
1328
|
`);
|
|
@@ -750,7 +1331,7 @@ async function runGroupedByRound(config, schedule, available, resolved, client,
|
|
|
750
1331
|
if (resolved.staggerDelayMs > 0 && i > 0) {
|
|
751
1332
|
await sleep(resolved.staggerDelayMs);
|
|
752
1333
|
}
|
|
753
|
-
|
|
1334
|
+
const roundFns = available.map((participant) => async () => {
|
|
754
1335
|
const reporter = reporters.get(participant.name) ?? null;
|
|
755
1336
|
const logBuffer = logBuffers.get(participant.name);
|
|
756
1337
|
const record = await runTaskRecord(
|
|
@@ -764,9 +1345,6 @@ async function runGroupedByRound(config, schedule, available, resolved, client,
|
|
|
764
1345
|
if (record.status !== "success") failed.set(participant.name, true);
|
|
765
1346
|
onResult(record, { iterations: schedule.length, participant: participant.name });
|
|
766
1347
|
reporter?.recordResult(record);
|
|
767
|
-
if (!recordsByParticipant.has(participant.name)) {
|
|
768
|
-
recordsByParticipant.set(participant.name, []);
|
|
769
|
-
}
|
|
770
1348
|
const participantRecords = recordsByParticipant.get(participant.name);
|
|
771
1349
|
participantRecords.push(record);
|
|
772
1350
|
if (reporter) {
|
|
@@ -778,7 +1356,22 @@ async function runGroupedByRound(config, schedule, available, resolved, client,
|
|
|
778
1356
|
});
|
|
779
1357
|
await reporter.heartbeat();
|
|
780
1358
|
}
|
|
781
|
-
}
|
|
1359
|
+
});
|
|
1360
|
+
await runWithConcurrency(roundFns, resolved.concurrency);
|
|
1361
|
+
if (metricsCollector) metricsSamples.push(await metricsCollector.sample());
|
|
1362
|
+
}
|
|
1363
|
+
if (metricsCollector) metricsSamples.push(await metricsCollector.sample());
|
|
1364
|
+
metricsCollector?.stop();
|
|
1365
|
+
const metricsReporter = available.map((p) => reporters.get(p.name)).find((r) => Boolean(r));
|
|
1366
|
+
if (metricsReporter && metricsSamples.length > 0) {
|
|
1367
|
+
await metricsReporter.uploadArtifact({
|
|
1368
|
+
kind: "system-metrics",
|
|
1369
|
+
contentType: "application/x-ndjson",
|
|
1370
|
+
name: "metrics.jsonl",
|
|
1371
|
+
metadata: { scope: "shared-process", participants: available.map((p) => p.name) },
|
|
1372
|
+
body: metricsSamples.map((sample) => JSON.stringify(sample)).join("\n") + "\n"
|
|
1373
|
+
}).catch(() => {
|
|
1374
|
+
});
|
|
782
1375
|
}
|
|
783
1376
|
for (const participant of available) {
|
|
784
1377
|
const reporter = reporters.get(participant.name) ?? null;
|
|
@@ -819,17 +1412,19 @@ async function runTaskRecord(task, participant, scheduleIndex, taskIndex, phase,
|
|
|
819
1412
|
completedAt: new Date(stepStartedAtMs).toISOString(),
|
|
820
1413
|
latencyMs: 0
|
|
821
1414
|
};
|
|
822
|
-
|
|
1415
|
+
const requestedParallelism = options?.parallelInvocations ?? options?.concurrency;
|
|
1416
|
+
if (requestedParallelism !== void 0) stepRecord.concurrency = requestedParallelism;
|
|
823
1417
|
if (options?.timeoutMs !== void 0) stepRecord.timeoutMs = options.timeoutMs;
|
|
824
1418
|
const previousStep = activeStep;
|
|
825
1419
|
activeStep = stepRecord;
|
|
826
1420
|
try {
|
|
827
|
-
const result2 = await runStepInvocations(name, fn, options);
|
|
828
|
-
|
|
1421
|
+
const result2 = await runStepInvocations(name, fn, options, participant.name);
|
|
1422
|
+
const outcome = options?.captureOutput !== false && !Array.isArray(result2) && isStepOutcome(result2) ? result2 : {};
|
|
1423
|
+
logBuffer.step(taskIndex, name, outcome);
|
|
829
1424
|
return result2;
|
|
830
1425
|
} catch (error) {
|
|
831
1426
|
stepRecord.status = "error";
|
|
832
|
-
stepRecord.errorCode = error
|
|
1427
|
+
stepRecord.errorCode = isTaskError(error) ? error.code ?? error.name : getErrorCode(error);
|
|
833
1428
|
logBuffer.step(taskIndex, name, { error: error instanceof Error ? error.message : String(error) });
|
|
834
1429
|
throw error;
|
|
835
1430
|
} finally {
|
|
@@ -846,8 +1441,8 @@ async function runTaskRecord(task, participant, scheduleIndex, taskIndex, phase,
|
|
|
846
1441
|
Object.assign(taskMeasures, data);
|
|
847
1442
|
}
|
|
848
1443
|
},
|
|
849
|
-
log(message,
|
|
850
|
-
logBuffer.line(`[task ${taskIndex}] ${message}`,
|
|
1444
|
+
log(message, metaOrOptions) {
|
|
1445
|
+
logBuffer.line(`[task ${taskIndex}] ${message}`, metaOrOptions);
|
|
851
1446
|
}
|
|
852
1447
|
};
|
|
853
1448
|
let result = void 0;
|
|
@@ -856,7 +1451,7 @@ async function runTaskRecord(task, participant, scheduleIndex, taskIndex, phase,
|
|
|
856
1451
|
record.data = mergeData({ ...taskMeasures, ...result?.data ?? {} }, phase);
|
|
857
1452
|
} catch (error) {
|
|
858
1453
|
record.status = "error";
|
|
859
|
-
if (error
|
|
1454
|
+
if (isTaskError(error)) {
|
|
860
1455
|
record.errorCode = error.code ?? error.name;
|
|
861
1456
|
record.data = mergeData({ ...taskMeasures, ...error.data ?? {} }, phase);
|
|
862
1457
|
if (error.steps?.length) frameworkSteps.push(...error.steps);
|
|
@@ -892,16 +1487,145 @@ async function runTaskRecord(task, participant, scheduleIndex, taskIndex, phase,
|
|
|
892
1487
|
// src/cli.ts
|
|
893
1488
|
var import_node_path = require("path");
|
|
894
1489
|
var import_node_url = require("url");
|
|
895
|
-
var
|
|
1490
|
+
var import_cli2 = require("@benchsdk/cli");
|
|
1491
|
+
var import_api2 = require("@benchsdk/api");
|
|
1492
|
+
var import_worker2 = require("@benchsdk/worker");
|
|
1493
|
+
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>]';
|
|
896
1494
|
function isBenchmarkConfig(value) {
|
|
897
1495
|
if (typeof value !== "object" || value === null) return false;
|
|
898
1496
|
const candidate = value;
|
|
899
1497
|
return typeof candidate.benchmarkSlug === "string" && Array.isArray(candidate.participants);
|
|
900
1498
|
}
|
|
1499
|
+
function shiftFlag(argv, name) {
|
|
1500
|
+
const prefix = `--${name}`;
|
|
1501
|
+
const prefixEq = `${prefix}=`;
|
|
1502
|
+
const result = [];
|
|
1503
|
+
let value;
|
|
1504
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1505
|
+
const arg = argv[i];
|
|
1506
|
+
if (arg === prefix) {
|
|
1507
|
+
const next = argv[++i];
|
|
1508
|
+
if (!next || next.startsWith("--")) throw new Error(USAGE);
|
|
1509
|
+
value = next;
|
|
1510
|
+
continue;
|
|
1511
|
+
}
|
|
1512
|
+
if (arg.startsWith(prefixEq)) {
|
|
1513
|
+
const eqValue = arg.slice(prefixEq.length);
|
|
1514
|
+
if (!eqValue) throw new Error(USAGE);
|
|
1515
|
+
value = eqValue;
|
|
1516
|
+
continue;
|
|
1517
|
+
}
|
|
1518
|
+
result.push(arg);
|
|
1519
|
+
}
|
|
1520
|
+
return { value, argv: result };
|
|
1521
|
+
}
|
|
1522
|
+
function shiftPlatformFlags(flags) {
|
|
1523
|
+
const { value: baseUrl, argv: withoutBaseUrl } = shiftFlag(flags, "base-url");
|
|
1524
|
+
const { value: apiKey, argv: rest } = shiftFlag(withoutBaseUrl, "api-key");
|
|
1525
|
+
return { baseUrl, apiKey, flags: rest };
|
|
1526
|
+
}
|
|
1527
|
+
async function runCheck(argv) {
|
|
1528
|
+
const [command, ...rest] = argv;
|
|
1529
|
+
const [file, ...flags] = rest;
|
|
1530
|
+
if (command !== "check" || !file || file.startsWith("-")) throw new Error(USAGE);
|
|
1531
|
+
const mod = await import((0, import_node_url.pathToFileURL)((0, import_node_path.resolve)(process.cwd(), file)).href);
|
|
1532
|
+
const config = mod.config;
|
|
1533
|
+
const task = mod.task ?? mod.default;
|
|
1534
|
+
if (!isBenchmarkConfig(config)) {
|
|
1535
|
+
throw new Error(`${file} must export a \`config\` created with defineBenchmarkConfig (with participants).`);
|
|
1536
|
+
}
|
|
1537
|
+
if (typeof task !== "function") {
|
|
1538
|
+
throw new Error(`${file} must export a \`task\` created with defineTask.`);
|
|
1539
|
+
}
|
|
1540
|
+
const cfg = config;
|
|
1541
|
+
const configIssues = validateBenchmarkConfig(cfg);
|
|
1542
|
+
if (configIssues.length > 0) {
|
|
1543
|
+
throw new BenchmarkConfigError(configIssues);
|
|
1544
|
+
}
|
|
1545
|
+
const { baseUrl, apiKey, flags: runnerFlags } = shiftPlatformFlags(flags);
|
|
1546
|
+
const parsed = parseCliArgs(runnerFlags, cfg.customCliFlags ?? []);
|
|
1547
|
+
resolveShape(cfg, parsed.shape);
|
|
1548
|
+
const dryRun = parsed.noIngest ?? false;
|
|
1549
|
+
let client;
|
|
1550
|
+
let apiOk = dryRun;
|
|
1551
|
+
let auth = null;
|
|
1552
|
+
if (!dryRun) {
|
|
1553
|
+
try {
|
|
1554
|
+
auth = await (0, import_cli2.resolveAuth)({ baseUrl, apiKey });
|
|
1555
|
+
client = (0, import_api2.createBenchmarkClient)({
|
|
1556
|
+
baseUrl: auth.apiBaseUrl,
|
|
1557
|
+
apiKey: auth.apiKey,
|
|
1558
|
+
token: auth.token,
|
|
1559
|
+
orgSlug: auth.orgSlug,
|
|
1560
|
+
orgId: auth.orgId
|
|
1561
|
+
});
|
|
1562
|
+
await client.listBenchmarks({ limit: 1 });
|
|
1563
|
+
apiOk = true;
|
|
1564
|
+
} catch (err) {
|
|
1565
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1566
|
+
console.warn(`[benchsdk] API connectivity check failed: ${message}`);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
const effectiveProviderNames = parsed.providers ?? cfg.defaultProviders;
|
|
1570
|
+
let selected;
|
|
1571
|
+
try {
|
|
1572
|
+
selected = (0, import_worker2.selectParticipants)(cfg.participants, effectiveProviderNames);
|
|
1573
|
+
} catch (err) {
|
|
1574
|
+
throw new Error(`Participant selection failed: ${err instanceof Error ? err.message : err}`);
|
|
1575
|
+
}
|
|
1576
|
+
const { available, skipped } = (0, import_worker2.filterParticipantsByEnv)(selected);
|
|
1577
|
+
let scoringOk = true;
|
|
1578
|
+
if (cfg.onScore) {
|
|
1579
|
+
try {
|
|
1580
|
+
const spec = await cfg.onScore(lowerIsBetter, higherIsBetter);
|
|
1581
|
+
validateScoringSpec(spec);
|
|
1582
|
+
} catch (err) {
|
|
1583
|
+
scoringOk = false;
|
|
1584
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1585
|
+
console.warn(`[benchsdk] Scoring validation failed: ${message}`);
|
|
1586
|
+
}
|
|
1587
|
+
} else if (cfg.scoring) {
|
|
1588
|
+
try {
|
|
1589
|
+
const spec = scoringConfigToSpec(cfg.scoring, cfg.dimensions);
|
|
1590
|
+
validateScoringSpec(spec);
|
|
1591
|
+
} catch (err) {
|
|
1592
|
+
scoringOk = false;
|
|
1593
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1594
|
+
console.warn(`[benchsdk] Scoring validation failed: ${message}`);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
const missingPlatformAuth = dryRun ? [] : auth ? [] : [["BENCHMARKS_PLATFORM_API_KEY or BENCHMARKS_PLATFORM_TOKEN", void 0]];
|
|
1598
|
+
for (const [name] of missingPlatformAuth) {
|
|
1599
|
+
console.warn(`[benchsdk] ${name} is not set`);
|
|
1600
|
+
}
|
|
1601
|
+
const report = {
|
|
1602
|
+
file,
|
|
1603
|
+
benchmarkSlug: cfg.benchmarkSlug,
|
|
1604
|
+
apiOk,
|
|
1605
|
+
envOk: dryRun || missingPlatformAuth.length === 0,
|
|
1606
|
+
participants: {
|
|
1607
|
+
requested: selected.map((p) => p.name),
|
|
1608
|
+
available: available.map((p) => p.name),
|
|
1609
|
+
skipped: skipped.map((s) => ({ name: s.name, missing: s.missing }))
|
|
1610
|
+
},
|
|
1611
|
+
scoringOk: cfg.scoring || cfg.onScore ? scoringOk : void 0
|
|
1612
|
+
};
|
|
1613
|
+
console.log(JSON.stringify(report, null, 2));
|
|
1614
|
+
const authFailure = !dryRun && missingPlatformAuth.length > 0;
|
|
1615
|
+
if (!apiOk || available.length === 0 || scoringOk === false || authFailure) {
|
|
1616
|
+
throw new Error("Benchmark check failed. See warnings above for details.");
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
901
1619
|
async function runBenchmarkFile(argv) {
|
|
902
1620
|
const [command, ...rest] = argv;
|
|
903
1621
|
const [file, ...flags] = rest;
|
|
904
1622
|
if (command !== "run" || !file || file.startsWith("-")) throw new Error(USAGE);
|
|
1623
|
+
const check = flags.includes("--check") || flags.includes("--validate");
|
|
1624
|
+
if (check) {
|
|
1625
|
+
const checkFlags = flags.filter((f) => f !== "--check" && f !== "--validate");
|
|
1626
|
+
return runCheck(["check", file, ...checkFlags]);
|
|
1627
|
+
}
|
|
1628
|
+
const { baseUrl, apiKey, flags: runnerFlags } = shiftPlatformFlags(flags);
|
|
905
1629
|
const mod = await import((0, import_node_url.pathToFileURL)((0, import_node_path.resolve)(process.cwd(), file)).href);
|
|
906
1630
|
const config = mod.config;
|
|
907
1631
|
const task = mod.task ?? mod.default;
|
|
@@ -911,34 +1635,78 @@ async function runBenchmarkFile(argv) {
|
|
|
911
1635
|
if (typeof task !== "function") {
|
|
912
1636
|
throw new Error(`${file} must export a \`task\` created with defineTask.`);
|
|
913
1637
|
}
|
|
914
|
-
|
|
1638
|
+
const envFlags = [];
|
|
1639
|
+
if (process.env.BENCHMARK_SLUG) {
|
|
1640
|
+
envFlags.push("--benchmark", process.env.BENCHMARK_SLUG);
|
|
1641
|
+
}
|
|
1642
|
+
if (process.env.BENCHMARK_NAME) {
|
|
1643
|
+
envFlags.push("--name", process.env.BENCHMARK_NAME);
|
|
1644
|
+
}
|
|
1645
|
+
await runBenchmark(
|
|
1646
|
+
config,
|
|
1647
|
+
task,
|
|
1648
|
+
[...envFlags, ...runnerFlags],
|
|
1649
|
+
{ baseUrl, apiKey }
|
|
1650
|
+
);
|
|
915
1651
|
}
|
|
916
1652
|
async function run(argv) {
|
|
1653
|
+
const [command, ...rest] = argv;
|
|
917
1654
|
try {
|
|
918
|
-
|
|
1655
|
+
if (command === "run") {
|
|
1656
|
+
await runBenchmarkFile(argv);
|
|
1657
|
+
} else if (command === "check") {
|
|
1658
|
+
await runCheck(argv);
|
|
1659
|
+
} else {
|
|
1660
|
+
return (0, import_cli2.run)(argv);
|
|
1661
|
+
}
|
|
919
1662
|
process.exit(0);
|
|
920
1663
|
} catch (err) {
|
|
921
1664
|
if (err instanceof NoAvailableParticipantsError) {
|
|
922
1665
|
console.log(err.message);
|
|
923
1666
|
process.exit(0);
|
|
924
1667
|
}
|
|
925
|
-
console.error("Benchmark failed:", err
|
|
1668
|
+
console.error("Benchmark failed:", String(err));
|
|
926
1669
|
process.exit(1);
|
|
927
1670
|
}
|
|
928
1671
|
}
|
|
1672
|
+
|
|
1673
|
+
// src/index.ts
|
|
1674
|
+
var import_cli4 = require("@benchsdk/cli");
|
|
1675
|
+
var import_worker3 = require("@benchsdk/worker");
|
|
1676
|
+
var import_api3 = require("@benchsdk/api");
|
|
1677
|
+
var BENCHSDK_RUNNER_VERSION = "0.5.2";
|
|
929
1678
|
// Annotate the CommonJS export names for ESM import in node:
|
|
930
1679
|
0 && (module.exports = {
|
|
1680
|
+
AuthError,
|
|
1681
|
+
BENCHSDK_RUNNER_VERSION,
|
|
1682
|
+
BenchmarkApiError,
|
|
1683
|
+
BenchmarkConfigError,
|
|
1684
|
+
BenchmarkReporter,
|
|
931
1685
|
NoAvailableParticipantsError,
|
|
1686
|
+
ScoringSpecError,
|
|
932
1687
|
TaskError,
|
|
1688
|
+
claimBenchmarkReporter,
|
|
1689
|
+
createApiClient,
|
|
1690
|
+
createBenchmarkClient,
|
|
1691
|
+
createSystemMetricsCollector,
|
|
933
1692
|
defineBenchmarkConfig,
|
|
1693
|
+
defineOnComplete,
|
|
934
1694
|
defineTask,
|
|
1695
|
+
filterParticipantsByEnv,
|
|
935
1696
|
higherIsBetter,
|
|
936
1697
|
lowerIsBetter,
|
|
937
1698
|
mergeConfig,
|
|
938
1699
|
parseCliArgs,
|
|
1700
|
+
resolveAuth,
|
|
939
1701
|
run,
|
|
940
1702
|
runBenchmark,
|
|
941
1703
|
runBenchmarkFile,
|
|
942
|
-
|
|
1704
|
+
runBenchmarkWorker,
|
|
1705
|
+
runWorker,
|
|
1706
|
+
score,
|
|
1707
|
+
scoringConfigToSpec,
|
|
1708
|
+
selectParticipants,
|
|
1709
|
+
validateBenchmarkConfig,
|
|
1710
|
+
validateScoringSpec
|
|
943
1711
|
});
|
|
944
1712
|
//# sourceMappingURL=index.cjs.map
|