@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.js
CHANGED
|
@@ -11,12 +11,101 @@ var TaskError = class extends Error {
|
|
|
11
11
|
this.steps = opts?.steps;
|
|
12
12
|
}
|
|
13
13
|
};
|
|
14
|
+
function assertNonEmptyString(value, field) {
|
|
15
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
16
|
+
throw new Error(`${field} must be a non-empty string`);
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
function assertOnlyAllowedKeys(value, allowed, field) {
|
|
21
|
+
for (const key of Object.keys(value)) {
|
|
22
|
+
if (!allowed.includes(key)) {
|
|
23
|
+
throw new Error(`${field} contains unexpected key: '${key}'`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
14
27
|
function assertPositiveInt(value, field) {
|
|
15
28
|
if (value === void 0) return;
|
|
16
29
|
if (!Number.isInteger(value) || value < 1) {
|
|
17
30
|
throw new Error(`${field} must be an integer >= 1 (got ${value})`);
|
|
18
31
|
}
|
|
19
32
|
}
|
|
33
|
+
function assertFiniteNumber(value, field) {
|
|
34
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
35
|
+
throw new Error(`${field} must be a finite number (got ${value})`);
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
function validateBenchmarkScoringConfig(scoring, display) {
|
|
40
|
+
if (!Array.isArray(scoring.metrics) || scoring.metrics.length === 0) {
|
|
41
|
+
throw new Error("scoring.metrics must be a non-empty array");
|
|
42
|
+
}
|
|
43
|
+
if (scoring.success !== void 0) {
|
|
44
|
+
const requireData = scoring.success.requireData;
|
|
45
|
+
if (requireData === null || typeof requireData !== "object" || Array.isArray(requireData)) {
|
|
46
|
+
throw new Error("scoring.success.requireData must be a plain object");
|
|
47
|
+
}
|
|
48
|
+
if (Object.keys(requireData).length === 0) {
|
|
49
|
+
throw new Error("scoring.success.requireData must declare at least one data field");
|
|
50
|
+
}
|
|
51
|
+
for (const [key, value] of Object.entries(requireData)) {
|
|
52
|
+
const type = typeof value;
|
|
53
|
+
if (type !== "string" && type !== "number" && type !== "boolean") {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`scoring.success.requireData.${key} must be a string, number, or boolean (got ${type})`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const seen = /* @__PURE__ */ new Set();
|
|
61
|
+
let totalWeight = 0;
|
|
62
|
+
for (let i = 0; i < scoring.metrics.length; i++) {
|
|
63
|
+
const metric = scoring.metrics[i];
|
|
64
|
+
if (metric === null || typeof metric !== "object" || Array.isArray(metric)) {
|
|
65
|
+
throw new Error(`scoring.metrics[${i}] must be an object`);
|
|
66
|
+
}
|
|
67
|
+
const key = metric.key;
|
|
68
|
+
if (typeof key !== "string" || key.trim() === "") {
|
|
69
|
+
throw new Error(`scoring.metrics[${i}].key must be a non-empty string`);
|
|
70
|
+
}
|
|
71
|
+
if (seen.has(key)) {
|
|
72
|
+
throw new Error(`duplicate scoring metric key: ${key}`);
|
|
73
|
+
}
|
|
74
|
+
seen.add(key);
|
|
75
|
+
const displayMetrics = Array.isArray(display?.metrics) ? display.metrics : void 0;
|
|
76
|
+
const displayMetric = displayMetrics?.find((m) => m?.key === key);
|
|
77
|
+
if (metric.unit !== void 0) {
|
|
78
|
+
if (typeof metric.unit !== "string") {
|
|
79
|
+
throw new Error(`scoring.metrics[${i}].unit must be a string`);
|
|
80
|
+
}
|
|
81
|
+
if (displayMetric?.unit !== void 0 && metric.unit !== displayMetric.unit) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`scoring.metrics[${i}].unit '${metric.unit}' conflicts with display.metrics[${i}].unit '${displayMetric.unit}' for key '${key}'`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
assertFiniteNumber(metric.ceiling, `scoring.metrics[${i}].ceiling`);
|
|
88
|
+
if (metric.floor !== void 0) {
|
|
89
|
+
assertFiniteNumber(metric.floor, `scoring.metrics[${i}].floor`);
|
|
90
|
+
}
|
|
91
|
+
if (metric.weights === null || typeof metric.weights !== "object" || Array.isArray(metric.weights)) {
|
|
92
|
+
throw new Error(`scoring.metrics[${i}].weights must be an object`);
|
|
93
|
+
}
|
|
94
|
+
const median = assertFiniteNumber(metric.weights.median, `scoring.metrics[${i}].weights.median`);
|
|
95
|
+
const p95 = assertFiniteNumber(metric.weights.p95, `scoring.metrics[${i}].weights.p95`);
|
|
96
|
+
const p99 = assertFiniteNumber(metric.weights.p99, `scoring.metrics[${i}].weights.p99`);
|
|
97
|
+
if (median < 0 || p95 < 0 || p99 < 0) {
|
|
98
|
+
throw new Error(`scoring.metrics[${i}].weights must be non-negative`);
|
|
99
|
+
}
|
|
100
|
+
totalWeight += median + p95 + p99;
|
|
101
|
+
if (metric.trim !== void 0) {
|
|
102
|
+
assertFiniteNumber(metric.trim, `scoring.metrics[${i}].trim`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (Math.abs(totalWeight - 1) > 0.01) {
|
|
106
|
+
throw new Error(`scoring metric weights must sum to 1.0 (got ${totalWeight.toFixed(3)})`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
20
109
|
function defineBenchmarkConfig(config) {
|
|
21
110
|
if (!config.benchmarkSlug || typeof config.benchmarkSlug !== "string") {
|
|
22
111
|
throw new Error("benchmarkSlug is required");
|
|
@@ -64,6 +153,100 @@ function defineBenchmarkConfig(config) {
|
|
|
64
153
|
}
|
|
65
154
|
}
|
|
66
155
|
}
|
|
156
|
+
if (config.dimensions !== void 0) {
|
|
157
|
+
if (config.dimensions === null || typeof config.dimensions !== "object" || Array.isArray(config.dimensions)) {
|
|
158
|
+
throw new Error("dimensions must be a plain object");
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (config.scoring !== void 0) {
|
|
162
|
+
validateBenchmarkScoringConfig(config.scoring, config.display);
|
|
163
|
+
}
|
|
164
|
+
if (config.customCliFlags !== void 0) {
|
|
165
|
+
if (!Array.isArray(config.customCliFlags) || !config.customCliFlags.every((f) => typeof f === "string" && f.startsWith("--"))) {
|
|
166
|
+
throw new Error('customCliFlags must be an array of strings starting with "--"');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (config.display !== void 0) {
|
|
170
|
+
if (typeof config.display !== "object" || config.display === null || Array.isArray(config.display)) {
|
|
171
|
+
throw new Error("display must be an object");
|
|
172
|
+
}
|
|
173
|
+
assertOnlyAllowedKeys(config.display, ["metrics", "steps", "overview"], "display");
|
|
174
|
+
const displayMetricKeys = /* @__PURE__ */ new Set();
|
|
175
|
+
if (config.display.metrics !== void 0) {
|
|
176
|
+
if (!Array.isArray(config.display.metrics)) {
|
|
177
|
+
throw new Error("display.metrics must be an array");
|
|
178
|
+
}
|
|
179
|
+
for (let i = 0; i < config.display.metrics.length; i++) {
|
|
180
|
+
const metric = config.display.metrics[i];
|
|
181
|
+
if (metric === null || typeof metric !== "object" || Array.isArray(metric)) {
|
|
182
|
+
throw new Error(`display.metrics[${i}] must be an object`);
|
|
183
|
+
}
|
|
184
|
+
assertOnlyAllowedKeys(metric, ["key", "label", "unit", "direction", "decimals", "order"], `display.metrics[${i}]`);
|
|
185
|
+
const key = assertNonEmptyString(metric.key, `display.metrics[${i}].key`);
|
|
186
|
+
if (displayMetricKeys.has(key)) {
|
|
187
|
+
throw new Error(`duplicate display metric key: ${key}`);
|
|
188
|
+
}
|
|
189
|
+
displayMetricKeys.add(key);
|
|
190
|
+
assertNonEmptyString(metric.label, `display.metrics[${i}].label`);
|
|
191
|
+
if (metric.unit !== void 0 && typeof metric.unit !== "string") {
|
|
192
|
+
throw new Error(`display.metrics[${i}].unit must be a string`);
|
|
193
|
+
}
|
|
194
|
+
if (metric.direction !== void 0 && metric.direction !== "higher-better" && metric.direction !== "lower-better") {
|
|
195
|
+
throw new Error(`display.metrics[${i}].direction must be 'higher-better' or 'lower-better'`);
|
|
196
|
+
}
|
|
197
|
+
if (metric.decimals !== void 0 && (!Number.isInteger(metric.decimals) || metric.decimals < 0)) {
|
|
198
|
+
throw new Error(`display.metrics[${i}].decimals must be a non-negative integer`);
|
|
199
|
+
}
|
|
200
|
+
if (metric.order !== void 0 && (!Number.isInteger(metric.order) || metric.order < 0)) {
|
|
201
|
+
throw new Error(`display.metrics[${i}].order must be a non-negative integer`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (config.display.steps !== void 0) {
|
|
206
|
+
if (!Array.isArray(config.display.steps)) {
|
|
207
|
+
throw new Error("display.steps must be an array");
|
|
208
|
+
}
|
|
209
|
+
const seenStepKeys = /* @__PURE__ */ new Set();
|
|
210
|
+
for (let i = 0; i < config.display.steps.length; i++) {
|
|
211
|
+
const step = config.display.steps[i];
|
|
212
|
+
if (step === null || typeof step !== "object" || Array.isArray(step)) {
|
|
213
|
+
throw new Error(`display.steps[${i}] must be an object`);
|
|
214
|
+
}
|
|
215
|
+
assertOnlyAllowedKeys(step, ["key", "label", "order"], `display.steps[${i}]`);
|
|
216
|
+
const key = assertNonEmptyString(step.key, `display.steps[${i}].key`);
|
|
217
|
+
if (seenStepKeys.has(key)) {
|
|
218
|
+
throw new Error(`duplicate display step key: ${key}`);
|
|
219
|
+
}
|
|
220
|
+
seenStepKeys.add(key);
|
|
221
|
+
assertNonEmptyString(step.label, `display.steps[${i}].label`);
|
|
222
|
+
if (step.order !== void 0 && (!Number.isInteger(step.order) || step.order < 0)) {
|
|
223
|
+
throw new Error(`display.steps[${i}].order must be a non-negative integer`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (config.display.overview !== void 0) {
|
|
228
|
+
if (typeof config.display.overview !== "object" || config.display.overview === null || Array.isArray(config.display.overview)) {
|
|
229
|
+
throw new Error("display.overview must be an object");
|
|
230
|
+
}
|
|
231
|
+
assertOnlyAllowedKeys(config.display.overview, ["defaultMetric", "defaultLayout"], "display.overview");
|
|
232
|
+
const { defaultMetric, defaultLayout } = config.display.overview;
|
|
233
|
+
if (defaultMetric !== void 0) {
|
|
234
|
+
const metric = assertNonEmptyString(defaultMetric, "display.overview.defaultMetric");
|
|
235
|
+
const validDefaultMetrics = new Set(displayMetricKeys);
|
|
236
|
+
validDefaultMetrics.add("compositeScore");
|
|
237
|
+
validDefaultMetrics.add("task");
|
|
238
|
+
if (config.display.metrics !== void 0 && !validDefaultMetrics.has(metric)) {
|
|
239
|
+
throw new Error(`display.overview.defaultMetric '${metric}' is not declared in display.metrics and is not a known default (compositeScore, task)`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (defaultLayout !== void 0 && !["ranking", "cards", "chart", "leaderboard"].includes(defaultLayout)) {
|
|
243
|
+
throw new Error("display.overview.defaultLayout must be 'ranking', 'cards', 'chart', or 'leaderboard'");
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (config.display?.overview?.defaultMetric === "compositeScore" && config.scoring === void 0 && config.onScore === void 0) {
|
|
248
|
+
throw new Error("display.overview.defaultMetric cannot be 'compositeScore' without config.scoring or config.onScore");
|
|
249
|
+
}
|
|
67
250
|
return config;
|
|
68
251
|
}
|
|
69
252
|
function defineTask(task) {
|
|
@@ -88,12 +271,15 @@ var NoAvailableParticipantsError = class extends Error {
|
|
|
88
271
|
// src/runner.ts
|
|
89
272
|
import { execSync } from "child_process";
|
|
90
273
|
import os from "os";
|
|
274
|
+
import { createBenchmarkClient } from "@benchsdk/api";
|
|
275
|
+
import { resolveAuth } from "@benchsdk/cli";
|
|
91
276
|
import {
|
|
92
277
|
BenchmarkReporter,
|
|
93
|
-
|
|
278
|
+
createSystemMetricsCollector,
|
|
94
279
|
filterParticipantsByEnv,
|
|
280
|
+
runWorker,
|
|
95
281
|
selectParticipants
|
|
96
|
-
} from "@benchsdk/
|
|
282
|
+
} from "@benchsdk/worker";
|
|
97
283
|
|
|
98
284
|
// src/scoring.ts
|
|
99
285
|
function isFiniteNumber(v) {
|
|
@@ -149,40 +335,121 @@ var higherIsBetter = (name, opts) => ({
|
|
|
149
335
|
...opts,
|
|
150
336
|
higherIsBetter: true
|
|
151
337
|
});
|
|
152
|
-
|
|
338
|
+
var WEIGHT_SUM_TOLERANCE = 0.01;
|
|
339
|
+
var ScoringSpecError = class extends Error {
|
|
340
|
+
constructor(message) {
|
|
341
|
+
super(message);
|
|
342
|
+
this.name = "ScoringSpecError";
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
function validateScoringSpec(spec) {
|
|
346
|
+
const totalWeight = spec.metrics.reduce(
|
|
347
|
+
(sum, m) => sum + m.weights.median + m.weights.p95 + m.weights.p99,
|
|
348
|
+
0
|
|
349
|
+
);
|
|
350
|
+
if (Math.abs(totalWeight - 1) > WEIGHT_SUM_TOLERANCE) {
|
|
351
|
+
const breakdown = spec.metrics.map((m) => `${m.name}=${(m.weights.median + m.weights.p95 + m.weights.p99).toFixed(3)}`).join(", ");
|
|
352
|
+
throw new ScoringSpecError(
|
|
353
|
+
`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)"}`
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
function groupRecordsByKey(records, key) {
|
|
358
|
+
const groups = /* @__PURE__ */ new Map();
|
|
359
|
+
for (const record of records) {
|
|
360
|
+
const raw = record.data?.[key];
|
|
361
|
+
const value = raw === void 0 ? void 0 : raw;
|
|
362
|
+
const mapKey = value === void 0 ? "__undefined__" : JSON.stringify(value);
|
|
363
|
+
let group = groups.get(mapKey);
|
|
364
|
+
if (!group) {
|
|
365
|
+
group = { value, records: [] };
|
|
366
|
+
groups.set(mapKey, group);
|
|
367
|
+
}
|
|
368
|
+
group.records.push(record);
|
|
369
|
+
}
|
|
370
|
+
return Array.from(groups.values());
|
|
371
|
+
}
|
|
372
|
+
function scoreGroup(records, spec, baseDimensions, groupKey, groupValue, provider, unitByMetric) {
|
|
153
373
|
const successFilter = spec.success ?? ((r) => r.status === "success");
|
|
154
|
-
const
|
|
374
|
+
const passing = records.filter(successFilter);
|
|
375
|
+
const successRate = records.length === 0 ? 0 : passing.length / records.length;
|
|
376
|
+
const skipped = records.length === 0;
|
|
377
|
+
let metricScoresSum = 0;
|
|
378
|
+
const metrics = [];
|
|
379
|
+
for (const metric of spec.metrics) {
|
|
380
|
+
const samples = collectSamples(metric, passing);
|
|
381
|
+
if (samples.length === 0) {
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
const { median, p95, p99 } = computeStats(samples, metric.trim ?? 0.05);
|
|
385
|
+
const metricScore = metric.weights.median * scoreStat(median, metric) + metric.weights.p95 * scoreStat(p95, metric) + metric.weights.p99 * scoreStat(p99, metric);
|
|
386
|
+
metricScoresSum += metricScore;
|
|
387
|
+
const unitKey = typeof metric.value === "string" ? metric.value : metric.name;
|
|
388
|
+
const unit = metric.unit ?? unitByMetric.get(unitKey) ?? "";
|
|
389
|
+
metrics.push({ name: metric.name, unit, median, p95, p99 });
|
|
390
|
+
}
|
|
391
|
+
const compositeScore = successRate === 0 ? 0 : Math.round(metricScoresSum * successRate * 100) / 100;
|
|
392
|
+
const dimensions = toJsonObject({
|
|
393
|
+
...baseDimensions,
|
|
394
|
+
...groupKey !== void 0 && groupValue !== void 0 ? { [groupKey]: groupValue } : {}
|
|
395
|
+
});
|
|
396
|
+
return {
|
|
397
|
+
provider,
|
|
398
|
+
dimensions,
|
|
399
|
+
metrics,
|
|
400
|
+
compositeScore,
|
|
401
|
+
successRate,
|
|
402
|
+
skipped
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
function score(outcome, spec, displayMetrics) {
|
|
406
|
+
validateScoringSpec(spec);
|
|
407
|
+
const baseDimensions = toJsonObject(spec.dimensions ?? {});
|
|
408
|
+
const unitByMetric = new Map(displayMetrics?.map((m) => [m.key, m.unit]));
|
|
155
409
|
const results = [];
|
|
156
410
|
for (const { participant, records } of outcome.participants) {
|
|
157
|
-
const
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const metrics = [];
|
|
162
|
-
for (const metric of spec.metrics) {
|
|
163
|
-
const samples = collectSamples(metric, passing);
|
|
164
|
-
if (samples.length === 0) {
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
const { median, p95, p99 } = computeStats(samples, metric.trim ?? 0.05);
|
|
168
|
-
const metricScore = metric.weights.median * scoreStat(median, metric) + metric.weights.p95 * scoreStat(p95, metric) + metric.weights.p99 * scoreStat(p99, metric);
|
|
169
|
-
metricScoresSum += metricScore;
|
|
170
|
-
metrics.push({ name: metric.name, unit: metric.unit, median, p95, p99 });
|
|
171
|
-
}
|
|
172
|
-
const compositeScore = successRate === 0 ? 0 : Math.round(metricScoresSum * successRate * 100) / 100;
|
|
173
|
-
results.push({
|
|
174
|
-
provider: participant,
|
|
175
|
-
dimensions,
|
|
176
|
-
metrics,
|
|
177
|
-
compositeScore,
|
|
178
|
-
successRate,
|
|
179
|
-
skipped
|
|
180
|
-
});
|
|
411
|
+
const groups = spec.groupBy && records.length > 0 ? [{ value: void 0, records }, ...groupRecordsByKey(records, spec.groupBy)] : [{ value: void 0, records }];
|
|
412
|
+
for (const group of groups) {
|
|
413
|
+
results.push(scoreGroup(group.records, spec, baseDimensions, spec.groupBy, group.value, participant, unitByMetric));
|
|
414
|
+
}
|
|
181
415
|
}
|
|
182
416
|
return results;
|
|
183
417
|
}
|
|
418
|
+
function scoringConfigToSpec(config, dimensions, display) {
|
|
419
|
+
const success = config.success;
|
|
420
|
+
const unitByMetric = new Map(display?.metrics?.map((m) => [m.key, m.unit]));
|
|
421
|
+
return {
|
|
422
|
+
...dimensions ? { dimensions: toJsonObject(dimensions) } : {},
|
|
423
|
+
...config.groupBy ? { groupBy: config.groupBy } : {},
|
|
424
|
+
...success ? {
|
|
425
|
+
success: (record) => record.status === "success" && Object.entries(success.requireData).every(([key, value]) => record.data?.[key] === value)
|
|
426
|
+
} : {},
|
|
427
|
+
metrics: config.metrics.map((metric) => ({
|
|
428
|
+
name: metric.key,
|
|
429
|
+
value: metric.key,
|
|
430
|
+
unit: (display?.metrics === void 0 ? metric.unit : unitByMetric.get(metric.key) ?? metric.unit) ?? "",
|
|
431
|
+
ceiling: metric.ceiling,
|
|
432
|
+
floor: metric.floor,
|
|
433
|
+
higherIsBetter: metric.higherIsBetter,
|
|
434
|
+
weights: metric.weights,
|
|
435
|
+
trim: metric.trim
|
|
436
|
+
}))
|
|
437
|
+
};
|
|
438
|
+
}
|
|
184
439
|
|
|
185
440
|
// src/log-buffer.ts
|
|
441
|
+
var LOG_LEVEL_ORDER = ["debug", "info", "warn", "error"];
|
|
442
|
+
function isLogOptions(value) {
|
|
443
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
444
|
+
const o = value;
|
|
445
|
+
const keys = Object.keys(o);
|
|
446
|
+
if (keys.length === 0) return false;
|
|
447
|
+
if (!keys.every((k) => k === "level" || k === "meta")) return false;
|
|
448
|
+
if (o.level !== void 0 && (typeof o.level !== "string" || !LOG_LEVEL_ORDER.includes(o.level))) {
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
return true;
|
|
452
|
+
}
|
|
186
453
|
var LogBuffer = class {
|
|
187
454
|
lines = [];
|
|
188
455
|
step(taskIndex, stepName, outcome) {
|
|
@@ -199,9 +466,15 @@ var LogBuffer = class {
|
|
|
199
466
|
}
|
|
200
467
|
}
|
|
201
468
|
/** Appends a free-form narration line (backs the task context's `log`). */
|
|
202
|
-
line(message,
|
|
203
|
-
const
|
|
204
|
-
|
|
469
|
+
line(message, metaOrOptions) {
|
|
470
|
+
const opts = isLogOptions(metaOrOptions) ? { level: metaOrOptions.level ?? "info", meta: metaOrOptions.meta } : { level: "info", meta: metaOrOptions };
|
|
471
|
+
const suffix = opts.meta && Object.keys(opts.meta).length > 0 ? ` ${JSON.stringify(opts.meta)}` : "";
|
|
472
|
+
const taskMatch = message.match(/^(\[task \d+\])\s*(.*)$/);
|
|
473
|
+
if (taskMatch) {
|
|
474
|
+
this.lines.push(`${(/* @__PURE__ */ new Date()).toISOString()} ${taskMatch[1]} [${opts.level}] ${taskMatch[2]}${suffix}`);
|
|
475
|
+
} else {
|
|
476
|
+
this.lines.push(`${(/* @__PURE__ */ new Date()).toISOString()} [${opts.level}] ${message}${suffix}`);
|
|
477
|
+
}
|
|
205
478
|
}
|
|
206
479
|
isEmpty() {
|
|
207
480
|
return this.lines.length === 0;
|
|
@@ -215,7 +488,6 @@ function indent(text, prefix = "") {
|
|
|
215
488
|
}
|
|
216
489
|
|
|
217
490
|
// src/runner.ts
|
|
218
|
-
var DEFAULT_PLATFORM_URL = "https://platform.computesdk.com";
|
|
219
491
|
function isEnvNoIngest() {
|
|
220
492
|
const v = process.env.BENCHSDK_NO_INGEST;
|
|
221
493
|
return v === "1" || v?.toLowerCase() === "true";
|
|
@@ -223,8 +495,55 @@ function isEnvNoIngest() {
|
|
|
223
495
|
function sleep(ms) {
|
|
224
496
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
225
497
|
}
|
|
498
|
+
function runWithConcurrency(fns, limit) {
|
|
499
|
+
if (limit >= fns.length || fns.length === 0) {
|
|
500
|
+
return Promise.all(fns.map((fn) => fn()));
|
|
501
|
+
}
|
|
502
|
+
const results = new Array(fns.length);
|
|
503
|
+
let running = 0;
|
|
504
|
+
let completed = 0;
|
|
505
|
+
let nextIndex = 0;
|
|
506
|
+
return new Promise((resolve2, reject) => {
|
|
507
|
+
const runNext = () => {
|
|
508
|
+
if (completed === fns.length) {
|
|
509
|
+
resolve2(results);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
while (running < limit && nextIndex < fns.length) {
|
|
513
|
+
const index = nextIndex++;
|
|
514
|
+
running++;
|
|
515
|
+
fns[index]().then(
|
|
516
|
+
(value) => {
|
|
517
|
+
results[index] = value;
|
|
518
|
+
running--;
|
|
519
|
+
completed++;
|
|
520
|
+
runNext();
|
|
521
|
+
},
|
|
522
|
+
(error) => reject(error)
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
runNext();
|
|
527
|
+
});
|
|
528
|
+
}
|
|
226
529
|
function getErrorCode(error) {
|
|
227
|
-
|
|
530
|
+
if (error instanceof Error && "code" in error && typeof error.code === "string" && error.code) {
|
|
531
|
+
return error.code;
|
|
532
|
+
}
|
|
533
|
+
if (error instanceof Error && error.name) return error.name;
|
|
534
|
+
return "ERROR";
|
|
535
|
+
}
|
|
536
|
+
function isTaskError(error) {
|
|
537
|
+
return error instanceof Error && (error instanceof TaskError || error.name === "TaskError");
|
|
538
|
+
}
|
|
539
|
+
var STEP_OUTCOME_KEYS = /* @__PURE__ */ new Set(["stdout", "stderr", "error", "exitCode", "code", "signal", "pid"]);
|
|
540
|
+
function isStepOutcome(value) {
|
|
541
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
542
|
+
const o = value;
|
|
543
|
+
const keys = Object.keys(o);
|
|
544
|
+
if (keys.length === 0) return false;
|
|
545
|
+
if (!keys.every((k) => STEP_OUTCOME_KEYS.has(k))) return false;
|
|
546
|
+
return typeof o.stdout === "string" || typeof o.stderr === "string" || typeof o.error === "string";
|
|
228
547
|
}
|
|
229
548
|
function withTimeout(promise, ms, name) {
|
|
230
549
|
return new Promise((resolve2, reject) => {
|
|
@@ -285,8 +604,10 @@ async function runStepWithClient(clientStep, name, fn, options) {
|
|
|
285
604
|
const result = await clientStep(name, () => runStepInvocations(name, fn, options), clientStepOptions);
|
|
286
605
|
return result;
|
|
287
606
|
}
|
|
288
|
-
function parseCliArgs(argv) {
|
|
607
|
+
function parseCliArgs(argv, allowedCustomFlags) {
|
|
289
608
|
const args = {};
|
|
609
|
+
const unknown = [];
|
|
610
|
+
const allowed = new Set(allowedCustomFlags ?? []);
|
|
290
611
|
const readValue = (raw, i) => {
|
|
291
612
|
const eq = raw.indexOf("=");
|
|
292
613
|
if (eq !== -1) return { value: raw.slice(eq + 1), nextIndex: i };
|
|
@@ -378,22 +699,46 @@ function parseCliArgs(argv) {
|
|
|
378
699
|
case "--dry-run":
|
|
379
700
|
args.noIngest = true;
|
|
380
701
|
break;
|
|
381
|
-
default:
|
|
702
|
+
default: {
|
|
703
|
+
if (allowed.has(name)) {
|
|
704
|
+
if (!arg.includes("=")) {
|
|
705
|
+
const next = argv[i + 1];
|
|
706
|
+
if (next && !next.startsWith("-")) {
|
|
707
|
+
i++;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
} else {
|
|
711
|
+
unknown.push(name);
|
|
712
|
+
if (!arg.includes("=")) {
|
|
713
|
+
const next = argv[i + 1];
|
|
714
|
+
if (next && !next.startsWith("-")) {
|
|
715
|
+
i++;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
382
719
|
break;
|
|
720
|
+
}
|
|
383
721
|
}
|
|
384
722
|
}
|
|
723
|
+
if (unknown.length > 0) {
|
|
724
|
+
throw new Error(`Unknown flag(s): ${unknown.join(", ")}`);
|
|
725
|
+
}
|
|
385
726
|
if (!args.noIngest && isEnvNoIngest()) {
|
|
386
727
|
args.noIngest = true;
|
|
387
728
|
}
|
|
388
729
|
return args;
|
|
389
730
|
}
|
|
390
731
|
function mergeConfig(config, args) {
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
732
|
+
const phases = config.phases;
|
|
733
|
+
const unevenPhases = phases !== void 0 && phases.some((p) => p.iterations !== phases[0].iterations);
|
|
734
|
+
if (unevenPhases && args.iterations !== void 0) {
|
|
735
|
+
console.warn("--iterations is ignored because this benchmark sizes its phases individually.");
|
|
394
736
|
}
|
|
737
|
+
const phaseIterations = phases !== void 0 && !unevenPhases ? args.iterations : void 0;
|
|
738
|
+
const phaseTotal = phases !== void 0 ? phaseIterations !== void 0 ? phaseIterations * phases.length : phases.reduce((sum, p) => sum + p.iterations, 0) : void 0;
|
|
395
739
|
const resolved = {
|
|
396
740
|
iterations: phaseTotal ?? args.iterations ?? config.iterations ?? 1,
|
|
741
|
+
phaseIterations,
|
|
397
742
|
concurrency: args.concurrency ?? config.concurrency ?? 1,
|
|
398
743
|
staggerDelayMs: args.staggerDelayMs ?? config.staggerDelayMs ?? 0,
|
|
399
744
|
groupBy: args.groupBy ?? config.groupBy ?? "participant",
|
|
@@ -407,13 +752,16 @@ function mergeConfig(config, args) {
|
|
|
407
752
|
}
|
|
408
753
|
return resolved;
|
|
409
754
|
}
|
|
410
|
-
function buildSchedule(config,
|
|
755
|
+
function buildSchedule(config, resolved, task) {
|
|
411
756
|
if (config.phases?.length) {
|
|
412
757
|
return config.phases.flatMap(
|
|
413
|
-
(phase) => Array.from({ length: phase.iterations }, () => ({
|
|
758
|
+
(phase) => Array.from({ length: resolved.phaseIterations ?? phase.iterations }, () => ({
|
|
759
|
+
phase: phase.name,
|
|
760
|
+
task
|
|
761
|
+
}))
|
|
414
762
|
);
|
|
415
763
|
}
|
|
416
|
-
return Array.from({ length: iterations }, () => ({ phase: void 0, task }));
|
|
764
|
+
return Array.from({ length: resolved.iterations }, () => ({ phase: void 0, task }));
|
|
417
765
|
}
|
|
418
766
|
function defaultOnResult(record, meta) {
|
|
419
767
|
const n = record.taskIndex + 1;
|
|
@@ -424,19 +772,6 @@ function defaultOnResult(record, meta) {
|
|
|
424
772
|
console.log(` [${meta.participant}] Task ${n}/${meta.iterations}: FAILED \u2014 ${record.errorCode ?? "unknown error"}`);
|
|
425
773
|
}
|
|
426
774
|
}
|
|
427
|
-
function resolvePlatform() {
|
|
428
|
-
const root = (process.env.BENCHMARKS_PLATFORM_URL || DEFAULT_PLATFORM_URL).replace(/\/+$/, "");
|
|
429
|
-
const apiKey = process.env.BENCHMARKS_PLATFORM_API_KEY;
|
|
430
|
-
if (!apiKey) {
|
|
431
|
-
throw new Error(
|
|
432
|
-
"BENCHMARKS_PLATFORM_API_KEY is required. Create an org-scoped API key in your organization settings on the platform and set it in your .env."
|
|
433
|
-
);
|
|
434
|
-
}
|
|
435
|
-
return {
|
|
436
|
-
baseUrl: `${root}/api/v1`,
|
|
437
|
-
apiKey
|
|
438
|
-
};
|
|
439
|
-
}
|
|
440
775
|
function resolveShape(config, shapeName) {
|
|
441
776
|
if (!shapeName) return void 0;
|
|
442
777
|
const shape = config.shapes?.[shapeName];
|
|
@@ -475,23 +810,44 @@ function resolveParticipants(config, resolved) {
|
|
|
475
810
|
if (available.length === 0) throw new NoAvailableParticipantsError(skipped);
|
|
476
811
|
return available;
|
|
477
812
|
}
|
|
813
|
+
function runConfigToJson(config, resolved, participants) {
|
|
814
|
+
const phases = config.phases?.map((phase) => ({
|
|
815
|
+
name: phase.name,
|
|
816
|
+
iterations: resolved.phaseIterations ?? phase.iterations
|
|
817
|
+
}));
|
|
818
|
+
const runConfig = {
|
|
819
|
+
benchmarkSlug: config.benchmarkSlug,
|
|
820
|
+
benchmarkName: config.benchmarkName,
|
|
821
|
+
...resolved.phaseIterations !== void 0 ? { phaseIterations: resolved.phaseIterations } : {},
|
|
822
|
+
...phases ? { phases } : {},
|
|
823
|
+
...!config.phases ? { iterations: resolved.iterations } : {},
|
|
824
|
+
concurrency: resolved.concurrency,
|
|
825
|
+
staggerDelayMs: resolved.staggerDelayMs,
|
|
826
|
+
groupBy: resolved.groupBy,
|
|
827
|
+
...config.dimensions ? { dimensions: config.dimensions } : {},
|
|
828
|
+
...config.scoring ? { scoring: config.scoring } : {},
|
|
829
|
+
participants
|
|
830
|
+
};
|
|
831
|
+
return JSON.parse(JSON.stringify(runConfig));
|
|
832
|
+
}
|
|
478
833
|
async function runBenchmark(fileConfig, task, argv = []) {
|
|
479
|
-
const args = parseCliArgs(argv);
|
|
834
|
+
const args = parseCliArgs(argv, fileConfig.customCliFlags);
|
|
480
835
|
const noIngest = args.noIngest ?? isEnvNoIngest();
|
|
481
836
|
const shaped = applyShape(fileConfig, resolveShape(fileConfig, args.shape));
|
|
482
837
|
const config = applyIdentityOverrides(shaped, args);
|
|
483
838
|
const resolved = mergeConfig(config, args);
|
|
839
|
+
const auth = await resolveAuth();
|
|
840
|
+
const client = createBenchmarkClient({
|
|
841
|
+
baseUrl: auth.apiBaseUrl,
|
|
842
|
+
apiKey: auth.apiKey,
|
|
843
|
+
token: auth.token,
|
|
844
|
+
orgSlug: auth.orgSlug,
|
|
845
|
+
orgId: auth.orgId
|
|
846
|
+
});
|
|
484
847
|
const available = resolveParticipants(config, resolved);
|
|
485
|
-
|
|
486
|
-
let apiKey = "";
|
|
487
|
-
let client = null;
|
|
488
|
-
if (!noIngest) {
|
|
489
|
-
({ baseUrl, apiKey } = resolvePlatform());
|
|
490
|
-
client = createBenchmarkClient({ baseUrl, apiKey });
|
|
491
|
-
}
|
|
492
|
-
const schedule = buildSchedule(config, resolved.iterations, task);
|
|
848
|
+
const schedule = buildSchedule(config, resolved, task);
|
|
493
849
|
const totalTasks = schedule.length;
|
|
494
|
-
const concurrencyLabel =
|
|
850
|
+
const concurrencyLabel = String(resolved.concurrency);
|
|
495
851
|
console.log(`${config.benchmarkName} (self-contained)`);
|
|
496
852
|
console.log(`Date: ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
497
853
|
if (noIngest) {
|
|
@@ -509,16 +865,23 @@ async function runBenchmark(fileConfig, task, argv = []) {
|
|
|
509
865
|
dashboardUrl = "";
|
|
510
866
|
} else {
|
|
511
867
|
if (identityIsOurs) {
|
|
868
|
+
const benchmarkConfig = {
|
|
869
|
+
...config.scoring ? { scoring: config.scoring } : {},
|
|
870
|
+
...config.display ? { display: config.display } : {}
|
|
871
|
+
};
|
|
512
872
|
await client.upsertBenchmark(config.benchmarkSlug, {
|
|
513
|
-
name: config.benchmarkName
|
|
873
|
+
name: config.benchmarkName,
|
|
874
|
+
...Object.keys(benchmarkConfig).length > 0 ? { config: benchmarkConfig } : {}
|
|
514
875
|
});
|
|
515
876
|
}
|
|
877
|
+
const runConfig = client ? runConfigToJson(config, resolved, available.map((p) => p.name)) : {};
|
|
516
878
|
if (args.runKey) {
|
|
517
879
|
const { run: run2, organizationSlug } = await client.createRun(config.benchmarkSlug, {
|
|
518
|
-
runKey: args.runKey
|
|
880
|
+
runKey: args.runKey,
|
|
881
|
+
config: runConfig
|
|
519
882
|
});
|
|
520
883
|
runId = run2.id;
|
|
521
|
-
dashboardUrl = dashboardUrlFor(
|
|
884
|
+
dashboardUrl = dashboardUrlFor(auth.apiBaseUrl, organizationSlug, config.benchmarkSlug, run2.id);
|
|
522
885
|
for (const participant of available) {
|
|
523
886
|
await client.upsertParticipant(config.benchmarkSlug, runId, participant.name, { totalTasks });
|
|
524
887
|
}
|
|
@@ -529,10 +892,11 @@ async function runBenchmark(fileConfig, task, argv = []) {
|
|
|
529
892
|
const { run: run2, organizationSlug } = await client.createRun(config.benchmarkSlug, {
|
|
530
893
|
totalTasks,
|
|
531
894
|
workerCount: 1,
|
|
532
|
-
participants: available.map((p) => p.name)
|
|
895
|
+
participants: available.map((p) => p.name),
|
|
896
|
+
config: runConfig
|
|
533
897
|
});
|
|
534
898
|
runId = run2.id;
|
|
535
|
-
dashboardUrl = dashboardUrlFor(
|
|
899
|
+
dashboardUrl = dashboardUrlFor(auth.apiBaseUrl, organizationSlug, config.benchmarkSlug, run2.id);
|
|
536
900
|
console.log(`Run created: ${run2.name} (${runId})`);
|
|
537
901
|
console.log(`View at: ${dashboardUrl}
|
|
538
902
|
`);
|
|
@@ -541,9 +905,9 @@ async function runBenchmark(fileConfig, task, argv = []) {
|
|
|
541
905
|
const onResult = defaultOnResult;
|
|
542
906
|
let participantRecords;
|
|
543
907
|
if (resolved.groupBy === "round") {
|
|
544
|
-
participantRecords = await runGroupedByRound(config, schedule, available, resolved, client, runId,
|
|
908
|
+
participantRecords = await runGroupedByRound(config, schedule, available, resolved, client, runId, auth.apiBaseUrl, auth.apiKey, auth.token, auth.orgSlug, auth.orgId, onResult, noIngest);
|
|
545
909
|
} else {
|
|
546
|
-
participantRecords = await runGroupedByParticipant(config, schedule, available, resolved, client, runId, onResult);
|
|
910
|
+
participantRecords = await runGroupedByParticipant(config, schedule, available, resolved, client, runId, onResult, noIngest);
|
|
547
911
|
}
|
|
548
912
|
console.log(`All done. ${noIngest ? "No platform run created." : `View at: ${dashboardUrl}`}`);
|
|
549
913
|
const outcome = {
|
|
@@ -552,10 +916,10 @@ async function runBenchmark(fileConfig, task, argv = []) {
|
|
|
552
916
|
participants: participantRecords,
|
|
553
917
|
config: resolved
|
|
554
918
|
};
|
|
555
|
-
if (
|
|
919
|
+
if (!noIngest && (config.onScore || config.scoring)) {
|
|
556
920
|
try {
|
|
557
|
-
const spec = await config.onScore(lowerIsBetter, higherIsBetter);
|
|
558
|
-
const scored = score(outcome, spec);
|
|
921
|
+
const spec = config.onScore ? await config.onScore(lowerIsBetter, higherIsBetter) : scoringConfigToSpec(config.scoring, config.dimensions, config.display);
|
|
922
|
+
const scored = score(outcome, spec, config.display?.metrics);
|
|
559
923
|
const run2 = {
|
|
560
924
|
gitSha: process.env.GITHUB_SHA ?? getGitSha(),
|
|
561
925
|
gitRef: process.env.GITHUB_REF_NAME ?? process.env.GITHUB_REF ?? getGitRef(),
|
|
@@ -564,8 +928,13 @@ async function runBenchmark(fileConfig, task, argv = []) {
|
|
|
564
928
|
platform: os.platform(),
|
|
565
929
|
arch: os.arch()
|
|
566
930
|
};
|
|
567
|
-
await client.submitRunSummary(config.benchmarkSlug, runId, {
|
|
931
|
+
await client.submitRunSummary(config.benchmarkSlug, runId, {
|
|
932
|
+
run: run2,
|
|
933
|
+
results: scored,
|
|
934
|
+
...config.scoring ? { scoring: config.scoring } : {}
|
|
935
|
+
});
|
|
568
936
|
} catch (err) {
|
|
937
|
+
if (err instanceof ScoringSpecError) throw err;
|
|
569
938
|
const message = err instanceof Error ? err.message : String(err);
|
|
570
939
|
console.warn(`[benchsdk-runner] failed to submit run summary: ${message}`);
|
|
571
940
|
}
|
|
@@ -590,13 +959,13 @@ function getGitRef() {
|
|
|
590
959
|
return void 0;
|
|
591
960
|
}
|
|
592
961
|
}
|
|
593
|
-
async function runGroupedByParticipant(config, schedule, available, resolved, client, runId, onResult) {
|
|
962
|
+
async function runGroupedByParticipant(config, schedule, available, resolved, client, runId, onResult, noIngest) {
|
|
594
963
|
const participantRecords = [];
|
|
595
964
|
for (const participant of available) {
|
|
596
965
|
console.log(`${"=".repeat(70)}`);
|
|
597
966
|
console.log(` Participant: ${participant.name}`);
|
|
598
967
|
console.log("=".repeat(70));
|
|
599
|
-
if (!client) {
|
|
968
|
+
if (noIngest || !client) {
|
|
600
969
|
const records = [];
|
|
601
970
|
let rampStartMs2;
|
|
602
971
|
let nextIndex = 0;
|
|
@@ -628,7 +997,7 @@ async function runGroupedByParticipant(config, schedule, available, resolved, cl
|
|
|
628
997
|
}
|
|
629
998
|
let rampStartMs;
|
|
630
999
|
await client.planWorkers(config.benchmarkSlug, runId, participant.name);
|
|
631
|
-
const result = await
|
|
1000
|
+
const result = await runWorker(client, {
|
|
632
1001
|
benchmarkSlug: config.benchmarkSlug,
|
|
633
1002
|
runId,
|
|
634
1003
|
participantSlug: participant.name,
|
|
@@ -641,16 +1010,21 @@ async function runGroupedByParticipant(config, schedule, available, resolved, cl
|
|
|
641
1010
|
if (waitMs > 0) await sleep(waitMs);
|
|
642
1011
|
}
|
|
643
1012
|
const slot = schedule[scheduleIndex];
|
|
644
|
-
const taskResult = await slot.task({
|
|
645
|
-
participant,
|
|
646
|
-
taskIndex: scheduleIndex,
|
|
647
|
-
phase: slot.phase,
|
|
648
|
-
step: (name, fn, options) => runStepWithClient(ctx.step, name, fn, options),
|
|
649
|
-
measure: ctx.measure,
|
|
650
|
-
log: ctx.log
|
|
651
|
-
});
|
|
652
1013
|
if (slot.phase) ctx.measure({ phase: slot.phase });
|
|
653
|
-
|
|
1014
|
+
try {
|
|
1015
|
+
const taskResult = await slot.task({
|
|
1016
|
+
participant,
|
|
1017
|
+
taskIndex: scheduleIndex,
|
|
1018
|
+
phase: slot.phase,
|
|
1019
|
+
step: (name, fn, options) => runStepWithClient(ctx.step, name, fn, options),
|
|
1020
|
+
measure: ctx.measure,
|
|
1021
|
+
log: ctx.log
|
|
1022
|
+
});
|
|
1023
|
+
return taskResult?.data;
|
|
1024
|
+
} catch (error) {
|
|
1025
|
+
if (isTaskError(error) && error.data) ctx.measure(error.data);
|
|
1026
|
+
throw error;
|
|
1027
|
+
}
|
|
654
1028
|
},
|
|
655
1029
|
onResult: (record) => onResult(record, { iterations: schedule.length, participant: participant.name })
|
|
656
1030
|
});
|
|
@@ -666,14 +1040,17 @@ async function runGroupedByParticipant(config, schedule, available, resolved, cl
|
|
|
666
1040
|
}
|
|
667
1041
|
return participantRecords;
|
|
668
1042
|
}
|
|
669
|
-
async function runGroupedByRound(config, schedule, available, resolved, client, runId, baseUrl, apiKey, onResult, noIngest = false) {
|
|
1043
|
+
async function runGroupedByRound(config, schedule, available, resolved, client, runId, baseUrl, apiKey, token, orgSlug, orgId, onResult, noIngest = false) {
|
|
670
1044
|
const reporters = /* @__PURE__ */ new Map();
|
|
671
1045
|
const logBuffers = /* @__PURE__ */ new Map();
|
|
672
1046
|
const failed = /* @__PURE__ */ new Map();
|
|
673
1047
|
const recordsByParticipant = /* @__PURE__ */ new Map();
|
|
1048
|
+
let metricsCollector;
|
|
1049
|
+
const metricsSamples = [];
|
|
674
1050
|
for (const participant of available) {
|
|
675
1051
|
logBuffers.set(participant.name, new LogBuffer());
|
|
676
1052
|
failed.set(participant.name, false);
|
|
1053
|
+
recordsByParticipant.set(participant.name, []);
|
|
677
1054
|
if (noIngest || !client) {
|
|
678
1055
|
reporters.set(participant.name, null);
|
|
679
1056
|
continue;
|
|
@@ -687,6 +1064,9 @@ async function runGroupedByRound(config, schedule, available, resolved, client,
|
|
|
687
1064
|
reporter = await BenchmarkReporter.claim({
|
|
688
1065
|
baseUrl,
|
|
689
1066
|
apiKey,
|
|
1067
|
+
token,
|
|
1068
|
+
orgSlug,
|
|
1069
|
+
orgId,
|
|
690
1070
|
benchmarkSlug: config.benchmarkSlug,
|
|
691
1071
|
runId,
|
|
692
1072
|
participantSlug: participant.name,
|
|
@@ -700,6 +1080,10 @@ async function runGroupedByRound(config, schedule, available, resolved, client,
|
|
|
700
1080
|
console.warn(` ${participant.name}: could not claim a platform worker \u2014 running without platform reporting.`);
|
|
701
1081
|
}
|
|
702
1082
|
reporters.set(participant.name, reporter);
|
|
1083
|
+
if (reporter && !metricsCollector) {
|
|
1084
|
+
metricsCollector = createSystemMetricsCollector();
|
|
1085
|
+
metricsSamples.push(await metricsCollector.sample());
|
|
1086
|
+
}
|
|
703
1087
|
}
|
|
704
1088
|
console.log(`Interleaving ${available.length} participant(s), ${schedule.length} round(s) each.
|
|
705
1089
|
`);
|
|
@@ -708,7 +1092,7 @@ async function runGroupedByRound(config, schedule, available, resolved, client,
|
|
|
708
1092
|
if (resolved.staggerDelayMs > 0 && i > 0) {
|
|
709
1093
|
await sleep(resolved.staggerDelayMs);
|
|
710
1094
|
}
|
|
711
|
-
|
|
1095
|
+
const roundFns = available.map((participant) => async () => {
|
|
712
1096
|
const reporter = reporters.get(participant.name) ?? null;
|
|
713
1097
|
const logBuffer = logBuffers.get(participant.name);
|
|
714
1098
|
const record = await runTaskRecord(
|
|
@@ -722,9 +1106,6 @@ async function runGroupedByRound(config, schedule, available, resolved, client,
|
|
|
722
1106
|
if (record.status !== "success") failed.set(participant.name, true);
|
|
723
1107
|
onResult(record, { iterations: schedule.length, participant: participant.name });
|
|
724
1108
|
reporter?.recordResult(record);
|
|
725
|
-
if (!recordsByParticipant.has(participant.name)) {
|
|
726
|
-
recordsByParticipant.set(participant.name, []);
|
|
727
|
-
}
|
|
728
1109
|
const participantRecords = recordsByParticipant.get(participant.name);
|
|
729
1110
|
participantRecords.push(record);
|
|
730
1111
|
if (reporter) {
|
|
@@ -736,7 +1117,22 @@ async function runGroupedByRound(config, schedule, available, resolved, client,
|
|
|
736
1117
|
});
|
|
737
1118
|
await reporter.heartbeat();
|
|
738
1119
|
}
|
|
739
|
-
}
|
|
1120
|
+
});
|
|
1121
|
+
await runWithConcurrency(roundFns, resolved.concurrency);
|
|
1122
|
+
if (metricsCollector) metricsSamples.push(await metricsCollector.sample());
|
|
1123
|
+
}
|
|
1124
|
+
if (metricsCollector) metricsSamples.push(await metricsCollector.sample());
|
|
1125
|
+
metricsCollector?.stop();
|
|
1126
|
+
const metricsReporter = available.map((p) => reporters.get(p.name)).find((r) => Boolean(r));
|
|
1127
|
+
if (metricsReporter && metricsSamples.length > 0) {
|
|
1128
|
+
await metricsReporter.uploadArtifact({
|
|
1129
|
+
kind: "system-metrics",
|
|
1130
|
+
contentType: "application/x-ndjson",
|
|
1131
|
+
name: "metrics.jsonl",
|
|
1132
|
+
metadata: { scope: "shared-process", participants: available.map((p) => p.name) },
|
|
1133
|
+
body: metricsSamples.map((sample) => JSON.stringify(sample)).join("\n") + "\n"
|
|
1134
|
+
}).catch(() => {
|
|
1135
|
+
});
|
|
740
1136
|
}
|
|
741
1137
|
for (const participant of available) {
|
|
742
1138
|
const reporter = reporters.get(participant.name) ?? null;
|
|
@@ -783,11 +1179,12 @@ async function runTaskRecord(task, participant, scheduleIndex, taskIndex, phase,
|
|
|
783
1179
|
activeStep = stepRecord;
|
|
784
1180
|
try {
|
|
785
1181
|
const result2 = await runStepInvocations(name, fn, options);
|
|
786
|
-
|
|
1182
|
+
const outcome = options?.captureOutput !== false && !Array.isArray(result2) && isStepOutcome(result2) ? result2 : {};
|
|
1183
|
+
logBuffer.step(taskIndex, name, outcome);
|
|
787
1184
|
return result2;
|
|
788
1185
|
} catch (error) {
|
|
789
1186
|
stepRecord.status = "error";
|
|
790
|
-
stepRecord.errorCode = error
|
|
1187
|
+
stepRecord.errorCode = isTaskError(error) ? error.code ?? error.name : getErrorCode(error);
|
|
791
1188
|
logBuffer.step(taskIndex, name, { error: error instanceof Error ? error.message : String(error) });
|
|
792
1189
|
throw error;
|
|
793
1190
|
} finally {
|
|
@@ -804,8 +1201,8 @@ async function runTaskRecord(task, participant, scheduleIndex, taskIndex, phase,
|
|
|
804
1201
|
Object.assign(taskMeasures, data);
|
|
805
1202
|
}
|
|
806
1203
|
},
|
|
807
|
-
log(message,
|
|
808
|
-
logBuffer.line(`[task ${taskIndex}] ${message}`,
|
|
1204
|
+
log(message, metaOrOptions) {
|
|
1205
|
+
logBuffer.line(`[task ${taskIndex}] ${message}`, metaOrOptions);
|
|
809
1206
|
}
|
|
810
1207
|
};
|
|
811
1208
|
let result = void 0;
|
|
@@ -814,7 +1211,7 @@ async function runTaskRecord(task, participant, scheduleIndex, taskIndex, phase,
|
|
|
814
1211
|
record.data = mergeData({ ...taskMeasures, ...result?.data ?? {} }, phase);
|
|
815
1212
|
} catch (error) {
|
|
816
1213
|
record.status = "error";
|
|
817
|
-
if (error
|
|
1214
|
+
if (isTaskError(error)) {
|
|
818
1215
|
record.errorCode = error.code ?? error.name;
|
|
819
1216
|
record.data = mergeData({ ...taskMeasures, ...error.data ?? {} }, phase);
|
|
820
1217
|
if (error.steps?.length) frameworkSteps.push(...error.steps);
|
|
@@ -850,6 +1247,7 @@ async function runTaskRecord(task, participant, scheduleIndex, taskIndex, phase,
|
|
|
850
1247
|
// src/cli.ts
|
|
851
1248
|
import { resolve } from "path";
|
|
852
1249
|
import { pathToFileURL } from "url";
|
|
1250
|
+
import { run as runPlatformCli } from "@benchsdk/cli";
|
|
853
1251
|
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]';
|
|
854
1252
|
function isBenchmarkConfig(value) {
|
|
855
1253
|
if (typeof value !== "object" || value === null) return false;
|
|
@@ -872,6 +1270,9 @@ async function runBenchmarkFile(argv) {
|
|
|
872
1270
|
await runBenchmark(config, task, flags);
|
|
873
1271
|
}
|
|
874
1272
|
async function run(argv) {
|
|
1273
|
+
if (argv[0] !== "run") {
|
|
1274
|
+
return runPlatformCli(argv);
|
|
1275
|
+
}
|
|
875
1276
|
try {
|
|
876
1277
|
await runBenchmarkFile(argv);
|
|
877
1278
|
process.exit(0);
|
|
@@ -884,8 +1285,13 @@ async function run(argv) {
|
|
|
884
1285
|
process.exit(1);
|
|
885
1286
|
}
|
|
886
1287
|
}
|
|
1288
|
+
|
|
1289
|
+
// src/index.ts
|
|
1290
|
+
var BENCHSDK_RUNNER_VERSION = "0.3.0";
|
|
887
1291
|
export {
|
|
1292
|
+
BENCHSDK_RUNNER_VERSION,
|
|
888
1293
|
NoAvailableParticipantsError,
|
|
1294
|
+
ScoringSpecError,
|
|
889
1295
|
TaskError,
|
|
890
1296
|
defineBenchmarkConfig,
|
|
891
1297
|
defineTask,
|
|
@@ -896,6 +1302,8 @@ export {
|
|
|
896
1302
|
run,
|
|
897
1303
|
runBenchmark,
|
|
898
1304
|
runBenchmarkFile,
|
|
899
|
-
score
|
|
1305
|
+
score,
|
|
1306
|
+
scoringConfigToSpec,
|
|
1307
|
+
validateScoringSpec
|
|
900
1308
|
};
|
|
901
1309
|
//# sourceMappingURL=index.js.map
|