@benchsdk/runner 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -4
- package/dist/index.cjs +512 -101
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +146 -14
- package/dist/index.d.ts +146 -14
- package/dist/index.js +507 -99
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
package/dist/index.cjs
CHANGED
|
@@ -30,7 +30,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var src_exports = {};
|
|
32
32
|
__export(src_exports, {
|
|
33
|
+
BENCHSDK_RUNNER_VERSION: () => BENCHSDK_RUNNER_VERSION,
|
|
33
34
|
NoAvailableParticipantsError: () => NoAvailableParticipantsError,
|
|
35
|
+
ScoringSpecError: () => ScoringSpecError,
|
|
34
36
|
TaskError: () => TaskError,
|
|
35
37
|
defineBenchmarkConfig: () => defineBenchmarkConfig,
|
|
36
38
|
defineTask: () => defineTask,
|
|
@@ -41,7 +43,9 @@ __export(src_exports, {
|
|
|
41
43
|
run: () => run,
|
|
42
44
|
runBenchmark: () => runBenchmark,
|
|
43
45
|
runBenchmarkFile: () => runBenchmarkFile,
|
|
44
|
-
score: () => score
|
|
46
|
+
score: () => score,
|
|
47
|
+
scoringConfigToSpec: () => scoringConfigToSpec,
|
|
48
|
+
validateScoringSpec: () => validateScoringSpec
|
|
45
49
|
});
|
|
46
50
|
module.exports = __toCommonJS(src_exports);
|
|
47
51
|
|
|
@@ -58,12 +62,101 @@ var TaskError = class extends Error {
|
|
|
58
62
|
this.steps = opts?.steps;
|
|
59
63
|
}
|
|
60
64
|
};
|
|
65
|
+
function assertNonEmptyString(value, field) {
|
|
66
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
67
|
+
throw new Error(`${field} must be a non-empty string`);
|
|
68
|
+
}
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
function assertOnlyAllowedKeys(value, allowed, field) {
|
|
72
|
+
for (const key of Object.keys(value)) {
|
|
73
|
+
if (!allowed.includes(key)) {
|
|
74
|
+
throw new Error(`${field} contains unexpected key: '${key}'`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
61
78
|
function assertPositiveInt(value, field) {
|
|
62
79
|
if (value === void 0) return;
|
|
63
80
|
if (!Number.isInteger(value) || value < 1) {
|
|
64
81
|
throw new Error(`${field} must be an integer >= 1 (got ${value})`);
|
|
65
82
|
}
|
|
66
83
|
}
|
|
84
|
+
function assertFiniteNumber(value, field) {
|
|
85
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
86
|
+
throw new Error(`${field} must be a finite number (got ${value})`);
|
|
87
|
+
}
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
function validateBenchmarkScoringConfig(scoring, display) {
|
|
91
|
+
if (!Array.isArray(scoring.metrics) || scoring.metrics.length === 0) {
|
|
92
|
+
throw new Error("scoring.metrics must be a non-empty array");
|
|
93
|
+
}
|
|
94
|
+
if (scoring.success !== void 0) {
|
|
95
|
+
const requireData = scoring.success.requireData;
|
|
96
|
+
if (requireData === null || typeof requireData !== "object" || Array.isArray(requireData)) {
|
|
97
|
+
throw new Error("scoring.success.requireData must be a plain object");
|
|
98
|
+
}
|
|
99
|
+
if (Object.keys(requireData).length === 0) {
|
|
100
|
+
throw new Error("scoring.success.requireData must declare at least one data field");
|
|
101
|
+
}
|
|
102
|
+
for (const [key, value] of Object.entries(requireData)) {
|
|
103
|
+
const type = typeof value;
|
|
104
|
+
if (type !== "string" && type !== "number" && type !== "boolean") {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`scoring.success.requireData.${key} must be a string, number, or boolean (got ${type})`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const seen = /* @__PURE__ */ new Set();
|
|
112
|
+
let totalWeight = 0;
|
|
113
|
+
for (let i = 0; i < scoring.metrics.length; i++) {
|
|
114
|
+
const metric = scoring.metrics[i];
|
|
115
|
+
if (metric === null || typeof metric !== "object" || Array.isArray(metric)) {
|
|
116
|
+
throw new Error(`scoring.metrics[${i}] must be an object`);
|
|
117
|
+
}
|
|
118
|
+
const key = metric.key;
|
|
119
|
+
if (typeof key !== "string" || key.trim() === "") {
|
|
120
|
+
throw new Error(`scoring.metrics[${i}].key must be a non-empty string`);
|
|
121
|
+
}
|
|
122
|
+
if (seen.has(key)) {
|
|
123
|
+
throw new Error(`duplicate scoring metric key: ${key}`);
|
|
124
|
+
}
|
|
125
|
+
seen.add(key);
|
|
126
|
+
const displayMetrics = Array.isArray(display?.metrics) ? display.metrics : void 0;
|
|
127
|
+
const displayMetric = displayMetrics?.find((m) => m?.key === key);
|
|
128
|
+
if (metric.unit !== void 0) {
|
|
129
|
+
if (typeof metric.unit !== "string") {
|
|
130
|
+
throw new Error(`scoring.metrics[${i}].unit must be a string`);
|
|
131
|
+
}
|
|
132
|
+
if (displayMetric?.unit !== void 0 && metric.unit !== displayMetric.unit) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`scoring.metrics[${i}].unit '${metric.unit}' conflicts with display.metrics[${i}].unit '${displayMetric.unit}' for key '${key}'`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
assertFiniteNumber(metric.ceiling, `scoring.metrics[${i}].ceiling`);
|
|
139
|
+
if (metric.floor !== void 0) {
|
|
140
|
+
assertFiniteNumber(metric.floor, `scoring.metrics[${i}].floor`);
|
|
141
|
+
}
|
|
142
|
+
if (metric.weights === null || typeof metric.weights !== "object" || Array.isArray(metric.weights)) {
|
|
143
|
+
throw new Error(`scoring.metrics[${i}].weights must be an object`);
|
|
144
|
+
}
|
|
145
|
+
const median = assertFiniteNumber(metric.weights.median, `scoring.metrics[${i}].weights.median`);
|
|
146
|
+
const p95 = assertFiniteNumber(metric.weights.p95, `scoring.metrics[${i}].weights.p95`);
|
|
147
|
+
const p99 = assertFiniteNumber(metric.weights.p99, `scoring.metrics[${i}].weights.p99`);
|
|
148
|
+
if (median < 0 || p95 < 0 || p99 < 0) {
|
|
149
|
+
throw new Error(`scoring.metrics[${i}].weights must be non-negative`);
|
|
150
|
+
}
|
|
151
|
+
totalWeight += median + p95 + p99;
|
|
152
|
+
if (metric.trim !== void 0) {
|
|
153
|
+
assertFiniteNumber(metric.trim, `scoring.metrics[${i}].trim`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (Math.abs(totalWeight - 1) > 0.01) {
|
|
157
|
+
throw new Error(`scoring metric weights must sum to 1.0 (got ${totalWeight.toFixed(3)})`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
67
160
|
function defineBenchmarkConfig(config) {
|
|
68
161
|
if (!config.benchmarkSlug || typeof config.benchmarkSlug !== "string") {
|
|
69
162
|
throw new Error("benchmarkSlug is required");
|
|
@@ -111,6 +204,100 @@ function defineBenchmarkConfig(config) {
|
|
|
111
204
|
}
|
|
112
205
|
}
|
|
113
206
|
}
|
|
207
|
+
if (config.dimensions !== void 0) {
|
|
208
|
+
if (config.dimensions === null || typeof config.dimensions !== "object" || Array.isArray(config.dimensions)) {
|
|
209
|
+
throw new Error("dimensions must be a plain object");
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (config.scoring !== void 0) {
|
|
213
|
+
validateBenchmarkScoringConfig(config.scoring, config.display);
|
|
214
|
+
}
|
|
215
|
+
if (config.customCliFlags !== void 0) {
|
|
216
|
+
if (!Array.isArray(config.customCliFlags) || !config.customCliFlags.every((f) => typeof f === "string" && f.startsWith("--"))) {
|
|
217
|
+
throw new Error('customCliFlags must be an array of strings starting with "--"');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (config.display !== void 0) {
|
|
221
|
+
if (typeof config.display !== "object" || config.display === null || Array.isArray(config.display)) {
|
|
222
|
+
throw new Error("display must be an object");
|
|
223
|
+
}
|
|
224
|
+
assertOnlyAllowedKeys(config.display, ["metrics", "steps", "overview"], "display");
|
|
225
|
+
const displayMetricKeys = /* @__PURE__ */ new Set();
|
|
226
|
+
if (config.display.metrics !== void 0) {
|
|
227
|
+
if (!Array.isArray(config.display.metrics)) {
|
|
228
|
+
throw new Error("display.metrics must be an array");
|
|
229
|
+
}
|
|
230
|
+
for (let i = 0; i < config.display.metrics.length; i++) {
|
|
231
|
+
const metric = config.display.metrics[i];
|
|
232
|
+
if (metric === null || typeof metric !== "object" || Array.isArray(metric)) {
|
|
233
|
+
throw new Error(`display.metrics[${i}] must be an object`);
|
|
234
|
+
}
|
|
235
|
+
assertOnlyAllowedKeys(metric, ["key", "label", "unit", "direction", "decimals", "order"], `display.metrics[${i}]`);
|
|
236
|
+
const key = assertNonEmptyString(metric.key, `display.metrics[${i}].key`);
|
|
237
|
+
if (displayMetricKeys.has(key)) {
|
|
238
|
+
throw new Error(`duplicate display metric key: ${key}`);
|
|
239
|
+
}
|
|
240
|
+
displayMetricKeys.add(key);
|
|
241
|
+
assertNonEmptyString(metric.label, `display.metrics[${i}].label`);
|
|
242
|
+
if (metric.unit !== void 0 && typeof metric.unit !== "string") {
|
|
243
|
+
throw new Error(`display.metrics[${i}].unit must be a string`);
|
|
244
|
+
}
|
|
245
|
+
if (metric.direction !== void 0 && metric.direction !== "higher-better" && metric.direction !== "lower-better") {
|
|
246
|
+
throw new Error(`display.metrics[${i}].direction must be 'higher-better' or 'lower-better'`);
|
|
247
|
+
}
|
|
248
|
+
if (metric.decimals !== void 0 && (!Number.isInteger(metric.decimals) || metric.decimals < 0)) {
|
|
249
|
+
throw new Error(`display.metrics[${i}].decimals must be a non-negative integer`);
|
|
250
|
+
}
|
|
251
|
+
if (metric.order !== void 0 && (!Number.isInteger(metric.order) || metric.order < 0)) {
|
|
252
|
+
throw new Error(`display.metrics[${i}].order must be a non-negative integer`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (config.display.steps !== void 0) {
|
|
257
|
+
if (!Array.isArray(config.display.steps)) {
|
|
258
|
+
throw new Error("display.steps must be an array");
|
|
259
|
+
}
|
|
260
|
+
const seenStepKeys = /* @__PURE__ */ new Set();
|
|
261
|
+
for (let i = 0; i < config.display.steps.length; i++) {
|
|
262
|
+
const step = config.display.steps[i];
|
|
263
|
+
if (step === null || typeof step !== "object" || Array.isArray(step)) {
|
|
264
|
+
throw new Error(`display.steps[${i}] must be an object`);
|
|
265
|
+
}
|
|
266
|
+
assertOnlyAllowedKeys(step, ["key", "label", "order"], `display.steps[${i}]`);
|
|
267
|
+
const key = assertNonEmptyString(step.key, `display.steps[${i}].key`);
|
|
268
|
+
if (seenStepKeys.has(key)) {
|
|
269
|
+
throw new Error(`duplicate display step key: ${key}`);
|
|
270
|
+
}
|
|
271
|
+
seenStepKeys.add(key);
|
|
272
|
+
assertNonEmptyString(step.label, `display.steps[${i}].label`);
|
|
273
|
+
if (step.order !== void 0 && (!Number.isInteger(step.order) || step.order < 0)) {
|
|
274
|
+
throw new Error(`display.steps[${i}].order must be a non-negative integer`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (config.display.overview !== void 0) {
|
|
279
|
+
if (typeof config.display.overview !== "object" || config.display.overview === null || Array.isArray(config.display.overview)) {
|
|
280
|
+
throw new Error("display.overview must be an object");
|
|
281
|
+
}
|
|
282
|
+
assertOnlyAllowedKeys(config.display.overview, ["defaultMetric", "defaultLayout"], "display.overview");
|
|
283
|
+
const { defaultMetric, defaultLayout } = config.display.overview;
|
|
284
|
+
if (defaultMetric !== void 0) {
|
|
285
|
+
const metric = assertNonEmptyString(defaultMetric, "display.overview.defaultMetric");
|
|
286
|
+
const validDefaultMetrics = new Set(displayMetricKeys);
|
|
287
|
+
validDefaultMetrics.add("compositeScore");
|
|
288
|
+
validDefaultMetrics.add("task");
|
|
289
|
+
if (config.display.metrics !== void 0 && !validDefaultMetrics.has(metric)) {
|
|
290
|
+
throw new Error(`display.overview.defaultMetric '${metric}' is not declared in display.metrics and is not a known default (compositeScore, task)`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (defaultLayout !== void 0 && !["ranking", "cards", "chart", "leaderboard"].includes(defaultLayout)) {
|
|
294
|
+
throw new Error("display.overview.defaultLayout must be 'ranking', 'cards', 'chart', or 'leaderboard'");
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (config.display?.overview?.defaultMetric === "compositeScore" && config.scoring === void 0 && config.onScore === void 0) {
|
|
299
|
+
throw new Error("display.overview.defaultMetric cannot be 'compositeScore' without config.scoring or config.onScore");
|
|
300
|
+
}
|
|
114
301
|
return config;
|
|
115
302
|
}
|
|
116
303
|
function defineTask(task) {
|
|
@@ -135,7 +322,9 @@ var NoAvailableParticipantsError = class extends Error {
|
|
|
135
322
|
// src/runner.ts
|
|
136
323
|
var import_node_child_process = require("child_process");
|
|
137
324
|
var import_node_os = __toESM(require("os"), 1);
|
|
138
|
-
var
|
|
325
|
+
var import_api = require("@benchsdk/api");
|
|
326
|
+
var import_cli = require("@benchsdk/cli");
|
|
327
|
+
var import_worker = require("@benchsdk/worker");
|
|
139
328
|
|
|
140
329
|
// src/scoring.ts
|
|
141
330
|
function isFiniteNumber(v) {
|
|
@@ -191,40 +380,121 @@ var higherIsBetter = (name, opts) => ({
|
|
|
191
380
|
...opts,
|
|
192
381
|
higherIsBetter: true
|
|
193
382
|
});
|
|
194
|
-
|
|
383
|
+
var WEIGHT_SUM_TOLERANCE = 0.01;
|
|
384
|
+
var ScoringSpecError = class extends Error {
|
|
385
|
+
constructor(message) {
|
|
386
|
+
super(message);
|
|
387
|
+
this.name = "ScoringSpecError";
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
function validateScoringSpec(spec) {
|
|
391
|
+
const totalWeight = spec.metrics.reduce(
|
|
392
|
+
(sum, m) => sum + m.weights.median + m.weights.p95 + m.weights.p99,
|
|
393
|
+
0
|
|
394
|
+
);
|
|
395
|
+
if (Math.abs(totalWeight - 1) > WEIGHT_SUM_TOLERANCE) {
|
|
396
|
+
const breakdown = spec.metrics.map((m) => `${m.name}=${(m.weights.median + m.weights.p95 + m.weights.p99).toFixed(3)}`).join(", ");
|
|
397
|
+
throw new ScoringSpecError(
|
|
398
|
+
`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)"}`
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
function groupRecordsByKey(records, key) {
|
|
403
|
+
const groups = /* @__PURE__ */ new Map();
|
|
404
|
+
for (const record of records) {
|
|
405
|
+
const raw = record.data?.[key];
|
|
406
|
+
const value = raw === void 0 ? void 0 : raw;
|
|
407
|
+
const mapKey = value === void 0 ? "__undefined__" : JSON.stringify(value);
|
|
408
|
+
let group = groups.get(mapKey);
|
|
409
|
+
if (!group) {
|
|
410
|
+
group = { value, records: [] };
|
|
411
|
+
groups.set(mapKey, group);
|
|
412
|
+
}
|
|
413
|
+
group.records.push(record);
|
|
414
|
+
}
|
|
415
|
+
return Array.from(groups.values());
|
|
416
|
+
}
|
|
417
|
+
function scoreGroup(records, spec, baseDimensions, groupKey, groupValue, provider, unitByMetric) {
|
|
195
418
|
const successFilter = spec.success ?? ((r) => r.status === "success");
|
|
196
|
-
const
|
|
419
|
+
const passing = records.filter(successFilter);
|
|
420
|
+
const successRate = records.length === 0 ? 0 : passing.length / records.length;
|
|
421
|
+
const skipped = records.length === 0;
|
|
422
|
+
let metricScoresSum = 0;
|
|
423
|
+
const metrics = [];
|
|
424
|
+
for (const metric of spec.metrics) {
|
|
425
|
+
const samples = collectSamples(metric, passing);
|
|
426
|
+
if (samples.length === 0) {
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
const { median, p95, p99 } = computeStats(samples, metric.trim ?? 0.05);
|
|
430
|
+
const metricScore = metric.weights.median * scoreStat(median, metric) + metric.weights.p95 * scoreStat(p95, metric) + metric.weights.p99 * scoreStat(p99, metric);
|
|
431
|
+
metricScoresSum += metricScore;
|
|
432
|
+
const unitKey = typeof metric.value === "string" ? metric.value : metric.name;
|
|
433
|
+
const unit = metric.unit ?? unitByMetric.get(unitKey) ?? "";
|
|
434
|
+
metrics.push({ name: metric.name, unit, median, p95, p99 });
|
|
435
|
+
}
|
|
436
|
+
const compositeScore = successRate === 0 ? 0 : Math.round(metricScoresSum * successRate * 100) / 100;
|
|
437
|
+
const dimensions = toJsonObject({
|
|
438
|
+
...baseDimensions,
|
|
439
|
+
...groupKey !== void 0 && groupValue !== void 0 ? { [groupKey]: groupValue } : {}
|
|
440
|
+
});
|
|
441
|
+
return {
|
|
442
|
+
provider,
|
|
443
|
+
dimensions,
|
|
444
|
+
metrics,
|
|
445
|
+
compositeScore,
|
|
446
|
+
successRate,
|
|
447
|
+
skipped
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
function score(outcome, spec, displayMetrics) {
|
|
451
|
+
validateScoringSpec(spec);
|
|
452
|
+
const baseDimensions = toJsonObject(spec.dimensions ?? {});
|
|
453
|
+
const unitByMetric = new Map(displayMetrics?.map((m) => [m.key, m.unit]));
|
|
197
454
|
const results = [];
|
|
198
455
|
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
|
-
});
|
|
456
|
+
const groups = spec.groupBy && records.length > 0 ? [{ value: void 0, records }, ...groupRecordsByKey(records, spec.groupBy)] : [{ value: void 0, records }];
|
|
457
|
+
for (const group of groups) {
|
|
458
|
+
results.push(scoreGroup(group.records, spec, baseDimensions, spec.groupBy, group.value, participant, unitByMetric));
|
|
459
|
+
}
|
|
223
460
|
}
|
|
224
461
|
return results;
|
|
225
462
|
}
|
|
463
|
+
function scoringConfigToSpec(config, dimensions, display) {
|
|
464
|
+
const success = config.success;
|
|
465
|
+
const unitByMetric = new Map(display?.metrics?.map((m) => [m.key, m.unit]));
|
|
466
|
+
return {
|
|
467
|
+
...dimensions ? { dimensions: toJsonObject(dimensions) } : {},
|
|
468
|
+
...config.groupBy ? { groupBy: config.groupBy } : {},
|
|
469
|
+
...success ? {
|
|
470
|
+
success: (record) => record.status === "success" && Object.entries(success.requireData).every(([key, value]) => record.data?.[key] === value)
|
|
471
|
+
} : {},
|
|
472
|
+
metrics: config.metrics.map((metric) => ({
|
|
473
|
+
name: metric.key,
|
|
474
|
+
value: metric.key,
|
|
475
|
+
unit: (display?.metrics === void 0 ? metric.unit : unitByMetric.get(metric.key) ?? metric.unit) ?? "",
|
|
476
|
+
ceiling: metric.ceiling,
|
|
477
|
+
floor: metric.floor,
|
|
478
|
+
higherIsBetter: metric.higherIsBetter,
|
|
479
|
+
weights: metric.weights,
|
|
480
|
+
trim: metric.trim
|
|
481
|
+
}))
|
|
482
|
+
};
|
|
483
|
+
}
|
|
226
484
|
|
|
227
485
|
// src/log-buffer.ts
|
|
486
|
+
var LOG_LEVEL_ORDER = ["debug", "info", "warn", "error"];
|
|
487
|
+
function isLogOptions(value) {
|
|
488
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
489
|
+
const o = value;
|
|
490
|
+
const keys = Object.keys(o);
|
|
491
|
+
if (keys.length === 0) return false;
|
|
492
|
+
if (!keys.every((k) => k === "level" || k === "meta")) return false;
|
|
493
|
+
if (o.level !== void 0 && (typeof o.level !== "string" || !LOG_LEVEL_ORDER.includes(o.level))) {
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
return true;
|
|
497
|
+
}
|
|
228
498
|
var LogBuffer = class {
|
|
229
499
|
lines = [];
|
|
230
500
|
step(taskIndex, stepName, outcome) {
|
|
@@ -241,9 +511,15 @@ var LogBuffer = class {
|
|
|
241
511
|
}
|
|
242
512
|
}
|
|
243
513
|
/** Appends a free-form narration line (backs the task context's `log`). */
|
|
244
|
-
line(message,
|
|
245
|
-
const
|
|
246
|
-
|
|
514
|
+
line(message, metaOrOptions) {
|
|
515
|
+
const opts = isLogOptions(metaOrOptions) ? { level: metaOrOptions.level ?? "info", meta: metaOrOptions.meta } : { level: "info", meta: metaOrOptions };
|
|
516
|
+
const suffix = opts.meta && Object.keys(opts.meta).length > 0 ? ` ${JSON.stringify(opts.meta)}` : "";
|
|
517
|
+
const taskMatch = message.match(/^(\[task \d+\])\s*(.*)$/);
|
|
518
|
+
if (taskMatch) {
|
|
519
|
+
this.lines.push(`${(/* @__PURE__ */ new Date()).toISOString()} ${taskMatch[1]} [${opts.level}] ${taskMatch[2]}${suffix}`);
|
|
520
|
+
} else {
|
|
521
|
+
this.lines.push(`${(/* @__PURE__ */ new Date()).toISOString()} [${opts.level}] ${message}${suffix}`);
|
|
522
|
+
}
|
|
247
523
|
}
|
|
248
524
|
isEmpty() {
|
|
249
525
|
return this.lines.length === 0;
|
|
@@ -257,7 +533,6 @@ function indent(text, prefix = "") {
|
|
|
257
533
|
}
|
|
258
534
|
|
|
259
535
|
// src/runner.ts
|
|
260
|
-
var DEFAULT_PLATFORM_URL = "https://platform.computesdk.com";
|
|
261
536
|
function isEnvNoIngest() {
|
|
262
537
|
const v = process.env.BENCHSDK_NO_INGEST;
|
|
263
538
|
return v === "1" || v?.toLowerCase() === "true";
|
|
@@ -265,8 +540,55 @@ function isEnvNoIngest() {
|
|
|
265
540
|
function sleep(ms) {
|
|
266
541
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
267
542
|
}
|
|
543
|
+
function runWithConcurrency(fns, limit) {
|
|
544
|
+
if (limit >= fns.length || fns.length === 0) {
|
|
545
|
+
return Promise.all(fns.map((fn) => fn()));
|
|
546
|
+
}
|
|
547
|
+
const results = new Array(fns.length);
|
|
548
|
+
let running = 0;
|
|
549
|
+
let completed = 0;
|
|
550
|
+
let nextIndex = 0;
|
|
551
|
+
return new Promise((resolve2, reject) => {
|
|
552
|
+
const runNext = () => {
|
|
553
|
+
if (completed === fns.length) {
|
|
554
|
+
resolve2(results);
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
while (running < limit && nextIndex < fns.length) {
|
|
558
|
+
const index = nextIndex++;
|
|
559
|
+
running++;
|
|
560
|
+
fns[index]().then(
|
|
561
|
+
(value) => {
|
|
562
|
+
results[index] = value;
|
|
563
|
+
running--;
|
|
564
|
+
completed++;
|
|
565
|
+
runNext();
|
|
566
|
+
},
|
|
567
|
+
(error) => reject(error)
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
runNext();
|
|
572
|
+
});
|
|
573
|
+
}
|
|
268
574
|
function getErrorCode(error) {
|
|
269
|
-
|
|
575
|
+
if (error instanceof Error && "code" in error && typeof error.code === "string" && error.code) {
|
|
576
|
+
return error.code;
|
|
577
|
+
}
|
|
578
|
+
if (error instanceof Error && error.name) return error.name;
|
|
579
|
+
return "ERROR";
|
|
580
|
+
}
|
|
581
|
+
function isTaskError(error) {
|
|
582
|
+
return error instanceof Error && (error instanceof TaskError || error.name === "TaskError");
|
|
583
|
+
}
|
|
584
|
+
var STEP_OUTCOME_KEYS = /* @__PURE__ */ new Set(["stdout", "stderr", "error", "exitCode", "code", "signal", "pid"]);
|
|
585
|
+
function isStepOutcome(value) {
|
|
586
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
587
|
+
const o = value;
|
|
588
|
+
const keys = Object.keys(o);
|
|
589
|
+
if (keys.length === 0) return false;
|
|
590
|
+
if (!keys.every((k) => STEP_OUTCOME_KEYS.has(k))) return false;
|
|
591
|
+
return typeof o.stdout === "string" || typeof o.stderr === "string" || typeof o.error === "string";
|
|
270
592
|
}
|
|
271
593
|
function withTimeout(promise, ms, name) {
|
|
272
594
|
return new Promise((resolve2, reject) => {
|
|
@@ -327,8 +649,10 @@ async function runStepWithClient(clientStep, name, fn, options) {
|
|
|
327
649
|
const result = await clientStep(name, () => runStepInvocations(name, fn, options), clientStepOptions);
|
|
328
650
|
return result;
|
|
329
651
|
}
|
|
330
|
-
function parseCliArgs(argv) {
|
|
652
|
+
function parseCliArgs(argv, allowedCustomFlags) {
|
|
331
653
|
const args = {};
|
|
654
|
+
const unknown = [];
|
|
655
|
+
const allowed = new Set(allowedCustomFlags ?? []);
|
|
332
656
|
const readValue = (raw, i) => {
|
|
333
657
|
const eq = raw.indexOf("=");
|
|
334
658
|
if (eq !== -1) return { value: raw.slice(eq + 1), nextIndex: i };
|
|
@@ -420,22 +744,46 @@ function parseCliArgs(argv) {
|
|
|
420
744
|
case "--dry-run":
|
|
421
745
|
args.noIngest = true;
|
|
422
746
|
break;
|
|
423
|
-
default:
|
|
747
|
+
default: {
|
|
748
|
+
if (allowed.has(name)) {
|
|
749
|
+
if (!arg.includes("=")) {
|
|
750
|
+
const next = argv[i + 1];
|
|
751
|
+
if (next && !next.startsWith("-")) {
|
|
752
|
+
i++;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
} else {
|
|
756
|
+
unknown.push(name);
|
|
757
|
+
if (!arg.includes("=")) {
|
|
758
|
+
const next = argv[i + 1];
|
|
759
|
+
if (next && !next.startsWith("-")) {
|
|
760
|
+
i++;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
424
764
|
break;
|
|
765
|
+
}
|
|
425
766
|
}
|
|
426
767
|
}
|
|
768
|
+
if (unknown.length > 0) {
|
|
769
|
+
throw new Error(`Unknown flag(s): ${unknown.join(", ")}`);
|
|
770
|
+
}
|
|
427
771
|
if (!args.noIngest && isEnvNoIngest()) {
|
|
428
772
|
args.noIngest = true;
|
|
429
773
|
}
|
|
430
774
|
return args;
|
|
431
775
|
}
|
|
432
776
|
function mergeConfig(config, args) {
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
777
|
+
const phases = config.phases;
|
|
778
|
+
const unevenPhases = phases !== void 0 && phases.some((p) => p.iterations !== phases[0].iterations);
|
|
779
|
+
if (unevenPhases && args.iterations !== void 0) {
|
|
780
|
+
console.warn("--iterations is ignored because this benchmark sizes its phases individually.");
|
|
436
781
|
}
|
|
782
|
+
const phaseIterations = phases !== void 0 && !unevenPhases ? args.iterations : void 0;
|
|
783
|
+
const phaseTotal = phases !== void 0 ? phaseIterations !== void 0 ? phaseIterations * phases.length : phases.reduce((sum, p) => sum + p.iterations, 0) : void 0;
|
|
437
784
|
const resolved = {
|
|
438
785
|
iterations: phaseTotal ?? args.iterations ?? config.iterations ?? 1,
|
|
786
|
+
phaseIterations,
|
|
439
787
|
concurrency: args.concurrency ?? config.concurrency ?? 1,
|
|
440
788
|
staggerDelayMs: args.staggerDelayMs ?? config.staggerDelayMs ?? 0,
|
|
441
789
|
groupBy: args.groupBy ?? config.groupBy ?? "participant",
|
|
@@ -449,13 +797,16 @@ function mergeConfig(config, args) {
|
|
|
449
797
|
}
|
|
450
798
|
return resolved;
|
|
451
799
|
}
|
|
452
|
-
function buildSchedule(config,
|
|
800
|
+
function buildSchedule(config, resolved, task) {
|
|
453
801
|
if (config.phases?.length) {
|
|
454
802
|
return config.phases.flatMap(
|
|
455
|
-
(phase) => Array.from({ length: phase.iterations }, () => ({
|
|
803
|
+
(phase) => Array.from({ length: resolved.phaseIterations ?? phase.iterations }, () => ({
|
|
804
|
+
phase: phase.name,
|
|
805
|
+
task
|
|
806
|
+
}))
|
|
456
807
|
);
|
|
457
808
|
}
|
|
458
|
-
return Array.from({ length: iterations }, () => ({ phase: void 0, task }));
|
|
809
|
+
return Array.from({ length: resolved.iterations }, () => ({ phase: void 0, task }));
|
|
459
810
|
}
|
|
460
811
|
function defaultOnResult(record, meta) {
|
|
461
812
|
const n = record.taskIndex + 1;
|
|
@@ -466,19 +817,6 @@ function defaultOnResult(record, meta) {
|
|
|
466
817
|
console.log(` [${meta.participant}] Task ${n}/${meta.iterations}: FAILED \u2014 ${record.errorCode ?? "unknown error"}`);
|
|
467
818
|
}
|
|
468
819
|
}
|
|
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
|
-
);
|
|
476
|
-
}
|
|
477
|
-
return {
|
|
478
|
-
baseUrl: `${root}/api/v1`,
|
|
479
|
-
apiKey
|
|
480
|
-
};
|
|
481
|
-
}
|
|
482
820
|
function resolveShape(config, shapeName) {
|
|
483
821
|
if (!shapeName) return void 0;
|
|
484
822
|
const shape = config.shapes?.[shapeName];
|
|
@@ -510,30 +848,51 @@ function dashboardUrlFor(baseUrl, organizationSlug, benchmarkSlug, runId) {
|
|
|
510
848
|
return `${baseUrl.replace(/\/api\/v1\/?$/, "")}/${organizationSlug}/benchmarks/${benchmarkSlug}/runs/${runId}`;
|
|
511
849
|
}
|
|
512
850
|
function resolveParticipants(config, resolved) {
|
|
513
|
-
const { available, skipped } = (0,
|
|
851
|
+
const { available, skipped } = (0, import_worker.filterParticipantsByEnv)((0, import_worker.selectParticipants)(config.participants, resolved.providers));
|
|
514
852
|
for (const s of skipped) {
|
|
515
853
|
console.log(`Skipping ${s.name}: missing ${s.missing.join(", ")}`);
|
|
516
854
|
}
|
|
517
855
|
if (available.length === 0) throw new NoAvailableParticipantsError(skipped);
|
|
518
856
|
return available;
|
|
519
857
|
}
|
|
858
|
+
function runConfigToJson(config, resolved, participants) {
|
|
859
|
+
const phases = config.phases?.map((phase) => ({
|
|
860
|
+
name: phase.name,
|
|
861
|
+
iterations: resolved.phaseIterations ?? phase.iterations
|
|
862
|
+
}));
|
|
863
|
+
const runConfig = {
|
|
864
|
+
benchmarkSlug: config.benchmarkSlug,
|
|
865
|
+
benchmarkName: config.benchmarkName,
|
|
866
|
+
...resolved.phaseIterations !== void 0 ? { phaseIterations: resolved.phaseIterations } : {},
|
|
867
|
+
...phases ? { phases } : {},
|
|
868
|
+
...!config.phases ? { iterations: resolved.iterations } : {},
|
|
869
|
+
concurrency: resolved.concurrency,
|
|
870
|
+
staggerDelayMs: resolved.staggerDelayMs,
|
|
871
|
+
groupBy: resolved.groupBy,
|
|
872
|
+
...config.dimensions ? { dimensions: config.dimensions } : {},
|
|
873
|
+
...config.scoring ? { scoring: config.scoring } : {},
|
|
874
|
+
participants
|
|
875
|
+
};
|
|
876
|
+
return JSON.parse(JSON.stringify(runConfig));
|
|
877
|
+
}
|
|
520
878
|
async function runBenchmark(fileConfig, task, argv = []) {
|
|
521
|
-
const args = parseCliArgs(argv);
|
|
879
|
+
const args = parseCliArgs(argv, fileConfig.customCliFlags);
|
|
522
880
|
const noIngest = args.noIngest ?? isEnvNoIngest();
|
|
523
881
|
const shaped = applyShape(fileConfig, resolveShape(fileConfig, args.shape));
|
|
524
882
|
const config = applyIdentityOverrides(shaped, args);
|
|
525
883
|
const resolved = mergeConfig(config, args);
|
|
884
|
+
const auth = await (0, import_cli.resolveAuth)();
|
|
885
|
+
const client = (0, import_api.createBenchmarkClient)({
|
|
886
|
+
baseUrl: auth.apiBaseUrl,
|
|
887
|
+
apiKey: auth.apiKey,
|
|
888
|
+
token: auth.token,
|
|
889
|
+
orgSlug: auth.orgSlug,
|
|
890
|
+
orgId: auth.orgId
|
|
891
|
+
});
|
|
526
892
|
const available = resolveParticipants(config, resolved);
|
|
527
|
-
|
|
528
|
-
let apiKey = "";
|
|
529
|
-
let client = null;
|
|
530
|
-
if (!noIngest) {
|
|
531
|
-
({ baseUrl, apiKey } = resolvePlatform());
|
|
532
|
-
client = (0, import_client.createBenchmarkClient)({ baseUrl, apiKey });
|
|
533
|
-
}
|
|
534
|
-
const schedule = buildSchedule(config, resolved.iterations, task);
|
|
893
|
+
const schedule = buildSchedule(config, resolved, task);
|
|
535
894
|
const totalTasks = schedule.length;
|
|
536
|
-
const concurrencyLabel =
|
|
895
|
+
const concurrencyLabel = String(resolved.concurrency);
|
|
537
896
|
console.log(`${config.benchmarkName} (self-contained)`);
|
|
538
897
|
console.log(`Date: ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
539
898
|
if (noIngest) {
|
|
@@ -551,16 +910,23 @@ async function runBenchmark(fileConfig, task, argv = []) {
|
|
|
551
910
|
dashboardUrl = "";
|
|
552
911
|
} else {
|
|
553
912
|
if (identityIsOurs) {
|
|
913
|
+
const benchmarkConfig = {
|
|
914
|
+
...config.scoring ? { scoring: config.scoring } : {},
|
|
915
|
+
...config.display ? { display: config.display } : {}
|
|
916
|
+
};
|
|
554
917
|
await client.upsertBenchmark(config.benchmarkSlug, {
|
|
555
|
-
name: config.benchmarkName
|
|
918
|
+
name: config.benchmarkName,
|
|
919
|
+
...Object.keys(benchmarkConfig).length > 0 ? { config: benchmarkConfig } : {}
|
|
556
920
|
});
|
|
557
921
|
}
|
|
922
|
+
const runConfig = client ? runConfigToJson(config, resolved, available.map((p) => p.name)) : {};
|
|
558
923
|
if (args.runKey) {
|
|
559
924
|
const { run: run2, organizationSlug } = await client.createRun(config.benchmarkSlug, {
|
|
560
|
-
runKey: args.runKey
|
|
925
|
+
runKey: args.runKey,
|
|
926
|
+
config: runConfig
|
|
561
927
|
});
|
|
562
928
|
runId = run2.id;
|
|
563
|
-
dashboardUrl = dashboardUrlFor(
|
|
929
|
+
dashboardUrl = dashboardUrlFor(auth.apiBaseUrl, organizationSlug, config.benchmarkSlug, run2.id);
|
|
564
930
|
for (const participant of available) {
|
|
565
931
|
await client.upsertParticipant(config.benchmarkSlug, runId, participant.name, { totalTasks });
|
|
566
932
|
}
|
|
@@ -571,10 +937,11 @@ async function runBenchmark(fileConfig, task, argv = []) {
|
|
|
571
937
|
const { run: run2, organizationSlug } = await client.createRun(config.benchmarkSlug, {
|
|
572
938
|
totalTasks,
|
|
573
939
|
workerCount: 1,
|
|
574
|
-
participants: available.map((p) => p.name)
|
|
940
|
+
participants: available.map((p) => p.name),
|
|
941
|
+
config: runConfig
|
|
575
942
|
});
|
|
576
943
|
runId = run2.id;
|
|
577
|
-
dashboardUrl = dashboardUrlFor(
|
|
944
|
+
dashboardUrl = dashboardUrlFor(auth.apiBaseUrl, organizationSlug, config.benchmarkSlug, run2.id);
|
|
578
945
|
console.log(`Run created: ${run2.name} (${runId})`);
|
|
579
946
|
console.log(`View at: ${dashboardUrl}
|
|
580
947
|
`);
|
|
@@ -583,9 +950,9 @@ async function runBenchmark(fileConfig, task, argv = []) {
|
|
|
583
950
|
const onResult = defaultOnResult;
|
|
584
951
|
let participantRecords;
|
|
585
952
|
if (resolved.groupBy === "round") {
|
|
586
|
-
participantRecords = await runGroupedByRound(config, schedule, available, resolved, client, runId,
|
|
953
|
+
participantRecords = await runGroupedByRound(config, schedule, available, resolved, client, runId, auth.apiBaseUrl, auth.apiKey, auth.token, auth.orgSlug, auth.orgId, onResult, noIngest);
|
|
587
954
|
} else {
|
|
588
|
-
participantRecords = await runGroupedByParticipant(config, schedule, available, resolved, client, runId, onResult);
|
|
955
|
+
participantRecords = await runGroupedByParticipant(config, schedule, available, resolved, client, runId, onResult, noIngest);
|
|
589
956
|
}
|
|
590
957
|
console.log(`All done. ${noIngest ? "No platform run created." : `View at: ${dashboardUrl}`}`);
|
|
591
958
|
const outcome = {
|
|
@@ -594,10 +961,10 @@ async function runBenchmark(fileConfig, task, argv = []) {
|
|
|
594
961
|
participants: participantRecords,
|
|
595
962
|
config: resolved
|
|
596
963
|
};
|
|
597
|
-
if (
|
|
964
|
+
if (!noIngest && (config.onScore || config.scoring)) {
|
|
598
965
|
try {
|
|
599
|
-
const spec = await config.onScore(lowerIsBetter, higherIsBetter);
|
|
600
|
-
const scored = score(outcome, spec);
|
|
966
|
+
const spec = config.onScore ? await config.onScore(lowerIsBetter, higherIsBetter) : scoringConfigToSpec(config.scoring, config.dimensions, config.display);
|
|
967
|
+
const scored = score(outcome, spec, config.display?.metrics);
|
|
601
968
|
const run2 = {
|
|
602
969
|
gitSha: process.env.GITHUB_SHA ?? getGitSha(),
|
|
603
970
|
gitRef: process.env.GITHUB_REF_NAME ?? process.env.GITHUB_REF ?? getGitRef(),
|
|
@@ -606,8 +973,13 @@ async function runBenchmark(fileConfig, task, argv = []) {
|
|
|
606
973
|
platform: import_node_os.default.platform(),
|
|
607
974
|
arch: import_node_os.default.arch()
|
|
608
975
|
};
|
|
609
|
-
await client.submitRunSummary(config.benchmarkSlug, runId, {
|
|
976
|
+
await client.submitRunSummary(config.benchmarkSlug, runId, {
|
|
977
|
+
run: run2,
|
|
978
|
+
results: scored,
|
|
979
|
+
...config.scoring ? { scoring: config.scoring } : {}
|
|
980
|
+
});
|
|
610
981
|
} catch (err) {
|
|
982
|
+
if (err instanceof ScoringSpecError) throw err;
|
|
611
983
|
const message = err instanceof Error ? err.message : String(err);
|
|
612
984
|
console.warn(`[benchsdk-runner] failed to submit run summary: ${message}`);
|
|
613
985
|
}
|
|
@@ -632,13 +1004,13 @@ function getGitRef() {
|
|
|
632
1004
|
return void 0;
|
|
633
1005
|
}
|
|
634
1006
|
}
|
|
635
|
-
async function runGroupedByParticipant(config, schedule, available, resolved, client, runId, onResult) {
|
|
1007
|
+
async function runGroupedByParticipant(config, schedule, available, resolved, client, runId, onResult, noIngest) {
|
|
636
1008
|
const participantRecords = [];
|
|
637
1009
|
for (const participant of available) {
|
|
638
1010
|
console.log(`${"=".repeat(70)}`);
|
|
639
1011
|
console.log(` Participant: ${participant.name}`);
|
|
640
1012
|
console.log("=".repeat(70));
|
|
641
|
-
if (!client) {
|
|
1013
|
+
if (noIngest || !client) {
|
|
642
1014
|
const records = [];
|
|
643
1015
|
let rampStartMs2;
|
|
644
1016
|
let nextIndex = 0;
|
|
@@ -670,7 +1042,7 @@ async function runGroupedByParticipant(config, schedule, available, resolved, cl
|
|
|
670
1042
|
}
|
|
671
1043
|
let rampStartMs;
|
|
672
1044
|
await client.planWorkers(config.benchmarkSlug, runId, participant.name);
|
|
673
|
-
const result = await
|
|
1045
|
+
const result = await (0, import_worker.runWorker)(client, {
|
|
674
1046
|
benchmarkSlug: config.benchmarkSlug,
|
|
675
1047
|
runId,
|
|
676
1048
|
participantSlug: participant.name,
|
|
@@ -683,16 +1055,21 @@ async function runGroupedByParticipant(config, schedule, available, resolved, cl
|
|
|
683
1055
|
if (waitMs > 0) await sleep(waitMs);
|
|
684
1056
|
}
|
|
685
1057
|
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
1058
|
if (slot.phase) ctx.measure({ phase: slot.phase });
|
|
695
|
-
|
|
1059
|
+
try {
|
|
1060
|
+
const taskResult = await slot.task({
|
|
1061
|
+
participant,
|
|
1062
|
+
taskIndex: scheduleIndex,
|
|
1063
|
+
phase: slot.phase,
|
|
1064
|
+
step: (name, fn, options) => runStepWithClient(ctx.step, name, fn, options),
|
|
1065
|
+
measure: ctx.measure,
|
|
1066
|
+
log: ctx.log
|
|
1067
|
+
});
|
|
1068
|
+
return taskResult?.data;
|
|
1069
|
+
} catch (error) {
|
|
1070
|
+
if (isTaskError(error) && error.data) ctx.measure(error.data);
|
|
1071
|
+
throw error;
|
|
1072
|
+
}
|
|
696
1073
|
},
|
|
697
1074
|
onResult: (record) => onResult(record, { iterations: schedule.length, participant: participant.name })
|
|
698
1075
|
});
|
|
@@ -708,14 +1085,17 @@ async function runGroupedByParticipant(config, schedule, available, resolved, cl
|
|
|
708
1085
|
}
|
|
709
1086
|
return participantRecords;
|
|
710
1087
|
}
|
|
711
|
-
async function runGroupedByRound(config, schedule, available, resolved, client, runId, baseUrl, apiKey, onResult, noIngest = false) {
|
|
1088
|
+
async function runGroupedByRound(config, schedule, available, resolved, client, runId, baseUrl, apiKey, token, orgSlug, orgId, onResult, noIngest = false) {
|
|
712
1089
|
const reporters = /* @__PURE__ */ new Map();
|
|
713
1090
|
const logBuffers = /* @__PURE__ */ new Map();
|
|
714
1091
|
const failed = /* @__PURE__ */ new Map();
|
|
715
1092
|
const recordsByParticipant = /* @__PURE__ */ new Map();
|
|
1093
|
+
let metricsCollector;
|
|
1094
|
+
const metricsSamples = [];
|
|
716
1095
|
for (const participant of available) {
|
|
717
1096
|
logBuffers.set(participant.name, new LogBuffer());
|
|
718
1097
|
failed.set(participant.name, false);
|
|
1098
|
+
recordsByParticipant.set(participant.name, []);
|
|
719
1099
|
if (noIngest || !client) {
|
|
720
1100
|
reporters.set(participant.name, null);
|
|
721
1101
|
continue;
|
|
@@ -726,9 +1106,12 @@ async function runGroupedByRound(config, schedule, available, resolved, client,
|
|
|
726
1106
|
});
|
|
727
1107
|
let reporter = null;
|
|
728
1108
|
try {
|
|
729
|
-
reporter = await
|
|
1109
|
+
reporter = await import_worker.BenchmarkReporter.claim({
|
|
730
1110
|
baseUrl,
|
|
731
1111
|
apiKey,
|
|
1112
|
+
token,
|
|
1113
|
+
orgSlug,
|
|
1114
|
+
orgId,
|
|
732
1115
|
benchmarkSlug: config.benchmarkSlug,
|
|
733
1116
|
runId,
|
|
734
1117
|
participantSlug: participant.name,
|
|
@@ -742,6 +1125,10 @@ async function runGroupedByRound(config, schedule, available, resolved, client,
|
|
|
742
1125
|
console.warn(` ${participant.name}: could not claim a platform worker \u2014 running without platform reporting.`);
|
|
743
1126
|
}
|
|
744
1127
|
reporters.set(participant.name, reporter);
|
|
1128
|
+
if (reporter && !metricsCollector) {
|
|
1129
|
+
metricsCollector = (0, import_worker.createSystemMetricsCollector)();
|
|
1130
|
+
metricsSamples.push(await metricsCollector.sample());
|
|
1131
|
+
}
|
|
745
1132
|
}
|
|
746
1133
|
console.log(`Interleaving ${available.length} participant(s), ${schedule.length} round(s) each.
|
|
747
1134
|
`);
|
|
@@ -750,7 +1137,7 @@ async function runGroupedByRound(config, schedule, available, resolved, client,
|
|
|
750
1137
|
if (resolved.staggerDelayMs > 0 && i > 0) {
|
|
751
1138
|
await sleep(resolved.staggerDelayMs);
|
|
752
1139
|
}
|
|
753
|
-
|
|
1140
|
+
const roundFns = available.map((participant) => async () => {
|
|
754
1141
|
const reporter = reporters.get(participant.name) ?? null;
|
|
755
1142
|
const logBuffer = logBuffers.get(participant.name);
|
|
756
1143
|
const record = await runTaskRecord(
|
|
@@ -764,9 +1151,6 @@ async function runGroupedByRound(config, schedule, available, resolved, client,
|
|
|
764
1151
|
if (record.status !== "success") failed.set(participant.name, true);
|
|
765
1152
|
onResult(record, { iterations: schedule.length, participant: participant.name });
|
|
766
1153
|
reporter?.recordResult(record);
|
|
767
|
-
if (!recordsByParticipant.has(participant.name)) {
|
|
768
|
-
recordsByParticipant.set(participant.name, []);
|
|
769
|
-
}
|
|
770
1154
|
const participantRecords = recordsByParticipant.get(participant.name);
|
|
771
1155
|
participantRecords.push(record);
|
|
772
1156
|
if (reporter) {
|
|
@@ -778,7 +1162,22 @@ async function runGroupedByRound(config, schedule, available, resolved, client,
|
|
|
778
1162
|
});
|
|
779
1163
|
await reporter.heartbeat();
|
|
780
1164
|
}
|
|
781
|
-
}
|
|
1165
|
+
});
|
|
1166
|
+
await runWithConcurrency(roundFns, resolved.concurrency);
|
|
1167
|
+
if (metricsCollector) metricsSamples.push(await metricsCollector.sample());
|
|
1168
|
+
}
|
|
1169
|
+
if (metricsCollector) metricsSamples.push(await metricsCollector.sample());
|
|
1170
|
+
metricsCollector?.stop();
|
|
1171
|
+
const metricsReporter = available.map((p) => reporters.get(p.name)).find((r) => Boolean(r));
|
|
1172
|
+
if (metricsReporter && metricsSamples.length > 0) {
|
|
1173
|
+
await metricsReporter.uploadArtifact({
|
|
1174
|
+
kind: "system-metrics",
|
|
1175
|
+
contentType: "application/x-ndjson",
|
|
1176
|
+
name: "metrics.jsonl",
|
|
1177
|
+
metadata: { scope: "shared-process", participants: available.map((p) => p.name) },
|
|
1178
|
+
body: metricsSamples.map((sample) => JSON.stringify(sample)).join("\n") + "\n"
|
|
1179
|
+
}).catch(() => {
|
|
1180
|
+
});
|
|
782
1181
|
}
|
|
783
1182
|
for (const participant of available) {
|
|
784
1183
|
const reporter = reporters.get(participant.name) ?? null;
|
|
@@ -825,11 +1224,12 @@ async function runTaskRecord(task, participant, scheduleIndex, taskIndex, phase,
|
|
|
825
1224
|
activeStep = stepRecord;
|
|
826
1225
|
try {
|
|
827
1226
|
const result2 = await runStepInvocations(name, fn, options);
|
|
828
|
-
|
|
1227
|
+
const outcome = options?.captureOutput !== false && !Array.isArray(result2) && isStepOutcome(result2) ? result2 : {};
|
|
1228
|
+
logBuffer.step(taskIndex, name, outcome);
|
|
829
1229
|
return result2;
|
|
830
1230
|
} catch (error) {
|
|
831
1231
|
stepRecord.status = "error";
|
|
832
|
-
stepRecord.errorCode = error
|
|
1232
|
+
stepRecord.errorCode = isTaskError(error) ? error.code ?? error.name : getErrorCode(error);
|
|
833
1233
|
logBuffer.step(taskIndex, name, { error: error instanceof Error ? error.message : String(error) });
|
|
834
1234
|
throw error;
|
|
835
1235
|
} finally {
|
|
@@ -846,8 +1246,8 @@ async function runTaskRecord(task, participant, scheduleIndex, taskIndex, phase,
|
|
|
846
1246
|
Object.assign(taskMeasures, data);
|
|
847
1247
|
}
|
|
848
1248
|
},
|
|
849
|
-
log(message,
|
|
850
|
-
logBuffer.line(`[task ${taskIndex}] ${message}`,
|
|
1249
|
+
log(message, metaOrOptions) {
|
|
1250
|
+
logBuffer.line(`[task ${taskIndex}] ${message}`, metaOrOptions);
|
|
851
1251
|
}
|
|
852
1252
|
};
|
|
853
1253
|
let result = void 0;
|
|
@@ -856,7 +1256,7 @@ async function runTaskRecord(task, participant, scheduleIndex, taskIndex, phase,
|
|
|
856
1256
|
record.data = mergeData({ ...taskMeasures, ...result?.data ?? {} }, phase);
|
|
857
1257
|
} catch (error) {
|
|
858
1258
|
record.status = "error";
|
|
859
|
-
if (error
|
|
1259
|
+
if (isTaskError(error)) {
|
|
860
1260
|
record.errorCode = error.code ?? error.name;
|
|
861
1261
|
record.data = mergeData({ ...taskMeasures, ...error.data ?? {} }, phase);
|
|
862
1262
|
if (error.steps?.length) frameworkSteps.push(...error.steps);
|
|
@@ -892,6 +1292,7 @@ async function runTaskRecord(task, participant, scheduleIndex, taskIndex, phase,
|
|
|
892
1292
|
// src/cli.ts
|
|
893
1293
|
var import_node_path = require("path");
|
|
894
1294
|
var import_node_url = require("url");
|
|
1295
|
+
var import_cli2 = require("@benchsdk/cli");
|
|
895
1296
|
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]';
|
|
896
1297
|
function isBenchmarkConfig(value) {
|
|
897
1298
|
if (typeof value !== "object" || value === null) return false;
|
|
@@ -914,6 +1315,9 @@ async function runBenchmarkFile(argv) {
|
|
|
914
1315
|
await runBenchmark(config, task, flags);
|
|
915
1316
|
}
|
|
916
1317
|
async function run(argv) {
|
|
1318
|
+
if (argv[0] !== "run") {
|
|
1319
|
+
return (0, import_cli2.run)(argv);
|
|
1320
|
+
}
|
|
917
1321
|
try {
|
|
918
1322
|
await runBenchmarkFile(argv);
|
|
919
1323
|
process.exit(0);
|
|
@@ -926,9 +1330,14 @@ async function run(argv) {
|
|
|
926
1330
|
process.exit(1);
|
|
927
1331
|
}
|
|
928
1332
|
}
|
|
1333
|
+
|
|
1334
|
+
// src/index.ts
|
|
1335
|
+
var BENCHSDK_RUNNER_VERSION = "0.3.0";
|
|
929
1336
|
// Annotate the CommonJS export names for ESM import in node:
|
|
930
1337
|
0 && (module.exports = {
|
|
1338
|
+
BENCHSDK_RUNNER_VERSION,
|
|
931
1339
|
NoAvailableParticipantsError,
|
|
1340
|
+
ScoringSpecError,
|
|
932
1341
|
TaskError,
|
|
933
1342
|
defineBenchmarkConfig,
|
|
934
1343
|
defineTask,
|
|
@@ -939,6 +1348,8 @@ async function run(argv) {
|
|
|
939
1348
|
run,
|
|
940
1349
|
runBenchmark,
|
|
941
1350
|
runBenchmarkFile,
|
|
942
|
-
score
|
|
1351
|
+
score,
|
|
1352
|
+
scoringConfigToSpec,
|
|
1353
|
+
validateScoringSpec
|
|
943
1354
|
});
|
|
944
1355
|
//# sourceMappingURL=index.cjs.map
|