@benchsdk/runner 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +74 -0
- package/dist/bin.cjs +7 -0
- package/dist/bin.cjs.map +1 -0
- package/dist/bin.js +6 -0
- package/dist/bin.js.map +1 -0
- package/dist/index.cjs +944 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +366 -0
- package/dist/index.d.ts +366 -0
- package/dist/index.js +901 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { TaskResultRecord, JsonObject, BaseParticipant, DefineStepOptions, TaskStepRecord } from '@benchsdk/client';
|
|
2
|
+
|
|
3
|
+
type MetricValue = string | ((record: TaskResultRecord) => number | number[] | undefined);
|
|
4
|
+
interface MetricScoring {
|
|
5
|
+
name: string;
|
|
6
|
+
value?: MetricValue;
|
|
7
|
+
unit: string;
|
|
8
|
+
ceiling: number;
|
|
9
|
+
floor?: number;
|
|
10
|
+
higherIsBetter?: boolean;
|
|
11
|
+
weights: {
|
|
12
|
+
median: number;
|
|
13
|
+
p95: number;
|
|
14
|
+
p99: number;
|
|
15
|
+
};
|
|
16
|
+
trim?: number;
|
|
17
|
+
}
|
|
18
|
+
interface ScoringSpec {
|
|
19
|
+
dimensions?: Record<string, unknown>;
|
|
20
|
+
success?: (record: TaskResultRecord) => boolean;
|
|
21
|
+
metrics: MetricScoring[];
|
|
22
|
+
}
|
|
23
|
+
interface BenchmarkScoreResult {
|
|
24
|
+
provider: string;
|
|
25
|
+
dimensions: JsonObject;
|
|
26
|
+
metrics: {
|
|
27
|
+
name: string;
|
|
28
|
+
unit: string;
|
|
29
|
+
median: number;
|
|
30
|
+
p95: number;
|
|
31
|
+
p99: number;
|
|
32
|
+
}[];
|
|
33
|
+
scalars?: {
|
|
34
|
+
name: string;
|
|
35
|
+
value: number;
|
|
36
|
+
unit: string;
|
|
37
|
+
}[];
|
|
38
|
+
compositeScore: number;
|
|
39
|
+
successRate: number;
|
|
40
|
+
scoringVersion?: string;
|
|
41
|
+
skipped: boolean;
|
|
42
|
+
skipReason?: string;
|
|
43
|
+
}
|
|
44
|
+
type LowerIsBetter = (name: string, opts: {
|
|
45
|
+
unit: string;
|
|
46
|
+
ceiling: number;
|
|
47
|
+
value?: MetricValue;
|
|
48
|
+
weights: {
|
|
49
|
+
median: number;
|
|
50
|
+
p95: number;
|
|
51
|
+
p99: number;
|
|
52
|
+
};
|
|
53
|
+
trim?: number;
|
|
54
|
+
}) => MetricScoring;
|
|
55
|
+
type HigherIsBetter = (name: string, opts: {
|
|
56
|
+
unit: string;
|
|
57
|
+
floor?: number;
|
|
58
|
+
ceiling: number;
|
|
59
|
+
value?: MetricValue;
|
|
60
|
+
weights: {
|
|
61
|
+
median: number;
|
|
62
|
+
p95: number;
|
|
63
|
+
p99: number;
|
|
64
|
+
};
|
|
65
|
+
trim?: number;
|
|
66
|
+
}) => MetricScoring;
|
|
67
|
+
declare const lowerIsBetter: LowerIsBetter;
|
|
68
|
+
declare const higherIsBetter: HigherIsBetter;
|
|
69
|
+
declare function score(outcome: BenchmarkRunOutcome, spec: ScoringSpec): BenchmarkScoreResult[];
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* A `*.bench.ts` file is the composition of a **config** and a **task**:
|
|
73
|
+
*
|
|
74
|
+
* export const config = defineBenchmarkConfig({ benchmarkSlug, participants, ... });
|
|
75
|
+
* export const task = defineTask(async (ctx) => { await ctx.step('work', () => ...); });
|
|
76
|
+
*
|
|
77
|
+
* `defineBenchmarkConfig` holds the orchestration knobs (including the
|
|
78
|
+
* participants and an optional `onComplete` hook); `defineTask` holds the
|
|
79
|
+
* workload. The `bench run <file>` binary imports the module, reads those two
|
|
80
|
+
* exports, and drives the run. There is no "mode": all orchestration shapes
|
|
81
|
+
* emerge from the knobs.
|
|
82
|
+
*
|
|
83
|
+
* iterations total tasks to run (default 1)
|
|
84
|
+
* concurrency max tasks in flight at once — 1 = sequential, N = burst (default 1)
|
|
85
|
+
* staggerDelayMs delay each task's start by taskIndex * staggerDelayMs (default 0)
|
|
86
|
+
*
|
|
87
|
+
* Common shapes:
|
|
88
|
+
* sequential { iterations: N, concurrency: 1 }
|
|
89
|
+
* burst { iterations: N, concurrency: N }
|
|
90
|
+
* staggered { iterations: N, concurrency: N, staggerDelayMs: 200 }
|
|
91
|
+
*
|
|
92
|
+
* A benchmark can name these variants up front via `shapes`, so one file backs
|
|
93
|
+
* several platform benchmarks (`bench run <file> --shape burst`) without
|
|
94
|
+
* restating each one's slug/name in scripts and CI.
|
|
95
|
+
*
|
|
96
|
+
* A task is comprised of steps, declared via `ctx.step` inside a task function
|
|
97
|
+
* — it supports closures, conditionals and try/finally, so values (a created
|
|
98
|
+
* sandbox, say) flow naturally between steps. A task that declares no steps is
|
|
99
|
+
* recorded as a single implicit `task` step. Measurements reach the platform
|
|
100
|
+
* via `ctx.measure(...)`; step return values are control flow and never
|
|
101
|
+
* recorded.
|
|
102
|
+
*/
|
|
103
|
+
|
|
104
|
+
/** How tasks are ordered across participants. */
|
|
105
|
+
type GroupBy = 'participant' | 'round';
|
|
106
|
+
/**
|
|
107
|
+
* A named variant of a benchmark, selected with `--shape <name>`. A shape
|
|
108
|
+
* carries only the parts that make it a distinct *benchmark* — its platform
|
|
109
|
+
* identity plus any stable distinguishing knob (e.g. staggered's delay). The
|
|
110
|
+
* scale knobs that vary per environment (`--iterations`, `--concurrency`) stay
|
|
111
|
+
* on the invocation, so a shape never sets a value only to have the CLI
|
|
112
|
+
* override it.
|
|
113
|
+
*/
|
|
114
|
+
interface BenchmarkShape {
|
|
115
|
+
/** Platform slug this shape reports under (e.g. 'sandbox-tti'). */
|
|
116
|
+
slug: string;
|
|
117
|
+
/** Display name shown on the platform; defaults to the slug. */
|
|
118
|
+
name?: string;
|
|
119
|
+
/** Default stagger delay (ms) for this shape; overridable with `--stagger-delay-ms`. */
|
|
120
|
+
staggerDelayMs?: number;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* What a task returns: whatever it measured itself. This replaces the
|
|
124
|
+
* assumption that the framework owns all timing. A plain data payload is
|
|
125
|
+
* written explicitly as `{ data: {...} }`.
|
|
126
|
+
*/
|
|
127
|
+
interface TaskResult {
|
|
128
|
+
/** Free-form domain payload attached to the record (tokens, receipts, ...). */
|
|
129
|
+
data?: JsonObject;
|
|
130
|
+
/**
|
|
131
|
+
* Pre-measured steps the task timed itself (e.g. socket phases).
|
|
132
|
+
* Only honored in `groupBy: 'round'` runs, where the runner builds records
|
|
133
|
+
* manually. In `groupBy: 'participant'` runs the platform worker
|
|
134
|
+
* (`client.runWorker`) owns steps, so `steps` and `latencyMs` are ignored.
|
|
135
|
+
*/
|
|
136
|
+
steps?: TaskStepRecord[];
|
|
137
|
+
/** Task-owned overall latency; overrides framework wall-clock (round mode only). */
|
|
138
|
+
latencyMs?: number;
|
|
139
|
+
}
|
|
140
|
+
/** Options for a single `ctx.step` invocation. */
|
|
141
|
+
interface TaskStepOptions extends Omit<DefineStepOptions, 'concurrency' | 'stepConcurrency'> {
|
|
142
|
+
/** Per-iteration timeout in milliseconds. If an invocation exceeds this, it is aborted and a `step_timeout` TaskError is thrown. */
|
|
143
|
+
timeoutMs?: number;
|
|
144
|
+
/** Number of times to invoke `fn` in parallel. Defaults to 1. When greater than 1, the step returns an array of results. */
|
|
145
|
+
concurrency?: number;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Throw this from a task to record a failure while preserving domain data and
|
|
149
|
+
* any pre-measured steps (a plain thrown Error loses them).
|
|
150
|
+
*/
|
|
151
|
+
declare class TaskError extends Error {
|
|
152
|
+
readonly code?: string;
|
|
153
|
+
readonly data?: JsonObject;
|
|
154
|
+
readonly steps?: TaskStepRecord[];
|
|
155
|
+
constructor(message: string, opts?: {
|
|
156
|
+
code?: string;
|
|
157
|
+
data?: JsonObject;
|
|
158
|
+
steps?: TaskStepRecord[];
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
/** Context handed to a benchmark `task` for a single iteration. */
|
|
162
|
+
interface TaskContext<T extends BaseParticipant = BaseParticipant> {
|
|
163
|
+
/** The participant this task is running for. */
|
|
164
|
+
participant: T;
|
|
165
|
+
/** Zero-based global task ordinal (matches the platform record's taskIndex). */
|
|
166
|
+
taskIndex: number;
|
|
167
|
+
/** Current phase name, when the benchmark declares `phases`. */
|
|
168
|
+
phase?: string;
|
|
169
|
+
/**
|
|
170
|
+
* Runs `fn` as a named platform step. Mirrors `@benchsdk/client`'s
|
|
171
|
+
* `RunWorkerContext.step`; supports closures and try/finally. A `concurrency`
|
|
172
|
+
* greater than 1 invokes `fn` that many times in parallel and returns an array.
|
|
173
|
+
* `timeoutMs` aborts any invocation that exceeds it with a `step_timeout` TaskError.
|
|
174
|
+
*/
|
|
175
|
+
step<R, C extends number = 1>(name: string, fn: () => Promise<R> | R, options?: TaskStepOptions & {
|
|
176
|
+
concurrency?: C;
|
|
177
|
+
}): Promise<C extends 1 ? R : R[]>;
|
|
178
|
+
/**
|
|
179
|
+
* Attaches a JSON measurement to the platform. Inside a `step` it lands on
|
|
180
|
+
* that step's data; at task top-level it lands on the task record's data.
|
|
181
|
+
*/
|
|
182
|
+
measure(data: JsonObject): void;
|
|
183
|
+
/** Appends a line to the worker log, uploaded as an artifact when the worker finishes. */
|
|
184
|
+
log(message: string, meta?: JsonObject): void;
|
|
185
|
+
}
|
|
186
|
+
type BenchmarkTask<T extends BaseParticipant = BaseParticipant> = (ctx: TaskContext<T>) => Promise<TaskResult | void> | TaskResult | void;
|
|
187
|
+
/**
|
|
188
|
+
* A named run segment with its own iteration count. Phases run in order; each
|
|
189
|
+
* record is tagged with the phase name via `data.phase`, and `ctx.phase` lets
|
|
190
|
+
* the task branch on identity instead of index arithmetic.
|
|
191
|
+
*/
|
|
192
|
+
interface Phase {
|
|
193
|
+
/** Phase name, tagged onto every record produced in this phase. */
|
|
194
|
+
name: string;
|
|
195
|
+
/** Iterations to run in this phase. */
|
|
196
|
+
iterations: number;
|
|
197
|
+
}
|
|
198
|
+
/** One participant's collected task records from a run. */
|
|
199
|
+
interface ParticipantRecords {
|
|
200
|
+
participant: string;
|
|
201
|
+
records: TaskResultRecord[];
|
|
202
|
+
}
|
|
203
|
+
/** The orchestration knobs a run actually used, after CLI overrides. */
|
|
204
|
+
interface ResolvedRunConfig {
|
|
205
|
+
iterations: number;
|
|
206
|
+
concurrency: number;
|
|
207
|
+
staggerDelayMs: number;
|
|
208
|
+
groupBy: GroupBy;
|
|
209
|
+
providers?: string[];
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Result of a benchmark run, passed to `config.onComplete`. Exposes the raw
|
|
213
|
+
* per-participant records so completion hooks can write legacy local results.
|
|
214
|
+
*/
|
|
215
|
+
interface BenchmarkRunOutcome {
|
|
216
|
+
runId: string;
|
|
217
|
+
/** Link to this run on the platform dashboard. */
|
|
218
|
+
dashboardUrl: string;
|
|
219
|
+
participants: ParticipantRecords[];
|
|
220
|
+
config: ResolvedRunConfig;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Orchestration config for a benchmark. Holds identity, the knobs, the
|
|
224
|
+
* participants, and the optional completion hook — the workload lives in a
|
|
225
|
+
* separate `defineTask`. `bench run <file>` reads the `config` and `task`
|
|
226
|
+
* exports from the module and drives the run.
|
|
227
|
+
*/
|
|
228
|
+
interface BenchmarkConfig<T extends BaseParticipant = BaseParticipant> {
|
|
229
|
+
/**
|
|
230
|
+
* Stable platform slug for this benchmark (e.g. 'sandbox-tti-local').
|
|
231
|
+
* Selectable per run with `--shape` (or overridable with `--benchmark`), so
|
|
232
|
+
* one entrypoint can report under several benchmarks.
|
|
233
|
+
*/
|
|
234
|
+
benchmarkSlug: string;
|
|
235
|
+
/** Human-readable name shown on the platform. Overridable with `--name`. */
|
|
236
|
+
benchmarkName: string;
|
|
237
|
+
/**
|
|
238
|
+
* Named variants of this benchmark, selected with `--shape <name>`. Each
|
|
239
|
+
* shape swaps in its own platform identity (and optional stable knob) while
|
|
240
|
+
* reusing the same task and participants, so one bench file can back several
|
|
241
|
+
* platform benchmarks without duplicating the slug/name triple across
|
|
242
|
+
* package scripts and CI.
|
|
243
|
+
*/
|
|
244
|
+
shapes?: Record<string, BenchmarkShape>;
|
|
245
|
+
/**
|
|
246
|
+
* Total tasks to run per participant. Default: 1. Mutually exclusive with
|
|
247
|
+
* `phases` — when `phases` is set, total iterations = sum of phase iterations.
|
|
248
|
+
*/
|
|
249
|
+
iterations?: number;
|
|
250
|
+
/**
|
|
251
|
+
* Named run segments (e.g. cold/warm). Runs in order; each record is tagged
|
|
252
|
+
* with the phase name via `data.phase`. Mutually exclusive with `iterations`.
|
|
253
|
+
*/
|
|
254
|
+
phases?: Phase[];
|
|
255
|
+
/** Max tasks in flight at once. 1 = sequential, N = burst. Default: 1. */
|
|
256
|
+
concurrency?: number;
|
|
257
|
+
/** Delay each task's start by `taskIndex * staggerDelayMs`. Default: 0. */
|
|
258
|
+
staggerDelayMs?: number;
|
|
259
|
+
/**
|
|
260
|
+
* Task ordering across participants. Default: 'participant' (run each
|
|
261
|
+
* participant's tasks to completion, then the next). 'round' takes turns:
|
|
262
|
+
* every participant runs its Nth task before anyone runs their (N+1)th, so
|
|
263
|
+
* all participants' Nth tasks happen back-to-back under the same conditions.
|
|
264
|
+
*/
|
|
265
|
+
groupBy?: GroupBy;
|
|
266
|
+
/**
|
|
267
|
+
* Default participant names to run when `--provider` is not passed. Omit to
|
|
268
|
+
* run all env-available participants. `--provider` always overrides this.
|
|
269
|
+
*/
|
|
270
|
+
defaultProviders?: string[];
|
|
271
|
+
/** The participants this benchmark can run against. `--provider` selects a subset by name. */
|
|
272
|
+
participants: T[];
|
|
273
|
+
/**
|
|
274
|
+
* Run-level scoring hook, called once with `lowerIsBetter` and `higherIsBetter`
|
|
275
|
+
* primitives after the outcome is assembled but before `onComplete`. Use it to
|
|
276
|
+
* define how the run should be scored and reported to the platform.
|
|
277
|
+
*/
|
|
278
|
+
onScore?: (lowerIsBetter: LowerIsBetter, higherIsBetter: HigherIsBetter) => ScoringSpec | Promise<ScoringSpec>;
|
|
279
|
+
/**
|
|
280
|
+
* Run-level completion hook, called once with the full outcome after every
|
|
281
|
+
* participant finishes. Use it for aggregate output (legacy JSON/SVG
|
|
282
|
+
* writers). This is the run-level counterpart to per-step `ctx.measure`.
|
|
283
|
+
*/
|
|
284
|
+
onComplete?: (outcome: BenchmarkRunOutcome) => void | Promise<void>;
|
|
285
|
+
}
|
|
286
|
+
/** Validates `config` at file-evaluation time so mistakes surface immediately. */
|
|
287
|
+
declare function defineBenchmarkConfig<T extends BaseParticipant = BaseParticipant>(config: BenchmarkConfig<T>): BenchmarkConfig<T>;
|
|
288
|
+
/**
|
|
289
|
+
* Declares the workload for a benchmark: a function invoked once per iteration.
|
|
290
|
+
* Steps are named via `ctx.step`, which supports closures and try/finally so
|
|
291
|
+
* values flow naturally between steps.
|
|
292
|
+
*
|
|
293
|
+
* export const task = defineTask(async (ctx) => {
|
|
294
|
+
* const sandbox = await ctx.step('create', () => provider.create());
|
|
295
|
+
* try { await ctx.step('exec', () => sandbox.run('node -v')); }
|
|
296
|
+
* finally { await ctx.step('destroy', () => sandbox.destroy()); }
|
|
297
|
+
* });
|
|
298
|
+
*/
|
|
299
|
+
declare function defineTask<T extends BaseParticipant = BaseParticipant>(task: BenchmarkTask<T>): BenchmarkTask<T>;
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Thrown when every selected participant was env-gated out, i.e. none of their
|
|
303
|
+
* `requiredEnvVars` are set. This is a "nothing to do" outcome rather than a
|
|
304
|
+
* failure — a benchmark job for a provider whose credentials aren't provisioned
|
|
305
|
+
* should skip, not go red — so callers are expected to exit 0 on it.
|
|
306
|
+
*/
|
|
307
|
+
declare class NoAvailableParticipantsError extends Error {
|
|
308
|
+
readonly skipped: {
|
|
309
|
+
name: string;
|
|
310
|
+
missing: string[];
|
|
311
|
+
}[];
|
|
312
|
+
constructor(skipped: {
|
|
313
|
+
name: string;
|
|
314
|
+
missing: string[];
|
|
315
|
+
}[]);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
interface CliArgs {
|
|
319
|
+
/** Which platform benchmark to report as (`--benchmark`, aka the benchmark slug). */
|
|
320
|
+
benchmark?: string;
|
|
321
|
+
name?: string;
|
|
322
|
+
/** Named variant from the bench file's `shapes` (`--shape`), swapping in its identity. */
|
|
323
|
+
shape?: string;
|
|
324
|
+
/**
|
|
325
|
+
* Idempotency key (`--run-key`): sibling processes passing the same key share
|
|
326
|
+
* one run (get-or-created), instead of each opening its own.
|
|
327
|
+
*/
|
|
328
|
+
runKey?: string;
|
|
329
|
+
iterations?: number;
|
|
330
|
+
concurrency?: number;
|
|
331
|
+
staggerDelayMs?: number;
|
|
332
|
+
groupBy?: GroupBy;
|
|
333
|
+
/** Participant names from `--provider a,b` (repeatable). */
|
|
334
|
+
providers?: string[];
|
|
335
|
+
/** When true, run locally and do not ingest/report to the platform. */
|
|
336
|
+
noIngest?: boolean;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Parses the orchestration flags this runner understands, ignoring anything
|
|
340
|
+
* else. Supports both `--flag value` and `--flag=value`; `--provider` accepts
|
|
341
|
+
* a comma-separated list and may be repeated.
|
|
342
|
+
*/
|
|
343
|
+
declare function parseCliArgs(argv: string[]): CliArgs;
|
|
344
|
+
/** Merges CLI overrides over config defaults, filling in knob fallbacks. */
|
|
345
|
+
declare function mergeConfig<T extends BaseParticipant>(config: BenchmarkConfig<T>, args: CliArgs): ResolvedRunConfig;
|
|
346
|
+
/**
|
|
347
|
+
* Runs `config`'s `task` against its participants. Selects participants by
|
|
348
|
+
* `--provider` (if given), env-gates them, then drives them per the resolved
|
|
349
|
+
* `groupBy`. `--shape` swaps in a declared variant's identity; `--benchmark`/
|
|
350
|
+
* `--name` retarget the run at a different platform benchmark, so one entrypoint
|
|
351
|
+
* can report under several slugs. With `--run-key`, sibling processes (e.g. one
|
|
352
|
+
* CI job per provider) get-or-create one shared run and each registers only its
|
|
353
|
+
* own participants.
|
|
354
|
+
*/
|
|
355
|
+
declare function runBenchmark<T extends BaseParticipant>(fileConfig: BenchmarkConfig<T>, task: BenchmarkTask<T>, argv?: string[]): Promise<BenchmarkRunOutcome>;
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Dispatches one CLI invocation. Throws on bad usage / invalid exports and lets
|
|
359
|
+
* `NoAvailableParticipantsError` propagate so the caller can map it to a clean
|
|
360
|
+
* exit. Does not call `process.exit`.
|
|
361
|
+
*/
|
|
362
|
+
declare function runBenchmarkFile(argv: string[]): Promise<void>;
|
|
363
|
+
/** Executable entry: runs the file and maps outcomes to process exit codes. */
|
|
364
|
+
declare function run(argv: string[]): Promise<void>;
|
|
365
|
+
|
|
366
|
+
export { type BenchmarkConfig, type BenchmarkRunOutcome, type BenchmarkScoreResult, type BenchmarkTask, type CliArgs, type GroupBy, type MetricScoring, NoAvailableParticipantsError, type ParticipantRecords, type Phase, type ResolvedRunConfig, type ScoringSpec, type TaskContext, TaskError, type TaskResult, type TaskStepOptions, defineBenchmarkConfig, defineTask, higherIsBetter, lowerIsBetter, mergeConfig, parseCliArgs, run, runBenchmark, runBenchmarkFile, score };
|