ostia 0.2.9 → 0.2.11
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 +42 -24
- package/ab-runner.ts +6 -4
- package/chunk-5m601yfv.js +15 -0
- package/chunk-82vs1rw1.js +2 -0
- package/chunk-dvqggjkn.js +14 -0
- package/chunk-fc36fd0b.js +3 -0
- package/chunk-pdv5zb3w.js +27 -0
- package/chunk-r6hwb84z.js +25 -0
- package/chunk-scvezzm9.js +229 -0
- package/cli.js +1 -224
- package/index.d.ts +571 -417
- package/index.js +1 -1
- package/package.json +1 -1
- package/runner.ts +5 -16
- package/chunk-cqy5fzae.js +0 -16
- package/chunk-t6q9jbb6.js +0 -25
package/index.d.ts
CHANGED
|
@@ -1,119 +1,24 @@
|
|
|
1
1
|
export declare function defineConfig(config: OstiaConfigInput): OstiaConfigInput;
|
|
2
2
|
|
|
3
|
-
export interface CommandSpec {
|
|
4
|
-
command: string | string[];
|
|
5
|
-
label?: string;
|
|
6
|
-
/** Overrides `TimeOptions.prepare` for this command. */
|
|
7
|
-
prepare?: PrepareHook;
|
|
8
|
-
/** Overrides `TimeOptions.timeSource` for this command. */
|
|
9
|
-
timeSource?: TimeSource;
|
|
10
|
-
/** Overrides `TimeOptions.timeoutMs` for this command. */
|
|
11
|
-
timeoutMs?: number;
|
|
12
|
-
/** Overrides `TimeOptions.ignoreExitCodes` for this command. */
|
|
13
|
-
ignoreExitCodes?: number[];
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface TimeOptions {
|
|
17
|
-
commands: (string | string[] | CommandSpec)[];
|
|
18
|
-
/** Runs before every trial of every command (warmup and instrumented
|
|
19
|
-
* trials included), unmeasured, in `cwd`/`env`: a command string / argv
|
|
20
|
-
* array spawned and awaited, or a function. hyperfine's `--prepare`. A
|
|
21
|
-
* `CommandSpec.prepare` overrides it per command. */
|
|
22
|
-
prepare?: PrepareHook;
|
|
23
|
-
/** Take every command's timing from a number in its own output instead of
|
|
24
|
-
* its wall clock (e.g. a build tool's `built in 342ms` line, which
|
|
25
|
-
* excludes runtime startup). A `CommandSpec.timeSource` overrides it per
|
|
26
|
-
* command. The parsed value becomes `timing.samples`; each trial keeps
|
|
27
|
-
* `wallNs` too. */
|
|
28
|
-
timeSource?: TimeSource;
|
|
29
|
-
/** Exact trial count. When set, `budgetMs` is ignored. */
|
|
30
|
-
samples?: number;
|
|
31
|
-
/** Wall-clock time budget for the sampling loop, ms (default: a
|
|
32
|
-
* hyperfine-style ~3s min-total-time loop when neither `samples` nor
|
|
33
|
-
* `budgetMs` is given). */
|
|
34
|
-
budgetMs?: number;
|
|
35
|
-
/** Hard floor on trials when no exact `samples` count is given. */
|
|
36
|
-
minSamples?: number;
|
|
37
|
-
warmup?: number;
|
|
38
|
-
/** Round-robin trials across commands (one trial per command, repeated)
|
|
39
|
-
* instead of running each command's whole trial loop to completion before
|
|
40
|
-
* the next command starts. Default: true when 2+ commands are given (a
|
|
41
|
-
* single command has nothing to interleave against). Spreads any drift
|
|
42
|
-
* over the run's wall-clock span (thermal throttling, a noisy neighbor
|
|
43
|
-
* process) evenly across every command instead of favoring whichever ran
|
|
44
|
-
* first or last. */
|
|
45
|
-
interleave?: boolean;
|
|
46
|
-
cwd?: string;
|
|
47
|
-
env?: Record<string, string>;
|
|
48
|
-
cpu?: boolean;
|
|
49
|
-
heap?: boolean;
|
|
50
|
-
cpuIntervalUs?: number;
|
|
51
|
-
outDir?: string;
|
|
52
|
-
/** Measure this machine's noise floor before the first command (default:
|
|
53
|
-
* true) and stamp it on the document as `environment`. Set false to skip
|
|
54
|
-
* the ~200ms reference measurement. */
|
|
55
|
-
noiseCheck?: boolean;
|
|
56
|
-
/** Kills a trial (or prepare hook) that hasn't finished after this many
|
|
57
|
-
* ms, with SIGKILL. No default: an unset `timeoutMs` never times out. A
|
|
58
|
-
* timed-out trial contributes no sample; if every trial of a command times
|
|
59
|
-
* out, that command has no timing stats. `CommandSpec.timeoutMs` overrides
|
|
60
|
-
* it per command. */
|
|
61
|
-
timeoutMs?: number;
|
|
62
|
-
/** Aborting cancels the run: in-flight trials are killed with SIGKILL, no
|
|
63
|
-
* further trials are scheduled, and `time()` resolves (never rejects)
|
|
64
|
-
* with the document built from whatever measurements had already
|
|
65
|
-
* completed, plus an `aborted` warning on the document's last
|
|
66
|
-
* measurement. */
|
|
67
|
-
signal?: AbortSignal;
|
|
68
|
-
/** Exit codes to treat as success (hyperfine's `--ignore-failure`): a
|
|
69
|
-
* trial exiting with one of these still contributes its sample and gets
|
|
70
|
-
* no `nonzero-exit` warning, as if it had exited 0. `CommandSpec.ignoreExitCodes`
|
|
71
|
-
* overrides it per command. */
|
|
72
|
-
ignoreExitCodes?: number[];
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export declare function time(opts: TimeOptions): Promise<ProfileDocument>;
|
|
76
|
-
|
|
77
|
-
interface ProfileOptions {
|
|
78
|
-
intervalUs?: number;
|
|
79
|
-
origin?: "inspector" | "jsc";
|
|
80
|
-
/** `profile()` runs `fn` in this process, so there's no child to kill: an
|
|
81
|
-
* already-aborted signal skips the profiler instrumentation entirely and
|
|
82
|
-
* just calls `fn` plain (still returning its `result`), with an `aborted`
|
|
83
|
-
* warning in place of CPU evidence. A signal that fires mid-capture can't
|
|
84
|
-
* interrupt `fn` once it's running. */
|
|
85
|
-
signal?: AbortSignal;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
interface ProfileResult<T> {
|
|
89
|
-
result: T;
|
|
90
|
-
measurement: Measurement;
|
|
91
|
-
document: ProfileDocument;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export declare function profile<T>(fn: () => T | Promise<T>, opts?: ProfileOptions): Promise<ProfileResult<T>>;
|
|
95
|
-
|
|
96
3
|
export interface WorkloadConfig {
|
|
97
4
|
label?: string;
|
|
98
5
|
command?: string[];
|
|
99
6
|
suites?: string[];
|
|
100
|
-
/** `command` only. Globs of the files
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* nothing and caches until the command or config changes. */
|
|
7
|
+
/** `command` only. Globs of the files the timing depends on: `ostia ci`
|
|
8
|
+
* reuses a cached run while their contents are unchanged. Omitted, the
|
|
9
|
+
* workload always reruns; `[]` caches until the command or config changes. */
|
|
104
10
|
inputs?: string[];
|
|
105
11
|
/** `command` only. Runs before every trial (warmup included), unmeasured:
|
|
106
|
-
* a command string / argv array in
|
|
107
|
-
* function
|
|
108
|
-
* uncacheable for `ostia ci` (its effect can't be fingerprinted), so it
|
|
109
|
-
* always executes. */
|
|
12
|
+
* a command string / argv array, or in `ostia.config.ts` a function. A
|
|
13
|
+
* function makes the workload uncacheable for `ostia ci`. */
|
|
110
14
|
prepare?: PrepareHook;
|
|
111
15
|
/** `command` only. Take timing from a number in the command's own output
|
|
112
16
|
* instead of its wall clock; see `TimeSource`. */
|
|
113
17
|
timeSource?: TimeSource;
|
|
114
|
-
/** `command
|
|
115
|
-
*
|
|
116
|
-
*
|
|
18
|
+
/** `command`: kills a trial (or prepare hook) after this many ms.
|
|
19
|
+
* `suites`: kills a suite file's (or isolated task's) process after this
|
|
20
|
+
* many ms, overriding `bench.timeoutMs`. Overrides `ostia ci`'s 10-minute
|
|
21
|
+
* default. */
|
|
117
22
|
timeoutMs?: number;
|
|
118
23
|
/** `command` only. Exit codes to treat as success; see
|
|
119
24
|
* `TimeOptions.ignoreExitCodes`. */
|
|
@@ -121,10 +26,9 @@ export interface WorkloadConfig {
|
|
|
121
26
|
}
|
|
122
27
|
|
|
123
28
|
interface BenchConfig {
|
|
124
|
-
/** Suite file globs, resolved
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
* with it. */
|
|
29
|
+
/** Suite file globs, resolved against the config's directory (e.g.
|
|
30
|
+
* "bench/**\/*.bench.ts"). Suite files given on the command line replace
|
|
31
|
+
* this list. */
|
|
128
32
|
suites?: string[];
|
|
129
33
|
preload?: string[];
|
|
130
34
|
/** Extra flags for the `bun` process that runs each suite file; see
|
|
@@ -142,18 +46,37 @@ interface BenchConfig {
|
|
|
142
46
|
filter?: string;
|
|
143
47
|
isolate?: boolean;
|
|
144
48
|
outDir?: string;
|
|
145
|
-
/** Kills a suite file's (or isolated task's) subprocess
|
|
146
|
-
*
|
|
49
|
+
/** Kills a suite file's (or isolated task's) subprocess after this many
|
|
50
|
+
* ms; overrides `ostia ci`'s 10-minute default. */
|
|
147
51
|
timeoutMs?: number;
|
|
148
52
|
}
|
|
149
53
|
|
|
54
|
+
export interface AbConfig {
|
|
55
|
+
/** Shell command(s) run once in a freshly extracted base tree, e.g. to
|
|
56
|
+
* build gitignored files the suites import; see `AbOptions.baseSetup`.
|
|
57
|
+
* `--base-setup` replaces this list. */
|
|
58
|
+
setup?: string | string[];
|
|
59
|
+
/** Base trees to keep cached; see `AbOptions.keepTrees`. `--keep-trees`
|
|
60
|
+
* overrides. Default: 5. */
|
|
61
|
+
keepTrees?: number;
|
|
62
|
+
/** Compare retained heap per call; see `AbOptions.alloc`. `--alloc` /
|
|
63
|
+
* `--no-alloc` override. `bench.alloc` doesn't apply to `ab`. */
|
|
64
|
+
alloc?: boolean;
|
|
65
|
+
/** Compare peak RSS of the first call; see `AbOptions.peakMem`.
|
|
66
|
+
* `--peak-mem` / `--no-peak-mem` override. `bench.peakMem` doesn't apply
|
|
67
|
+
* to `ab`. */
|
|
68
|
+
peakMem?: boolean;
|
|
69
|
+
/** Memory verdict threshold, percent; see `AbOptions.memThresholdPct`.
|
|
70
|
+
* `--mem-threshold` overrides. Default: 10. */
|
|
71
|
+
memThresholdPct?: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
150
74
|
export interface OstiaConfig {
|
|
151
|
-
/** `command` workloads: exact trial count
|
|
152
|
-
* Unset, the `budgetMs`/`minSamples` loop decides. */
|
|
75
|
+
/** `command` workloads: exact trial count; see `time()`. */
|
|
153
76
|
samples?: number;
|
|
154
|
-
/** `command` workloads:
|
|
77
|
+
/** `command` workloads: sampling budget in ms; see `time()`. */
|
|
155
78
|
budgetMs?: number;
|
|
156
|
-
/** `command` workloads:
|
|
79
|
+
/** `command` workloads: floor on trials; see `time()`. */
|
|
157
80
|
minSamples?: number;
|
|
158
81
|
warmup: number;
|
|
159
82
|
outDir: string;
|
|
@@ -162,20 +85,15 @@ export interface OstiaConfig {
|
|
|
162
85
|
thresholds: Thresholds;
|
|
163
86
|
workloads: WorkloadConfig[];
|
|
164
87
|
bench?: BenchConfig;
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
* error, a handful of new workloads next to an otherwise-matching
|
|
171
|
-
* baseline is not. */
|
|
88
|
+
ab?: AbConfig;
|
|
89
|
+
/** `ostia ci`'s policy for a workload with no matching baseline row (by
|
|
90
|
+
* workload id): `"fail"` exits 2 naming the baseline file, `"warn"` lists
|
|
91
|
+
* it in the report. Unset: `"fail"` when every workload is missing (a
|
|
92
|
+
* stale or wrong baseline), `"warn"` otherwise. */
|
|
172
93
|
onMissingBaseline?: "warn" | "fail";
|
|
173
|
-
/**
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
* so `compare`'s noise-floor threshold widening applies to `ci` too, not
|
|
177
|
-
* only to ad hoc `time`/`bench` runs. `--no-noise-check` overrides this
|
|
178
|
-
* to false per invocation. */
|
|
94
|
+
/** Measure the machine's noise floor once per `ostia ci` run (default
|
|
95
|
+
* true) and stamp it on the candidate document, so `compare` widens its
|
|
96
|
+
* thresholds accordingly. `--no-noise-check` overrides this per run. */
|
|
179
97
|
noiseCheck?: boolean;
|
|
180
98
|
}
|
|
181
99
|
|
|
@@ -188,11 +106,9 @@ export interface Thresholds {
|
|
|
188
106
|
frameSelfPct: number;
|
|
189
107
|
heapTypePct: number;
|
|
190
108
|
minFrameSelfUs: number;
|
|
191
|
-
/**
|
|
192
|
-
* `improved` verdict also requires `pValue < alpha`. */
|
|
109
|
+
/** A `regressed` / `improved` verdict also requires Mann-Whitney `pValue < alpha`. */
|
|
193
110
|
alpha: number;
|
|
194
|
-
/** Bootstrap resample rounds for the timing CI.
|
|
195
|
-
* see `bootstrapMedianDiffCi`. */
|
|
111
|
+
/** Bootstrap resample rounds for the timing CI. */
|
|
196
112
|
bootstrapIterations: number;
|
|
197
113
|
}
|
|
198
114
|
|
|
@@ -204,6 +120,8 @@ export interface CompareResult {
|
|
|
204
120
|
baseOnly: Workload[];
|
|
205
121
|
candOnly: Workload[];
|
|
206
122
|
};
|
|
123
|
+
/** Every task on either side; see `ProfileDocument.compared`. */
|
|
124
|
+
compared: ComparedTask[];
|
|
207
125
|
summary: ComparisonSummary;
|
|
208
126
|
}
|
|
209
127
|
|
|
@@ -217,36 +135,31 @@ export interface ProfileDocument {
|
|
|
217
135
|
os: string;
|
|
218
136
|
arch: string;
|
|
219
137
|
};
|
|
138
|
+
/** ISO timestamp; metadata only, never part of an id. */
|
|
220
139
|
createdAt: string;
|
|
221
140
|
workloads: Workload[];
|
|
222
141
|
measurements: Measurement[];
|
|
223
142
|
comparisons?: Comparison[];
|
|
224
|
-
/** Aggregate across `comparisons
|
|
225
|
-
* `ostia ci`. Additive, no schema bump. Absent wherever `comparisons` is. */
|
|
143
|
+
/** Aggregate across `comparisons`; present wherever `comparisons` is. */
|
|
226
144
|
comparisonSummary?: ComparisonSummary;
|
|
227
|
-
/** Workload ids
|
|
228
|
-
*
|
|
229
|
-
* Additive, no schema bump; absent wherever `comparisons` is. Ids only
|
|
230
|
-
* (not full `Workload`s) to keep the document small; `ostia compare`'s
|
|
231
|
-
* printed report resolves labels from the two source documents directly. */
|
|
145
|
+
/** Workload ids on only one side of the comparison; present wherever
|
|
146
|
+
* `comparisons` is. */
|
|
232
147
|
unmatched?: {
|
|
233
148
|
baseOnly: string[];
|
|
234
149
|
candOnly: string[];
|
|
235
150
|
};
|
|
236
|
-
/**
|
|
237
|
-
*
|
|
238
|
-
*
|
|
151
|
+
/** Every task on either side of the comparison, one entry each: what
|
|
152
|
+
* `--format markdown` prints, flat for scripts. Present wherever
|
|
153
|
+
* `comparisons` is. */
|
|
154
|
+
compared?: ComparedTask[];
|
|
155
|
+
/** Machine conditions at measurement time; absent when the noise check was
|
|
156
|
+
* skipped. */
|
|
239
157
|
environment?: Environment;
|
|
240
|
-
/** Run-level result of
|
|
241
|
-
* candidate was paired against and the gate's verdict. Additive, no schema
|
|
242
|
-
* bump. Present only on documents from `ab()`, whose measurements are
|
|
158
|
+
/** Run-level result of `ostia ab` / `ab()`, whose measurements are
|
|
243
159
|
* `phase: "paired"`. */
|
|
244
160
|
ab?: AbSummary;
|
|
245
|
-
/** Repo state
|
|
246
|
-
*
|
|
247
|
-
* bump. Absent outside a git repo (or when `git` itself isn't
|
|
248
|
-
* available). Metadata only: never part of any fingerprint or id, so a
|
|
249
|
-
* commit or a dirty working tree never orphans a cached run or baseline. */
|
|
161
|
+
/** Repo state of the process's cwd; absent outside a git repo. Never part of
|
|
162
|
+
* an id or fingerprint, so a commit or dirty tree doesn't orphan a baseline. */
|
|
250
163
|
git?: GitMetadata;
|
|
251
164
|
}
|
|
252
165
|
|
|
@@ -257,12 +170,15 @@ interface GitMetadata {
|
|
|
257
170
|
}
|
|
258
171
|
|
|
259
172
|
interface NoiseFloor {
|
|
260
|
-
/** `mad / median` of
|
|
261
|
-
*
|
|
262
|
-
* measured. */
|
|
173
|
+
/** `mad / median` of a reference workload, percent: how noisy the machine is
|
|
174
|
+
* right now, independent of what's measured. */
|
|
263
175
|
floorPct: number;
|
|
264
176
|
referenceMedianNs: number;
|
|
265
177
|
samples: number;
|
|
178
|
+
/** How much slower (positive) the reference ran at the end of the run than
|
|
179
|
+
* at the start, percent: the machine's drift over the run. Absent when the
|
|
180
|
+
* run didn't re-measure it (`profile()`, older documents). */
|
|
181
|
+
driftPct?: number;
|
|
266
182
|
}
|
|
267
183
|
|
|
268
184
|
interface Environment {
|
|
@@ -274,69 +190,51 @@ interface Environment {
|
|
|
274
190
|
}
|
|
275
191
|
|
|
276
192
|
export interface Workload {
|
|
277
|
-
/** Identifies what is measured, not where or when
|
|
278
|
-
* `
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
* measured from another. For an `inprocess` workload, a hash of the
|
|
283
|
-
* function source (or, for a registry entry, the file/task name plus
|
|
284
|
-
* `params`). `label`, `description`, and `baseline` are annotations and
|
|
285
|
-
* never affect it. */
|
|
193
|
+
/** Identifies what is measured, not where or when. Subprocess: a hash of
|
|
194
|
+
* argv, `prepare` and `timeSource`, excluding `process.cwd()`. In-process: a
|
|
195
|
+
* hash of the function source, or for a registry entry the file, task name
|
|
196
|
+
* and `params`. Annotations (`label`, `description`, `baseline`) never
|
|
197
|
+
* affect it. */
|
|
286
198
|
id: string;
|
|
287
199
|
kind: "subprocess" | "inprocess";
|
|
288
200
|
label?: string;
|
|
289
201
|
command?: string[];
|
|
290
|
-
/** Command
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
* step measures different things. A function-form hook isn't
|
|
294
|
-
* serializable and is omitted here (its source text is still hashed into
|
|
295
|
-
* the id). */
|
|
202
|
+
/** Command-form `prepare` hook run before every trial; part of the id. A
|
|
203
|
+
* function-form hook isn't serializable and is omitted (its source is still
|
|
204
|
+
* hashed into the id). */
|
|
296
205
|
prepare?: string[];
|
|
297
|
-
/**
|
|
298
|
-
* wall clock
|
|
299
|
-
* number it captures. Part of the workload id, so the same command timed
|
|
300
|
-
* by wall clock and by its own report are two workloads. */
|
|
206
|
+
/** Takes samples from a number in the command's own output instead of the
|
|
207
|
+
* wall clock; part of the id. */
|
|
301
208
|
timeSource?: {
|
|
302
209
|
pattern: string;
|
|
303
210
|
group?: number;
|
|
304
211
|
unit?: "ns" | "us" | "ms" | "s";
|
|
305
212
|
};
|
|
306
|
-
/** `task` is the "group/name" id
|
|
307
|
-
*
|
|
308
|
-
* splitting `task` on "/", so task names may contain slashes. */
|
|
213
|
+
/** `task` is the registry's "group/name" id; `group` is the enclosing
|
|
214
|
+
* `group()` name. Renderers prefer `group` over splitting `task` on "/". */
|
|
309
215
|
entry?: {
|
|
310
216
|
file: string;
|
|
311
217
|
task: string;
|
|
312
218
|
group?: string;
|
|
313
219
|
};
|
|
314
|
-
/**
|
|
315
|
-
* `task(name, fn, { baseline: true })`). At most one per group is
|
|
316
|
-
* meaningful; renderers use the first they encounter. */
|
|
220
|
+
/** The Relative reference for its group; renderers use the first one found. */
|
|
317
221
|
baseline?: boolean;
|
|
318
|
-
/** What this task measures and why, from `task(
|
|
319
|
-
* Travels with the data so a reader of the document has intent, not just
|
|
320
|
-
* numbers. */
|
|
222
|
+
/** What this task measures and why, from `task(..., { description })`. */
|
|
321
223
|
description?: string;
|
|
322
|
-
/** The enclosing group's
|
|
323
|
-
* workload in the group so each record is self-contained. */
|
|
224
|
+
/** The enclosing group's description, repeated on every workload in it. */
|
|
324
225
|
groupDescription?: string;
|
|
325
|
-
/**
|
|
326
|
-
* on the task, its group, or the suite), vs. sharing its suite file's
|
|
327
|
-
* subprocess with other tasks. */
|
|
226
|
+
/** Ran in a subprocess of its own rather than sharing its suite file's. */
|
|
328
227
|
isolated?: boolean;
|
|
329
|
-
/**
|
|
330
|
-
*
|
|
331
|
-
* Lets renderers pivot and `compare` match on them instead of only on the
|
|
332
|
-
* task name. Part of the workload id when present, so two points that
|
|
333
|
-
* share a task name (a `sweep()`'s whole point) don't collide. */
|
|
228
|
+
/** From `task(..., { params })` or a `sweep()` point. Part of the id, so
|
|
229
|
+
* points sharing a task name don't collide. */
|
|
334
230
|
params?: Record<string, string | number | boolean>;
|
|
335
|
-
/**
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
|
|
339
|
-
|
|
231
|
+
/** Ids this task had under its former names (`task(..., { aliases })`), so
|
|
232
|
+
* `compare` and `ab` match a base measured before a rename. Never part of
|
|
233
|
+
* the id. */
|
|
234
|
+
formerIds?: string[];
|
|
235
|
+
/** From `task.skip()` / `group.skip()`: never measured, so no `Measurement`.
|
|
236
|
+
* Renderers print a "- skipped" row; `compare` treats it as `unchanged` with
|
|
237
|
+
* a `skipped` warning. */
|
|
340
238
|
skipped?: boolean;
|
|
341
239
|
}
|
|
342
240
|
|
|
@@ -355,38 +253,39 @@ interface Measurement {
|
|
|
355
253
|
heap?: HeapEvidence;
|
|
356
254
|
memory?: MemoryEvidence;
|
|
357
255
|
jit?: JitTierBreakdown;
|
|
358
|
-
/** `phase: "paired"` only
|
|
359
|
-
* measurement's `timing` is the candidate side. */
|
|
256
|
+
/** `phase: "paired"` only; `timing` is then the candidate side. */
|
|
360
257
|
paired?: PairedEvidence;
|
|
258
|
+
/** `phase: "paired"` only: the task threw on one side or both
|
|
259
|
+
* (`"both"`). Usually instead of `timing`/`paired`; when it threw in a
|
|
260
|
+
* confirmation repeat, `repeat` (from 1) says which, and `timing`/`paired`
|
|
261
|
+
* stay as the first process measured them. Either way the task isn't in
|
|
262
|
+
* `matched` or the geomean. */
|
|
263
|
+
threw?: {
|
|
264
|
+
side: "base" | "cand" | "both";
|
|
265
|
+
message: string;
|
|
266
|
+
repeat?: number;
|
|
267
|
+
};
|
|
361
268
|
warnings: Warning[];
|
|
362
269
|
artifacts: ArtifactRef[];
|
|
363
|
-
/**
|
|
364
|
-
*
|
|
365
|
-
* for 2+ commands) rather than run to completion before the next command
|
|
366
|
-
* started, so drift over the run's wall-clock span (thermal throttling, a
|
|
367
|
-
* noisy neighbor process) lands on every command equally instead of
|
|
368
|
-
* favoring whichever ran first or last. */
|
|
270
|
+
/** Trials ran round-robin with the other commands of the same `time()` call,
|
|
271
|
+
* so drift over the run hits every command equally. */
|
|
369
272
|
interleaved?: boolean;
|
|
370
273
|
}
|
|
371
274
|
|
|
372
275
|
interface Trial {
|
|
373
276
|
i: number;
|
|
374
277
|
wallNs: number;
|
|
375
|
-
/** The command's self-reported time (ns)
|
|
376
|
-
* `
|
|
377
|
-
* `wallNs` stays alongside so a document keeps both. */
|
|
278
|
+
/** The command's self-reported time (ns) under a `timeSource`; these, not
|
|
279
|
+
* `wallNs`, are then the `timing.samples`. */
|
|
378
280
|
reportedNs?: number;
|
|
379
281
|
exitCode?: number;
|
|
380
282
|
userNs?: number;
|
|
381
283
|
systemNs?: number;
|
|
382
284
|
maxRssBytes?: number;
|
|
383
|
-
/**
|
|
384
|
-
* own. `exitCode` is absent in that case: the kill signal, not the
|
|
385
|
-
* command, decided how the process ended. Contributes no sample. */
|
|
285
|
+
/** Killed by `timeoutMs`; `exitCode` is absent. Contributes no sample. */
|
|
386
286
|
timedOut?: true;
|
|
387
|
-
/**
|
|
388
|
-
*
|
|
389
|
-
* fallback to `wallNs`). Contributes no sample. */
|
|
287
|
+
/** Output didn't match the `timeSource` pattern; `reportedNs` is absent
|
|
288
|
+
* (never a `wallNs` fallback). Contributes no sample. */
|
|
390
289
|
timeSourceNoMatch?: true;
|
|
391
290
|
}
|
|
392
291
|
|
|
@@ -408,15 +307,11 @@ interface TimingStats {
|
|
|
408
307
|
p75: number;
|
|
409
308
|
/** 99th percentile, ns. */
|
|
410
309
|
p99: number;
|
|
411
|
-
/** Median absolute deviation, ns:
|
|
412
|
-
*
|
|
413
|
-
* by the long right tail typical of wall-clock timings. */
|
|
310
|
+
/** Median absolute deviation, ns: a spread measure that, unlike stddev, the
|
|
311
|
+
* long right tail of wall-clock timings doesn't skew. */
|
|
414
312
|
mad: number;
|
|
415
|
-
/**
|
|
416
|
-
*
|
|
417
|
-
* Absent for every subprocess timing measurement, and for an in-process
|
|
418
|
-
* one whose single call already cleared the batching threshold - a
|
|
419
|
-
* renderer treats an absent value the same as `1`. */
|
|
313
|
+
/** Calls batched into one timed block; absent (read as 1) unless batching
|
|
314
|
+
* occurred, and always absent for subprocess timing. */
|
|
420
315
|
batch?: number;
|
|
421
316
|
}
|
|
422
317
|
|
|
@@ -466,26 +361,22 @@ interface HeapEvidence {
|
|
|
466
361
|
|
|
467
362
|
interface MemoryEvidence {
|
|
468
363
|
origin: "resourceUsage" | "heapStats";
|
|
469
|
-
/**
|
|
470
|
-
* `--
|
|
471
|
-
* Absent on subprocess timing's `maxRssBytes`, and on `memstats`
|
|
472
|
-
* measurements written before this field existed, which are always
|
|
473
|
-
* `"retained"`. */
|
|
364
|
+
/** `memstats` only: `"retained"` (`--alloc`, `bytesPerOp`) or `"peak"`
|
|
365
|
+
* (`--peak-mem`, `peakBytes`). Absent means `"retained"` on older documents. */
|
|
474
366
|
kind?: "retained" | "peak";
|
|
475
367
|
/** Largest `Trial.maxRssBytes` across a timing measurement's trials. */
|
|
476
368
|
maxRssBytes?: number;
|
|
477
|
-
/**
|
|
478
|
-
*
|
|
479
|
-
* `process.memoryUsage().heapUsed`) around one batch bracketed by
|
|
480
|
-
* `Bun.gc(true)` on both sides, divided by the batch size. Garbage the
|
|
481
|
-
* calls create and drop is collected before the second reading, so this
|
|
482
|
-
* is what the calls keep alive (a leak check), not what they allocate. */
|
|
369
|
+
/** `--alloc`: heap growth per call across one batch bracketed by full GCs,
|
|
370
|
+
* so what the calls keep alive (a leak check), not what they allocate. */
|
|
483
371
|
bytesPerOp?: number;
|
|
484
|
-
/**
|
|
485
|
-
*
|
|
486
|
-
* processes, each making the task's first call after its `before` hooks
|
|
487
|
-
* and a full GC. A `peak-hidden` warning says when it may read low. */
|
|
372
|
+
/** `--peak-mem`: how far one call raised RSS, garbage included; median over
|
|
373
|
+
* 3 fresh processes. A `peak-hidden` warning says when it may read low. */
|
|
488
374
|
peakBytes?: number;
|
|
375
|
+
/** In-process timing: the JS heap's size as of its last collection, just
|
|
376
|
+
* before the task's warmup and just after its last trial. Read without
|
|
377
|
+
* forcing a GC, so it never perturbs the timing. */
|
|
378
|
+
heapUsedBeforeBytes?: number;
|
|
379
|
+
heapUsedAfterBytes?: number;
|
|
489
380
|
}
|
|
490
381
|
|
|
491
382
|
interface JitTierBreakdown {
|
|
@@ -504,8 +395,7 @@ interface JitTierBreakdown {
|
|
|
504
395
|
}
|
|
505
396
|
|
|
506
397
|
interface PairedEvidence {
|
|
507
|
-
/**
|
|
508
|
-
* which goes first. */
|
|
398
|
+
/** One base batch and one candidate batch per round, alternating order. */
|
|
509
399
|
rounds: number;
|
|
510
400
|
/** Calls per side per round. */
|
|
511
401
|
batch: number;
|
|
@@ -517,27 +407,48 @@ interface PairedEvidence {
|
|
|
517
407
|
ratios: number[];
|
|
518
408
|
medianRatio: number;
|
|
519
409
|
/** 25th/75th percentile of `ratios`. */
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
/**
|
|
523
|
-
*
|
|
524
|
-
* quarters of rounds. */
|
|
410
|
+
ratioP25: number;
|
|
411
|
+
ratioP75: number;
|
|
412
|
+
/** Median ratio past `1 ± threshold` and on the same side of 1 in at least
|
|
413
|
+
* three quarters of rounds. */
|
|
525
414
|
flagged?: "regressed" | "improved";
|
|
526
415
|
/** Each fresh-process re-measurement of a flagged workload. */
|
|
527
416
|
repeats?: {
|
|
528
417
|
medianRatio: number;
|
|
529
|
-
|
|
530
|
-
|
|
418
|
+
ratioP25: number;
|
|
419
|
+
ratioP75: number;
|
|
531
420
|
flagged?: "regressed" | "improved";
|
|
532
421
|
}[];
|
|
533
422
|
/** Set when `flagged` is: whether every repeat flagged the same way. */
|
|
534
423
|
confirmed?: boolean;
|
|
535
424
|
/** `flagged` when confirmed, else `"unchanged"`. */
|
|
536
425
|
verdict: "regressed" | "improved" | "unchanged";
|
|
537
|
-
/**
|
|
538
|
-
*
|
|
539
|
-
* `false` for a change in behavior, never a failure. */
|
|
426
|
+
/** First call's return values were deep-equal on both sides; informational,
|
|
427
|
+
* never a failure. */
|
|
540
428
|
sameOutput: boolean;
|
|
429
|
+
/** Set when the suite file's text differs from the base ref's copy, so
|
|
430
|
+
* each side may run a different benchmark (a fixed fixture, a changed
|
|
431
|
+
* input). With `sameOutput: false` too, the task is "not comparable": its
|
|
432
|
+
* verdict is `"unchanged"` and it's left out of the geomean. */
|
|
433
|
+
suiteChanged?: true;
|
|
434
|
+
/** `--alloc`: heap each call keeps alive after a full GC, per side, after
|
|
435
|
+
* a warmup batch; each the median of 3 fresh processes. */
|
|
436
|
+
retained?: MemoryChange;
|
|
437
|
+
/** `--peak-mem`: how far the task's first call raises RSS, per side,
|
|
438
|
+
* each the median of 3 fresh processes. */
|
|
439
|
+
peak?: MemoryChange;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
interface MemoryChange {
|
|
443
|
+
baseBytes: number;
|
|
444
|
+
candBytes: number;
|
|
445
|
+
/** Past both `AbSummary.memory.thresholdPct` of the base and
|
|
446
|
+
* `floorBytes`; always `"unchanged"` on a not-comparable task. */
|
|
447
|
+
verdict: "regressed" | "improved" | "unchanged";
|
|
448
|
+
/** The smallest change that counts, bytes: 16 KiB over the batch's calls
|
|
449
|
+
* for `retained`; for `peak`, 1 MiB, or the freed memory a `peak-hidden`
|
|
450
|
+
* warning reports. */
|
|
451
|
+
floorBytes: number;
|
|
541
452
|
}
|
|
542
453
|
|
|
543
454
|
interface AbSummary {
|
|
@@ -545,6 +456,9 @@ interface AbSummary {
|
|
|
545
456
|
ref: string;
|
|
546
457
|
sha: string;
|
|
547
458
|
};
|
|
459
|
+
/** Suite files, as given, that don't exist at the base ref: their tasks
|
|
460
|
+
* are all candidate-only. */
|
|
461
|
+
newSuites?: string[];
|
|
548
462
|
rounds: number;
|
|
549
463
|
thresholdPct: number;
|
|
550
464
|
geomeanThresholdPct: number;
|
|
@@ -559,17 +473,31 @@ interface AbSummary {
|
|
|
559
473
|
unconfirmed: number;
|
|
560
474
|
/** Workloads whose first call returned different values on each side. */
|
|
561
475
|
outputDiffers: number;
|
|
562
|
-
/**
|
|
563
|
-
*
|
|
564
|
-
*
|
|
565
|
-
|
|
476
|
+
/** Workloads whose suite file and output both changed (see
|
|
477
|
+
* `PairedEvidence.suiteChanged`); counted in `unchanged`, left out of
|
|
478
|
+
* `geomeanPct`. */
|
|
479
|
+
notComparable: number;
|
|
480
|
+
/** Workloads that threw on either side or both, and so weren't timed;
|
|
481
|
+
* not counted in `matched`. */
|
|
482
|
+
threw: number;
|
|
483
|
+
/** Present when any task had a memory reading (`--alloc`, `--peak-mem`):
|
|
484
|
+
* how many tasks' memory regressed or improved, on either reading. */
|
|
485
|
+
memory?: {
|
|
486
|
+
thresholdPct: number;
|
|
487
|
+
regressed: number;
|
|
488
|
+
improved: number;
|
|
489
|
+
};
|
|
490
|
+
/** Geometric mean of the workloads' median ratios, signed percent (negative:
|
|
491
|
+
* candidate faster); a flagged workload contributes its median over its main
|
|
492
|
+
* run and repeats. `null` when nothing was paired. */
|
|
566
493
|
geomeanPct: number | null;
|
|
567
|
-
/** `"fail"` when any workload regressed (confirmed)
|
|
568
|
-
* exceeds `geomeanThresholdPct
|
|
494
|
+
/** `"fail"` when any workload regressed (confirmed), threw on the
|
|
495
|
+
* candidate side only, or `geomeanPct` exceeds `geomeanThresholdPct`, or
|
|
496
|
+
* any task's memory regressed. */
|
|
569
497
|
verdict: "pass" | "fail";
|
|
570
498
|
}
|
|
571
499
|
|
|
572
|
-
declare const WARNING_CODES: readonly ["slow-first-run", "outliers-detected", "fast-command", "nonzero-exit", "artifact-missing", "empty-profile", "low-sample-count", "thin-comparison", "noisy-machine", "skipped", "jit-cold", "timeout", "aborted", "time-source-no-match", "environment-mismatch", "peak-hidden"];
|
|
500
|
+
declare const WARNING_CODES: readonly ["slow-first-run", "outliers-detected", "fast-command", "nonzero-exit", "artifact-missing", "empty-profile", "low-sample-count", "thin-comparison", "noisy-machine", "skipped", "jit-cold", "timeout", "aborted", "time-source-no-match", "environment-mismatch", "peak-hidden", "suite-changed"];
|
|
573
501
|
|
|
574
502
|
export type WarningCode = (typeof WARNING_CODES)[number];
|
|
575
503
|
|
|
@@ -594,19 +522,16 @@ export interface Comparison {
|
|
|
594
522
|
timing?: {
|
|
595
523
|
medianDeltaPct: number;
|
|
596
524
|
meanDeltaPct: number;
|
|
597
|
-
/** 95% bootstrap
|
|
598
|
-
*
|
|
599
|
-
* than 5 samples (see the `thin-comparison` warning). */
|
|
525
|
+
/** 95% bootstrap CI on the difference of medians, percent of the baseline
|
|
526
|
+
* median. Absent below 5 samples a side (`thin-comparison`). */
|
|
600
527
|
ci95?: [number, number];
|
|
601
|
-
/** Two-sided Mann-Whitney U p-value
|
|
602
|
-
* Same absence condition as `ci95`. */
|
|
528
|
+
/** Two-sided Mann-Whitney U p-value; absent whenever `ci95` is. */
|
|
603
529
|
pValue?: number;
|
|
604
|
-
/**
|
|
530
|
+
/** Bootstrap PRNG seed, so `ci95` is reproducible. */
|
|
605
531
|
seed?: number;
|
|
606
532
|
verdict: "improved" | "regressed" | "unchanged";
|
|
607
533
|
};
|
|
608
|
-
/**
|
|
609
|
-
* samples) or otherwise carries a caveat about this comparison. */
|
|
534
|
+
/** Caveats about this comparison, e.g. a thin-sample point-estimate verdict. */
|
|
610
535
|
warnings?: Warning[];
|
|
611
536
|
frames?: {
|
|
612
537
|
frameKey: string;
|
|
@@ -630,32 +555,81 @@ export interface Comparison {
|
|
|
630
555
|
minFrameSelfUs: number;
|
|
631
556
|
alpha: number;
|
|
632
557
|
bootstrapIterations: number;
|
|
633
|
-
/** `max(timingPct,
|
|
634
|
-
* cand.environment.noise.floorPct)` - the threshold timing was actually
|
|
635
|
-
* tested against, once machine noise widens it past `timingPct`. */
|
|
558
|
+
/** `max(timingPct, both documents' noise floors)`: what timing was tested against. */
|
|
636
559
|
effectiveTimingPct: number;
|
|
637
560
|
};
|
|
638
561
|
verdict: "pass" | "fail";
|
|
639
562
|
}
|
|
640
563
|
|
|
641
564
|
interface ComparisonSummary {
|
|
642
|
-
/** `comparisons.length
|
|
565
|
+
/** `comparisons.length`. */
|
|
643
566
|
matched: number;
|
|
644
567
|
regressed: number;
|
|
645
568
|
improved: number;
|
|
646
569
|
unchanged: number;
|
|
647
|
-
/** Geometric mean of `cand/base` median ratios
|
|
648
|
-
*
|
|
649
|
-
* average). `null` when no comparison had a finite timing ratio. */
|
|
570
|
+
/** Geometric mean of `cand/base` median ratios, signed percent (negative:
|
|
571
|
+
* candidate faster). `null` when no comparison had a finite ratio. */
|
|
650
572
|
geomeanPct: number | null;
|
|
651
|
-
/**
|
|
652
|
-
*
|
|
653
|
-
* the two documents' `environment.noise.floorPct`, never per-workload. */
|
|
573
|
+
/** `Comparison.thresholds.effectiveTimingPct`, which is the same for the
|
|
574
|
+
* whole document pair. */
|
|
654
575
|
effectiveTimingPct: number;
|
|
576
|
+
/** The machine's drift, percent, added to the timing threshold: the
|
|
577
|
+
* largest of each run's own drift (`NoiseFloor.driftPct`) and how far the
|
|
578
|
+
* reference loop's speed moved between the two runs. Absent when either
|
|
579
|
+
* document lacks a noise check. */
|
|
580
|
+
driftPct?: number;
|
|
655
581
|
/** `"fail"` when any comparison's verdict is `"fail"`. */
|
|
656
582
|
verdict: "pass" | "fail";
|
|
657
583
|
}
|
|
658
584
|
|
|
585
|
+
interface ComparedTask {
|
|
586
|
+
/** The candidate's workload id; the base's on a `missing-cand` task. */
|
|
587
|
+
workloadId: string;
|
|
588
|
+
/** Display name: the label, else the command, else the id. */
|
|
589
|
+
task: string;
|
|
590
|
+
group?: string;
|
|
591
|
+
/** Set when the base was matched through one of the task's `aliases`: the
|
|
592
|
+
* base's workload id and display name. */
|
|
593
|
+
renamedFrom?: {
|
|
594
|
+
workloadId: string;
|
|
595
|
+
task: string;
|
|
596
|
+
};
|
|
597
|
+
verdict: "improved" | "unchanged" | "regressed" | "missing-base" | "missing-cand";
|
|
598
|
+
/** The `Comparison` holding this task's frame and heap-type deltas and
|
|
599
|
+
* warnings; absent on a one-sided task. */
|
|
600
|
+
comparisonId?: string;
|
|
601
|
+
/** Whether the comparison passed the gate (timing, frames, heap types);
|
|
602
|
+
* absent on a one-sided task, which never fails it. */
|
|
603
|
+
pass?: boolean;
|
|
604
|
+
/** Absent on a one-sided or skipped task. */
|
|
605
|
+
timing?: {
|
|
606
|
+
unit: "ns";
|
|
607
|
+
baseMedian: number;
|
|
608
|
+
candMedian: number;
|
|
609
|
+
/** Absent on a document compared before ostia recorded it. */
|
|
610
|
+
baseSamples?: number;
|
|
611
|
+
candSamples: number;
|
|
612
|
+
medianDeltaPct: number;
|
|
613
|
+
/** Absent on a thin (<5 samples a side) comparison, with `pValue`. */
|
|
614
|
+
ci95?: [number, number];
|
|
615
|
+
pValue?: number;
|
|
616
|
+
/** `Comparison.thresholds.effectiveTimingPct`. */
|
|
617
|
+
thresholdPct: number;
|
|
618
|
+
};
|
|
619
|
+
/** Memory readings present on both sides; informational, never gated. */
|
|
620
|
+
memory?: MemoryDelta[];
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
interface MemoryDelta {
|
|
624
|
+
/** `heapUsedBefore`/`heapUsedAfter`: in-process heap around the timing
|
|
625
|
+
* run; `retained`: `--alloc`; `peak`: `--peak-mem`; `maxRss`: subprocess
|
|
626
|
+
* timing. */
|
|
627
|
+
reading: "heapUsedBefore" | "heapUsedAfter" | "retained" | "peak" | "maxRss";
|
|
628
|
+
baseBytes: number;
|
|
629
|
+
candBytes: number;
|
|
630
|
+
deltaPct: number;
|
|
631
|
+
}
|
|
632
|
+
|
|
659
633
|
export interface PrepareRun {
|
|
660
634
|
phase: "warmup" | "timing" | "cpu" | "heap";
|
|
661
635
|
index: number;
|
|
@@ -673,36 +647,36 @@ export interface TimeSource {
|
|
|
673
647
|
unit?: TimeUnit;
|
|
674
648
|
}
|
|
675
649
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
/**
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
650
|
+
export type AbProgress = {
|
|
651
|
+
phase: "setup";
|
|
652
|
+
command: string;
|
|
653
|
+
} | {
|
|
654
|
+
phase: "measure";
|
|
655
|
+
suite: number;
|
|
656
|
+
suites: number;
|
|
657
|
+
file: string;
|
|
658
|
+
/** Each suite runs twice, once with each side first. */
|
|
659
|
+
pass: number;
|
|
660
|
+
passes: number;
|
|
661
|
+
task: number;
|
|
662
|
+
tasks: number;
|
|
663
|
+
label: string;
|
|
664
|
+
} | {
|
|
665
|
+
phase: "confirm";
|
|
666
|
+
repeat: number;
|
|
667
|
+
repeats: number;
|
|
668
|
+
label: string;
|
|
669
|
+
} | {
|
|
670
|
+
phase: "memory";
|
|
671
|
+
run: number;
|
|
672
|
+
runs: number;
|
|
673
|
+
side: Side;
|
|
674
|
+
label: string;
|
|
675
|
+
};
|
|
702
676
|
|
|
703
|
-
interface AbOptions {
|
|
677
|
+
export interface AbOptions {
|
|
704
678
|
suites: string[];
|
|
705
|
-
/** Git ref whose committed tree is the base side (default: `"HEAD"`)
|
|
679
|
+
/** Git ref whose committed tree is the base side (default: `"HEAD"`); the
|
|
706
680
|
* candidate is the working tree, uncommitted changes included. */
|
|
707
681
|
base?: string;
|
|
708
682
|
/** Rounds per workload, each one base batch and one candidate batch of
|
|
@@ -715,9 +689,8 @@ interface AbOptions {
|
|
|
715
689
|
* this many percent (default: 1.5): a broad slowdown too small to flag any
|
|
716
690
|
* one workload. */
|
|
717
691
|
geomeanThresholdPct?: number;
|
|
718
|
-
/** Fresh-process repeats of each flagged workload
|
|
719
|
-
* every repeat flags it the same way (default: 2; 0 trusts the first
|
|
720
|
-
* process). */
|
|
692
|
+
/** Fresh-process repeats of each flagged workload, which only counts when
|
|
693
|
+
* every repeat flags it the same way (default: 2; 0 trusts the first). */
|
|
721
694
|
confirm?: number;
|
|
722
695
|
filter?: string;
|
|
723
696
|
preload?: string[];
|
|
@@ -725,98 +698,112 @@ interface AbOptions {
|
|
|
725
698
|
outDir?: string;
|
|
726
699
|
cwd?: string;
|
|
727
700
|
noiseCheck?: boolean;
|
|
728
|
-
/**
|
|
729
|
-
*
|
|
701
|
+
/** Also compare each side's retained heap per call (see
|
|
702
|
+
* `bench({ alloc })`) and how far its first call raises RSS (see
|
|
703
|
+
* `bench({ peakMem })`), each the median of 3 fresh processes per side.
|
|
704
|
+
* Task/group options override these. */
|
|
705
|
+
alloc?: boolean;
|
|
706
|
+
peakMem?: boolean;
|
|
707
|
+
/** Flag a memory reading that moves past this many percent of the base's
|
|
708
|
+
* and past its noise floor (default: 10). A memory regression fails the
|
|
709
|
+
* run. */
|
|
710
|
+
memThresholdPct?: number;
|
|
711
|
+
/** Shell commands run once, in order, in a freshly extracted base tree
|
|
712
|
+
* before it's used: to build what the suites import but git doesn't hold
|
|
713
|
+
* (generated or gitignored files). Each runs with its cwd at the tree's
|
|
714
|
+
* counterpart of `cwd`, the project's `node_modules` linked in, and
|
|
715
|
+
* `OSTIA_AB_SHA` / `OSTIA_AB_CANDIDATE_DIR` (the working tree's `cwd`) in
|
|
716
|
+
* its environment. The tree is cached per commit and command list. */
|
|
717
|
+
baseSetup?: string | string[];
|
|
718
|
+
/** Base trees to keep under `<outDir>/ab`, counting this run's: after a
|
|
719
|
+
* run, older ones go, least recently used first, unless one was used in
|
|
720
|
+
* the last hour (another run may still need it). Default: 5. */
|
|
721
|
+
keepTrees?: number;
|
|
722
|
+
/** SIGKILLs a runner process (one suite file or repeat), or a `baseSetup`
|
|
723
|
+
* command, after this many ms. */
|
|
730
724
|
timeoutMs?: number;
|
|
731
725
|
/** Aborting kills the running process and resolves with the suites that
|
|
732
|
-
* had finished, plus an `aborted` warning; repeats
|
|
726
|
+
* had finished, plus an `aborted` warning; unrun repeats leave their
|
|
733
727
|
* workloads unconfirmed. */
|
|
734
728
|
signal?: AbortSignal;
|
|
729
|
+
/** Called as each setup command, task, confirmation repeat and memory
|
|
730
|
+
* process starts. */
|
|
731
|
+
onProgress?: (progress: AbProgress) => void;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
export declare class AbBaseError extends OstiaUsageError {
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
export declare class AbSuiteError extends Error {
|
|
738
|
+
readonly side: Side;
|
|
739
|
+
constructor(message: string, side: Side);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
export declare class AbSetupError extends Error {
|
|
735
743
|
}
|
|
736
744
|
|
|
737
745
|
export declare function ab(opts: AbOptions): Promise<ProfileDocument>;
|
|
738
746
|
|
|
739
|
-
|
|
747
|
+
declare class OstiaUsageError extends Error {
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
type Side = "base" | "cand";
|
|
751
|
+
|
|
752
|
+
export interface BenchOptions {
|
|
740
753
|
suites: string[];
|
|
741
754
|
/** Wall-clock sampling budget per task, ms (default 500). */
|
|
742
755
|
budgetMs?: number;
|
|
743
|
-
/** Exact trial count per task
|
|
744
|
-
* in-process equivalent of `time()`'s `samples`. */
|
|
756
|
+
/** Exact trial count per task; the budget is ignored when set. */
|
|
745
757
|
samples?: number;
|
|
746
758
|
minSamples?: number;
|
|
747
759
|
gc?: boolean;
|
|
748
760
|
/** Capture one extra `phase: "cpu"` measurement per task (the task looped
|
|
749
|
-
* under the JSC sampling profiler for
|
|
750
|
-
*
|
|
751
|
-
* `TaskOptions.cpu` / `GroupOptions.cpu` override this per task or group. */
|
|
761
|
+
* under the JSC sampling profiler for ~2,000 samples, JIT tiers included),
|
|
762
|
+
* never mixed into the timing numbers. Task/group `cpu` overrides this. */
|
|
752
763
|
cpu?: boolean;
|
|
753
764
|
/** `cpu`'s sampling interval, µs (default: 100). A coarser interval
|
|
754
765
|
* lengthens the capture to keep the sample count, up to 10s per task. */
|
|
755
766
|
cpuIntervalUs?: number;
|
|
756
767
|
/** Capture one extra `phase: "memstats"` measurement per task: retained
|
|
757
768
|
* heap growth per call, from a batch with `Bun.gc(true)` on both sides.
|
|
758
|
-
* What the calls keep alive, not what they allocate
|
|
759
|
-
*
|
|
760
|
-
* override this per task or group. */
|
|
769
|
+
* What the calls keep alive, not what they allocate. Task/group `alloc`
|
|
770
|
+
* overrides this. */
|
|
761
771
|
alloc?: boolean;
|
|
762
772
|
/** Capture one extra `phase: "memstats"` measurement per task: how far the
|
|
763
|
-
* task's first call raises RSS, garbage included
|
|
764
|
-
*
|
|
765
|
-
* `
|
|
766
|
-
* `TaskOptions.peakMem` / `GroupOptions.peakMem` override this per task or
|
|
767
|
-
* group. */
|
|
773
|
+
* task's first call raises RSS, garbage included. Median of 3 fresh
|
|
774
|
+
* processes, each run with `OSTIA_PEAK_MEM=1` in its environment (see
|
|
775
|
+
* `measurePeakMem`). Task/group `peakMem` overrides this. */
|
|
768
776
|
peakMem?: boolean;
|
|
769
777
|
filter?: string;
|
|
770
|
-
/** Suite files to run at once, each
|
|
771
|
-
* 1).
|
|
772
|
-
*
|
|
773
|
-
*
|
|
774
|
-
* are noisier and not like-for-like with a baseline measured at 1. When
|
|
775
|
-
* `isolate` puts some tasks in their own subprocess, `jobs` pools across
|
|
776
|
-
* those per-task processes the same way - so the same noise/wall-clock
|
|
777
|
-
* tradeoff now scales with task count, not just file count. */
|
|
778
|
+
/** Suite files (or isolated tasks) to run at once, each in its own child
|
|
779
|
+
* process (default: 1). Concurrent CPU-bound processes contend for cores,
|
|
780
|
+
* caches and turbo headroom, so timings under `jobs > 1` are noisier and
|
|
781
|
+
* not like-for-like with a baseline measured at 1. */
|
|
778
782
|
jobs?: number;
|
|
779
783
|
outDir?: string;
|
|
780
784
|
cwd?: string;
|
|
781
785
|
/** Give every task its own subprocess instead of sharing its suite file's,
|
|
782
|
-
* isolating
|
|
783
|
-
*
|
|
784
|
-
*
|
|
785
|
-
* task or group for mixed suites (e.g. a few outlier-prone tasks isolated,
|
|
786
|
-
* many cheap ones sharing a process). Multiplies process-spawn overhead by
|
|
787
|
-
* task count instead of file count. */
|
|
786
|
+
* isolating JIT tier state, inline caches and heap shape between tasks.
|
|
787
|
+
* Task/group `isolate` overrides this. Multiplies spawn overhead by task
|
|
788
|
+
* count instead of file count. */
|
|
788
789
|
isolate?: boolean;
|
|
789
|
-
/** Scripts run, in order, before each suite file loads
|
|
790
|
-
* subprocess
|
|
791
|
-
*
|
|
792
|
-
* of the suite's own top-level code. Consumer-authored; ostia ships no
|
|
793
|
-
* preload scripts itself. */
|
|
790
|
+
/** Scripts run, in order, before each suite file loads, in the same
|
|
791
|
+
* subprocess: install globals (jsdom) or a `Bun.plugin()` loader ahead of
|
|
792
|
+
* the suite's top-level code. */
|
|
794
793
|
preload?: string[];
|
|
795
|
-
/** Extra flags
|
|
796
|
-
*
|
|
797
|
-
* script path so `bun` itself parses them rather than the runner. Useful
|
|
798
|
-
* for suites that import packages whose `exports` map branches on a
|
|
799
|
-
* resolution condition Bun doesn't set by default (e.g. Svelte/Vue's
|
|
800
|
-
* `browser` vs `default` builds). */
|
|
794
|
+
/** Extra flags for the `bun` invocation that runs each suite file (e.g.
|
|
795
|
+
* `["--conditions", "browser"]`), placed before the runner script path. */
|
|
801
796
|
bunFlags?: string[];
|
|
802
797
|
/** Measure this machine's noise floor once, before any suite runs
|
|
803
|
-
* (default: true), and stamp it on the document as
|
|
804
|
-
* `environment`. Set false to skip the ~200ms reference measurement. */
|
|
798
|
+
* (default: true), and stamp it on the document as `environment`. */
|
|
805
799
|
noiseCheck?: boolean;
|
|
806
|
-
/**
|
|
807
|
-
*
|
|
808
|
-
* ms. No default: an unset `timeoutMs` never times out. Applies to the
|
|
809
|
-
* whole subprocess, not per task - the same granularity `isolate` already
|
|
810
|
-
* runs at. */
|
|
800
|
+
/** SIGKILLs a suite file's subprocess (or, under `isolate`, one task's) that
|
|
801
|
+
* hasn't finished after this many ms. No default. */
|
|
811
802
|
timeoutMs?: number;
|
|
812
|
-
/** Aborting
|
|
813
|
-
*
|
|
814
|
-
*
|
|
815
|
-
*
|
|
816
|
-
* measurement. A suite subprocess killed mid-run contributes nothing (it
|
|
817
|
-
* only writes its result once, at the end), so a suite that was in flight
|
|
818
|
-
* when the signal fired is dropped entirely rather than partially
|
|
819
|
-
* represented. */
|
|
803
|
+
/** Aborting kills in-flight subprocesses, starts no new ones, and resolves
|
|
804
|
+
* (never rejects) with whatever suites/tasks had finished, plus an
|
|
805
|
+
* `aborted` warning on the last measurement. A suite killed mid-run is
|
|
806
|
+
* dropped entirely: it only writes its result once, at the end. */
|
|
820
807
|
signal?: AbortSignal;
|
|
821
808
|
}
|
|
822
809
|
|
|
@@ -838,6 +825,10 @@ export interface TaskOptions {
|
|
|
838
825
|
/** Per-task hard floor on trials; overrides the suite-wide `--min-samples` /
|
|
839
826
|
* `minSamples`. */
|
|
840
827
|
minSamples?: number;
|
|
828
|
+
/** Former names of this task in the same group, so `compare` and `ab`
|
|
829
|
+
* match a base measured before a rename instead of reporting the old name
|
|
830
|
+
* as removed and the new one as new. */
|
|
831
|
+
aliases?: string[];
|
|
841
832
|
/** What this task measures and why. Flows into `Workload.description` so the
|
|
842
833
|
* intent travels with the numbers instead of living only in a source comment. */
|
|
843
834
|
description?: string;
|
|
@@ -907,38 +898,39 @@ export interface GroupOptions {
|
|
|
907
898
|
after?: Hook;
|
|
908
899
|
}
|
|
909
900
|
|
|
910
|
-
|
|
911
|
-
|
|
901
|
+
type GroupRegistrar = (name: string, fn: () => void, opts?: GroupOptions) => void;
|
|
902
|
+
|
|
903
|
+
interface GroupFn extends GroupRegistrar {
|
|
912
904
|
/** Registers every task inside as skipped: the runner never measures
|
|
913
905
|
* them, but the document still carries their workloads (marked
|
|
914
906
|
* `Workload.skipped`). */
|
|
915
|
-
skip:
|
|
907
|
+
skip: GroupRegistrar;
|
|
916
908
|
/** When any task or group in the suite uses `.only`, the runner restricts
|
|
917
909
|
* the whole suite file to only those tasks (before `--filter` narrows
|
|
918
910
|
* further) and prints a one-line notice to stderr. */
|
|
919
|
-
only:
|
|
911
|
+
only: GroupRegistrar;
|
|
920
912
|
}
|
|
921
913
|
|
|
922
914
|
export declare const group: GroupFn;
|
|
923
915
|
|
|
924
|
-
|
|
925
|
-
|
|
916
|
+
type TaskRegistrar = (name: string, fn: () => unknown | Promise<unknown>, opts?: TaskOptions) => void;
|
|
917
|
+
|
|
918
|
+
interface TaskFn extends TaskRegistrar {
|
|
926
919
|
/** Registers the task as skipped: the runner never measures it, but the
|
|
927
920
|
* document still carries its workload (marked `Workload.skipped`) so a
|
|
928
921
|
* renderer or `compare` can say so explicitly instead of the task simply
|
|
929
922
|
* being absent. */
|
|
930
|
-
skip:
|
|
923
|
+
skip: TaskRegistrar;
|
|
931
924
|
/** When any task or group in the suite uses `.only`, the runner restricts
|
|
932
925
|
* the whole suite file to only those tasks (before `--filter` narrows
|
|
933
926
|
* further) and prints a one-line notice to stderr. */
|
|
934
|
-
only:
|
|
927
|
+
only: TaskRegistrar;
|
|
935
928
|
}
|
|
936
929
|
|
|
937
930
|
export declare const task: TaskFn;
|
|
938
931
|
|
|
939
932
|
export interface RunOptions extends MeasureTasksOpts {
|
|
940
|
-
/** Regex
|
|
941
|
-
* `ostia bench --filter` / `bench({ filter })`. */
|
|
933
|
+
/** Regex matched against "group/name" task ids, as in `bench({ filter })`. */
|
|
942
934
|
filter?: string;
|
|
943
935
|
/** Skip printing the report to stdout; still returns the document. */
|
|
944
936
|
quiet?: boolean;
|
|
@@ -950,6 +942,11 @@ export declare function run(opts?: RunOptions): Promise<ProfileDocument>;
|
|
|
950
942
|
|
|
951
943
|
type FormatName = "table" | "json" | "markdown" | "jsonl" | "minimal" | "collapsed" | "mermaid" | "speedscope" | "cpuprofile";
|
|
952
944
|
|
|
945
|
+
export interface VizOptions {
|
|
946
|
+
/** Render this measurement only, instead of every CPU measurement. */
|
|
947
|
+
measurementId?: string;
|
|
948
|
+
}
|
|
949
|
+
|
|
953
950
|
interface RenderResult {
|
|
954
951
|
text?: string;
|
|
955
952
|
files?: {
|
|
@@ -964,32 +961,44 @@ interface Renderer<O = unknown> {
|
|
|
964
961
|
}
|
|
965
962
|
|
|
966
963
|
interface MeasureTasksOpts extends InprocessTimingOptions {
|
|
967
|
-
/** Suite-wide default for capturing an extra `phase: "cpu"` measurement
|
|
968
|
-
*
|
|
964
|
+
/** Suite-wide default for capturing an extra `phase: "cpu"` measurement per
|
|
965
|
+
* task (task/group `cpu` still wins). */
|
|
969
966
|
cpu?: boolean;
|
|
970
967
|
/** `cpu`'s sampling interval, µs (default: 100). */
|
|
971
968
|
cpuIntervalUs?: number;
|
|
972
969
|
/** Suite-wide default for capturing an extra `phase: "memstats"`
|
|
973
|
-
* measurement per task (task/group `
|
|
974
|
-
* still win). */
|
|
970
|
+
* measurement per task (task/group `alloc` still wins). */
|
|
975
971
|
alloc?: boolean;
|
|
976
|
-
/** Stamped onto every workload
|
|
977
|
-
* ran in a subprocess dedicated to it alone. */
|
|
972
|
+
/** Stamped onto every workload: it ran in a subprocess dedicated to it. */
|
|
978
973
|
markIsolated?: boolean;
|
|
979
974
|
/** Measure this machine's noise floor before the first task (default:
|
|
980
|
-
* true) and stamp it on the document as `environment`.
|
|
981
|
-
* the ~200ms reference measurement. */
|
|
975
|
+
* true) and stamp it on the document as `environment`. */
|
|
982
976
|
noiseCheck?: boolean;
|
|
983
977
|
}
|
|
984
978
|
|
|
979
|
+
interface InprocessTimingOptions {
|
|
980
|
+
/** Wall-clock budget for the sampling loop, per task (default: 500). */
|
|
981
|
+
budgetMs?: number;
|
|
982
|
+
/** Exact trial count; the budget is ignored. */
|
|
983
|
+
samples?: number;
|
|
984
|
+
/** Keep sampling past the budget until this many trials exist. When unset
|
|
985
|
+
* the floor is cost-aware (see `defaultSampleFloor`). */
|
|
986
|
+
minSamples?: number;
|
|
987
|
+
/** Warmup budget as a fraction of `budgetMs` (default: 0.1); unlike
|
|
988
|
+
* `time()`'s `warmup`, which is a trial count. Always runs at least one call. */
|
|
989
|
+
warmup?: number;
|
|
990
|
+
gc?: boolean;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
export declare function keep(value: unknown): void;
|
|
994
|
+
|
|
985
995
|
type SweepPoint<T extends Record<string, readonly unknown[]>> = {
|
|
986
996
|
[K in keyof T]: T[K][number];
|
|
987
997
|
};
|
|
988
998
|
|
|
989
999
|
export declare function sweep<T extends Record<string, readonly unknown[]>>(dims: T, fn: (point: SweepPoint<T>) => void): void;
|
|
990
1000
|
|
|
991
|
-
export declare function
|
|
992
|
-
export { newDocument as createDocument };
|
|
1001
|
+
export declare function createDocument(workloads: Workload[], measurements: Measurement[], environment?: Environment): ProfileDocument;
|
|
993
1002
|
|
|
994
1003
|
export declare function saveDocument(doc: ProfileDocument, path: string): Promise<void>;
|
|
995
1004
|
|
|
@@ -1005,9 +1014,45 @@ export declare class OstiaDocumentError extends Error {
|
|
|
1005
1014
|
|
|
1006
1015
|
export declare function loadDocument(path: string): Promise<ProfileDocument>;
|
|
1007
1016
|
|
|
1008
|
-
export
|
|
1017
|
+
export interface ProfileOptions {
|
|
1018
|
+
/** Sampling interval in µs; default 1000. Named like `time()`'s and
|
|
1019
|
+
* `bench()`'s option, the config key and `--cpu-interval`. */
|
|
1020
|
+
cpuIntervalUs?: number;
|
|
1021
|
+
/** Names the workload: it becomes the label and, instead of the function's
|
|
1022
|
+
* name and source, what the id hashes, so closures that differ only in
|
|
1023
|
+
* captured values get distinct ids and the id survives editing the function. */
|
|
1024
|
+
name?: string;
|
|
1025
|
+
/** `"inspector"` (default) captures through CDP and writes a portable
|
|
1026
|
+
* .cpuprofile; `"jsc"` uses `bun:jsc.profile` and adds LLInt/Baseline/DFG/FTL
|
|
1027
|
+
* tier data. */
|
|
1028
|
+
origin?: "inspector" | "jsc";
|
|
1029
|
+
/** There's no child to kill, so an already-aborted signal skips the profiler,
|
|
1030
|
+
* calls `fn` plain (still returning its `result`) and records an `aborted`
|
|
1031
|
+
* warning instead of CPU evidence. It can't interrupt a running `fn`. */
|
|
1032
|
+
signal?: AbortSignal;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
export interface ProfileResult<T> {
|
|
1036
|
+
result: T;
|
|
1037
|
+
measurement: Measurement;
|
|
1038
|
+
document: ProfileDocument;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
export declare function profile<T>(fn: () => T | Promise<T>, opts?: ProfileOptions): Promise<ProfileResult<T>>;
|
|
1042
|
+
|
|
1043
|
+
export declare const renderers: {
|
|
1044
|
+
table: Renderer<unknown>;
|
|
1045
|
+
json: Renderer<unknown>;
|
|
1046
|
+
markdown: Renderer<unknown>;
|
|
1047
|
+
jsonl: Renderer<unknown>;
|
|
1048
|
+
minimal: Renderer<MinimalRenderOptions>;
|
|
1049
|
+
collapsed: Renderer<VizOptions>;
|
|
1050
|
+
mermaid: Renderer<MermaidOptions>;
|
|
1051
|
+
speedscope: Renderer<VizOptions>;
|
|
1052
|
+
cpuprofile: Renderer<VizOptions>;
|
|
1053
|
+
};
|
|
1009
1054
|
|
|
1010
|
-
export declare const MINIMAL_PROTOCOL_VERSION:
|
|
1055
|
+
export declare const MINIMAL_PROTOCOL_VERSION: 2;
|
|
1011
1056
|
|
|
1012
1057
|
interface MinimalWarning {
|
|
1013
1058
|
code: string;
|
|
@@ -1015,64 +1060,72 @@ interface MinimalWarning {
|
|
|
1015
1060
|
}
|
|
1016
1061
|
|
|
1017
1062
|
interface MinimalDelta {
|
|
1063
|
+
/** The base's median, ns: the row's own `median` is the candidate's. */
|
|
1064
|
+
baseMedian?: number;
|
|
1018
1065
|
medianPct: number;
|
|
1019
1066
|
meanPct: number;
|
|
1020
1067
|
verdict: "improved" | "regressed" | "unchanged";
|
|
1021
1068
|
pass: boolean;
|
|
1022
|
-
/** 95% bootstrap CI on the difference of medians
|
|
1023
|
-
*
|
|
1024
|
-
* comparison, which falls back to a point-estimate threshold. */
|
|
1069
|
+
/** 95% bootstrap CI on the difference of medians; absent (with `pValue`)
|
|
1070
|
+
* on a thin (<5 samples/side) comparison. */
|
|
1025
1071
|
ci95?: [number, number];
|
|
1026
1072
|
pValue?: number;
|
|
1027
|
-
/** The threshold
|
|
1028
|
-
* noise widened it past `thresholds.timingPct` - see
|
|
1029
|
-
* `Comparison.thresholds.effectiveTimingPct`. */
|
|
1073
|
+
/** The threshold actually tested against, after machine noise widened it. */
|
|
1030
1074
|
effectiveTimingPct: number;
|
|
1031
1075
|
matched: true;
|
|
1076
|
+
/** The base task's name, when it was matched through an alias. */
|
|
1077
|
+
renamedFrom?: string;
|
|
1032
1078
|
}
|
|
1033
1079
|
|
|
1034
1080
|
interface MinimalPaired {
|
|
1035
1081
|
/** Base side's median per-call time, ns. */
|
|
1036
1082
|
baseMedian: number;
|
|
1037
1083
|
medianRatio: number;
|
|
1038
|
-
|
|
1039
|
-
|
|
1084
|
+
ratioP25: number;
|
|
1085
|
+
ratioP75: number;
|
|
1040
1086
|
rounds: number;
|
|
1041
|
-
/**
|
|
1042
|
-
* `"unchanged"`. */
|
|
1087
|
+
/** `flagged` when fresh processes confirmed it, else `"unchanged"`. */
|
|
1043
1088
|
verdict: "regressed" | "improved" | "unchanged";
|
|
1044
|
-
/** What
|
|
1089
|
+
/** What the first process saw, before confirmation. */
|
|
1045
1090
|
flagged?: "regressed" | "improved";
|
|
1046
1091
|
confirmed?: boolean;
|
|
1047
1092
|
/** Median ratio of each fresh-process repeat. */
|
|
1048
1093
|
repeats?: number[];
|
|
1049
1094
|
sameOutput: boolean;
|
|
1095
|
+
/** The suite file differs from the base's copy. */
|
|
1096
|
+
suiteChanged?: true;
|
|
1097
|
+
/** `--alloc`: retained heap per call on each side. */
|
|
1098
|
+
retained?: MinimalMemoryChange;
|
|
1099
|
+
/** `--peak-mem`: first-call RSS rise on each side. */
|
|
1100
|
+
peak?: MinimalMemoryChange;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
interface MinimalMemoryChange {
|
|
1104
|
+
base: number;
|
|
1105
|
+
cand: number;
|
|
1106
|
+
verdict: "regressed" | "improved" | "unchanged";
|
|
1107
|
+
floor: number;
|
|
1050
1108
|
}
|
|
1051
1109
|
|
|
1052
1110
|
interface MinimalRunLine {
|
|
1053
1111
|
event: "run";
|
|
1054
1112
|
protocolVersion: typeof MINIMAL_PROTOCOL_VERSION;
|
|
1055
1113
|
schemaVersion: ProfileDocument["schemaVersion"];
|
|
1056
|
-
/** Join key
|
|
1057
|
-
* stable across
|
|
1058
|
-
* unlike `task` (a label, not an identity). */
|
|
1114
|
+
/** Join key to `Workload.id` / `Comparison.candidateMeasurementId`; unlike
|
|
1115
|
+
* `task`, stable across invocations. */
|
|
1059
1116
|
workloadId: string;
|
|
1060
1117
|
task: string;
|
|
1061
1118
|
group?: string;
|
|
1062
1119
|
description?: string;
|
|
1063
1120
|
groupDescription?: string;
|
|
1064
|
-
/** From `task(name, fn, { params })` or a `sweep()` point. */
|
|
1065
1121
|
params?: Record<string, string | number | boolean>;
|
|
1066
|
-
/**
|
|
1067
|
-
* stats field below is absent on this line. */
|
|
1122
|
+
/** `task.skip()` / `group.skip()`: no measurement, so every stats field is absent. */
|
|
1068
1123
|
skipped?: true;
|
|
1069
1124
|
unit?: "ns";
|
|
1125
|
+
/** 0, with every stats field absent, when no trial produced a sample (all
|
|
1126
|
+
* timed out or missed the `timeSource` pattern); see `warnings`. */
|
|
1070
1127
|
samples?: number;
|
|
1071
|
-
/** In-process trials batched into one timed block
|
|
1072
|
-
* `measure/inprocess.ts`'s `sizeBatch`) so the timer's own resolution
|
|
1073
|
-
* doesn't dominate a sub-microsecond task's reading. 1 when the timing
|
|
1074
|
-
* engine never batched (every subprocess run, and any in-process run
|
|
1075
|
-
* whose single call already clears the batching threshold). */
|
|
1128
|
+
/** In-process trials batched into one timed block; 1 when never batched. */
|
|
1076
1129
|
batch: number;
|
|
1077
1130
|
mean?: number;
|
|
1078
1131
|
median?: number;
|
|
@@ -1087,24 +1140,30 @@ interface MinimalRunLine {
|
|
|
1087
1140
|
/** Median user / system CPU time per trial, ns: subprocess commands only. */
|
|
1088
1141
|
userNs?: number;
|
|
1089
1142
|
systemNs?: number;
|
|
1090
|
-
/** `--alloc`: heap each call keeps alive after a full GC,
|
|
1143
|
+
/** `--alloc`: heap each call keeps alive after a full GC, bytes. */
|
|
1091
1144
|
retainedBytesPerOp?: number;
|
|
1092
1145
|
/** `--peak-mem`: how far the task's first call raised RSS, bytes. */
|
|
1093
1146
|
peakBytes?: number;
|
|
1094
1147
|
/** Median over the group's reference median (its baseline task, else its
|
|
1095
|
-
* fastest)
|
|
1148
|
+
* fastest); only when the document has more than one timing run. */
|
|
1096
1149
|
relative?: number;
|
|
1097
1150
|
baseline?: true;
|
|
1098
|
-
/**
|
|
1099
|
-
*
|
|
1100
|
-
* From `document.environment`; absent when `noiseCheck: false` skipped it. */
|
|
1151
|
+
/** The machine's noise floor from `document.environment`; absent when the
|
|
1152
|
+
* noise check was skipped. */
|
|
1101
1153
|
noiseFloorPct?: number;
|
|
1102
1154
|
warnings: MinimalWarning[];
|
|
1103
|
-
/**
|
|
1104
|
-
* the baseline document for this task. */
|
|
1155
|
+
/** The change against the baseline document (`compare` / `ci`). */
|
|
1105
1156
|
delta?: MinimalDelta;
|
|
1106
|
-
/** `ab` only; see `MinimalPaired`. */
|
|
1107
1157
|
paired?: MinimalPaired;
|
|
1158
|
+
/** `ab` only, instead of stats and `paired`: the task threw on one side
|
|
1159
|
+
* (`"base"`/`"cand"`) or both, so it isn't judged on time. `message` is
|
|
1160
|
+
* the error's first lines; `repeat` (from 1) is set when it threw in a
|
|
1161
|
+
* confirmation repeat. */
|
|
1162
|
+
threw?: {
|
|
1163
|
+
side: "base" | "cand" | "both";
|
|
1164
|
+
message: string;
|
|
1165
|
+
repeat?: number;
|
|
1166
|
+
};
|
|
1108
1167
|
}
|
|
1109
1168
|
|
|
1110
1169
|
interface MinimalUnmatchedLine {
|
|
@@ -1131,6 +1190,8 @@ interface MinimalSummaryLine {
|
|
|
1131
1190
|
missingBaseline?: number;
|
|
1132
1191
|
geomeanPct: number | null;
|
|
1133
1192
|
effectiveTimingPct: number;
|
|
1193
|
+
/** `compare`/`ci`: the machine's drift, added to the timing threshold. */
|
|
1194
|
+
driftPct?: number;
|
|
1134
1195
|
noiseFloorPct?: number;
|
|
1135
1196
|
/** `ci` only. */
|
|
1136
1197
|
baseline?: {
|
|
@@ -1144,19 +1205,112 @@ interface MinimalSummaryLine {
|
|
|
1144
1205
|
};
|
|
1145
1206
|
/** `ab` only: the gate on `geomeanPct`. */
|
|
1146
1207
|
geomeanThresholdPct?: number;
|
|
1147
|
-
/** `ab` only: flagged in the first process but not reproduced
|
|
1148
|
-
* fresh-process repeats; counted in `unchanged`. */
|
|
1208
|
+
/** `ab` only: flagged in the first process but not reproduced; counted in `unchanged`. */
|
|
1149
1209
|
unconfirmed?: number;
|
|
1150
|
-
/** `ab` only: workloads whose first call returned different values on the
|
|
1151
|
-
* two sides. Informational. */
|
|
1210
|
+
/** `ab` only: workloads whose first call returned different values on the two sides. */
|
|
1152
1211
|
outputDiffers?: number;
|
|
1212
|
+
notComparable?: number;
|
|
1213
|
+
threw?: number;
|
|
1214
|
+
newSuites?: string[];
|
|
1215
|
+
/** `ab` only, with `--alloc` or `--peak-mem`: tasks whose memory
|
|
1216
|
+
* regressed or improved, and the threshold, percent. */
|
|
1217
|
+
memory?: {
|
|
1218
|
+
thresholdPct: number;
|
|
1219
|
+
regressed: number;
|
|
1220
|
+
improved: number;
|
|
1221
|
+
};
|
|
1153
1222
|
git?: {
|
|
1154
1223
|
base?: GitMetadata;
|
|
1155
1224
|
cand?: GitMetadata;
|
|
1156
1225
|
};
|
|
1157
1226
|
exportedTo?: string;
|
|
1158
|
-
|
|
1227
|
+
/** `pass` (exit 0), `fail` (exit 1: a regression), else `error` (exit 2, or
|
|
1228
|
+
* 130 when cancelled): no verdict was reached. */
|
|
1229
|
+
verdict: "pass" | "fail" | "error";
|
|
1159
1230
|
exitCode: number;
|
|
1160
1231
|
}
|
|
1161
1232
|
|
|
1162
1233
|
export type MinimalEvent = MinimalRunLine | MinimalUnmatchedLine | MinimalSummaryLine;
|
|
1234
|
+
|
|
1235
|
+
export interface MinimalProtocolContext {
|
|
1236
|
+
command: "compare" | "ci" | "ab";
|
|
1237
|
+
exitCode: number;
|
|
1238
|
+
unmatched?: {
|
|
1239
|
+
baseOnly: Workload[];
|
|
1240
|
+
candOnly: Workload[];
|
|
1241
|
+
};
|
|
1242
|
+
baseGit?: GitMetadata;
|
|
1243
|
+
candGit?: GitMetadata;
|
|
1244
|
+
baseline?: {
|
|
1245
|
+
name: string;
|
|
1246
|
+
path: string;
|
|
1247
|
+
};
|
|
1248
|
+
cached?: number;
|
|
1249
|
+
executed?: number;
|
|
1250
|
+
failed?: number;
|
|
1251
|
+
missingBaseline?: number;
|
|
1252
|
+
exportedTo?: string;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
export interface MinimalRenderOptions {
|
|
1256
|
+
protocol?: MinimalProtocolContext;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
export interface MermaidOptions extends VizOptions {
|
|
1260
|
+
topN?: number;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
export interface CommandSpec {
|
|
1264
|
+
command: string | string[];
|
|
1265
|
+
label?: string;
|
|
1266
|
+
/** Overrides `TimeOptions.prepare` for this command. */
|
|
1267
|
+
prepare?: PrepareHook;
|
|
1268
|
+
/** Overrides `TimeOptions.timeSource` for this command. */
|
|
1269
|
+
timeSource?: TimeSource;
|
|
1270
|
+
/** Overrides `TimeOptions.timeoutMs` for this command. */
|
|
1271
|
+
timeoutMs?: number;
|
|
1272
|
+
/** Overrides `TimeOptions.ignoreExitCodes` for this command. */
|
|
1273
|
+
ignoreExitCodes?: number[];
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
export interface TimeOptions {
|
|
1277
|
+
commands: (string | string[] | CommandSpec)[];
|
|
1278
|
+
/** Runs unmeasured before every trial (warmup and instrumented included), in
|
|
1279
|
+
* `cwd`/`env`: a command spawned and awaited, or a function. */
|
|
1280
|
+
prepare?: PrepareHook;
|
|
1281
|
+
/** Take timing from a number in each command's own output (e.g. a build
|
|
1282
|
+
* tool's `built in 342ms`) instead of its wall clock. */
|
|
1283
|
+
timeSource?: TimeSource;
|
|
1284
|
+
/** Exact trial count; when set, `budgetMs` is ignored. */
|
|
1285
|
+
samples?: number;
|
|
1286
|
+
/** Wall-clock budget for the sampling loop, ms. Default: ~3s when neither
|
|
1287
|
+
* `samples` nor `budgetMs` is given. */
|
|
1288
|
+
budgetMs?: number;
|
|
1289
|
+
/** Floor on trials when no exact `samples` is given. */
|
|
1290
|
+
minSamples?: number;
|
|
1291
|
+
warmup?: number;
|
|
1292
|
+
/** Round-robin trials across commands instead of finishing each command
|
|
1293
|
+
* first, so drift hits every command equally. Default: true with 2+ commands. */
|
|
1294
|
+
interleave?: boolean;
|
|
1295
|
+
cwd?: string;
|
|
1296
|
+
env?: Record<string, string>;
|
|
1297
|
+
cpu?: boolean;
|
|
1298
|
+
heap?: boolean;
|
|
1299
|
+
cpuIntervalUs?: number;
|
|
1300
|
+
outDir?: string;
|
|
1301
|
+
/** Measure the machine's noise floor first (default true, ~200ms) and stamp
|
|
1302
|
+
* it on the document as `environment`. */
|
|
1303
|
+
noiseCheck?: boolean;
|
|
1304
|
+
/** SIGKILLs a trial (or prepare hook) still running after this many ms. No
|
|
1305
|
+
* default. A timed-out trial contributes no sample. */
|
|
1306
|
+
timeoutMs?: number;
|
|
1307
|
+
/** Abort kills in-flight trials, schedules no more, and resolves (never
|
|
1308
|
+
* rejects) with the completed measurements plus an `aborted` warning on the
|
|
1309
|
+
* last one. */
|
|
1310
|
+
signal?: AbortSignal;
|
|
1311
|
+
/** Exit codes to treat as success (hyperfine's `--ignore-failure`): the
|
|
1312
|
+
* trial still contributes its sample and gets no `nonzero-exit` warning. */
|
|
1313
|
+
ignoreExitCodes?: number[];
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
export declare function time(opts: TimeOptions): Promise<ProfileDocument>;
|