@benchsdk/client 0.2.1 → 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.js CHANGED
@@ -1,957 +1,31 @@
1
1
  // src/client.ts
2
- var DEFAULT_BASE_URL = "https://platform.computesdk.com/api/v1";
3
- var DEFAULT_BATCH_SIZE = 1e3;
4
- var DEFAULT_HEARTBEAT_INTERVAL_MS = 3e4;
5
- var DEFAULT_FLUSH_INTERVAL_MS = 3e4;
6
- var DEFAULT_READY_POLL_INTERVAL_MS = 1e3;
7
- var MAX_TASK_RESULT_RECORDS = 5e3;
8
- var MAX_TASK_RECORD_STEPS = 100;
9
- var MAX_HEARTBEAT_CONCURRENCY_SAMPLES = 20;
10
- var BenchmarkApiError = class extends Error {
11
- constructor(message, status, body) {
12
- super(message);
13
- this.status = status;
14
- this.body = body;
15
- this.name = "BenchmarkApiError";
16
- }
17
- status;
18
- body;
19
- };
20
- function trimTrailingSlash(value) {
21
- return value.replace(/\/+$/, "");
22
- }
23
- function encodePath(value) {
24
- return encodeURIComponent(value);
25
- }
26
- function queryString(input) {
27
- const params = new URLSearchParams();
28
- for (const [key, value2] of Object.entries(input)) {
29
- if (value2 !== void 0) params.set(key, String(value2));
30
- }
31
- const value = params.toString();
32
- return value ? `?${value}` : "";
33
- }
34
- function getApiKey(input) {
35
- return input ?? (typeof process !== "undefined" ? process.env.COMPUTESDK_ADMIN_API_KEY ?? process.env.COMPUTESDK_API_KEY : void 0);
36
- }
37
- function getErrorCode(error) {
38
- if (error instanceof Error && error.name) return error.name;
39
- return "ERROR";
40
- }
41
- function toJsonObject(value) {
42
- if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
43
- return value;
44
- }
45
- function validateTaskResults(input) {
46
- if (input.records.length > MAX_TASK_RESULT_RECORDS) {
47
- throw new Error(`Benchmark task result batches are limited to ${MAX_TASK_RESULT_RECORDS} records.`);
48
- }
49
- for (const record of input.records) {
50
- if ((record.steps?.length ?? 0) > MAX_TASK_RECORD_STEPS) {
51
- throw new Error(`Benchmark task result records are limited to ${MAX_TASK_RECORD_STEPS} steps.`);
52
- }
53
- }
54
- }
55
- function validateHeartbeat(input) {
56
- const concurrency = input.concurrency ?? [];
57
- if (concurrency.length > MAX_HEARTBEAT_CONCURRENCY_SAMPLES) {
58
- throw new Error(`Benchmark heartbeat concurrency is limited to ${MAX_HEARTBEAT_CONCURRENCY_SAMPLES} samples.`);
59
- }
60
- const steps = /* @__PURE__ */ new Set();
61
- for (const sample of concurrency) {
62
- if (steps.has(sample.step)) {
63
- throw new Error(`Benchmark heartbeat concurrency step values must be unique per heartbeat.`);
64
- }
65
- steps.add(sample.step);
66
- }
67
- }
68
- function validatePositiveInteger(name, value) {
69
- if (!Number.isInteger(value) || value <= 0) {
70
- throw new Error(`Benchmark ${name} must be a positive integer.`);
71
- }
72
- }
73
- function validateBatchSize(value) {
74
- validatePositiveInteger("batchSize", value);
75
- if (value > MAX_TASK_RESULT_RECORDS) {
76
- throw new Error(`Benchmark batchSize must be at most ${MAX_TASK_RESULT_RECORDS}.`);
77
- }
78
- }
79
- function normalizeArtifacts(data) {
80
- return data.items ?? data.artifacts ?? [];
81
- }
82
- function bodySizeBytes(body) {
83
- if (typeof body === "string") return new TextEncoder().encode(body).byteLength;
84
- if (body instanceof Blob) return body.size;
85
- if (body instanceof ArrayBuffer) return body.byteLength;
86
- if (ArrayBuffer.isView(body)) return body.byteLength;
87
- if (body instanceof URLSearchParams) return new TextEncoder().encode(body.toString()).byteLength;
88
- return void 0;
89
- }
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
- function sleep(ms) {
98
- return new Promise((resolve) => setTimeout(resolve, ms));
99
- }
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
- async function mapPool(items, concurrency, fn) {
138
- let nextIndex = 0;
139
- const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
140
- while (nextIndex < items.length) {
141
- const item = items[nextIndex];
142
- nextIndex += 1;
143
- await fn(item);
144
- }
145
- });
146
- await Promise.all(workers);
147
- }
2
+ import { createBenchmarkClient as createApiClient, BenchmarkApiError } from "@benchsdk/api";
3
+ import { runWorker } from "@benchsdk/worker";
148
4
  function createBenchmarkClient(config = {}) {
149
- const baseUrl = trimTrailingSlash(config.baseUrl ?? DEFAULT_BASE_URL);
150
- const apiKey = getApiKey(config.apiKey);
151
- const fetchImpl = config.fetch ?? (typeof fetch !== "undefined" ? fetch : void 0);
152
- if (!fetchImpl) {
153
- throw new Error("fetch is not available");
154
- }
155
- const doFetch = fetchImpl;
156
- async function request(method, path, body) {
157
- const headers = { "Content-Type": "application/json" };
158
- if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
159
- const response = await doFetch(`${baseUrl}${path}`, {
160
- method,
161
- headers,
162
- body: body === void 0 ? void 0 : JSON.stringify(body)
163
- });
164
- const text = await response.text();
165
- if (!response.ok) {
166
- throw new BenchmarkApiError(
167
- `Benchmark API request failed: ${response.status} ${response.statusText}`,
168
- response.status,
169
- text
170
- );
171
- }
172
- return text ? JSON.parse(text) : {};
173
- }
174
- async function sendTaskResults(input) {
175
- if (input.records.length === 0) {
176
- return {};
177
- }
178
- validateTaskResults(input);
179
- return request(
180
- "POST",
181
- `/benchmarks/${encodePath(input.benchmarkSlug)}/runs/${encodePath(input.runId)}/workers/${encodePath(input.workerId)}/events`,
182
- {
183
- type: "task_results",
184
- attemptId: input.attemptId,
185
- sequenceNumber: input.sequenceNumber,
186
- isFinal: input.isFinal,
187
- records: input.records
188
- }
189
- );
190
- }
191
- async function updateWorkerLifecycle(action, benchmarkSlug, runId, workerId, attemptId, extra) {
192
- return request(
193
- "POST",
194
- `/benchmarks/${encodePath(benchmarkSlug)}/runs/${encodePath(runId)}/workers/${encodePath(workerId)}/${action}`,
195
- { attemptId, ...extra ?? {} }
196
- );
197
- }
198
- const client = {
199
- async upsertBenchmark(slug, input) {
200
- const data = await request("PUT", `/benchmarks/${encodePath(slug)}`, input);
201
- return data.benchmark;
202
- },
203
- async getBenchmark(slug) {
204
- const data = await request("GET", `/benchmarks/${encodePath(slug)}`);
205
- return data.benchmark;
206
- },
207
- async updateBenchmark(slug, input) {
208
- const data = await request("PATCH", `/benchmarks/${encodePath(slug)}`, input);
209
- return data.benchmark;
210
- },
211
- async listBenchmarks() {
212
- const data = await request("GET", "/benchmarks");
213
- return data.items ?? data.benchmarks ?? [];
214
- },
215
- async createRun(benchmarkSlug, input) {
216
- return request(
217
- "POST",
218
- `/benchmarks/${encodePath(benchmarkSlug)}/runs`,
219
- input
220
- );
221
- },
222
- async listRuns(benchmarkSlug) {
223
- const data = await request("GET", `/benchmarks/${encodePath(benchmarkSlug)}/runs`);
224
- return data.items;
225
- },
226
- async getRun(benchmarkSlug, runId) {
227
- const data = await request("GET", `/benchmarks/${encodePath(benchmarkSlug)}/runs/${encodePath(runId)}`);
228
- return data.run;
229
- },
230
- async updateRun(benchmarkSlug, runId, input) {
231
- const data = await request(
232
- "PATCH",
233
- `/benchmarks/${encodePath(benchmarkSlug)}/runs/${encodePath(runId)}`,
234
- input
235
- );
236
- return data.run;
237
- },
238
- async upsertParticipant(benchmarkSlug, runId, participantSlug, input = {}) {
239
- const data = await request(
240
- "PUT",
241
- `/benchmarks/${encodePath(benchmarkSlug)}/runs/${encodePath(runId)}/participants/${encodePath(participantSlug)}`,
242
- input
243
- );
244
- return data.participant;
245
- },
246
- async listParticipants(benchmarkSlug, runId) {
247
- const data = await request(
248
- "GET",
249
- `/benchmarks/${encodePath(benchmarkSlug)}/runs/${encodePath(runId)}/participants`
250
- );
251
- return data.items ?? data.participants ?? [];
252
- },
253
- async getParticipant(benchmarkSlug, runId, participantSlug) {
254
- const data = await request(
255
- "GET",
256
- `/benchmarks/${encodePath(benchmarkSlug)}/runs/${encodePath(runId)}/participants/${encodePath(participantSlug)}`
257
- );
258
- return data.participant;
259
- },
260
- async updateParticipant(benchmarkSlug, runId, participantSlug, input) {
261
- const data = await request(
262
- "PATCH",
263
- `/benchmarks/${encodePath(benchmarkSlug)}/runs/${encodePath(runId)}/participants/${encodePath(participantSlug)}`,
264
- input
265
- );
266
- return data.participant;
267
- },
268
- async getRunProgress(benchmarkSlug, runId) {
269
- return request(
270
- "GET",
271
- `/benchmarks/${encodePath(benchmarkSlug)}/runs/${encodePath(runId)}/progress`
272
- );
273
- },
274
- async listWorkers(benchmarkSlug, runId, participantSlug) {
275
- const data = await request(
276
- "GET",
277
- `/benchmarks/${encodePath(benchmarkSlug)}/runs/${encodePath(runId)}/participants/${encodePath(participantSlug)}/workers`
278
- );
279
- return data.items ?? data.workers ?? [];
280
- },
281
- async planWorkers(benchmarkSlug, runId, participantSlug, input = {}) {
282
- const data = await request(
283
- "POST",
284
- `/benchmarks/${encodePath(benchmarkSlug)}/runs/${encodePath(runId)}/participants/${encodePath(participantSlug)}/workers`,
285
- input
286
- );
287
- return data.items ?? data.workers ?? [];
288
- },
289
- async getWorker(benchmarkSlug, runId, workerId) {
290
- const data = await request(
291
- "GET",
292
- `/benchmarks/${encodePath(benchmarkSlug)}/runs/${encodePath(runId)}/workers/${encodePath(workerId)}`
293
- );
294
- return data.worker;
295
- },
296
- async updateWorker(benchmarkSlug, runId, workerId, input) {
297
- const data = await request(
298
- "PATCH",
299
- `/benchmarks/${encodePath(benchmarkSlug)}/runs/${encodePath(runId)}/workers/${encodePath(workerId)}`,
300
- input
301
- );
302
- return data.worker;
303
- },
304
- async claimWorker(benchmarkSlug, runId, participantSlug, input = {}) {
305
- const data = await request(
306
- "POST",
307
- `/benchmarks/${encodePath(benchmarkSlug)}/runs/${encodePath(runId)}/participants/${encodePath(participantSlug)}/workers/claim`,
308
- input
309
- );
310
- return data.assignment;
311
- },
312
- sendTaskResults,
313
- async heartbeatWorker(benchmarkSlug, runId, workerId, input) {
314
- validateHeartbeat(input);
315
- const { attemptId, ...extra } = input;
316
- if (extra.currentStep == null) {
317
- delete extra.currentStep;
318
- }
319
- return updateWorkerLifecycle("heartbeat", benchmarkSlug, runId, workerId, attemptId, extra);
320
- },
321
- releaseWorker(benchmarkSlug, runId, workerId, attemptId) {
322
- return updateWorkerLifecycle("release", benchmarkSlug, runId, workerId, attemptId);
323
- },
324
- completeWorker(benchmarkSlug, runId, workerId, attemptId) {
325
- return updateWorkerLifecycle("complete", benchmarkSlug, runId, workerId, attemptId);
326
- },
327
- failWorker(benchmarkSlug, runId, workerId, attemptId, error) {
328
- return updateWorkerLifecycle("fail", benchmarkSlug, runId, workerId, attemptId, {
329
- errorCode: getErrorCode(error),
330
- errorMessage: error instanceof Error ? error.message : String(error ?? "Unknown error")
331
- });
332
- },
333
- async createWorkerArtifact(benchmarkSlug, runId, workerId, input) {
334
- return request(
335
- "POST",
336
- `/benchmarks/${encodePath(benchmarkSlug)}/runs/${encodePath(runId)}/workers/${encodePath(workerId)}/artifacts`,
337
- input
338
- );
339
- },
340
- async uploadWorkerArtifact(benchmarkSlug, runId, workerId, input) {
341
- const sizeBytes = bodySizeBytes(input.body);
342
- const artifactInput = {
343
- attemptId: input.attemptId,
344
- kind: input.kind,
345
- contentType: input.contentType,
346
- name: input.name,
347
- metadata: sizeBytes === void 0 ? input.metadata : { ...input.metadata, sizeBytes }
348
- };
349
- const response = await client.createWorkerArtifact(benchmarkSlug, runId, workerId, artifactInput);
350
- const uploadUrl = response.uploadUrl ?? response.artifact?.uploadUrl;
351
- if (!uploadUrl) {
352
- throw new Error("Benchmark artifact upload URL is missing.");
353
- }
354
- const uploadResponse = await doFetch(uploadUrl, {
355
- method: "PUT",
356
- headers: input.contentType ? { "Content-Type": input.contentType } : void 0,
357
- body: input.body
358
- });
359
- if (!uploadResponse.ok) {
360
- const errorBody = await uploadResponse.text().catch(() => "");
361
- throw new BenchmarkApiError(
362
- `Benchmark artifact upload failed with ${uploadResponse.status}`,
363
- uploadResponse.status,
364
- errorBody
365
- );
366
- }
367
- return response;
368
- },
369
- async listRunArtifacts(benchmarkSlug, runId) {
370
- const data = await request(
371
- "GET",
372
- `/benchmarks/${encodePath(benchmarkSlug)}/runs/${encodePath(runId)}/artifacts`
373
- );
374
- return normalizeArtifacts(data);
375
- },
376
- async listWorkerArtifacts(benchmarkSlug, runId, workerId) {
377
- const data = await request(
378
- "GET",
379
- `/benchmarks/${encodePath(benchmarkSlug)}/runs/${encodePath(runId)}/workers/${encodePath(workerId)}/artifacts`
380
- );
381
- return normalizeArtifacts(data);
382
- },
383
- async getBenchmarkResults(benchmarkSlug, input = {}) {
384
- return request(
385
- "GET",
386
- `/benchmarks/${encodePath(benchmarkSlug)}/results${queryString({ limit: input.limit })}`
387
- );
388
- },
389
- async getRunResults(benchmarkSlug, runId) {
390
- return request(
391
- "GET",
392
- `/benchmarks/${encodePath(benchmarkSlug)}/runs/${encodePath(runId)}/results`
393
- );
394
- },
395
- async getRunTaskResults(benchmarkSlug, runId, input = {}) {
396
- return request(
397
- "GET",
398
- `/benchmarks/${encodePath(benchmarkSlug)}/runs/${encodePath(runId)}/results/tasks${queryString({ bucketSize: input.bucketSize, failureLimit: input.failureLimit })}`
399
- );
400
- },
401
- async getRunTimeline(benchmarkSlug, runId, input = {}) {
402
- return request(
403
- "GET",
404
- `/benchmarks/${encodePath(benchmarkSlug)}/runs/${encodePath(runId)}/results/timeline${queryString({ bucketMs: input.bucketMs })}`
405
- );
406
- },
407
- async getRunImports(benchmarkSlug, runId) {
408
- return request(
409
- "GET",
410
- `/benchmarks/${encodePath(benchmarkSlug)}/runs/${encodePath(runId)}/results/imports`
411
- );
412
- },
413
- async runWorker(options) {
414
- if (options.concurrency !== void 0) validatePositiveInteger("concurrency", options.concurrency);
415
- if (options.batchSize !== void 0) validateBatchSize(options.batchSize);
416
- if (options.flushIntervalMs !== void 0) validatePositiveInteger("flushIntervalMs", options.flushIntervalMs);
417
- const assignment = await client.claimWorker(options.benchmarkSlug, options.runId, options.participantSlug, {
418
- processKind: options.processKind,
419
- processKey: options.processKey
420
- });
421
- if (!assignment) return { assignment: null, records: [] };
422
- const claimed = assignment;
423
- let sequenceNumber = 0;
424
- const records = [];
425
- const pending = [];
426
- const batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
427
- const workerConcurrency = options.concurrency ?? claimed.targetConcurrency;
428
- validatePositiveInteger("concurrency", workerConcurrency);
429
- const taskIndices = Array.from({ length: claimed.taskRange.count }, (_, index) => claimed.taskRange.start + index);
430
- const activeByStep = /* @__PURE__ */ new Map();
431
- const targetByStep = /* @__PURE__ */ new Map();
432
- const readyWaitByStep = /* @__PURE__ */ new Map();
433
- let doneCount = 0;
434
- let errorCount = 0;
435
- let inFlightCount = 0;
436
- let flushChain = Promise.resolve();
437
- function concurrencySamples() {
438
- return Array.from(activeByStep.entries()).filter(([, active]) => active > 0).map(([step, active]) => ({
439
- step,
440
- active,
441
- target: targetByStep.get(step) ?? workerConcurrency
442
- })).sort((a, b) => b.active - a.active).slice(0, MAX_HEARTBEAT_CONCURRENCY_SAMPLES);
443
- }
444
- async function sendHeartbeat() {
445
- const concurrency = concurrencySamples();
446
- const step = concurrency[0]?.step ?? null;
447
- await client.heartbeatWorker(options.benchmarkSlug, options.runId, claimed.workerId, {
448
- attemptId: claimed.attemptId,
449
- progressDone: doneCount,
450
- progressInFlight: inFlightCount,
451
- progressErrors: errorCount,
452
- progressTotal: taskIndices.length,
453
- ...step ? { currentStep: step } : {},
454
- concurrency
455
- });
456
- }
457
- let heartbeatInFlight = null;
458
- let heartbeatRequested = false;
459
- function requestHeartbeat() {
460
- heartbeatRequested = true;
461
- if (heartbeatInFlight) return;
462
- heartbeatInFlight = (async () => {
463
- while (heartbeatRequested) {
464
- heartbeatRequested = false;
465
- await sendHeartbeat().catch(() => {
466
- });
467
- }
468
- })().finally(() => {
469
- heartbeatInFlight = null;
470
- if (heartbeatRequested) requestHeartbeat();
471
- });
472
- }
473
- async function pollStepReady(stepName, stepOptions) {
474
- const startedAt = Date.now();
475
- const pollInterval = stepOptions.readyPollIntervalMs ?? options.readyPollIntervalMs ?? DEFAULT_READY_POLL_INTERVAL_MS;
476
- while (true) {
477
- const progress = await client.getRunProgress(options.benchmarkSlug, options.runId);
478
- const participant = progress.participants.find((item) => item.slug === options.participantSlug);
479
- const step = participant?.concurrency.find((item) => item.step === stepName);
480
- if (step?.ready) return;
481
- if (typeof stepOptions.readyTimeoutMs === "number" && Date.now() - startedAt >= stepOptions.readyTimeoutMs) {
482
- throw new Error(`Timed out waiting for benchmark step "${stepName}" to become ready.`);
483
- }
484
- await sleep(pollInterval);
485
- }
486
- }
487
- async function waitForStepReady(stepName, stepOptions) {
488
- const existing = readyWaitByStep.get(stepName);
489
- if (existing) return existing;
490
- const wait = pollStepReady(stepName, stepOptions).finally(() => {
491
- if (readyWaitByStep.get(stepName) === wait) readyWaitByStep.delete(stepName);
492
- });
493
- readyWaitByStep.set(stepName, wait);
494
- return wait;
495
- }
496
- const heartbeat = setInterval(() => {
497
- requestHeartbeat();
498
- }, options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS);
499
- heartbeat.unref?.();
500
- async function flush(isFinal, force = false) {
501
- flushChain = flushChain.then(async () => {
502
- if (force && doneCount >= taskIndices.length) return;
503
- while (pending.length >= batchSize || (isFinal || force) && pending.length > 0) {
504
- const batch = pending.splice(0, batchSize);
505
- await sendTaskResults({
506
- benchmarkSlug: options.benchmarkSlug,
507
- runId: options.runId,
508
- workerId: claimed.workerId,
509
- attemptId: claimed.attemptId,
510
- sequenceNumber,
511
- isFinal: isFinal && pending.length === 0,
512
- records: batch
513
- });
514
- sequenceNumber += 1;
515
- }
516
- });
517
- await flushChain;
518
- }
519
- const resultFlush = setInterval(() => {
520
- if (doneCount < taskIndices.length) void flush(false, true).catch(() => {
521
- });
522
- }, options.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS);
523
- resultFlush.unref?.();
524
- async function runFinishHook(status) {
525
- await options.onFinish?.({
526
- assignment: claimed,
527
- records,
528
- status,
529
- client,
530
- uploadArtifact(input) {
531
- return client.uploadWorkerArtifact(options.benchmarkSlug, options.runId, claimed.workerId, {
532
- ...input,
533
- attemptId: claimed.attemptId
534
- });
535
- }
536
- });
537
- }
538
- try {
539
- await sendHeartbeat().catch(() => {
540
- });
541
- await mapPool(taskIndices, workerConcurrency, async (taskIndex) => {
542
- inFlightCount += 1;
543
- const startedAtDate = /* @__PURE__ */ new Date();
544
- const startedAtMs = Date.now();
545
- const record = {
546
- taskIndex,
547
- status: "success",
548
- startedAt: startedAtDate.toISOString()
549
- };
550
- const steps = [];
551
- async function step(name, fn, stepOptions = {}) {
552
- const stepStartedAtMs = Date.now();
553
- const stepRecord = {
554
- name,
555
- status: "success",
556
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
557
- completedAt: (/* @__PURE__ */ new Date()).toISOString(),
558
- latencyMs: 0
559
- };
560
- const shouldReportConcurrency = stepOptions.reportConcurrency ?? true;
561
- if (shouldReportConcurrency) {
562
- const stepConcurrency = stepOptions.concurrency ?? workerConcurrency;
563
- validatePositiveInteger(`step "${name}" concurrency`, stepConcurrency);
564
- targetByStep.set(name, stepConcurrency);
565
- activeByStep.set(name, (activeByStep.get(name) ?? 0) + 1);
566
- requestHeartbeat();
567
- }
568
- try {
569
- if (stepOptions.readiness === "poll") {
570
- await waitForStepReady(name, stepOptions);
571
- }
572
- return await fn();
573
- } catch (error) {
574
- stepRecord.status = "error";
575
- stepRecord.errorCode = getErrorCode(error);
576
- throw error;
577
- } finally {
578
- stepRecord.completedAt = (/* @__PURE__ */ new Date()).toISOString();
579
- stepRecord.latencyMs = Date.now() - stepStartedAtMs;
580
- steps.push(stepRecord);
581
- if (shouldReportConcurrency) {
582
- const nextActive = Math.max(0, (activeByStep.get(name) ?? 0) - 1);
583
- if (nextActive === 0) {
584
- activeByStep.delete(name);
585
- targetByStep.delete(name);
586
- } else {
587
- activeByStep.set(name, nextActive);
588
- }
589
- requestHeartbeat();
590
- }
591
- }
592
- }
593
- try {
594
- const data = await runWorkerTask(options.task, { assignment: claimed, taskIndex, step });
595
- record.data = toJsonObject(data);
596
- } catch (error) {
597
- record.status = "error";
598
- record.errorCode = getErrorCode(error);
599
- record.data = { errorMessage: error instanceof Error ? error.message : String(error) };
600
- } finally {
601
- record.completedAt = (/* @__PURE__ */ new Date()).toISOString();
602
- record.latencyMs = Date.now() - startedAtMs;
603
- record.steps = steps.length > 0 ? steps : void 0;
604
- doneCount += 1;
605
- inFlightCount = Math.max(0, inFlightCount - 1);
606
- if (record.status !== "success") errorCount += 1;
607
- }
608
- records.push(record);
609
- pending.push(record);
610
- options.onResult?.(record);
611
- if (pending.length >= batchSize) await flush(false);
612
- });
613
- await flush(true);
614
- const hasErrors = records.some((record) => record.status !== "success");
615
- try {
616
- await runFinishHook(hasErrors ? "error" : "success");
617
- } catch (error) {
618
- if (!hasErrors) throw error;
619
- }
620
- if (hasErrors) {
621
- await client.failWorker(options.benchmarkSlug, options.runId, claimed.workerId, claimed.attemptId, new Error("One or more tasks failed"));
622
- } else {
623
- await client.completeWorker(options.benchmarkSlug, options.runId, claimed.workerId, claimed.attemptId);
624
- }
625
- return { assignment: claimed, records };
626
- } catch (error) {
627
- await flush(true).catch(() => {
628
- });
629
- await runFinishHook("error").catch(() => {
630
- });
631
- await client.failWorker(options.benchmarkSlug, options.runId, claimed.workerId, claimed.attemptId, error).catch(() => {
632
- });
633
- throw error;
634
- } finally {
635
- clearInterval(heartbeat);
636
- clearInterval(resultFlush);
637
- }
638
- }
639
- };
640
- return client;
641
- }
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();
5
+ const apiClient = createApiClient(config);
674
6
  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
- }
7
+ ...apiClient,
8
+ runWorker: (options) => runWorker(apiClient, options)
691
9
  };
692
10
  }
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
-
721
- // src/reporter.ts
722
- var DEFAULT_REPORTER_BATCH_SIZE = 500;
723
- var DEFAULT_READY_POLL_INTERVAL_MS2 = 1e3;
724
- var BenchmarkReporter = class _BenchmarkReporter {
725
- client;
726
- assignment;
727
- cfg;
728
- pending = [];
729
- sequenceNumber = 0;
730
- flushChain = Promise.resolve();
731
- progress;
732
- barrier = null;
733
- constructor(client, cfg, assignment) {
734
- this.client = client;
735
- this.assignment = assignment;
736
- this.cfg = {
737
- benchmarkSlug: cfg.benchmarkSlug,
738
- runId: cfg.runId,
739
- participantSlug: cfg.participantSlug,
740
- batchSize: cfg.batchSize ?? DEFAULT_REPORTER_BATCH_SIZE
741
- };
742
- this.progress = { done: 0, inFlight: 0, errors: 0, total: assignment.taskRange.count };
743
- }
744
- static async claim(cfg) {
745
- const client = createBenchmarkClient(cfg);
746
- try {
747
- const assignment = await client.claimWorker(cfg.benchmarkSlug, cfg.runId, cfg.participantSlug, {
748
- processKind: cfg.processKind,
749
- processKey: cfg.processKey
750
- });
751
- return assignment ? new _BenchmarkReporter(client, cfg, assignment) : null;
752
- } catch {
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(() => null);
828
- }
829
- flush(isFinal = false) {
830
- this.flushChain = this.flushChain.then(async () => {
831
- while (this.pending.length >= this.cfg.batchSize || isFinal && this.pending.length > 0) {
832
- const batch = this.pending.slice(0, this.cfg.batchSize);
833
- try {
834
- await this.client.sendTaskResults({
835
- benchmarkSlug: this.cfg.benchmarkSlug,
836
- runId: this.cfg.runId,
837
- workerId: this.assignment.workerId,
838
- attemptId: this.assignment.attemptId,
839
- sequenceNumber: this.sequenceNumber,
840
- isFinal: isFinal && batch.length === this.pending.length,
841
- records: batch
842
- });
843
- } catch {
844
- break;
845
- }
846
- this.pending.splice(0, batch.length);
847
- this.sequenceNumber += 1;
848
- }
849
- });
850
- return this.flushChain;
851
- }
852
- async finish(failed = false, error) {
853
- await this.flush(true).catch(() => {
854
- });
855
- if (failed) {
856
- await this.client.failWorker(
857
- this.cfg.benchmarkSlug,
858
- this.cfg.runId,
859
- this.assignment.workerId,
860
- this.assignment.attemptId,
861
- error
862
- ).catch(() => {
863
- });
864
- return;
865
- }
866
- await this.client.completeWorker(
867
- this.cfg.benchmarkSlug,
868
- this.cfg.runId,
869
- this.assignment.workerId,
870
- this.assignment.attemptId
871
- ).catch(() => {
872
- });
873
- }
874
- };
875
- function claimBenchmarkReporter(config) {
876
- return BenchmarkReporter.claim(config);
877
- }
878
11
 
879
- // src/metrics.ts
880
- import * as fs from "fs";
881
- import * as os from "os";
882
- import { monitorEventLoopDelay } from "perf_hooks";
883
- function readSockstat() {
884
- try {
885
- const data = fs.readFileSync("/proc/net/sockstat", "utf-8");
886
- const out = {};
887
- for (const line of data.split("\n")) {
888
- const index = line.indexOf(":");
889
- if (index < 0) continue;
890
- const section = line.slice(0, index).trim().toLowerCase();
891
- const parts = line.slice(index + 1).trim().split(/\s+/);
892
- for (let i = 0; i + 1 < parts.length; i += 2) {
893
- const value = Number.parseInt(parts[i + 1], 10);
894
- if (!Number.isNaN(value)) out[`${section}_${parts[i]}`] = value;
895
- }
896
- }
897
- return out;
898
- } catch {
899
- return null;
900
- }
901
- }
902
- function countOpenFds() {
903
- try {
904
- return fs.readdirSync("/proc/self/fd").length;
905
- } catch {
906
- return null;
907
- }
908
- }
909
- function createSystemMetricsCollector() {
910
- const startedAt = Date.now();
911
- const cpuBaseline = process.cpuUsage();
912
- const eventLoop = monitorEventLoopDelay({ resolution: 20 });
913
- eventLoop.enable();
914
- return {
915
- sample() {
916
- const cpu = process.cpuUsage(cpuBaseline);
917
- const memory = process.memoryUsage();
918
- const loadavg2 = os.loadavg();
919
- const sample = {
920
- ts: (/* @__PURE__ */ new Date()).toISOString(),
921
- uptimeMs: Date.now() - startedAt,
922
- cpuUserUs: cpu.user,
923
- cpuSystemUs: cpu.system,
924
- memRssMb: Math.round(memory.rss / 1024 / 1024),
925
- memHeapUsedMb: Math.round(memory.heapUsed / 1024 / 1024),
926
- memHeapTotalMb: Math.round(memory.heapTotal / 1024 / 1024),
927
- memExternalMb: Math.round(memory.external / 1024 / 1024),
928
- eventLoopP50Ms: eventLoop.percentile(50) / 1e6,
929
- eventLoopP99Ms: eventLoop.percentile(99) / 1e6,
930
- eventLoopMaxMs: eventLoop.max / 1e6,
931
- loadavg1m: loadavg2[0],
932
- loadavg5m: loadavg2[1],
933
- loadavg15m: loadavg2[2],
934
- openFds: countOpenFds(),
935
- sockstat: readSockstat()
936
- };
937
- eventLoop.reset();
938
- return sample;
939
- },
940
- stop() {
941
- eventLoop.disable();
942
- }
943
- };
944
- }
12
+ // src/index.ts
13
+ import {
14
+ BenchmarkReporter,
15
+ claimBenchmarkReporter,
16
+ createSystemMetricsCollector,
17
+ filterParticipantsByEnv,
18
+ runWorker as runWorker2,
19
+ selectParticipants
20
+ } from "@benchsdk/worker";
945
21
  export {
946
22
  BenchmarkApiError,
947
23
  BenchmarkReporter,
948
24
  claimBenchmarkReporter,
949
25
  createBenchmarkClient,
950
26
  createSystemMetricsCollector,
951
- defineBench,
952
- defineStep,
953
- defineTask,
954
- defineWorker,
955
- runBenchmarkWorker
27
+ filterParticipantsByEnv,
28
+ runWorker2 as runWorker,
29
+ selectParticipants
956
30
  };
957
31
  //# sourceMappingURL=index.js.map