@benchsdk/runner 0.2.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/dist/index.cjs ADDED
@@ -0,0 +1,944 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var src_exports = {};
32
+ __export(src_exports, {
33
+ NoAvailableParticipantsError: () => NoAvailableParticipantsError,
34
+ TaskError: () => TaskError,
35
+ defineBenchmarkConfig: () => defineBenchmarkConfig,
36
+ defineTask: () => defineTask,
37
+ higherIsBetter: () => higherIsBetter,
38
+ lowerIsBetter: () => lowerIsBetter,
39
+ mergeConfig: () => mergeConfig,
40
+ parseCliArgs: () => parseCliArgs,
41
+ run: () => run,
42
+ runBenchmark: () => runBenchmark,
43
+ runBenchmarkFile: () => runBenchmarkFile,
44
+ score: () => score
45
+ });
46
+ module.exports = __toCommonJS(src_exports);
47
+
48
+ // src/bench-config.ts
49
+ var TaskError = class extends Error {
50
+ code;
51
+ data;
52
+ steps;
53
+ constructor(message, opts) {
54
+ super(message);
55
+ this.name = "TaskError";
56
+ this.code = opts?.code;
57
+ this.data = opts?.data;
58
+ this.steps = opts?.steps;
59
+ }
60
+ };
61
+ function assertPositiveInt(value, field) {
62
+ if (value === void 0) return;
63
+ if (!Number.isInteger(value) || value < 1) {
64
+ throw new Error(`${field} must be an integer >= 1 (got ${value})`);
65
+ }
66
+ }
67
+ function defineBenchmarkConfig(config) {
68
+ if (!config.benchmarkSlug || typeof config.benchmarkSlug !== "string") {
69
+ throw new Error("benchmarkSlug is required");
70
+ }
71
+ if (!config.benchmarkName || typeof config.benchmarkName !== "string") {
72
+ throw new Error("benchmarkName is required");
73
+ }
74
+ if (config.phases !== void 0) {
75
+ if (config.iterations !== void 0) {
76
+ throw new Error("phases and iterations are mutually exclusive");
77
+ }
78
+ if (!Array.isArray(config.phases) || config.phases.length === 0) {
79
+ throw new Error("phases must be a non-empty array");
80
+ }
81
+ const seen = /* @__PURE__ */ new Set();
82
+ for (const phase of config.phases) {
83
+ if (!phase.name || typeof phase.name !== "string") {
84
+ throw new Error("each phase requires a non-empty name");
85
+ }
86
+ if (seen.has(phase.name)) {
87
+ throw new Error(`duplicate phase name: ${phase.name}`);
88
+ }
89
+ seen.add(phase.name);
90
+ assertPositiveInt(phase.iterations, `phase '${phase.name}' iterations`);
91
+ }
92
+ }
93
+ assertPositiveInt(config.iterations, "iterations");
94
+ assertPositiveInt(config.concurrency, "concurrency");
95
+ if (config.staggerDelayMs !== void 0 && (!Number.isFinite(config.staggerDelayMs) || config.staggerDelayMs < 0)) {
96
+ throw new Error(`staggerDelayMs must be a number >= 0 (got ${config.staggerDelayMs})`);
97
+ }
98
+ if (config.groupBy !== void 0 && config.groupBy !== "participant" && config.groupBy !== "round") {
99
+ throw new Error(`groupBy must be 'participant' or 'round' (got ${config.groupBy})`);
100
+ }
101
+ if (config.shapes !== void 0) {
102
+ for (const [shapeName, shape] of Object.entries(config.shapes)) {
103
+ if (!shape.slug || !/^[a-z0-9][a-z0-9-]*$/.test(shape.slug)) {
104
+ throw new Error(`shape '${shapeName}' needs a lowercase slug (got ${JSON.stringify(shape.slug)})`);
105
+ }
106
+ if (shape.name !== void 0 && (typeof shape.name !== "string" || shape.name.trim() === "")) {
107
+ throw new Error(`shape '${shapeName}' name must be a non-empty string`);
108
+ }
109
+ if (shape.staggerDelayMs !== void 0 && (!Number.isFinite(shape.staggerDelayMs) || shape.staggerDelayMs < 0)) {
110
+ throw new Error(`shape '${shapeName}' staggerDelayMs must be a number >= 0 (got ${shape.staggerDelayMs})`);
111
+ }
112
+ }
113
+ }
114
+ return config;
115
+ }
116
+ function defineTask(task) {
117
+ if (typeof task !== "function") {
118
+ throw new Error("defineTask requires a task function.");
119
+ }
120
+ return task;
121
+ }
122
+
123
+ // src/no-available-participants.ts
124
+ var NoAvailableParticipantsError = class extends Error {
125
+ skipped;
126
+ constructor(skipped) {
127
+ super(
128
+ `No participants have their required env vars set \u2014 nothing to run${skipped.length > 0 ? ` (skipped: ${skipped.map((s) => s.name).join(", ")})` : ""}.`
129
+ );
130
+ this.name = "NoAvailableParticipantsError";
131
+ this.skipped = skipped;
132
+ }
133
+ };
134
+
135
+ // src/runner.ts
136
+ var import_node_child_process = require("child_process");
137
+ var import_node_os = __toESM(require("os"), 1);
138
+ var import_client = require("@benchsdk/client");
139
+
140
+ // src/scoring.ts
141
+ function isFiniteNumber(v) {
142
+ return typeof v === "number" && Number.isFinite(v);
143
+ }
144
+ function percentile(sorted, p) {
145
+ if (sorted.length === 0) return 0;
146
+ const idx = Math.max(0, Math.ceil(p / 100 * sorted.length) - 1);
147
+ return sorted[Math.min(idx, sorted.length - 1)];
148
+ }
149
+ function computeStats(values, trimPercent = 0.05) {
150
+ if (values.length === 0) return { median: 0, p95: 0, p99: 0 };
151
+ const sorted = [...values].sort((a, b) => a - b);
152
+ const trimCount = Math.floor(sorted.length * trimPercent);
153
+ const trimmed = trimCount > 0 && sorted.length - 2 * trimCount > 0 ? sorted.slice(trimCount, sorted.length - trimCount) : sorted;
154
+ const mid = Math.floor(trimmed.length / 2);
155
+ const median = trimmed.length % 2 === 0 ? (trimmed[mid - 1] + trimmed[mid]) / 2 : trimmed[mid];
156
+ return { median, p95: percentile(trimmed, 95), p99: percentile(trimmed, 99) };
157
+ }
158
+ function toJsonObject(value) {
159
+ return JSON.parse(JSON.stringify(value ?? {}));
160
+ }
161
+ function collectSamples(metric, records) {
162
+ const samples = [];
163
+ for (const record of records) {
164
+ const raw = typeof metric.value === "function" ? metric.value(record) : record.data?.[metric.value ?? metric.name];
165
+ if (Array.isArray(raw)) {
166
+ for (const item of raw) {
167
+ if (isFiniteNumber(item)) samples.push(item);
168
+ }
169
+ } else if (isFiniteNumber(raw)) {
170
+ samples.push(raw);
171
+ }
172
+ }
173
+ return samples;
174
+ }
175
+ function scoreStat(stat, metric) {
176
+ if (metric.higherIsBetter) {
177
+ const floor = metric.floor ?? 0;
178
+ if (stat <= floor) return 0;
179
+ if (stat >= metric.ceiling) return 100;
180
+ return (stat - floor) / (metric.ceiling - floor) * 100;
181
+ }
182
+ return Math.max(0, 100 * (1 - stat / metric.ceiling));
183
+ }
184
+ var lowerIsBetter = (name, opts) => ({
185
+ name,
186
+ ...opts,
187
+ higherIsBetter: false
188
+ });
189
+ var higherIsBetter = (name, opts) => ({
190
+ name,
191
+ ...opts,
192
+ higherIsBetter: true
193
+ });
194
+ function score(outcome, spec) {
195
+ const successFilter = spec.success ?? ((r) => r.status === "success");
196
+ const dimensions = toJsonObject(spec.dimensions ?? {});
197
+ const results = [];
198
+ for (const { participant, records } of outcome.participants) {
199
+ const passing = records.filter(successFilter);
200
+ const successRate = records.length === 0 ? 0 : passing.length / records.length;
201
+ const skipped = records.length === 0;
202
+ let metricScoresSum = 0;
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
+ });
223
+ }
224
+ return results;
225
+ }
226
+
227
+ // src/log-buffer.ts
228
+ var LogBuffer = class {
229
+ lines = [];
230
+ step(taskIndex, stepName, outcome) {
231
+ const header = `[task ${taskIndex}] ${stepName}`;
232
+ this.lines.push(`${(/* @__PURE__ */ new Date()).toISOString()} ${header}`);
233
+ if (outcome.stdout?.trim()) {
234
+ this.lines.push(indent(outcome.stdout));
235
+ }
236
+ if (outcome.stderr?.trim()) {
237
+ this.lines.push(indent(outcome.stderr, "stderr: "));
238
+ }
239
+ if (outcome.error) {
240
+ this.lines.push(indent(outcome.error, "error: "));
241
+ }
242
+ }
243
+ /** Appends a free-form narration line (backs the task context's `log`). */
244
+ line(message, meta) {
245
+ const suffix = meta && Object.keys(meta).length > 0 ? ` ${JSON.stringify(meta)}` : "";
246
+ this.lines.push(`${(/* @__PURE__ */ new Date()).toISOString()} ${message}${suffix}`);
247
+ }
248
+ isEmpty() {
249
+ return this.lines.length === 0;
250
+ }
251
+ toText() {
252
+ return this.lines.join("\n") + "\n";
253
+ }
254
+ };
255
+ function indent(text, prefix = "") {
256
+ return text.trimEnd().split("\n").map((line) => ` ${prefix}${line}`).join("\n");
257
+ }
258
+
259
+ // src/runner.ts
260
+ var DEFAULT_PLATFORM_URL = "https://platform.computesdk.com";
261
+ function isEnvNoIngest() {
262
+ const v = process.env.BENCHSDK_NO_INGEST;
263
+ return v === "1" || v?.toLowerCase() === "true";
264
+ }
265
+ function sleep(ms) {
266
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
267
+ }
268
+ function getErrorCode(error) {
269
+ return error instanceof Error && error.name ? error.name : "ERROR";
270
+ }
271
+ function withTimeout(promise, ms, name) {
272
+ return new Promise((resolve2, reject) => {
273
+ const timer = setTimeout(
274
+ () => reject(new TaskError(`Step "${name}" timed out after ${ms}ms`, { code: "step_timeout" })),
275
+ ms
276
+ );
277
+ promise.then(
278
+ (value) => {
279
+ clearTimeout(timer);
280
+ resolve2(value);
281
+ },
282
+ (error) => {
283
+ clearTimeout(timer);
284
+ reject(error);
285
+ }
286
+ );
287
+ });
288
+ }
289
+ async function runStepInvocations(name, fn, options) {
290
+ const requestedConcurrency = options?.concurrency;
291
+ if (requestedConcurrency !== void 0 && (!Number.isInteger(requestedConcurrency) || requestedConcurrency < 1)) {
292
+ throw new Error(`step "${name}" concurrency must be an integer >= 1 (got ${requestedConcurrency})`);
293
+ }
294
+ const timeoutMs = options?.timeoutMs;
295
+ if (timeoutMs !== void 0 && (!Number.isFinite(timeoutMs) || timeoutMs < 0)) {
296
+ throw new Error(`step "${name}" timeoutMs must be a number >= 0 (got ${timeoutMs})`);
297
+ }
298
+ const count = requestedConcurrency ?? 1;
299
+ const invocations = Array.from({ length: count }, () => {
300
+ const promise = Promise.resolve().then(() => fn());
301
+ if (timeoutMs === void 0) return promise;
302
+ return withTimeout(promise, timeoutMs, name);
303
+ });
304
+ if (count === 1) {
305
+ return invocations[0];
306
+ }
307
+ const outcomes = await Promise.allSettled(invocations);
308
+ const results = [];
309
+ let firstError;
310
+ for (const outcome of outcomes) {
311
+ if (outcome.status === "fulfilled") {
312
+ results.push(outcome.value);
313
+ } else if (firstError === void 0) {
314
+ firstError = outcome.reason;
315
+ }
316
+ }
317
+ if (firstError !== void 0) throw firstError;
318
+ return results;
319
+ }
320
+ async function runStepWithClient(clientStep, name, fn, options) {
321
+ const { concurrency: runnerConcurrency, timeoutMs, ...clientOptions } = options ?? {};
322
+ const clientStepOptions = {
323
+ ...clientOptions,
324
+ timeoutMs,
325
+ stepConcurrency: runnerConcurrency
326
+ };
327
+ const result = await clientStep(name, () => runStepInvocations(name, fn, options), clientStepOptions);
328
+ return result;
329
+ }
330
+ function parseCliArgs(argv) {
331
+ const args = {};
332
+ const readValue = (raw, i) => {
333
+ const eq = raw.indexOf("=");
334
+ if (eq !== -1) return { value: raw.slice(eq + 1), nextIndex: i };
335
+ return { value: argv[i + 1] ?? "", nextIndex: i + 1 };
336
+ };
337
+ const intFlag = (raw, flag) => {
338
+ if (raw.trim() === "") throw new Error(`${flag} expects a value`);
339
+ const n = Number(raw);
340
+ if (!Number.isInteger(n) || n < 1) throw new Error(`${flag} expects an integer >= 1 (got "${raw}")`);
341
+ return n;
342
+ };
343
+ const nonNegFlag = (raw, flag) => {
344
+ if (raw.trim() === "") throw new Error(`${flag} expects a value`);
345
+ const n = Number(raw);
346
+ if (!Number.isFinite(n) || n < 0) throw new Error(`${flag} expects a number >= 0 (got "${raw}")`);
347
+ return n;
348
+ };
349
+ for (let i = 0; i < argv.length; i++) {
350
+ const arg = argv[i];
351
+ const name = arg.includes("=") ? arg.slice(0, arg.indexOf("=")) : arg;
352
+ switch (name) {
353
+ // `--slug` is the pre-`--benchmark` spelling, kept working for existing scripts.
354
+ case "--slug":
355
+ case "--benchmark": {
356
+ const { value, nextIndex } = readValue(arg, i);
357
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(value)) {
358
+ throw new Error(`${name} expects a lowercase benchmark slug (got "${value}")`);
359
+ }
360
+ args.benchmark = value;
361
+ i = nextIndex;
362
+ break;
363
+ }
364
+ case "--name": {
365
+ const { value, nextIndex } = readValue(arg, i);
366
+ if (value.trim() === "") throw new Error("--name expects a value");
367
+ args.name = value;
368
+ i = nextIndex;
369
+ break;
370
+ }
371
+ case "--shape": {
372
+ const { value, nextIndex } = readValue(arg, i);
373
+ if (value.trim() === "") throw new Error("--shape expects a value");
374
+ args.shape = value;
375
+ i = nextIndex;
376
+ break;
377
+ }
378
+ case "--run-key": {
379
+ const { value, nextIndex } = readValue(arg, i);
380
+ if (value.trim() === "") throw new Error("--run-key expects a value");
381
+ args.runKey = value;
382
+ i = nextIndex;
383
+ break;
384
+ }
385
+ case "--iterations": {
386
+ const { value, nextIndex } = readValue(arg, i);
387
+ args.iterations = intFlag(value, "--iterations");
388
+ i = nextIndex;
389
+ break;
390
+ }
391
+ case "--concurrency": {
392
+ const { value, nextIndex } = readValue(arg, i);
393
+ args.concurrency = intFlag(value, "--concurrency");
394
+ i = nextIndex;
395
+ break;
396
+ }
397
+ case "--stagger-delay-ms": {
398
+ const { value, nextIndex } = readValue(arg, i);
399
+ args.staggerDelayMs = nonNegFlag(value, "--stagger-delay-ms");
400
+ i = nextIndex;
401
+ break;
402
+ }
403
+ case "--group-by": {
404
+ const { value, nextIndex } = readValue(arg, i);
405
+ if (value !== "participant" && value !== "round") {
406
+ throw new Error(`--group-by expects 'participant' or 'round' (got "${value}")`);
407
+ }
408
+ args.groupBy = value;
409
+ i = nextIndex;
410
+ break;
411
+ }
412
+ case "--provider": {
413
+ const { value, nextIndex } = readValue(arg, i);
414
+ const names = value.split(",").map((s) => s.trim()).filter(Boolean);
415
+ args.providers = [...args.providers ?? [], ...names];
416
+ i = nextIndex;
417
+ break;
418
+ }
419
+ case "--no-ingest":
420
+ case "--dry-run":
421
+ args.noIngest = true;
422
+ break;
423
+ default:
424
+ break;
425
+ }
426
+ }
427
+ if (!args.noIngest && isEnvNoIngest()) {
428
+ args.noIngest = true;
429
+ }
430
+ return args;
431
+ }
432
+ function mergeConfig(config, args) {
433
+ const phaseTotal = config.phases?.reduce((sum, p) => sum + p.iterations, 0);
434
+ if (phaseTotal !== void 0 && args.iterations !== void 0) {
435
+ console.warn("--iterations is ignored because this benchmark declares phases.");
436
+ }
437
+ const resolved = {
438
+ iterations: phaseTotal ?? args.iterations ?? config.iterations ?? 1,
439
+ concurrency: args.concurrency ?? config.concurrency ?? 1,
440
+ staggerDelayMs: args.staggerDelayMs ?? config.staggerDelayMs ?? 0,
441
+ groupBy: args.groupBy ?? config.groupBy ?? "participant",
442
+ providers: args.providers ?? config.defaultProviders
443
+ };
444
+ if (!Number.isInteger(resolved.iterations) || resolved.iterations < 1) {
445
+ throw new Error(`iterations must be an integer >= 1 (got ${resolved.iterations})`);
446
+ }
447
+ if (!Number.isInteger(resolved.concurrency) || resolved.concurrency < 1) {
448
+ throw new Error(`concurrency must be an integer >= 1 (got ${resolved.concurrency})`);
449
+ }
450
+ return resolved;
451
+ }
452
+ function buildSchedule(config, iterations, task) {
453
+ if (config.phases?.length) {
454
+ return config.phases.flatMap(
455
+ (phase) => Array.from({ length: phase.iterations }, () => ({ phase: phase.name, task }))
456
+ );
457
+ }
458
+ return Array.from({ length: iterations }, () => ({ phase: void 0, task }));
459
+ }
460
+ function defaultOnResult(record, meta) {
461
+ const n = record.taskIndex + 1;
462
+ if (record.status === "success") {
463
+ const data = record.data && Object.keys(record.data).length > 0 ? ` ${JSON.stringify(record.data)}` : "";
464
+ console.log(` [${meta.participant}] Task ${n}/${meta.iterations}: success${data}`);
465
+ } else {
466
+ console.log(` [${meta.participant}] Task ${n}/${meta.iterations}: FAILED \u2014 ${record.errorCode ?? "unknown error"}`);
467
+ }
468
+ }
469
+ function resolvePlatform() {
470
+ const root = (process.env.BENCHMARKS_PLATFORM_URL || DEFAULT_PLATFORM_URL).replace(/\/+$/, "");
471
+ const apiKey = process.env.BENCHMARKS_PLATFORM_API_KEY;
472
+ if (!apiKey) {
473
+ throw new Error(
474
+ "BENCHMARKS_PLATFORM_API_KEY is required. Create an org-scoped API key in your organization settings on the platform and set it in your .env."
475
+ );
476
+ }
477
+ return {
478
+ baseUrl: `${root}/api/v1`,
479
+ apiKey
480
+ };
481
+ }
482
+ function resolveShape(config, shapeName) {
483
+ if (!shapeName) return void 0;
484
+ const shape = config.shapes?.[shapeName];
485
+ if (!shape) {
486
+ const known = Object.keys(config.shapes ?? {});
487
+ throw new Error(
488
+ known.length > 0 ? `Unknown --shape "${shapeName}". Known shapes: ${known.join(", ")}.` : `Unknown --shape "${shapeName}": this benchmark declares no shapes.`
489
+ );
490
+ }
491
+ return shape;
492
+ }
493
+ function applyShape(config, shape) {
494
+ if (!shape) return config;
495
+ return {
496
+ ...config,
497
+ benchmarkSlug: shape.slug,
498
+ benchmarkName: shape.name ?? shape.slug,
499
+ ...shape.staggerDelayMs !== void 0 ? { staggerDelayMs: shape.staggerDelayMs } : {}
500
+ };
501
+ }
502
+ function applyIdentityOverrides(fileConfig, args) {
503
+ return {
504
+ ...fileConfig,
505
+ ...args.benchmark ? { benchmarkSlug: args.benchmark } : {},
506
+ ...args.name ? { benchmarkName: args.name } : {}
507
+ };
508
+ }
509
+ function dashboardUrlFor(baseUrl, organizationSlug, benchmarkSlug, runId) {
510
+ return `${baseUrl.replace(/\/api\/v1\/?$/, "")}/${organizationSlug}/benchmarks/${benchmarkSlug}/runs/${runId}`;
511
+ }
512
+ function resolveParticipants(config, resolved) {
513
+ const { available, skipped } = (0, import_client.filterParticipantsByEnv)((0, import_client.selectParticipants)(config.participants, resolved.providers));
514
+ for (const s of skipped) {
515
+ console.log(`Skipping ${s.name}: missing ${s.missing.join(", ")}`);
516
+ }
517
+ if (available.length === 0) throw new NoAvailableParticipantsError(skipped);
518
+ return available;
519
+ }
520
+ async function runBenchmark(fileConfig, task, argv = []) {
521
+ const args = parseCliArgs(argv);
522
+ const noIngest = args.noIngest ?? isEnvNoIngest();
523
+ const shaped = applyShape(fileConfig, resolveShape(fileConfig, args.shape));
524
+ const config = applyIdentityOverrides(shaped, args);
525
+ const resolved = mergeConfig(config, args);
526
+ const available = resolveParticipants(config, resolved);
527
+ let baseUrl = "";
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);
535
+ const totalTasks = schedule.length;
536
+ const concurrencyLabel = resolved.groupBy === "round" ? "n/a (round mode)" : String(resolved.concurrency);
537
+ console.log(`${config.benchmarkName} (self-contained)`);
538
+ console.log(`Date: ${(/* @__PURE__ */ new Date()).toISOString()}`);
539
+ if (noIngest) {
540
+ console.log("Dry run: no platform ingest or reporting.\n");
541
+ }
542
+ console.log(
543
+ `Knobs: iterations=${totalTasks}, concurrency=${concurrencyLabel}, staggerDelayMs=${resolved.staggerDelayMs}, groupBy=${resolved.groupBy}
544
+ `
545
+ );
546
+ const identityIsOurs = args.shape !== void 0 || args.name !== void 0 || !args.benchmark || args.benchmark === fileConfig.benchmarkSlug;
547
+ let runId;
548
+ let dashboardUrl;
549
+ if (noIngest) {
550
+ runId = "no-ingest";
551
+ dashboardUrl = "";
552
+ } else {
553
+ if (identityIsOurs) {
554
+ await client.upsertBenchmark(config.benchmarkSlug, {
555
+ name: config.benchmarkName
556
+ });
557
+ }
558
+ if (args.runKey) {
559
+ const { run: run2, organizationSlug } = await client.createRun(config.benchmarkSlug, {
560
+ runKey: args.runKey
561
+ });
562
+ runId = run2.id;
563
+ dashboardUrl = dashboardUrlFor(baseUrl, organizationSlug, config.benchmarkSlug, run2.id);
564
+ for (const participant of available) {
565
+ await client.upsertParticipant(config.benchmarkSlug, runId, participant.name, { totalTasks });
566
+ }
567
+ console.log(`Shared run (key "${args.runKey}"): ${run2.name} (${runId})`);
568
+ console.log(`View at: ${dashboardUrl}
569
+ `);
570
+ } else {
571
+ const { run: run2, organizationSlug } = await client.createRun(config.benchmarkSlug, {
572
+ totalTasks,
573
+ workerCount: 1,
574
+ participants: available.map((p) => p.name)
575
+ });
576
+ runId = run2.id;
577
+ dashboardUrl = dashboardUrlFor(baseUrl, organizationSlug, config.benchmarkSlug, run2.id);
578
+ console.log(`Run created: ${run2.name} (${runId})`);
579
+ console.log(`View at: ${dashboardUrl}
580
+ `);
581
+ }
582
+ }
583
+ const onResult = defaultOnResult;
584
+ let participantRecords;
585
+ if (resolved.groupBy === "round") {
586
+ participantRecords = await runGroupedByRound(config, schedule, available, resolved, client, runId, baseUrl, apiKey, onResult, noIngest);
587
+ } else {
588
+ participantRecords = await runGroupedByParticipant(config, schedule, available, resolved, client, runId, onResult);
589
+ }
590
+ console.log(`All done. ${noIngest ? "No platform run created." : `View at: ${dashboardUrl}`}`);
591
+ const outcome = {
592
+ runId,
593
+ dashboardUrl,
594
+ participants: participantRecords,
595
+ config: resolved
596
+ };
597
+ if (client && config.onScore) {
598
+ try {
599
+ const spec = await config.onScore(lowerIsBetter, higherIsBetter);
600
+ const scored = score(outcome, spec);
601
+ const run2 = {
602
+ gitSha: process.env.GITHUB_SHA ?? getGitSha(),
603
+ gitRef: process.env.GITHUB_REF_NAME ?? process.env.GITHUB_REF ?? getGitRef(),
604
+ triggeredBy: process.env.GITHUB_EVENT_NAME ?? "manual",
605
+ nodeVersion: process.version,
606
+ platform: import_node_os.default.platform(),
607
+ arch: import_node_os.default.arch()
608
+ };
609
+ await client.submitRunSummary(config.benchmarkSlug, runId, { run: run2, results: scored });
610
+ } catch (err) {
611
+ const message = err instanceof Error ? err.message : String(err);
612
+ console.warn(`[benchsdk-runner] failed to submit run summary: ${message}`);
613
+ }
614
+ }
615
+ if (config.onComplete) await config.onComplete(outcome);
616
+ return outcome;
617
+ }
618
+ function getGitSha() {
619
+ if (process.env.GITHUB_SHA) return process.env.GITHUB_SHA;
620
+ try {
621
+ return (0, import_node_child_process.execSync)("git rev-parse HEAD", { encoding: "utf8", stdio: "pipe" }).trim();
622
+ } catch {
623
+ return void 0;
624
+ }
625
+ }
626
+ function getGitRef() {
627
+ if (process.env.GITHUB_REF_NAME) return process.env.GITHUB_REF_NAME;
628
+ if (process.env.GITHUB_REF) return process.env.GITHUB_REF;
629
+ try {
630
+ return (0, import_node_child_process.execSync)("git rev-parse --abbrev-ref HEAD", { encoding: "utf8", stdio: "pipe" }).trim();
631
+ } catch {
632
+ return void 0;
633
+ }
634
+ }
635
+ async function runGroupedByParticipant(config, schedule, available, resolved, client, runId, onResult) {
636
+ const participantRecords = [];
637
+ for (const participant of available) {
638
+ console.log(`${"=".repeat(70)}`);
639
+ console.log(` Participant: ${participant.name}`);
640
+ console.log("=".repeat(70));
641
+ if (!client) {
642
+ const records = [];
643
+ let rampStartMs2;
644
+ let nextIndex = 0;
645
+ const logBuffer = new LogBuffer();
646
+ const runSlot = async (scheduleIndex) => {
647
+ if (resolved.staggerDelayMs > 0) {
648
+ rampStartMs2 ??= Date.now();
649
+ const waitMs = rampStartMs2 + scheduleIndex * resolved.staggerDelayMs - Date.now();
650
+ if (waitMs > 0) await sleep(waitMs);
651
+ }
652
+ const slot = schedule[scheduleIndex];
653
+ const record = await runTaskRecord(slot.task, participant, scheduleIndex, scheduleIndex, slot.phase, logBuffer);
654
+ onResult(record, { iterations: schedule.length, participant: participant.name });
655
+ records.push(record);
656
+ };
657
+ const worker = async () => {
658
+ while (nextIndex < schedule.length) {
659
+ const index = nextIndex++;
660
+ await runSlot(index);
661
+ }
662
+ };
663
+ await Promise.all(Array.from({ length: resolved.concurrency }, () => worker()));
664
+ records.sort((a, b) => a.taskIndex - b.taskIndex);
665
+ const ok2 = records.filter((r) => r.status === "success").length;
666
+ console.log(` Done: ${ok2}/${records.length} succeeded.
667
+ `);
668
+ participantRecords.push({ participant: participant.name, records });
669
+ continue;
670
+ }
671
+ let rampStartMs;
672
+ await client.planWorkers(config.benchmarkSlug, runId, participant.name);
673
+ const result = await client.runWorker({
674
+ benchmarkSlug: config.benchmarkSlug,
675
+ runId,
676
+ participantSlug: participant.name,
677
+ concurrency: resolved.concurrency,
678
+ task: async (ctx) => {
679
+ const scheduleIndex = ctx.taskIndex - ctx.assignment.taskRange.start;
680
+ if (resolved.staggerDelayMs > 0) {
681
+ rampStartMs ??= Date.now();
682
+ const waitMs = rampStartMs + scheduleIndex * resolved.staggerDelayMs - Date.now();
683
+ if (waitMs > 0) await sleep(waitMs);
684
+ }
685
+ 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
+ if (slot.phase) ctx.measure({ phase: slot.phase });
695
+ return taskResult?.data;
696
+ },
697
+ onResult: (record) => onResult(record, { iterations: schedule.length, participant: participant.name })
698
+ });
699
+ if (!result.assignment) {
700
+ console.error(` No pending worker to claim for run ${runId} \u2014 it may already be fully claimed.`);
701
+ participantRecords.push({ participant: participant.name, records: result.records ?? [] });
702
+ continue;
703
+ }
704
+ const ok = result.records.filter((r) => r.status === "success").length;
705
+ console.log(` Done: ${ok}/${result.records.length} succeeded.
706
+ `);
707
+ participantRecords.push({ participant: participant.name, records: result.records });
708
+ }
709
+ return participantRecords;
710
+ }
711
+ async function runGroupedByRound(config, schedule, available, resolved, client, runId, baseUrl, apiKey, onResult, noIngest = false) {
712
+ const reporters = /* @__PURE__ */ new Map();
713
+ const logBuffers = /* @__PURE__ */ new Map();
714
+ const failed = /* @__PURE__ */ new Map();
715
+ const recordsByParticipant = /* @__PURE__ */ new Map();
716
+ for (const participant of available) {
717
+ logBuffers.set(participant.name, new LogBuffer());
718
+ failed.set(participant.name, false);
719
+ if (noIngest || !client) {
720
+ reporters.set(participant.name, null);
721
+ continue;
722
+ }
723
+ await client.planWorkers(config.benchmarkSlug, runId, participant.name, {
724
+ workerCount: 1,
725
+ targetConcurrency: schedule.length
726
+ });
727
+ let reporter = null;
728
+ try {
729
+ reporter = await import_client.BenchmarkReporter.claim({
730
+ baseUrl,
731
+ apiKey,
732
+ benchmarkSlug: config.benchmarkSlug,
733
+ runId,
734
+ participantSlug: participant.name,
735
+ processKind: "process",
736
+ processKey: process.env.HOSTNAME ?? "local"
737
+ });
738
+ } catch (error) {
739
+ console.warn(` ${participant.name}: reporter claim failed (${error instanceof Error ? error.message : String(error)}) \u2014 running without platform reporting.`);
740
+ }
741
+ if (!reporter) {
742
+ console.warn(` ${participant.name}: could not claim a platform worker \u2014 running without platform reporting.`);
743
+ }
744
+ reporters.set(participant.name, reporter);
745
+ }
746
+ console.log(`Interleaving ${available.length} participant(s), ${schedule.length} round(s) each.
747
+ `);
748
+ for (let i = 0; i < schedule.length; i++) {
749
+ const slot = schedule[i];
750
+ if (resolved.staggerDelayMs > 0 && i > 0) {
751
+ await sleep(resolved.staggerDelayMs);
752
+ }
753
+ for (const participant of available) {
754
+ const reporter = reporters.get(participant.name) ?? null;
755
+ const logBuffer = logBuffers.get(participant.name);
756
+ const record = await runTaskRecord(
757
+ slot.task,
758
+ participant,
759
+ i,
760
+ (reporter?.taskIndexStart ?? 0) + i,
761
+ slot.phase,
762
+ logBuffer
763
+ );
764
+ if (record.status !== "success") failed.set(participant.name, true);
765
+ onResult(record, { iterations: schedule.length, participant: participant.name });
766
+ reporter?.recordResult(record);
767
+ if (!recordsByParticipant.has(participant.name)) {
768
+ recordsByParticipant.set(participant.name, []);
769
+ }
770
+ const participantRecords = recordsByParticipant.get(participant.name);
771
+ participantRecords.push(record);
772
+ if (reporter) {
773
+ reporter.setProgress({
774
+ done: participantRecords.length,
775
+ inFlight: 0,
776
+ errors: participantRecords.filter((item) => item.status !== "success").length,
777
+ total: schedule.length
778
+ });
779
+ await reporter.heartbeat();
780
+ }
781
+ }
782
+ }
783
+ for (const participant of available) {
784
+ const reporter = reporters.get(participant.name) ?? null;
785
+ const logBuffer = logBuffers.get(participant.name);
786
+ if (reporter && !logBuffer.isEmpty()) {
787
+ await reporter.uploadArtifact({ kind: "coordinator.log", contentType: "text/plain", name: "worker.log", body: logBuffer.toText() }).catch(() => {
788
+ });
789
+ }
790
+ await reporter?.finish(failed.get(participant.name) ?? false);
791
+ console.log(` ${participant.name}: done${failed.get(participant.name) ? " (with errors)" : ""}.`);
792
+ }
793
+ return available.map((p) => ({ participant: p.name, records: recordsByParticipant.get(p.name) ?? [] }));
794
+ }
795
+ function mergeData(data, phase) {
796
+ const merged = { ...data ?? {}, ...phase ? { phase } : {} };
797
+ return Object.keys(merged).length > 0 ? merged : void 0;
798
+ }
799
+ async function runTaskRecord(task, participant, scheduleIndex, taskIndex, phase, logBuffer) {
800
+ const startedAtMs = Date.now();
801
+ const record = {
802
+ taskIndex,
803
+ status: "success",
804
+ startedAt: new Date(startedAtMs).toISOString()
805
+ };
806
+ const frameworkSteps = [];
807
+ const taskMeasures = {};
808
+ let activeStep = null;
809
+ const ctx = {
810
+ participant,
811
+ taskIndex: scheduleIndex,
812
+ phase,
813
+ async step(name, fn, options) {
814
+ const stepStartedAtMs = Date.now();
815
+ const stepRecord = {
816
+ name,
817
+ status: "success",
818
+ startedAt: new Date(stepStartedAtMs).toISOString(),
819
+ completedAt: new Date(stepStartedAtMs).toISOString(),
820
+ latencyMs: 0
821
+ };
822
+ if (options?.concurrency !== void 0) stepRecord.concurrency = options.concurrency;
823
+ if (options?.timeoutMs !== void 0) stepRecord.timeoutMs = options.timeoutMs;
824
+ const previousStep = activeStep;
825
+ activeStep = stepRecord;
826
+ try {
827
+ const result2 = await runStepInvocations(name, fn, options);
828
+ logBuffer.step(taskIndex, name, {});
829
+ return result2;
830
+ } catch (error) {
831
+ stepRecord.status = "error";
832
+ stepRecord.errorCode = error instanceof TaskError ? error.code ?? error.name : getErrorCode(error);
833
+ logBuffer.step(taskIndex, name, { error: error instanceof Error ? error.message : String(error) });
834
+ throw error;
835
+ } finally {
836
+ activeStep = previousStep;
837
+ stepRecord.completedAt = (/* @__PURE__ */ new Date()).toISOString();
838
+ stepRecord.latencyMs = Date.now() - stepStartedAtMs;
839
+ frameworkSteps.push(stepRecord);
840
+ }
841
+ },
842
+ measure(data) {
843
+ if (activeStep) {
844
+ activeStep.data = { ...activeStep.data ?? {}, ...data };
845
+ } else {
846
+ Object.assign(taskMeasures, data);
847
+ }
848
+ },
849
+ log(message, meta) {
850
+ logBuffer.line(`[task ${taskIndex}] ${message}`, meta);
851
+ }
852
+ };
853
+ let result = void 0;
854
+ try {
855
+ result = await task(ctx);
856
+ record.data = mergeData({ ...taskMeasures, ...result?.data ?? {} }, phase);
857
+ } catch (error) {
858
+ record.status = "error";
859
+ if (error instanceof TaskError) {
860
+ record.errorCode = error.code ?? error.name;
861
+ record.data = mergeData({ ...taskMeasures, ...error.data ?? {} }, phase);
862
+ if (error.steps?.length) frameworkSteps.push(...error.steps);
863
+ } else {
864
+ record.errorCode = getErrorCode(error);
865
+ record.data = mergeData(
866
+ { ...taskMeasures, errorMessage: error instanceof Error ? error.message : String(error) },
867
+ phase
868
+ );
869
+ }
870
+ } finally {
871
+ const endMs = Date.now();
872
+ record.completedAt = new Date(endMs).toISOString();
873
+ record.latencyMs = record.status === "success" && result && typeof result.latencyMs === "number" ? result.latencyMs : endMs - startedAtMs;
874
+ const taskSteps = record.status === "success" && result?.steps ? result.steps : [];
875
+ const allSteps = [...frameworkSteps, ...taskSteps];
876
+ if (allSteps.length === 0) {
877
+ allSteps.push({
878
+ name: "task",
879
+ status: record.status === "success" ? "success" : "error",
880
+ startedAt: record.startedAt,
881
+ completedAt: record.completedAt,
882
+ latencyMs: record.latencyMs,
883
+ errorCode: record.errorCode ?? null,
884
+ data: Object.keys(taskMeasures).length > 0 ? { ...taskMeasures } : void 0
885
+ });
886
+ }
887
+ record.steps = allSteps;
888
+ }
889
+ return record;
890
+ }
891
+
892
+ // src/cli.ts
893
+ var import_node_path = require("path");
894
+ var import_node_url = require("url");
895
+ 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
+ function isBenchmarkConfig(value) {
897
+ if (typeof value !== "object" || value === null) return false;
898
+ const candidate = value;
899
+ return typeof candidate.benchmarkSlug === "string" && Array.isArray(candidate.participants);
900
+ }
901
+ async function runBenchmarkFile(argv) {
902
+ const [command, ...rest] = argv;
903
+ const [file, ...flags] = rest;
904
+ if (command !== "run" || !file || file.startsWith("-")) throw new Error(USAGE);
905
+ const mod = await import((0, import_node_url.pathToFileURL)((0, import_node_path.resolve)(process.cwd(), file)).href);
906
+ const config = mod.config;
907
+ const task = mod.task ?? mod.default;
908
+ if (!isBenchmarkConfig(config)) {
909
+ throw new Error(`${file} must export a \`config\` created with defineBenchmarkConfig (with participants).`);
910
+ }
911
+ if (typeof task !== "function") {
912
+ throw new Error(`${file} must export a \`task\` created with defineTask.`);
913
+ }
914
+ await runBenchmark(config, task, flags);
915
+ }
916
+ async function run(argv) {
917
+ try {
918
+ await runBenchmarkFile(argv);
919
+ process.exit(0);
920
+ } catch (err) {
921
+ if (err instanceof NoAvailableParticipantsError) {
922
+ console.log(err.message);
923
+ process.exit(0);
924
+ }
925
+ console.error("Benchmark failed:", err instanceof Error ? err.message : err);
926
+ process.exit(1);
927
+ }
928
+ }
929
+ // Annotate the CommonJS export names for ESM import in node:
930
+ 0 && (module.exports = {
931
+ NoAvailableParticipantsError,
932
+ TaskError,
933
+ defineBenchmarkConfig,
934
+ defineTask,
935
+ higherIsBetter,
936
+ lowerIsBetter,
937
+ mergeConfig,
938
+ parseCliArgs,
939
+ run,
940
+ runBenchmark,
941
+ runBenchmarkFile,
942
+ score
943
+ });
944
+ //# sourceMappingURL=index.cjs.map