@benchsdk/client 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +293 -0
- package/dist/index.cjs +1003 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +727 -0
- package/dist/index.d.ts +727 -0
- package/dist/index.js +957 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
package/README.md
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# @benchsdk/client
|
|
2
|
+
|
|
3
|
+
Client and worker helpers for the ComputeSDK benchmark orchestrator.
|
|
4
|
+
|
|
5
|
+
This package talks to the platform-owned benchmark/run/participant/worker API. It does not mint canonical run, worker, attempt, event, or task IDs. Workers claim platform-assigned work, execute task indexes in their assigned range, and send `task_results` batches back to the platform.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @benchsdk/client
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Define A Worker
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { defineStep, defineTask, defineWorker } from '@benchsdk/client';
|
|
17
|
+
import { compute } from 'computesdk';
|
|
18
|
+
|
|
19
|
+
const worker = defineWorker({
|
|
20
|
+
benchmarkSlug: 'scale',
|
|
21
|
+
runId: process.env.BENCHMARK_RUN_ID!,
|
|
22
|
+
participantSlug: 'e2b',
|
|
23
|
+
processKind: 'container',
|
|
24
|
+
processKey: process.env.HOSTNAME,
|
|
25
|
+
concurrency: 100,
|
|
26
|
+
task: defineTask('sandbox.lifecycle', [
|
|
27
|
+
defineStep('create', async ({ assignment, state }) => {
|
|
28
|
+
state.sandbox = await compute.sandbox.create({
|
|
29
|
+
provider: assignment.provider ?? 'e2b',
|
|
30
|
+
});
|
|
31
|
+
}),
|
|
32
|
+
defineStep('readiness', async ({ state }) => {
|
|
33
|
+
await (state.sandbox as any).runCommand('true');
|
|
34
|
+
}),
|
|
35
|
+
defineStep('exec.first-command', async ({ state }) => {
|
|
36
|
+
await (state.sandbox as any).runCommand('node -v');
|
|
37
|
+
}),
|
|
38
|
+
defineStep('pause', { readiness: 'poll' }, async () => {
|
|
39
|
+
// Every worker reports active pause concurrency and waits here until
|
|
40
|
+
// the platform reports the participant's pause step is ready.
|
|
41
|
+
}),
|
|
42
|
+
defineStep('destroy', async ({ state }) => {
|
|
43
|
+
await (state.sandbox as any).destroy();
|
|
44
|
+
}),
|
|
45
|
+
]),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
await worker.run();
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`worker.run()` claims the next pending platform assignment for the participant. If no work is available, it returns `{ assignment: null, records: [] }`.
|
|
52
|
+
|
|
53
|
+
Task results are flushed to the platform in batches of 1,000 records by default. Set `batchSize` to tune this per worker; the SDK validates the platform limit of 5,000 records per batch. Workers also flush partial batches every 30 seconds by default via `flushIntervalMs`, and always flush pending records during final completion or shutdown.
|
|
54
|
+
|
|
55
|
+
## Reuse A Bench Definition
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { defineBench, defineStep, defineTask } from '@benchsdk/client';
|
|
59
|
+
|
|
60
|
+
const lifecycleTask = defineTask('sandbox.lifecycle', [
|
|
61
|
+
defineStep('create', async ({ state }) => {
|
|
62
|
+
state.sandboxId = 'sandbox_123';
|
|
63
|
+
}),
|
|
64
|
+
defineStep('exec.first-command', async ({ state }) => ({
|
|
65
|
+
sandboxId: String(state.sandboxId),
|
|
66
|
+
})),
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
const bench = defineBench({
|
|
70
|
+
slug: 'scale',
|
|
71
|
+
participantSlug: 'e2b',
|
|
72
|
+
concurrency: 100,
|
|
73
|
+
task: lifecycleTask,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const worker = bench.defineWorker({
|
|
77
|
+
runId: process.env.BENCHMARK_RUN_ID!,
|
|
78
|
+
processKey: process.env.HOSTNAME,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
await worker.run();
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Create A Platform Run
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { createBenchmarkClient } from '@benchsdk/client';
|
|
88
|
+
|
|
89
|
+
const client = createBenchmarkClient({
|
|
90
|
+
apiKey: process.env.COMPUTESDK_ADMIN_API_KEY,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
await client.upsertBenchmark('scale', {
|
|
94
|
+
name: 'Scale',
|
|
95
|
+
kind: 'scale',
|
|
96
|
+
config: { timeoutMs: 120_000 },
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const { run } = await client.createRun('scale', {
|
|
100
|
+
name: '10k smoke',
|
|
101
|
+
totalTasks: 10_000,
|
|
102
|
+
workerCount: 20,
|
|
103
|
+
participants: ['e2b', 'modal'],
|
|
104
|
+
config: { timeoutMs: 120_000 },
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await client.planWorkers('scale', run.id, 'e2b');
|
|
108
|
+
await client.planWorkers('scale', run.id, 'modal');
|
|
109
|
+
|
|
110
|
+
console.log(run.id);
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Workers must be planned before `worker.run()` can claim assignments.
|
|
114
|
+
|
|
115
|
+
## API
|
|
116
|
+
|
|
117
|
+
### Definition Helpers
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
defineStep(name, fn)
|
|
121
|
+
defineTask(name, steps)
|
|
122
|
+
defineWorker(options)
|
|
123
|
+
defineBench(options)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Step functions receive:
|
|
127
|
+
|
|
128
|
+
| Field | Type | Description |
|
|
129
|
+
|-------|------|-------------|
|
|
130
|
+
| `assignment` | `BenchmarkAssignment` | Platform-owned assignment for this worker |
|
|
131
|
+
| `taskIndex` | `number` | Deterministic task index within the benchmark run |
|
|
132
|
+
| `state` | `Record<string, unknown>` | Mutable per-task state shared across steps |
|
|
133
|
+
|
|
134
|
+
If a step returns a JSON object, it is merged into the task result `data` object. Defined tasks also include `taskName` in `data`.
|
|
135
|
+
|
|
136
|
+
`defineTask(name, steps, options)` supports task cleanup:
|
|
137
|
+
|
|
138
|
+
| Option | Type | Description |
|
|
139
|
+
|--------|------|-------------|
|
|
140
|
+
| `cleanup` | `(context) => Promise<void> \| void` | Runs after the task finishes, whether steps succeeded or failed. Use shared `state` to tear down resources created by earlier steps. |
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
type SandboxState = {
|
|
144
|
+
sandbox?: Awaited<ReturnType<typeof compute.sandbox.create>>;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
defineTask<SandboxState>('sandbox.lifecycle', [
|
|
148
|
+
defineStep<SandboxState>('create', async ({ state }) => {
|
|
149
|
+
state.sandbox = await compute.sandbox.create();
|
|
150
|
+
}),
|
|
151
|
+
defineStep<SandboxState>('exec', async ({ state }) => {
|
|
152
|
+
await state.sandbox.runCommand('node -v');
|
|
153
|
+
}),
|
|
154
|
+
], {
|
|
155
|
+
cleanup: async ({ state }) => {
|
|
156
|
+
await state.sandbox?.destroy?.();
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`defineStep(name, options, fn)` supports step-level progress coordination:
|
|
162
|
+
|
|
163
|
+
| Option | Type | Description |
|
|
164
|
+
|--------|------|-------------|
|
|
165
|
+
| `reportConcurrency` | `boolean?` | Include active count for this step in worker heartbeats. Defaults to `true` |
|
|
166
|
+
| `concurrency` | `number?` | Per-worker target for this step. Defaults to worker concurrency/assignment target |
|
|
167
|
+
| `readiness` | `'poll' \| 'internal'?` | Readiness coordination mode. Defaults to `'internal'`. Use `'poll'` for platform-coordinated barrier steps |
|
|
168
|
+
| `readyPollIntervalMs` | `number?` | Poll interval while waiting. Defaults to `1000` |
|
|
169
|
+
| `readyTimeoutMs` | `number?` | Maximum readiness wait time |
|
|
170
|
+
|
|
171
|
+
### Low-Level Client
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
client.updateBenchmark(benchmarkSlug, input)
|
|
175
|
+
client.updateRun(benchmarkSlug, runId, input)
|
|
176
|
+
client.updateParticipant(benchmarkSlug, runId, participantSlug, input)
|
|
177
|
+
client.planWorkers(benchmarkSlug, runId, participantSlug)
|
|
178
|
+
client.getWorker(benchmarkSlug, runId, workerId)
|
|
179
|
+
client.updateWorker(benchmarkSlug, runId, workerId, input)
|
|
180
|
+
client.claimWorker(benchmarkSlug, runId, participantSlug, { processKind, processKey })
|
|
181
|
+
client.sendTaskResults({ benchmarkSlug, runId, workerId, attemptId, sequenceNumber, isFinal, records })
|
|
182
|
+
client.uploadWorkerArtifact(benchmarkSlug, runId, workerId, {
|
|
183
|
+
attemptId,
|
|
184
|
+
kind: 'log',
|
|
185
|
+
name: 'coordinator.log',
|
|
186
|
+
contentType: 'text/plain; charset=utf-8',
|
|
187
|
+
body: logText,
|
|
188
|
+
})
|
|
189
|
+
client.heartbeatWorker(benchmarkSlug, runId, workerId, {
|
|
190
|
+
attemptId,
|
|
191
|
+
currentStep: 'pause',
|
|
192
|
+
concurrency: [{ step: 'pause', active: 100, target: 100 }],
|
|
193
|
+
})
|
|
194
|
+
client.getRunProgress(benchmarkSlug, runId)
|
|
195
|
+
client.getBenchmarkResults(benchmarkSlug, { limit })
|
|
196
|
+
client.getRunResults(benchmarkSlug, runId)
|
|
197
|
+
client.getRunTaskResults(benchmarkSlug, runId, { bucketSize, failureLimit })
|
|
198
|
+
client.getRunTimeline(benchmarkSlug, runId, { bucketMs })
|
|
199
|
+
client.getRunImports(benchmarkSlug, runId)
|
|
200
|
+
client.completeWorker(benchmarkSlug, runId, workerId, attemptId)
|
|
201
|
+
client.failWorker(benchmarkSlug, runId, workerId, attemptId, error)
|
|
202
|
+
client.runWorker(options)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
For custom coordinators that do not fit `defineWorker`, use the best-effort reporter wrapper:
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
const reporter = await BenchmarkReporter.claim({
|
|
209
|
+
benchmarkSlug: 'scale',
|
|
210
|
+
runId,
|
|
211
|
+
participantSlug: 'e2b',
|
|
212
|
+
processKind: 'container',
|
|
213
|
+
processKey: instanceId,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
reporter?.setProgress({ done, inFlight, errors });
|
|
217
|
+
reporter?.recordResult(record);
|
|
218
|
+
await reporter?.waitForStepReady({ step: 'ready.barrier', timeoutMs: 15 * 60_000 });
|
|
219
|
+
await reporter?.uploadArtifact({
|
|
220
|
+
kind: 'log',
|
|
221
|
+
name: 'coordinator.log',
|
|
222
|
+
contentType: 'text/plain; charset=utf-8',
|
|
223
|
+
body: logText,
|
|
224
|
+
});
|
|
225
|
+
await reporter?.finish(false);
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
`BenchmarkReporter` swallows platform telemetry failures for claim, heartbeat, result flushing, artifact upload, and finish calls. Benchmark work can continue even when reporting is temporarily unavailable.
|
|
229
|
+
|
|
230
|
+
For `defineWorker` / `runWorker`, use `onFinish` to upload worker-level logs once, after final task results are flushed and before the worker attempt is completed or failed:
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
defineWorker({
|
|
234
|
+
benchmarkSlug: 'scale',
|
|
235
|
+
runId,
|
|
236
|
+
participantSlug: 'e2b',
|
|
237
|
+
task,
|
|
238
|
+
onFinish: async ({ uploadArtifact }) => {
|
|
239
|
+
await uploadArtifact({
|
|
240
|
+
kind: 'log',
|
|
241
|
+
name: 'coordinator.log',
|
|
242
|
+
contentType: 'text/plain; charset=utf-8',
|
|
243
|
+
body: logText,
|
|
244
|
+
});
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
For coordinator health artifacts, sample system metrics:
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
const metrics = createSystemMetricsCollector();
|
|
253
|
+
const samples = [metrics.sample()];
|
|
254
|
+
metrics.stop();
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
`client.getRunProgress(...)` returns a run summary plus per-participant worker, task, and concurrency progress:
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
const progress = await client.getRunProgress('scale', runId);
|
|
261
|
+
|
|
262
|
+
console.log(progress.summary.status);
|
|
263
|
+
console.log(progress.summary.participants);
|
|
264
|
+
|
|
265
|
+
const participant = progress.participants.find((item) => item.slug === 'e2b');
|
|
266
|
+
console.log(participant?.status);
|
|
267
|
+
console.log(participant?.workers);
|
|
268
|
+
console.log(participant?.tasks.completionRatio);
|
|
269
|
+
console.log(participant?.concurrency.find((item) => item.step === 'pause')?.ready);
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Most workers should use `defineWorker(...).run()`.
|
|
273
|
+
|
|
274
|
+
## Task Result Shape
|
|
275
|
+
|
|
276
|
+
```json
|
|
277
|
+
{
|
|
278
|
+
"taskIndex": 0,
|
|
279
|
+
"status": "success",
|
|
280
|
+
"startedAt": "2026-06-03T00:00:00.000Z",
|
|
281
|
+
"completedAt": "2026-06-03T00:00:01.000Z",
|
|
282
|
+
"latencyMs": 1000,
|
|
283
|
+
"steps": [
|
|
284
|
+
{ "name": "create", "status": "success", "startedAt": "...", "completedAt": "...", "latencyMs": 700 },
|
|
285
|
+
{ "name": "exec.first-command", "status": "success", "startedAt": "...", "completedAt": "...", "latencyMs": 120 },
|
|
286
|
+
{ "name": "destroy", "status": "success", "startedAt": "...", "completedAt": "...", "latencyMs": 180 }
|
|
287
|
+
],
|
|
288
|
+
"data": {
|
|
289
|
+
"taskName": "sandbox.lifecycle",
|
|
290
|
+
"sandboxId": "..."
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
```
|