@benchsdk/client 0.2.1 → 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/dist/index.js CHANGED
@@ -7,6 +7,7 @@ var DEFAULT_READY_POLL_INTERVAL_MS = 1e3;
7
7
  var MAX_TASK_RESULT_RECORDS = 5e3;
8
8
  var MAX_TASK_RECORD_STEPS = 100;
9
9
  var MAX_HEARTBEAT_CONCURRENCY_SAMPLES = 20;
10
+ var MAX_WORKER_LOG_LINES = 1e5;
10
11
  var BenchmarkApiError = class extends Error {
11
12
  constructor(message, status, body) {
12
13
  super(message);
@@ -35,6 +36,9 @@ function getApiKey(input) {
35
36
  return input ?? (typeof process !== "undefined" ? process.env.COMPUTESDK_ADMIN_API_KEY ?? process.env.COMPUTESDK_API_KEY : void 0);
36
37
  }
37
38
  function getErrorCode(error) {
39
+ if (error instanceof Error && "code" in error && typeof error.code === "string" && error.code) {
40
+ return error.code;
41
+ }
38
42
  if (error instanceof Error && error.name) return error.name;
39
43
  return "ERROR";
40
44
  }
@@ -42,6 +46,21 @@ function toJsonObject(value) {
42
46
  if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
43
47
  return value;
44
48
  }
49
+ function mergeMeasures(measures, returned) {
50
+ const merged = { ...measures, ...returned ?? {} };
51
+ return Object.keys(merged).length > 0 ? merged : void 0;
52
+ }
53
+ function implicitTaskStep(record, measures) {
54
+ return {
55
+ name: "task",
56
+ status: record.status === "success" ? "success" : "error",
57
+ startedAt: record.startedAt,
58
+ completedAt: record.completedAt,
59
+ latencyMs: record.latencyMs,
60
+ errorCode: record.errorCode ?? null,
61
+ data: Object.keys(measures).length > 0 ? { ...measures } : void 0
62
+ };
63
+ }
45
64
  function validateTaskResults(input) {
46
65
  if (input.records.length > MAX_TASK_RESULT_RECORDS) {
47
66
  throw new Error(`Benchmark task result batches are limited to ${MAX_TASK_RESULT_RECORDS} records.`);
@@ -87,53 +106,9 @@ function bodySizeBytes(body) {
87
106
  if (body instanceof URLSearchParams) return new TextEncoder().encode(body.toString()).byteLength;
88
107
  return void 0;
89
108
  }
90
- function isDefinedTask(task) {
91
- return typeof task === "object" && task !== null && Array.isArray(task.steps);
92
- }
93
- function mergeJsonObjects(target, source) {
94
- if (!source) return;
95
- Object.assign(target, source);
96
- }
97
109
  function sleep(ms) {
98
110
  return new Promise((resolve) => setTimeout(resolve, ms));
99
111
  }
100
- async function runWorkerTask(task, context) {
101
- if (!isDefinedTask(task)) {
102
- return task(context);
103
- }
104
- const state = {};
105
- const data = { taskName: task.name };
106
- try {
107
- for (const definedStep of task.steps) {
108
- const stepData = await context.step(
109
- definedStep.name,
110
- () => definedStep.fn({
111
- assignment: context.assignment,
112
- taskIndex: context.taskIndex,
113
- state
114
- }),
115
- definedStep.options
116
- );
117
- mergeJsonObjects(data, stepData);
118
- }
119
- } catch (error) {
120
- try {
121
- await task.options?.cleanup?.({
122
- assignment: context.assignment,
123
- taskIndex: context.taskIndex,
124
- state
125
- });
126
- } catch {
127
- }
128
- throw error;
129
- }
130
- await task.options?.cleanup?.({
131
- assignment: context.assignment,
132
- taskIndex: context.taskIndex,
133
- state
134
- });
135
- return data;
136
- }
137
112
  async function mapPool(items, concurrency, fn) {
138
113
  let nextIndex = 0;
139
114
  const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
@@ -410,6 +385,13 @@ function createBenchmarkClient(config = {}) {
410
385
  `/benchmarks/${encodePath(benchmarkSlug)}/runs/${encodePath(runId)}/results/imports`
411
386
  );
412
387
  },
388
+ async submitRunSummary(benchmarkSlug, runId, input) {
389
+ await request(
390
+ "POST",
391
+ `/benchmarks/${encodePath(benchmarkSlug)}/runs/${encodePath(runId)}/summary`,
392
+ input
393
+ );
394
+ },
413
395
  async runWorker(options) {
414
396
  if (options.concurrency !== void 0) validatePositiveInteger("concurrency", options.concurrency);
415
397
  if (options.batchSize !== void 0) validateBatchSize(options.batchSize);
@@ -521,6 +503,18 @@ function createBenchmarkClient(config = {}) {
521
503
  });
522
504
  }, options.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS);
523
505
  resultFlush.unref?.();
506
+ const workerLogLines = [];
507
+ let workerLogTruncated = false;
508
+ function appendWorkerLog(line) {
509
+ if (workerLogLines.length >= MAX_WORKER_LOG_LINES) {
510
+ if (!workerLogTruncated) {
511
+ workerLogTruncated = true;
512
+ workerLogLines.push("... (worker log truncated)");
513
+ }
514
+ return;
515
+ }
516
+ workerLogLines.push(line);
517
+ }
524
518
  async function runFinishHook(status) {
525
519
  await options.onFinish?.({
526
520
  assignment: claimed,
@@ -535,6 +529,19 @@ function createBenchmarkClient(config = {}) {
535
529
  }
536
530
  });
537
531
  }
532
+ async function uploadWorkerLogArtifact() {
533
+ if (workerLogLines.length === 0) return;
534
+ try {
535
+ await client.uploadWorkerArtifact(options.benchmarkSlug, options.runId, claimed.workerId, {
536
+ attemptId: claimed.attemptId,
537
+ kind: "coordinator.log",
538
+ contentType: "text/plain",
539
+ name: "worker.log",
540
+ body: workerLogLines.join("\n") + "\n"
541
+ });
542
+ } catch {
543
+ }
544
+ }
538
545
  try {
539
546
  await sendHeartbeat().catch(() => {
540
547
  });
@@ -548,6 +555,19 @@ function createBenchmarkClient(config = {}) {
548
555
  startedAt: startedAtDate.toISOString()
549
556
  };
550
557
  const steps = [];
558
+ const taskMeasures = {};
559
+ let activeStep = null;
560
+ function measure(data) {
561
+ if (activeStep) {
562
+ activeStep.data = { ...activeStep.data ?? {}, ...data };
563
+ } else {
564
+ Object.assign(taskMeasures, data);
565
+ }
566
+ }
567
+ function log(message, meta) {
568
+ const suffix = meta && Object.keys(meta).length > 0 ? ` ${JSON.stringify(meta)}` : "";
569
+ appendWorkerLog(`${(/* @__PURE__ */ new Date()).toISOString()} [task ${taskIndex}] ${message}${suffix}`);
570
+ }
551
571
  async function step(name, fn, stepOptions = {}) {
552
572
  const stepStartedAtMs = Date.now();
553
573
  const stepRecord = {
@@ -557,6 +577,8 @@ function createBenchmarkClient(config = {}) {
557
577
  completedAt: (/* @__PURE__ */ new Date()).toISOString(),
558
578
  latencyMs: 0
559
579
  };
580
+ if (stepOptions.timeoutMs !== void 0) stepRecord.timeoutMs = stepOptions.timeoutMs;
581
+ if (stepOptions.stepConcurrency !== void 0) stepRecord.concurrency = stepOptions.stepConcurrency;
560
582
  const shouldReportConcurrency = stepOptions.reportConcurrency ?? true;
561
583
  if (shouldReportConcurrency) {
562
584
  const stepConcurrency = stepOptions.concurrency ?? workerConcurrency;
@@ -565,16 +587,23 @@ function createBenchmarkClient(config = {}) {
565
587
  activeByStep.set(name, (activeByStep.get(name) ?? 0) + 1);
566
588
  requestHeartbeat();
567
589
  }
590
+ const previousStep = activeStep;
591
+ activeStep = stepRecord;
568
592
  try {
569
593
  if (stepOptions.readiness === "poll") {
570
594
  await waitForStepReady(name, stepOptions);
571
595
  }
572
- return await fn();
596
+ const value = await fn();
597
+ appendWorkerLog(`${(/* @__PURE__ */ new Date()).toISOString()} [task ${taskIndex}] ${name}`);
598
+ return value;
573
599
  } catch (error) {
574
600
  stepRecord.status = "error";
575
601
  stepRecord.errorCode = getErrorCode(error);
602
+ appendWorkerLog(`${(/* @__PURE__ */ new Date()).toISOString()} [task ${taskIndex}] ${name}`);
603
+ appendWorkerLog(` error: ${error instanceof Error ? error.message : String(error)}`);
576
604
  throw error;
577
605
  } finally {
606
+ activeStep = previousStep;
578
607
  stepRecord.completedAt = (/* @__PURE__ */ new Date()).toISOString();
579
608
  stepRecord.latencyMs = Date.now() - stepStartedAtMs;
580
609
  steps.push(stepRecord);
@@ -591,16 +620,19 @@ function createBenchmarkClient(config = {}) {
591
620
  }
592
621
  }
593
622
  try {
594
- const data = await runWorkerTask(options.task, { assignment: claimed, taskIndex, step });
595
- record.data = toJsonObject(data);
623
+ const data = await options.task({ assignment: claimed, taskIndex, step, measure, log });
624
+ record.data = mergeMeasures(taskMeasures, toJsonObject(data));
596
625
  } catch (error) {
597
626
  record.status = "error";
598
627
  record.errorCode = getErrorCode(error);
599
- record.data = { errorMessage: error instanceof Error ? error.message : String(error) };
628
+ record.data = mergeMeasures(taskMeasures, { errorMessage: error instanceof Error ? error.message : String(error) });
600
629
  } finally {
601
630
  record.completedAt = (/* @__PURE__ */ new Date()).toISOString();
602
631
  record.latencyMs = Date.now() - startedAtMs;
603
- record.steps = steps.length > 0 ? steps : void 0;
632
+ if (steps.length === 0) {
633
+ steps.push(implicitTaskStep(record, taskMeasures));
634
+ }
635
+ record.steps = steps;
604
636
  doneCount += 1;
605
637
  inFlightCount = Math.max(0, inFlightCount - 1);
606
638
  if (record.status !== "success") errorCount += 1;
@@ -632,6 +664,7 @@ function createBenchmarkClient(config = {}) {
632
664
  });
633
665
  throw error;
634
666
  } finally {
667
+ await uploadWorkerLogArtifact();
635
668
  clearInterval(heartbeat);
636
669
  clearInterval(resultFlush);
637
670
  }
@@ -639,84 +672,6 @@ function createBenchmarkClient(config = {}) {
639
672
  };
640
673
  return client;
641
674
  }
642
- async function runBenchmarkWorker(config, options) {
643
- return createBenchmarkClient(config).runWorker(options);
644
- }
645
- function defineStep(name, optionsOrFn, maybeFn) {
646
- if (name.trim() === "") {
647
- throw new Error("Benchmark step name must be non-empty.");
648
- }
649
- const hasOptions = typeof optionsOrFn !== "function";
650
- const fn = hasOptions ? maybeFn : optionsOrFn;
651
- if (!fn) {
652
- throw new Error("Benchmark step function is required.");
653
- }
654
- return { name, options: hasOptions ? optionsOrFn : void 0, fn };
655
- }
656
- function defineTask(name, steps, options) {
657
- if (name.trim() === "") {
658
- throw new Error("Benchmark task name must be non-empty.");
659
- }
660
- if (steps.length === 0) {
661
- throw new Error("Benchmark task must define at least one step.");
662
- }
663
- const names = /* @__PURE__ */ new Set();
664
- for (const step of steps) {
665
- if (names.has(step.name)) {
666
- throw new Error(`Benchmark task step names must be unique. Duplicate step: "${step.name}".`);
667
- }
668
- names.add(step.name);
669
- }
670
- return { name, steps, options };
671
- }
672
- function defineWorker(options) {
673
- const client = options.client ?? createBenchmarkClient();
674
- return {
675
- run(overrides = {}) {
676
- return client.runWorker({
677
- benchmarkSlug: options.benchmarkSlug,
678
- runId: options.runId,
679
- participantSlug: options.participantSlug,
680
- processKind: options.processKind,
681
- processKey: options.processKey,
682
- concurrency: overrides.concurrency ?? options.concurrency,
683
- batchSize: overrides.batchSize ?? options.batchSize,
684
- flushIntervalMs: overrides.flushIntervalMs ?? options.flushIntervalMs,
685
- heartbeatIntervalMs: overrides.heartbeatIntervalMs ?? options.heartbeatIntervalMs,
686
- readyPollIntervalMs: overrides.readyPollIntervalMs ?? options.readyPollIntervalMs,
687
- onFinish: options.onFinish,
688
- task: options.task
689
- });
690
- }
691
- };
692
- }
693
- function defineBench(options) {
694
- return {
695
- slug: options.slug,
696
- task: options.task,
697
- defineWorker(workerOptions) {
698
- const participantSlug = workerOptions.participantSlug ?? options.participantSlug;
699
- if (!participantSlug) {
700
- throw new Error("Benchmark worker participantSlug is required.");
701
- }
702
- return defineWorker({
703
- benchmarkSlug: options.slug,
704
- runId: workerOptions.runId,
705
- participantSlug,
706
- processKind: workerOptions.processKind,
707
- processKey: workerOptions.processKey,
708
- concurrency: workerOptions.concurrency ?? options.concurrency,
709
- batchSize: workerOptions.batchSize ?? options.batchSize,
710
- flushIntervalMs: workerOptions.flushIntervalMs ?? options.flushIntervalMs,
711
- heartbeatIntervalMs: workerOptions.heartbeatIntervalMs ?? options.heartbeatIntervalMs,
712
- readyPollIntervalMs: workerOptions.readyPollIntervalMs ?? options.readyPollIntervalMs,
713
- onFinish: workerOptions.onFinish,
714
- client: workerOptions.client ?? options.client,
715
- task: workerOptions.task ?? options.task
716
- });
717
- }
718
- };
719
- }
720
675
 
721
676
  // src/reporter.ts
722
677
  var DEFAULT_REPORTER_BATCH_SIZE = 500;
@@ -749,7 +704,10 @@ var BenchmarkReporter = class _BenchmarkReporter {
749
704
  processKey: cfg.processKey
750
705
  });
751
706
  return assignment ? new _BenchmarkReporter(client, cfg, assignment) : null;
752
- } catch {
707
+ } catch (error) {
708
+ console.warn(
709
+ `[benchsdk] failed to claim worker for ${cfg.benchmarkSlug}/${cfg.participantSlug}: ${error instanceof Error ? error.message : String(error)}`
710
+ );
753
711
  return null;
754
712
  }
755
713
  }
@@ -824,7 +782,12 @@ var BenchmarkReporter = class _BenchmarkReporter {
824
782
  contentType: input.contentType,
825
783
  metadata: input.metadata,
826
784
  body: input.body
827
- }).catch(() => null);
785
+ }).catch((error) => {
786
+ console.warn(
787
+ `[benchsdk] failed to upload ${input.kind} artifact for worker ${this.assignment.workerId}: ${error instanceof Error ? error.message : String(error)}`
788
+ );
789
+ return null;
790
+ });
828
791
  }
829
792
  flush(isFinal = false) {
830
793
  this.flushChain = this.flushChain.then(async () => {
@@ -840,7 +803,10 @@ var BenchmarkReporter = class _BenchmarkReporter {
840
803
  isFinal: isFinal && batch.length === this.pending.length,
841
804
  records: batch
842
805
  });
843
- } catch {
806
+ } catch (error) {
807
+ console.warn(
808
+ `[benchsdk] dropping ${this.pending.length} unsent task result(s) for worker ${this.assignment.workerId}: ${error instanceof Error ? error.message : String(error)}`
809
+ );
844
810
  break;
845
811
  }
846
812
  this.pending.splice(0, batch.length);
@@ -942,16 +908,38 @@ function createSystemMetricsCollector() {
942
908
  }
943
909
  };
944
910
  }
911
+
912
+ // src/participants.ts
913
+ function filterParticipantsByEnv(participants) {
914
+ const available = [];
915
+ const skipped = [];
916
+ for (const p of participants) {
917
+ const missing = p.requiredEnvVars.filter((v) => !process.env[v]);
918
+ if (missing.length > 0) {
919
+ skipped.push({ name: p.name, missing });
920
+ } else {
921
+ available.push(p);
922
+ }
923
+ }
924
+ return { available, skipped };
925
+ }
926
+ function selectParticipants(all, names) {
927
+ if (!names) return all;
928
+ const unknown = names.filter((n) => !all.some((p) => p.name === n));
929
+ if (unknown.length > 0) {
930
+ console.error(`Unknown participant(s): ${unknown.join(", ")}`);
931
+ console.error(`Available: ${all.map((p) => p.name).join(", ")}`);
932
+ process.exit(1);
933
+ }
934
+ return all.filter((p) => names.includes(p.name));
935
+ }
945
936
  export {
946
937
  BenchmarkApiError,
947
938
  BenchmarkReporter,
948
939
  claimBenchmarkReporter,
949
940
  createBenchmarkClient,
950
941
  createSystemMetricsCollector,
951
- defineBench,
952
- defineStep,
953
- defineTask,
954
- defineWorker,
955
- runBenchmarkWorker
942
+ filterParticipantsByEnv,
943
+ selectParticipants
956
944
  };
957
945
  //# sourceMappingURL=index.js.map