@benchsdk/client 0.3.0 → 0.4.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 CHANGED
@@ -1,9 +1,7 @@
1
1
  "use strict";
2
- var __create = Object.create;
3
2
  var __defProp = Object.defineProperty;
4
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
6
  var __export = (target, all) => {
9
7
  for (var name in all)
@@ -17,964 +15,35 @@ var __copyProps = (to, from, except, desc) => {
17
15
  }
18
16
  return to;
19
17
  };
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
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
19
 
30
20
  // src/index.ts
31
21
  var src_exports = {};
32
22
  __export(src_exports, {
33
- BenchmarkApiError: () => BenchmarkApiError,
34
- BenchmarkReporter: () => BenchmarkReporter,
35
- claimBenchmarkReporter: () => claimBenchmarkReporter,
23
+ BenchmarkApiError: () => import_api.BenchmarkApiError,
24
+ BenchmarkReporter: () => import_worker2.BenchmarkReporter,
25
+ claimBenchmarkReporter: () => import_worker2.claimBenchmarkReporter,
36
26
  createBenchmarkClient: () => createBenchmarkClient,
37
- createSystemMetricsCollector: () => createSystemMetricsCollector,
38
- filterParticipantsByEnv: () => filterParticipantsByEnv,
39
- selectParticipants: () => selectParticipants
27
+ createSystemMetricsCollector: () => import_worker2.createSystemMetricsCollector,
28
+ filterParticipantsByEnv: () => import_worker2.filterParticipantsByEnv,
29
+ runWorker: () => import_worker2.runWorker,
30
+ selectParticipants: () => import_worker2.selectParticipants
40
31
  });
41
32
  module.exports = __toCommonJS(src_exports);
42
33
 
43
34
  // src/client.ts
44
- var DEFAULT_BASE_URL = "https://platform.computesdk.com/api/v1";
45
- var DEFAULT_BATCH_SIZE = 1e3;
46
- var DEFAULT_HEARTBEAT_INTERVAL_MS = 3e4;
47
- var DEFAULT_FLUSH_INTERVAL_MS = 3e4;
48
- var DEFAULT_READY_POLL_INTERVAL_MS = 1e3;
49
- var MAX_TASK_RESULT_RECORDS = 5e3;
50
- var MAX_TASK_RECORD_STEPS = 100;
51
- var MAX_HEARTBEAT_CONCURRENCY_SAMPLES = 20;
52
- var MAX_WORKER_LOG_LINES = 1e5;
53
- var BenchmarkApiError = class extends Error {
54
- constructor(message, status, body) {
55
- super(message);
56
- this.status = status;
57
- this.body = body;
58
- this.name = "BenchmarkApiError";
59
- }
60
- status;
61
- body;
62
- };
63
- function trimTrailingSlash(value) {
64
- return value.replace(/\/+$/, "");
65
- }
66
- function encodePath(value) {
67
- return encodeURIComponent(value);
68
- }
69
- function queryString(input) {
70
- const params = new URLSearchParams();
71
- for (const [key, value2] of Object.entries(input)) {
72
- if (value2 !== void 0) params.set(key, String(value2));
73
- }
74
- const value = params.toString();
75
- return value ? `?${value}` : "";
76
- }
77
- function getApiKey(input) {
78
- return input ?? (typeof process !== "undefined" ? process.env.COMPUTESDK_ADMIN_API_KEY ?? process.env.COMPUTESDK_API_KEY : void 0);
79
- }
80
- function getErrorCode(error) {
81
- if (error instanceof Error && "code" in error && typeof error.code === "string" && error.code) {
82
- return error.code;
83
- }
84
- if (error instanceof Error && error.name) return error.name;
85
- return "ERROR";
86
- }
87
- function toJsonObject(value) {
88
- if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
89
- return value;
90
- }
91
- function mergeMeasures(measures, returned) {
92
- const merged = { ...measures, ...returned ?? {} };
93
- return Object.keys(merged).length > 0 ? merged : void 0;
94
- }
95
- function implicitTaskStep(record, measures) {
96
- return {
97
- name: "task",
98
- status: record.status === "success" ? "success" : "error",
99
- startedAt: record.startedAt,
100
- completedAt: record.completedAt,
101
- latencyMs: record.latencyMs,
102
- errorCode: record.errorCode ?? null,
103
- data: Object.keys(measures).length > 0 ? { ...measures } : void 0
104
- };
105
- }
106
- function validateTaskResults(input) {
107
- if (input.records.length > MAX_TASK_RESULT_RECORDS) {
108
- throw new Error(`Benchmark task result batches are limited to ${MAX_TASK_RESULT_RECORDS} records.`);
109
- }
110
- for (const record of input.records) {
111
- if ((record.steps?.length ?? 0) > MAX_TASK_RECORD_STEPS) {
112
- throw new Error(`Benchmark task result records are limited to ${MAX_TASK_RECORD_STEPS} steps.`);
113
- }
114
- }
115
- }
116
- function validateHeartbeat(input) {
117
- const concurrency = input.concurrency ?? [];
118
- if (concurrency.length > MAX_HEARTBEAT_CONCURRENCY_SAMPLES) {
119
- throw new Error(`Benchmark heartbeat concurrency is limited to ${MAX_HEARTBEAT_CONCURRENCY_SAMPLES} samples.`);
120
- }
121
- const steps = /* @__PURE__ */ new Set();
122
- for (const sample of concurrency) {
123
- if (steps.has(sample.step)) {
124
- throw new Error(`Benchmark heartbeat concurrency step values must be unique per heartbeat.`);
125
- }
126
- steps.add(sample.step);
127
- }
128
- }
129
- function validatePositiveInteger(name, value) {
130
- if (!Number.isInteger(value) || value <= 0) {
131
- throw new Error(`Benchmark ${name} must be a positive integer.`);
132
- }
133
- }
134
- function validateBatchSize(value) {
135
- validatePositiveInteger("batchSize", value);
136
- if (value > MAX_TASK_RESULT_RECORDS) {
137
- throw new Error(`Benchmark batchSize must be at most ${MAX_TASK_RESULT_RECORDS}.`);
138
- }
139
- }
140
- function normalizeArtifacts(data) {
141
- return data.items ?? data.artifacts ?? [];
142
- }
143
- function bodySizeBytes(body) {
144
- if (typeof body === "string") return new TextEncoder().encode(body).byteLength;
145
- if (body instanceof Blob) return body.size;
146
- if (body instanceof ArrayBuffer) return body.byteLength;
147
- if (ArrayBuffer.isView(body)) return body.byteLength;
148
- if (body instanceof URLSearchParams) return new TextEncoder().encode(body.toString()).byteLength;
149
- return void 0;
150
- }
151
- function sleep(ms) {
152
- return new Promise((resolve) => setTimeout(resolve, ms));
153
- }
154
- async function mapPool(items, concurrency, fn) {
155
- let nextIndex = 0;
156
- const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
157
- while (nextIndex < items.length) {
158
- const item = items[nextIndex];
159
- nextIndex += 1;
160
- await fn(item);
161
- }
162
- });
163
- await Promise.all(workers);
164
- }
35
+ var import_api = require("@benchsdk/api");
36
+ var import_worker = require("@benchsdk/worker");
165
37
  function createBenchmarkClient(config = {}) {
166
- const baseUrl = trimTrailingSlash(config.baseUrl ?? DEFAULT_BASE_URL);
167
- const apiKey = getApiKey(config.apiKey);
168
- const fetchImpl = config.fetch ?? (typeof fetch !== "undefined" ? fetch : void 0);
169
- if (!fetchImpl) {
170
- throw new Error("fetch is not available");
171
- }
172
- const doFetch = fetchImpl;
173
- async function request(method, path, body) {
174
- const headers = { "Content-Type": "application/json" };
175
- if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
176
- const response = await doFetch(`${baseUrl}${path}`, {
177
- method,
178
- headers,
179
- body: body === void 0 ? void 0 : JSON.stringify(body)
180
- });
181
- const text = await response.text();
182
- if (!response.ok) {
183
- throw new BenchmarkApiError(
184
- `Benchmark API request failed: ${response.status} ${response.statusText}`,
185
- response.status,
186
- text
187
- );
188
- }
189
- return text ? JSON.parse(text) : {};
190
- }
191
- async function sendTaskResults(input) {
192
- if (input.records.length === 0) {
193
- return {};
194
- }
195
- validateTaskResults(input);
196
- return request(
197
- "POST",
198
- `/benchmarks/${encodePath(input.benchmarkSlug)}/runs/${encodePath(input.runId)}/workers/${encodePath(input.workerId)}/events`,
199
- {
200
- type: "task_results",
201
- attemptId: input.attemptId,
202
- sequenceNumber: input.sequenceNumber,
203
- isFinal: input.isFinal,
204
- records: input.records
205
- }
206
- );
207
- }
208
- async function updateWorkerLifecycle(action, benchmarkSlug, runId, workerId, attemptId, extra) {
209
- return request(
210
- "POST",
211
- `/benchmarks/${encodePath(benchmarkSlug)}/runs/${encodePath(runId)}/workers/${encodePath(workerId)}/${action}`,
212
- { attemptId, ...extra ?? {} }
213
- );
214
- }
215
- const client = {
216
- async upsertBenchmark(slug, input) {
217
- const data = await request("PUT", `/benchmarks/${encodePath(slug)}`, input);
218
- return data.benchmark;
219
- },
220
- async getBenchmark(slug) {
221
- const data = await request("GET", `/benchmarks/${encodePath(slug)}`);
222
- return data.benchmark;
223
- },
224
- async updateBenchmark(slug, input) {
225
- const data = await request("PATCH", `/benchmarks/${encodePath(slug)}`, input);
226
- return data.benchmark;
227
- },
228
- async listBenchmarks() {
229
- const data = await request("GET", "/benchmarks");
230
- return data.items ?? data.benchmarks ?? [];
231
- },
232
- async createRun(benchmarkSlug, input) {
233
- return request(
234
- "POST",
235
- `/benchmarks/${encodePath(benchmarkSlug)}/runs`,
236
- input
237
- );
238
- },
239
- async listRuns(benchmarkSlug) {
240
- const data = await request("GET", `/benchmarks/${encodePath(benchmarkSlug)}/runs`);
241
- return data.items;
242
- },
243
- async getRun(benchmarkSlug, runId) {
244
- const data = await request("GET", `/benchmarks/${encodePath(benchmarkSlug)}/runs/${encodePath(runId)}`);
245
- return data.run;
246
- },
247
- async updateRun(benchmarkSlug, runId, input) {
248
- const data = await request(
249
- "PATCH",
250
- `/benchmarks/${encodePath(benchmarkSlug)}/runs/${encodePath(runId)}`,
251
- input
252
- );
253
- return data.run;
254
- },
255
- async upsertParticipant(benchmarkSlug, runId, participantSlug, input = {}) {
256
- const data = await request(
257
- "PUT",
258
- `/benchmarks/${encodePath(benchmarkSlug)}/runs/${encodePath(runId)}/participants/${encodePath(participantSlug)}`,
259
- input
260
- );
261
- return data.participant;
262
- },
263
- async listParticipants(benchmarkSlug, runId) {
264
- const data = await request(
265
- "GET",
266
- `/benchmarks/${encodePath(benchmarkSlug)}/runs/${encodePath(runId)}/participants`
267
- );
268
- return data.items ?? data.participants ?? [];
269
- },
270
- async getParticipant(benchmarkSlug, runId, participantSlug) {
271
- const data = await request(
272
- "GET",
273
- `/benchmarks/${encodePath(benchmarkSlug)}/runs/${encodePath(runId)}/participants/${encodePath(participantSlug)}`
274
- );
275
- return data.participant;
276
- },
277
- async updateParticipant(benchmarkSlug, runId, participantSlug, input) {
278
- const data = await request(
279
- "PATCH",
280
- `/benchmarks/${encodePath(benchmarkSlug)}/runs/${encodePath(runId)}/participants/${encodePath(participantSlug)}`,
281
- input
282
- );
283
- return data.participant;
284
- },
285
- async getRunProgress(benchmarkSlug, runId) {
286
- return request(
287
- "GET",
288
- `/benchmarks/${encodePath(benchmarkSlug)}/runs/${encodePath(runId)}/progress`
289
- );
290
- },
291
- async listWorkers(benchmarkSlug, runId, participantSlug) {
292
- const data = await request(
293
- "GET",
294
- `/benchmarks/${encodePath(benchmarkSlug)}/runs/${encodePath(runId)}/participants/${encodePath(participantSlug)}/workers`
295
- );
296
- return data.items ?? data.workers ?? [];
297
- },
298
- async planWorkers(benchmarkSlug, runId, participantSlug, input = {}) {
299
- const data = await request(
300
- "POST",
301
- `/benchmarks/${encodePath(benchmarkSlug)}/runs/${encodePath(runId)}/participants/${encodePath(participantSlug)}/workers`,
302
- input
303
- );
304
- return data.items ?? data.workers ?? [];
305
- },
306
- async getWorker(benchmarkSlug, runId, workerId) {
307
- const data = await request(
308
- "GET",
309
- `/benchmarks/${encodePath(benchmarkSlug)}/runs/${encodePath(runId)}/workers/${encodePath(workerId)}`
310
- );
311
- return data.worker;
312
- },
313
- async updateWorker(benchmarkSlug, runId, workerId, input) {
314
- const data = await request(
315
- "PATCH",
316
- `/benchmarks/${encodePath(benchmarkSlug)}/runs/${encodePath(runId)}/workers/${encodePath(workerId)}`,
317
- input
318
- );
319
- return data.worker;
320
- },
321
- async claimWorker(benchmarkSlug, runId, participantSlug, input = {}) {
322
- const data = await request(
323
- "POST",
324
- `/benchmarks/${encodePath(benchmarkSlug)}/runs/${encodePath(runId)}/participants/${encodePath(participantSlug)}/workers/claim`,
325
- input
326
- );
327
- return data.assignment;
328
- },
329
- sendTaskResults,
330
- async heartbeatWorker(benchmarkSlug, runId, workerId, input) {
331
- validateHeartbeat(input);
332
- const { attemptId, ...extra } = input;
333
- if (extra.currentStep == null) {
334
- delete extra.currentStep;
335
- }
336
- return updateWorkerLifecycle("heartbeat", benchmarkSlug, runId, workerId, attemptId, extra);
337
- },
338
- releaseWorker(benchmarkSlug, runId, workerId, attemptId) {
339
- return updateWorkerLifecycle("release", benchmarkSlug, runId, workerId, attemptId);
340
- },
341
- completeWorker(benchmarkSlug, runId, workerId, attemptId) {
342
- return updateWorkerLifecycle("complete", benchmarkSlug, runId, workerId, attemptId);
343
- },
344
- failWorker(benchmarkSlug, runId, workerId, attemptId, error) {
345
- return updateWorkerLifecycle("fail", benchmarkSlug, runId, workerId, attemptId, {
346
- errorCode: getErrorCode(error),
347
- errorMessage: error instanceof Error ? error.message : String(error ?? "Unknown error")
348
- });
349
- },
350
- async createWorkerArtifact(benchmarkSlug, runId, workerId, input) {
351
- return request(
352
- "POST",
353
- `/benchmarks/${encodePath(benchmarkSlug)}/runs/${encodePath(runId)}/workers/${encodePath(workerId)}/artifacts`,
354
- input
355
- );
356
- },
357
- async uploadWorkerArtifact(benchmarkSlug, runId, workerId, input) {
358
- const sizeBytes = bodySizeBytes(input.body);
359
- const artifactInput = {
360
- attemptId: input.attemptId,
361
- kind: input.kind,
362
- contentType: input.contentType,
363
- name: input.name,
364
- metadata: sizeBytes === void 0 ? input.metadata : { ...input.metadata, sizeBytes }
365
- };
366
- const response = await client.createWorkerArtifact(benchmarkSlug, runId, workerId, artifactInput);
367
- const uploadUrl = response.uploadUrl ?? response.artifact?.uploadUrl;
368
- if (!uploadUrl) {
369
- throw new Error("Benchmark artifact upload URL is missing.");
370
- }
371
- const uploadResponse = await doFetch(uploadUrl, {
372
- method: "PUT",
373
- headers: input.contentType ? { "Content-Type": input.contentType } : void 0,
374
- body: input.body
375
- });
376
- if (!uploadResponse.ok) {
377
- const errorBody = await uploadResponse.text().catch(() => "");
378
- throw new BenchmarkApiError(
379
- `Benchmark artifact upload failed with ${uploadResponse.status}`,
380
- uploadResponse.status,
381
- errorBody
382
- );
383
- }
384
- return response;
385
- },
386
- async listRunArtifacts(benchmarkSlug, runId) {
387
- const data = await request(
388
- "GET",
389
- `/benchmarks/${encodePath(benchmarkSlug)}/runs/${encodePath(runId)}/artifacts`
390
- );
391
- return normalizeArtifacts(data);
392
- },
393
- async listWorkerArtifacts(benchmarkSlug, runId, workerId) {
394
- const data = await request(
395
- "GET",
396
- `/benchmarks/${encodePath(benchmarkSlug)}/runs/${encodePath(runId)}/workers/${encodePath(workerId)}/artifacts`
397
- );
398
- return normalizeArtifacts(data);
399
- },
400
- async getBenchmarkResults(benchmarkSlug, input = {}) {
401
- return request(
402
- "GET",
403
- `/benchmarks/${encodePath(benchmarkSlug)}/results${queryString({ limit: input.limit })}`
404
- );
405
- },
406
- async getRunResults(benchmarkSlug, runId) {
407
- return request(
408
- "GET",
409
- `/benchmarks/${encodePath(benchmarkSlug)}/runs/${encodePath(runId)}/results`
410
- );
411
- },
412
- async getRunTaskResults(benchmarkSlug, runId, input = {}) {
413
- return request(
414
- "GET",
415
- `/benchmarks/${encodePath(benchmarkSlug)}/runs/${encodePath(runId)}/results/tasks${queryString({ bucketSize: input.bucketSize, failureLimit: input.failureLimit })}`
416
- );
417
- },
418
- async getRunTimeline(benchmarkSlug, runId, input = {}) {
419
- return request(
420
- "GET",
421
- `/benchmarks/${encodePath(benchmarkSlug)}/runs/${encodePath(runId)}/results/timeline${queryString({ bucketMs: input.bucketMs })}`
422
- );
423
- },
424
- async getRunImports(benchmarkSlug, runId) {
425
- return request(
426
- "GET",
427
- `/benchmarks/${encodePath(benchmarkSlug)}/runs/${encodePath(runId)}/results/imports`
428
- );
429
- },
430
- async submitRunSummary(benchmarkSlug, runId, input) {
431
- await request(
432
- "POST",
433
- `/benchmarks/${encodePath(benchmarkSlug)}/runs/${encodePath(runId)}/summary`,
434
- input
435
- );
436
- },
437
- async runWorker(options) {
438
- if (options.concurrency !== void 0) validatePositiveInteger("concurrency", options.concurrency);
439
- if (options.batchSize !== void 0) validateBatchSize(options.batchSize);
440
- if (options.flushIntervalMs !== void 0) validatePositiveInteger("flushIntervalMs", options.flushIntervalMs);
441
- const assignment = await client.claimWorker(options.benchmarkSlug, options.runId, options.participantSlug, {
442
- processKind: options.processKind,
443
- processKey: options.processKey
444
- });
445
- if (!assignment) return { assignment: null, records: [] };
446
- const claimed = assignment;
447
- let sequenceNumber = 0;
448
- const records = [];
449
- const pending = [];
450
- const batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
451
- const workerConcurrency = options.concurrency ?? claimed.targetConcurrency;
452
- validatePositiveInteger("concurrency", workerConcurrency);
453
- const taskIndices = Array.from({ length: claimed.taskRange.count }, (_, index) => claimed.taskRange.start + index);
454
- const activeByStep = /* @__PURE__ */ new Map();
455
- const targetByStep = /* @__PURE__ */ new Map();
456
- const readyWaitByStep = /* @__PURE__ */ new Map();
457
- let doneCount = 0;
458
- let errorCount = 0;
459
- let inFlightCount = 0;
460
- let flushChain = Promise.resolve();
461
- function concurrencySamples() {
462
- return Array.from(activeByStep.entries()).filter(([, active]) => active > 0).map(([step, active]) => ({
463
- step,
464
- active,
465
- target: targetByStep.get(step) ?? workerConcurrency
466
- })).sort((a, b) => b.active - a.active).slice(0, MAX_HEARTBEAT_CONCURRENCY_SAMPLES);
467
- }
468
- async function sendHeartbeat() {
469
- const concurrency = concurrencySamples();
470
- const step = concurrency[0]?.step ?? null;
471
- await client.heartbeatWorker(options.benchmarkSlug, options.runId, claimed.workerId, {
472
- attemptId: claimed.attemptId,
473
- progressDone: doneCount,
474
- progressInFlight: inFlightCount,
475
- progressErrors: errorCount,
476
- progressTotal: taskIndices.length,
477
- ...step ? { currentStep: step } : {},
478
- concurrency
479
- });
480
- }
481
- let heartbeatInFlight = null;
482
- let heartbeatRequested = false;
483
- function requestHeartbeat() {
484
- heartbeatRequested = true;
485
- if (heartbeatInFlight) return;
486
- heartbeatInFlight = (async () => {
487
- while (heartbeatRequested) {
488
- heartbeatRequested = false;
489
- await sendHeartbeat().catch(() => {
490
- });
491
- }
492
- })().finally(() => {
493
- heartbeatInFlight = null;
494
- if (heartbeatRequested) requestHeartbeat();
495
- });
496
- }
497
- async function pollStepReady(stepName, stepOptions) {
498
- const startedAt = Date.now();
499
- const pollInterval = stepOptions.readyPollIntervalMs ?? options.readyPollIntervalMs ?? DEFAULT_READY_POLL_INTERVAL_MS;
500
- while (true) {
501
- const progress = await client.getRunProgress(options.benchmarkSlug, options.runId);
502
- const participant = progress.participants.find((item) => item.slug === options.participantSlug);
503
- const step = participant?.concurrency.find((item) => item.step === stepName);
504
- if (step?.ready) return;
505
- if (typeof stepOptions.readyTimeoutMs === "number" && Date.now() - startedAt >= stepOptions.readyTimeoutMs) {
506
- throw new Error(`Timed out waiting for benchmark step "${stepName}" to become ready.`);
507
- }
508
- await sleep(pollInterval);
509
- }
510
- }
511
- async function waitForStepReady(stepName, stepOptions) {
512
- const existing = readyWaitByStep.get(stepName);
513
- if (existing) return existing;
514
- const wait = pollStepReady(stepName, stepOptions).finally(() => {
515
- if (readyWaitByStep.get(stepName) === wait) readyWaitByStep.delete(stepName);
516
- });
517
- readyWaitByStep.set(stepName, wait);
518
- return wait;
519
- }
520
- const heartbeat = setInterval(() => {
521
- requestHeartbeat();
522
- }, options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS);
523
- heartbeat.unref?.();
524
- async function flush(isFinal, force = false) {
525
- flushChain = flushChain.then(async () => {
526
- if (force && doneCount >= taskIndices.length) return;
527
- while (pending.length >= batchSize || (isFinal || force) && pending.length > 0) {
528
- const batch = pending.splice(0, batchSize);
529
- await sendTaskResults({
530
- benchmarkSlug: options.benchmarkSlug,
531
- runId: options.runId,
532
- workerId: claimed.workerId,
533
- attemptId: claimed.attemptId,
534
- sequenceNumber,
535
- isFinal: isFinal && pending.length === 0,
536
- records: batch
537
- });
538
- sequenceNumber += 1;
539
- }
540
- });
541
- await flushChain;
542
- }
543
- const resultFlush = setInterval(() => {
544
- if (doneCount < taskIndices.length) void flush(false, true).catch(() => {
545
- });
546
- }, options.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS);
547
- resultFlush.unref?.();
548
- const workerLogLines = [];
549
- let workerLogTruncated = false;
550
- function appendWorkerLog(line) {
551
- if (workerLogLines.length >= MAX_WORKER_LOG_LINES) {
552
- if (!workerLogTruncated) {
553
- workerLogTruncated = true;
554
- workerLogLines.push("... (worker log truncated)");
555
- }
556
- return;
557
- }
558
- workerLogLines.push(line);
559
- }
560
- async function runFinishHook(status) {
561
- await options.onFinish?.({
562
- assignment: claimed,
563
- records,
564
- status,
565
- client,
566
- uploadArtifact(input) {
567
- return client.uploadWorkerArtifact(options.benchmarkSlug, options.runId, claimed.workerId, {
568
- ...input,
569
- attemptId: claimed.attemptId
570
- });
571
- }
572
- });
573
- }
574
- async function uploadWorkerLogArtifact() {
575
- if (workerLogLines.length === 0) return;
576
- try {
577
- await client.uploadWorkerArtifact(options.benchmarkSlug, options.runId, claimed.workerId, {
578
- attemptId: claimed.attemptId,
579
- kind: "coordinator.log",
580
- contentType: "text/plain",
581
- name: "worker.log",
582
- body: workerLogLines.join("\n") + "\n"
583
- });
584
- } catch {
585
- }
586
- }
587
- try {
588
- await sendHeartbeat().catch(() => {
589
- });
590
- await mapPool(taskIndices, workerConcurrency, async (taskIndex) => {
591
- inFlightCount += 1;
592
- const startedAtDate = /* @__PURE__ */ new Date();
593
- const startedAtMs = Date.now();
594
- const record = {
595
- taskIndex,
596
- status: "success",
597
- startedAt: startedAtDate.toISOString()
598
- };
599
- const steps = [];
600
- const taskMeasures = {};
601
- let activeStep = null;
602
- function measure(data) {
603
- if (activeStep) {
604
- activeStep.data = { ...activeStep.data ?? {}, ...data };
605
- } else {
606
- Object.assign(taskMeasures, data);
607
- }
608
- }
609
- function log(message, meta) {
610
- const suffix = meta && Object.keys(meta).length > 0 ? ` ${JSON.stringify(meta)}` : "";
611
- appendWorkerLog(`${(/* @__PURE__ */ new Date()).toISOString()} [task ${taskIndex}] ${message}${suffix}`);
612
- }
613
- async function step(name, fn, stepOptions = {}) {
614
- const stepStartedAtMs = Date.now();
615
- const stepRecord = {
616
- name,
617
- status: "success",
618
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
619
- completedAt: (/* @__PURE__ */ new Date()).toISOString(),
620
- latencyMs: 0
621
- };
622
- if (stepOptions.timeoutMs !== void 0) stepRecord.timeoutMs = stepOptions.timeoutMs;
623
- if (stepOptions.stepConcurrency !== void 0) stepRecord.concurrency = stepOptions.stepConcurrency;
624
- const shouldReportConcurrency = stepOptions.reportConcurrency ?? true;
625
- if (shouldReportConcurrency) {
626
- const stepConcurrency = stepOptions.concurrency ?? workerConcurrency;
627
- validatePositiveInteger(`step "${name}" concurrency`, stepConcurrency);
628
- targetByStep.set(name, stepConcurrency);
629
- activeByStep.set(name, (activeByStep.get(name) ?? 0) + 1);
630
- requestHeartbeat();
631
- }
632
- const previousStep = activeStep;
633
- activeStep = stepRecord;
634
- try {
635
- if (stepOptions.readiness === "poll") {
636
- await waitForStepReady(name, stepOptions);
637
- }
638
- const value = await fn();
639
- appendWorkerLog(`${(/* @__PURE__ */ new Date()).toISOString()} [task ${taskIndex}] ${name}`);
640
- return value;
641
- } catch (error) {
642
- stepRecord.status = "error";
643
- stepRecord.errorCode = getErrorCode(error);
644
- appendWorkerLog(`${(/* @__PURE__ */ new Date()).toISOString()} [task ${taskIndex}] ${name}`);
645
- appendWorkerLog(` error: ${error instanceof Error ? error.message : String(error)}`);
646
- throw error;
647
- } finally {
648
- activeStep = previousStep;
649
- stepRecord.completedAt = (/* @__PURE__ */ new Date()).toISOString();
650
- stepRecord.latencyMs = Date.now() - stepStartedAtMs;
651
- steps.push(stepRecord);
652
- if (shouldReportConcurrency) {
653
- const nextActive = Math.max(0, (activeByStep.get(name) ?? 0) - 1);
654
- if (nextActive === 0) {
655
- activeByStep.delete(name);
656
- targetByStep.delete(name);
657
- } else {
658
- activeByStep.set(name, nextActive);
659
- }
660
- requestHeartbeat();
661
- }
662
- }
663
- }
664
- try {
665
- const data = await options.task({ assignment: claimed, taskIndex, step, measure, log });
666
- record.data = mergeMeasures(taskMeasures, toJsonObject(data));
667
- } catch (error) {
668
- record.status = "error";
669
- record.errorCode = getErrorCode(error);
670
- record.data = mergeMeasures(taskMeasures, { errorMessage: error instanceof Error ? error.message : String(error) });
671
- } finally {
672
- record.completedAt = (/* @__PURE__ */ new Date()).toISOString();
673
- record.latencyMs = Date.now() - startedAtMs;
674
- if (steps.length === 0) {
675
- steps.push(implicitTaskStep(record, taskMeasures));
676
- }
677
- record.steps = steps;
678
- doneCount += 1;
679
- inFlightCount = Math.max(0, inFlightCount - 1);
680
- if (record.status !== "success") errorCount += 1;
681
- }
682
- records.push(record);
683
- pending.push(record);
684
- options.onResult?.(record);
685
- if (pending.length >= batchSize) await flush(false);
686
- });
687
- await flush(true);
688
- const hasErrors = records.some((record) => record.status !== "success");
689
- try {
690
- await runFinishHook(hasErrors ? "error" : "success");
691
- } catch (error) {
692
- if (!hasErrors) throw error;
693
- }
694
- if (hasErrors) {
695
- await client.failWorker(options.benchmarkSlug, options.runId, claimed.workerId, claimed.attemptId, new Error("One or more tasks failed"));
696
- } else {
697
- await client.completeWorker(options.benchmarkSlug, options.runId, claimed.workerId, claimed.attemptId);
698
- }
699
- return { assignment: claimed, records };
700
- } catch (error) {
701
- await flush(true).catch(() => {
702
- });
703
- await runFinishHook("error").catch(() => {
704
- });
705
- await client.failWorker(options.benchmarkSlug, options.runId, claimed.workerId, claimed.attemptId, error).catch(() => {
706
- });
707
- throw error;
708
- } finally {
709
- await uploadWorkerLogArtifact();
710
- clearInterval(heartbeat);
711
- clearInterval(resultFlush);
712
- }
713
- }
714
- };
715
- return client;
716
- }
717
-
718
- // src/reporter.ts
719
- var DEFAULT_REPORTER_BATCH_SIZE = 500;
720
- var DEFAULT_READY_POLL_INTERVAL_MS2 = 1e3;
721
- var BenchmarkReporter = class _BenchmarkReporter {
722
- client;
723
- assignment;
724
- cfg;
725
- pending = [];
726
- sequenceNumber = 0;
727
- flushChain = Promise.resolve();
728
- progress;
729
- barrier = null;
730
- constructor(client, cfg, assignment) {
731
- this.client = client;
732
- this.assignment = assignment;
733
- this.cfg = {
734
- benchmarkSlug: cfg.benchmarkSlug,
735
- runId: cfg.runId,
736
- participantSlug: cfg.participantSlug,
737
- batchSize: cfg.batchSize ?? DEFAULT_REPORTER_BATCH_SIZE
738
- };
739
- this.progress = { done: 0, inFlight: 0, errors: 0, total: assignment.taskRange.count };
740
- }
741
- static async claim(cfg) {
742
- const client = createBenchmarkClient(cfg);
743
- try {
744
- const assignment = await client.claimWorker(cfg.benchmarkSlug, cfg.runId, cfg.participantSlug, {
745
- processKind: cfg.processKind,
746
- processKey: cfg.processKey
747
- });
748
- return assignment ? new _BenchmarkReporter(client, cfg, assignment) : null;
749
- } catch (error) {
750
- console.warn(
751
- `[benchsdk] failed to claim worker for ${cfg.benchmarkSlug}/${cfg.participantSlug}: ${error instanceof Error ? error.message : String(error)}`
752
- );
753
- return null;
754
- }
755
- }
756
- get workerAssignment() {
757
- return this.assignment;
758
- }
759
- get taskCount() {
760
- return this.assignment.taskRange.count;
761
- }
762
- get taskIndexStart() {
763
- return this.assignment.taskRange.start;
764
- }
765
- setProgress(progress) {
766
- this.progress = { ...progress, total: progress.total ?? this.assignment.taskRange.count };
767
- }
768
- recordResult(record) {
769
- this.pending.push(record);
770
- if (this.pending.length >= this.cfg.batchSize) void this.flush(false);
771
- }
772
- async heartbeat(input = {}) {
773
- const barrier = this.barrier;
774
- const currentStep = barrier?.step ?? input.currentStep;
775
- const concurrency = barrier?.concurrency ?? input.concurrency;
776
- await this.client.heartbeatWorker(this.cfg.benchmarkSlug, this.cfg.runId, this.assignment.workerId, {
777
- attemptId: this.assignment.attemptId,
778
- progressDone: this.progress.done,
779
- progressInFlight: this.progress.inFlight,
780
- progressErrors: this.progress.errors,
781
- progressTotal: this.progress.total,
782
- ...currentStep ? { currentStep } : {},
783
- ...concurrency ? { concurrency } : {}
784
- }).catch(() => {
785
- });
786
- }
787
- async waitForStepReady(input) {
788
- const startedAt = Date.now();
789
- const pollIntervalMs = input.pollIntervalMs ?? DEFAULT_READY_POLL_INTERVAL_MS2;
790
- const concurrency = input.concurrency ?? [{
791
- step: input.step,
792
- active: input.active ?? this.assignment.taskRange.count,
793
- target: input.target ?? this.assignment.taskRange.count
794
- }];
795
- this.barrier = { step: input.step, concurrency };
796
- try {
797
- while (true) {
798
- await this.heartbeat({ currentStep: input.step, concurrency });
799
- const progress = await this.client.getRunProgress(this.cfg.benchmarkSlug, this.cfg.runId).catch(() => null);
800
- const participant = progress?.participants.find((item) => item.slug === this.cfg.participantSlug);
801
- const step = participant?.concurrency.find((item) => item.step === input.step);
802
- if (step?.ready) {
803
- return {
804
- active: step.active,
805
- target: step.target,
806
- ready: true,
807
- measuredAt: progress?.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString()
808
- };
809
- }
810
- if (input.timeoutMs !== void 0 && Date.now() - startedAt >= input.timeoutMs) {
811
- throw new Error(`Timed out waiting for benchmark step "${input.step}" to become ready.`);
812
- }
813
- await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
814
- }
815
- } finally {
816
- this.barrier = null;
817
- }
818
- }
819
- uploadArtifact(input) {
820
- return this.client.uploadWorkerArtifact(this.cfg.benchmarkSlug, this.cfg.runId, this.assignment.workerId, {
821
- attemptId: this.assignment.attemptId,
822
- kind: input.kind,
823
- name: input.name,
824
- contentType: input.contentType,
825
- metadata: input.metadata,
826
- body: input.body
827
- }).catch((error) => {
828
- console.warn(
829
- `[benchsdk] failed to upload ${input.kind} artifact for worker ${this.assignment.workerId}: ${error instanceof Error ? error.message : String(error)}`
830
- );
831
- return null;
832
- });
833
- }
834
- flush(isFinal = false) {
835
- this.flushChain = this.flushChain.then(async () => {
836
- while (this.pending.length >= this.cfg.batchSize || isFinal && this.pending.length > 0) {
837
- const batch = this.pending.slice(0, this.cfg.batchSize);
838
- try {
839
- await this.client.sendTaskResults({
840
- benchmarkSlug: this.cfg.benchmarkSlug,
841
- runId: this.cfg.runId,
842
- workerId: this.assignment.workerId,
843
- attemptId: this.assignment.attemptId,
844
- sequenceNumber: this.sequenceNumber,
845
- isFinal: isFinal && batch.length === this.pending.length,
846
- records: batch
847
- });
848
- } catch (error) {
849
- console.warn(
850
- `[benchsdk] dropping ${this.pending.length} unsent task result(s) for worker ${this.assignment.workerId}: ${error instanceof Error ? error.message : String(error)}`
851
- );
852
- break;
853
- }
854
- this.pending.splice(0, batch.length);
855
- this.sequenceNumber += 1;
856
- }
857
- });
858
- return this.flushChain;
859
- }
860
- async finish(failed = false, error) {
861
- await this.flush(true).catch(() => {
862
- });
863
- if (failed) {
864
- await this.client.failWorker(
865
- this.cfg.benchmarkSlug,
866
- this.cfg.runId,
867
- this.assignment.workerId,
868
- this.assignment.attemptId,
869
- error
870
- ).catch(() => {
871
- });
872
- return;
873
- }
874
- await this.client.completeWorker(
875
- this.cfg.benchmarkSlug,
876
- this.cfg.runId,
877
- this.assignment.workerId,
878
- this.assignment.attemptId
879
- ).catch(() => {
880
- });
881
- }
882
- };
883
- function claimBenchmarkReporter(config) {
884
- return BenchmarkReporter.claim(config);
885
- }
886
-
887
- // src/metrics.ts
888
- var fs = __toESM(require("fs"), 1);
889
- var os = __toESM(require("os"), 1);
890
- var import_node_perf_hooks = require("perf_hooks");
891
- function readSockstat() {
892
- try {
893
- const data = fs.readFileSync("/proc/net/sockstat", "utf-8");
894
- const out = {};
895
- for (const line of data.split("\n")) {
896
- const index = line.indexOf(":");
897
- if (index < 0) continue;
898
- const section = line.slice(0, index).trim().toLowerCase();
899
- const parts = line.slice(index + 1).trim().split(/\s+/);
900
- for (let i = 0; i + 1 < parts.length; i += 2) {
901
- const value = Number.parseInt(parts[i + 1], 10);
902
- if (!Number.isNaN(value)) out[`${section}_${parts[i]}`] = value;
903
- }
904
- }
905
- return out;
906
- } catch {
907
- return null;
908
- }
909
- }
910
- function countOpenFds() {
911
- try {
912
- return fs.readdirSync("/proc/self/fd").length;
913
- } catch {
914
- return null;
915
- }
916
- }
917
- function createSystemMetricsCollector() {
918
- const startedAt = Date.now();
919
- const cpuBaseline = process.cpuUsage();
920
- const eventLoop = (0, import_node_perf_hooks.monitorEventLoopDelay)({ resolution: 20 });
921
- eventLoop.enable();
38
+ const apiClient = (0, import_api.createBenchmarkClient)(config);
922
39
  return {
923
- sample() {
924
- const cpu = process.cpuUsage(cpuBaseline);
925
- const memory = process.memoryUsage();
926
- const loadavg2 = os.loadavg();
927
- const sample = {
928
- ts: (/* @__PURE__ */ new Date()).toISOString(),
929
- uptimeMs: Date.now() - startedAt,
930
- cpuUserUs: cpu.user,
931
- cpuSystemUs: cpu.system,
932
- memRssMb: Math.round(memory.rss / 1024 / 1024),
933
- memHeapUsedMb: Math.round(memory.heapUsed / 1024 / 1024),
934
- memHeapTotalMb: Math.round(memory.heapTotal / 1024 / 1024),
935
- memExternalMb: Math.round(memory.external / 1024 / 1024),
936
- eventLoopP50Ms: eventLoop.percentile(50) / 1e6,
937
- eventLoopP99Ms: eventLoop.percentile(99) / 1e6,
938
- eventLoopMaxMs: eventLoop.max / 1e6,
939
- loadavg1m: loadavg2[0],
940
- loadavg5m: loadavg2[1],
941
- loadavg15m: loadavg2[2],
942
- openFds: countOpenFds(),
943
- sockstat: readSockstat()
944
- };
945
- eventLoop.reset();
946
- return sample;
947
- },
948
- stop() {
949
- eventLoop.disable();
950
- }
40
+ ...apiClient,
41
+ runWorker: (options) => (0, import_worker.runWorker)(apiClient, options)
951
42
  };
952
43
  }
953
44
 
954
- // src/participants.ts
955
- function filterParticipantsByEnv(participants) {
956
- const available = [];
957
- const skipped = [];
958
- for (const p of participants) {
959
- const missing = p.requiredEnvVars.filter((v) => !process.env[v]);
960
- if (missing.length > 0) {
961
- skipped.push({ name: p.name, missing });
962
- } else {
963
- available.push(p);
964
- }
965
- }
966
- return { available, skipped };
967
- }
968
- function selectParticipants(all, names) {
969
- if (!names) return all;
970
- const unknown = names.filter((n) => !all.some((p) => p.name === n));
971
- if (unknown.length > 0) {
972
- console.error(`Unknown participant(s): ${unknown.join(", ")}`);
973
- console.error(`Available: ${all.map((p) => p.name).join(", ")}`);
974
- process.exit(1);
975
- }
976
- return all.filter((p) => names.includes(p.name));
977
- }
45
+ // src/index.ts
46
+ var import_worker2 = require("@benchsdk/worker");
978
47
  // Annotate the CommonJS export names for ESM import in node:
979
48
  0 && (module.exports = {
980
49
  BenchmarkApiError,
@@ -983,6 +52,7 @@ function selectParticipants(all, names) {
983
52
  createBenchmarkClient,
984
53
  createSystemMetricsCollector,
985
54
  filterParticipantsByEnv,
55
+ runWorker,
986
56
  selectParticipants
987
57
  });
988
58
  //# sourceMappingURL=index.cjs.map