ostia 0.1.6 → 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/README.md +460 -96
- package/chunk-qbt85kmg.js +4 -0
- package/chunk-r5b8vf8w.js +21 -0
- package/cli.js +147 -62
- package/index.d.ts +301 -31
- package/index.js +1 -1
- package/package.json +1 -1
- package/runner.ts +5 -4
- package/chunk-nfgy545q.js +0 -4
- package/chunk-tw064qem.js +0 -21
package/README.md
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
# ostia
|
|
2
2
|
|
|
3
|
-
ostia is a profiling and benchmarking toolkit for Bun.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
ostia is a profiling and benchmarking toolkit for Bun. One schema-versioned JSON
|
|
4
|
+
document (`ProfileDocument`) holds wall-clock timing, CPU hotspots, heap summaries,
|
|
5
|
+
JIT tier data, and allocation counts for a subprocess command or an in-process
|
|
6
|
+
function - the CLI and the library speak the same language, so anything you can do
|
|
7
|
+
with `ostia time`/`ostia bench` you can do with `time()`/`bench()` too.
|
|
8
|
+
|
|
9
|
+
Comparing two documents reports a bootstrap confidence interval and a Mann-Whitney
|
|
10
|
+
p-value, not a bare percentage past a threshold, and widens the regression bar to
|
|
11
|
+
the machine's own noise floor so ambient jitter is never mistaken for a real
|
|
12
|
+
regression. `ostia ci` gates a whole `ostia.config.json`/`ostia.config.ts` of
|
|
13
|
+
workloads - subprocess commands and in-process `group()`/`task()` suites alike -
|
|
14
|
+
against a saved baseline, skipping anything whose input fingerprint hasn't changed.
|
|
15
|
+
Any task can run in its own subprocess for clean JIT/heap isolation from its
|
|
16
|
+
suite-mates, and every renderer has a `minimal` JSON-lines mode built for piping
|
|
17
|
+
straight into an LLM agent's context.
|
|
9
18
|
|
|
10
19
|
Zero runtime dependencies. Requires Bun ≥ 1.4.
|
|
11
20
|
|
|
@@ -20,31 +29,52 @@ bun add ostia
|
|
|
20
29
|
Compare two commands:
|
|
21
30
|
|
|
22
31
|
```sh
|
|
23
|
-
ostia
|
|
32
|
+
ostia time --samples 10 --warmup 2 "bun fixtures/fast.ts" "bun fixtures/slow.ts"
|
|
24
33
|
```
|
|
25
34
|
|
|
26
35
|
```
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
36
|
+
Apple M2 · 8 cores · load 10.1 · noise floor 6.2%
|
|
37
|
+
|
|
38
|
+
Task Median Spread Range Relative
|
|
39
|
+
--------------------------------------------------------------------------------
|
|
40
|
+
bun fixtures/fast.ts 16.6 ms 25.3 ms…32.8 ms 10.4 ms…33.2 ms 1.00×
|
|
41
|
+
! noisy-machine
|
|
42
|
+
bun fixtures/slow.ts 40.2 ms 48.2 ms…63.9 ms 28.6 ms…64.7 ms 2.43× slower
|
|
43
|
+
|
|
44
|
+
Warnings:
|
|
45
|
+
bun fixtures/fast.ts: Load average 10.10 exceeds 75% of 8 available core(s); timing noise may be elevated.
|
|
31
46
|
```
|
|
32
47
|
|
|
48
|
+
The header line (machine, cores, load, noise floor) prints whenever a document
|
|
49
|
+
carries an `environment` - on by default; skip the ~200ms reference
|
|
50
|
+
measurement with `--no-noise-check`. `compare`/`ci` widen the regression
|
|
51
|
+
threshold to at least the noise floor, so a change smaller than the machine's
|
|
52
|
+
own jitter is never called a regression - see
|
|
53
|
+
[Statistics](#statistics-a-real-significance-test-not-a-percentage-threshold)
|
|
54
|
+
below.
|
|
55
|
+
|
|
33
56
|
Find a CPU hotspot (profiler runs as a separate labeled trial, never mixed into the
|
|
34
57
|
timing numbers above):
|
|
35
58
|
|
|
36
59
|
```sh
|
|
37
|
-
ostia
|
|
60
|
+
ostia time --samples 5 --cpu --cpu-interval 200 --export-json node_modules/.cache/ostia/doc.json "bun fixtures/work.ts"
|
|
38
61
|
```
|
|
39
62
|
|
|
40
63
|
```
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
64
|
+
Apple M2 · 8 cores · load 8.5 · noise floor 0.9%
|
|
65
|
+
|
|
66
|
+
Task Median Spread Range
|
|
67
|
+
-----------------------------------------------------------------------
|
|
68
|
+
bun fixtures/work.ts 470.6 ms 478.7 ms…499.3 ms 400.8 ms…500.1 ms
|
|
69
|
+
! noisy-machine
|
|
70
|
+
|
|
71
|
+
Warnings:
|
|
72
|
+
bun fixtures/work.ts: Load average 8.48 exceeds 75% of 8 available core(s); timing noise may be elevated.
|
|
44
73
|
|
|
45
|
-
CPU capture - bun fixtures/work.ts (instrumented, 200µs interval, diagnostic wall
|
|
46
|
-
100.0%
|
|
74
|
+
CPU capture - bun fixtures/work.ts (instrumented, 200µs interval, diagnostic wall 531.143ms)
|
|
75
|
+
100.0% 502.67ms self hashLoop
|
|
47
76
|
0.0% 0.00ms self (root)
|
|
77
|
+
0.0% 0.00ms self (module)
|
|
48
78
|
artifact: node_modules/.cache/ostia/artifacts/<run-id>-cpu.cpuprofile
|
|
49
79
|
```
|
|
50
80
|
|
|
@@ -57,8 +87,8 @@ they need to survive `node_modules` reinstalls between branches and CI jobs, so
|
|
|
57
87
|
Gate a change against a local baseline:
|
|
58
88
|
|
|
59
89
|
```sh
|
|
60
|
-
|
|
61
|
-
ostia ci
|
|
90
|
+
ostia baseline save # on known-good: measure ostia.config.json -> .ostia/baselines/main.json
|
|
91
|
+
ostia ci # on your branch: rerun changed workloads, exit 1 on regression
|
|
62
92
|
```
|
|
63
93
|
|
|
64
94
|
```
|
|
@@ -83,56 +113,81 @@ Profile CI: ✓
|
|
|
83
113
|
## CLI reference
|
|
84
114
|
|
|
85
115
|
```
|
|
86
|
-
ostia
|
|
116
|
+
ostia time <command...> time commands; optional --cpu / --heap capture
|
|
87
117
|
ostia bench <suite.ts...> in-process group()/task() suites (time-budgeted)
|
|
88
118
|
ostia compare <a> <b> diff two ProfileDocuments
|
|
89
|
-
ostia report <document.json> render a saved document
|
|
90
|
-
ostia viz <document.json> render CPU evidence to a file format
|
|
119
|
+
ostia report <document.json> render a saved document (table/json/markdown/collapsed/...)
|
|
91
120
|
ostia ci run configured workloads vs a baseline, gate regressions
|
|
121
|
+
ostia baseline save|list|show manage baseline ProfileDocuments
|
|
92
122
|
```
|
|
93
123
|
|
|
94
124
|
Every subcommand takes `--help` for its full flag list.
|
|
95
125
|
|
|
96
|
-
### `ostia
|
|
126
|
+
### `ostia time`
|
|
97
127
|
|
|
98
128
|
Clean wall-clock timing by default. `--cpu` / `--heap` schedule one extra instrumented
|
|
99
129
|
trial each, labeled separately in the document.
|
|
100
130
|
|
|
101
131
|
```sh
|
|
102
|
-
ostia
|
|
103
|
-
ostia
|
|
104
|
-
ostia
|
|
105
|
-
```
|
|
132
|
+
ostia time "bun a.ts" "bun b.ts"
|
|
133
|
+
ostia time --samples 25 --warmup 3 --cpu --heap "bun src/server.ts"
|
|
134
|
+
ostia time --format json --export-json out.json "bun a.ts"
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`--samples N` is an exact trial count; `--budget MS`
|
|
138
|
+
is a wall-clock time budget instead (default: a hyperfine-style ~3s min-total-time
|
|
139
|
+
loop when neither is given); `--min-samples N` is a hard floor when `--samples` isn't
|
|
140
|
+
given. The same three names work on `ostia bench` (`--budget`/`--samples`/
|
|
141
|
+
`--min-samples`), where `--budget` is a per-task sampling window -
|
|
142
|
+
`warmup` differs by surface, though: a trial count here, a
|
|
143
|
+
*fraction* of the budget for `ostia bench`, since in-process warmup has no natural
|
|
144
|
+
"N calls" unit before the JIT has even seen the function once.
|
|
145
|
+
|
|
146
|
+
With 2+ commands, trials round-robin across them by default (one trial per command,
|
|
147
|
+
repeated) rather than running one command's whole loop to completion before the next
|
|
148
|
+
starts - drift over the run's wall-clock span (thermal throttling, a noisy neighbor
|
|
149
|
+
process) then lands on every command equally instead of favoring whichever ran first
|
|
150
|
+
or last. `--no-interleave` (`interleave: false`) goes back to running each command's
|
|
151
|
+
loop to completion in turn. Interleaved measurements carry `Measurement.interleaved: true`.
|
|
152
|
+
Meaningless (and ignored) with a single command.
|
|
106
153
|
|
|
107
154
|
Timing table (two commands get a Relative column automatically):
|
|
108
155
|
|
|
109
156
|
```
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
bun fixtures/fast.ts 8.
|
|
113
|
-
bun fixtures/slow.ts 21.
|
|
157
|
+
Task Median Spread Range Relative
|
|
158
|
+
--------------------------------------------------------------------------------
|
|
159
|
+
bun fixtures/fast.ts 7.94 ms 8.31 ms…8.56 ms 7.68 ms…8.57 ms 1.00×
|
|
160
|
+
bun fixtures/slow.ts 21.3 ms 21.5 ms…22.1 ms 21.1 ms…22.1 ms 2.69× slower
|
|
161
|
+
! outliers-detected
|
|
162
|
+
|
|
163
|
+
Warnings:
|
|
164
|
+
bun fixtures/slow.ts: 1 outlier(s) detected (1 severe, 0 mild).
|
|
114
165
|
```
|
|
115
166
|
|
|
116
167
|
Heap summary (type counts from the snapshot trial):
|
|
117
168
|
|
|
118
169
|
```
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
bun fixtures/allocate.ts
|
|
170
|
+
Task Median Spread Range
|
|
171
|
+
---------------------------------------------------------------------------
|
|
172
|
+
bun fixtures/allocate.ts 23.8 ms 24.9 ms…29.4 ms 22.6 ms…30.0 ms
|
|
173
|
+
! outliers-detected
|
|
174
|
+
|
|
175
|
+
Warnings:
|
|
176
|
+
bun fixtures/allocate.ts: 8 outlier(s) detected (1 severe, 7 mild).
|
|
122
177
|
|
|
123
|
-
Heap snapshot - bun fixtures/allocate.ts (instrumented,
|
|
178
|
+
Heap snapshot - bun fixtures/allocate.ts (instrumented, 2516 objects, 0.12MB)
|
|
124
179
|
1369 string
|
|
125
|
-
|
|
126
|
-
|
|
180
|
+
423 code
|
|
181
|
+
319 closure
|
|
127
182
|
216 object shape
|
|
128
|
-
|
|
183
|
+
105 hidden
|
|
129
184
|
artifact: node_modules/.cache/ostia/artifacts/<run-id>-heap.heapsnapshot
|
|
130
185
|
```
|
|
131
186
|
|
|
132
187
|
### `ostia bench`
|
|
133
188
|
|
|
134
189
|
In-process microbenchmarks registered with `group()` / `task()`. Each task samples
|
|
135
|
-
for `--
|
|
190
|
+
for `--budget` (default 500ms). `--min-samples` is a hard floor kept even when it
|
|
136
191
|
overruns the budget. Left unset, the floor is cost-aware in both directions: as many
|
|
137
192
|
trials as fit in the budget (capped at 20) so one slow task can't blow the suite's total,
|
|
138
193
|
but never below the floor a task's per-trial cost earns it - 3 at ≤1ms, two more per
|
|
@@ -155,7 +210,7 @@ without re-deriving the policy from the raw sample array.
|
|
|
155
210
|
|
|
156
211
|
```sh
|
|
157
212
|
ostia bench bench/*.ts
|
|
158
|
-
ostia bench --
|
|
213
|
+
ostia bench --budget 500 --min-samples 50 bench/stats.ts
|
|
159
214
|
ostia bench bench/*.ts --jobs auto # suite files in parallel, see below
|
|
160
215
|
ostia bench bench/*.ts --format minimal # one compact JSON object per task
|
|
161
216
|
```
|
|
@@ -182,13 +237,31 @@ Bun/V8 batch calls together and amortize it away). `task(name, fn, { gc })` /
|
|
|
182
237
|
override pattern as `isolate` - useful when a few allocation-heavy tasks need GC settled
|
|
183
238
|
between trials but the rest of the suite doesn't.
|
|
184
239
|
|
|
240
|
+
`--cpu` captures one extra `phase: "cpu"` measurement per task on top of its timing
|
|
241
|
+
numbers: the task looped for a fixed 200ms window under the JSC sampling profiler
|
|
242
|
+
(JIT tiers included), never mixed into the timing numbers themselves. `--alloc` captures
|
|
243
|
+
an extra `phase: "memstats"` measurement: bytes allocated per call, from a
|
|
244
|
+
`Bun.gc(true)`-bracketed batch of 100 calls (`MemoryEvidence.bytesPerOp`). Both follow the
|
|
245
|
+
same per-task/per-group override pattern as `isolate`/`gc`: `task(name, fn, { cpu, alloc })`
|
|
246
|
+
/ `group(name, fn, { cpu, alloc })`. The terminal table prints an `Alloc/op` column when a
|
|
247
|
+
`memstats` measurement is present. With `--cpu` on, `ostia compare` reports per-frame CPU
|
|
248
|
+
deltas for bench tasks the same way it already does for `ostia time --cpu`.
|
|
249
|
+
|
|
250
|
+
When a `--cpu` capture spends more than 20% of its samples in the llint/baseline tiers,
|
|
251
|
+
the JIT never warmed the task up in that 200ms window, so its CPU numbers (and by
|
|
252
|
+
extension its timing) may not reflect steady state - the cpu measurement carries a
|
|
253
|
+
`jit-cold` warning (`{ llintPct, baselinePct, dfgPct, ftlPct }`), printed alongside the
|
|
254
|
+
CPU capture in the terminal table and folded into the task's line in `--format minimal`.
|
|
255
|
+
|
|
185
256
|
`--preload PATH` (repeatable) imports a script before each suite file loads, in the same
|
|
186
257
|
subprocess - the same shape as Bun's own `--preload` / `bunfig.toml`'s `preload` array. Use
|
|
187
258
|
it to install globals a suite needs at import time (jsdom's `document`/`window`) or register
|
|
188
259
|
a `Bun.plugin()` file-loader (e.g. compiling `.svelte`/`.vue` SFCs) before the suite's own
|
|
189
260
|
top-level code runs. Multiple `--preload` scripts run in the order given, so state one
|
|
190
261
|
installs (a plugin registration, a global) is visible to the next and to the suite itself.
|
|
191
|
-
ostia ships none of this itself - just the hook point
|
|
262
|
+
ostia ships none of this itself - just the hook point (for a full jsdom/happy-dom global
|
|
263
|
+
setup or a `Bun.plugin()` component-compile hook, see
|
|
264
|
+
[docs/preload-recipes.md](docs/preload-recipes.md)):
|
|
192
265
|
|
|
193
266
|
```ts
|
|
194
267
|
// bench/jsdom-setup.ts
|
|
@@ -201,15 +274,45 @@ Object.assign(globalThis, { document: dom.window.document, window: dom.window })
|
|
|
201
274
|
ostia bench --preload ./bench/jsdom-setup.ts bench/*.dom.bench.ts
|
|
202
275
|
```
|
|
203
276
|
|
|
277
|
+
`--bun-flags FLAGS` (repeatable, space-separated flags within one value are all appended)
|
|
278
|
+
passes extra flags through to the `bun` invocation that spawns each suite file - the fix for
|
|
279
|
+
packages whose `package.json` `exports` map branches on a resolution condition Bun doesn't
|
|
280
|
+
set by default. Svelte 5's `exports` map, for example, is `{ "browser": "./src/index-client.js",
|
|
281
|
+
"default": "./src/index-server.js" }`: without `--conditions browser`, Bun resolves `default`
|
|
282
|
+
(the server-rendering build), and mounting a component via `@testing-library/svelte` throws
|
|
283
|
+
`lifecycle_function_unavailable` since `mount()` isn't available server-side. The same applies
|
|
284
|
+
to Vue and other dual-target frameworks:
|
|
285
|
+
|
|
286
|
+
```sh
|
|
287
|
+
ostia bench --bun-flags="--conditions=browser" bench/*.dom.bench.ts
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Unlike `BUN_OPTIONS` (an env var Bun's CLI reads to prepend flags, which only reaches the
|
|
291
|
+
spawned suite process today because ostia's `Bun.spawn()` happens to inherit `process.env`),
|
|
292
|
+
`--bun-flags` is a declared, documented integration point that doesn't depend on the parent
|
|
293
|
+
shell's environment.
|
|
294
|
+
|
|
204
295
|
|
|
205
296
|
```
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
stats
|
|
209
|
-
stats/computeTimingStats (
|
|
210
|
-
|
|
297
|
+
Task Median Spread Range Relative
|
|
298
|
+
----------------------------------------------------------------------------------------------------
|
|
299
|
+
stats:
|
|
300
|
+
stats/computeTimingStats (1e3 samples) 24.7 µs 33.8 µs…151.6 µs 22.4 µs…1073.2 µs 1.00×
|
|
301
|
+
! outliers-detected
|
|
302
|
+
stats/computeTimingStats (1e4 samples) 255.2 µs 331.7 µs…1067.6 µs 223.2 µs…20789.4 µs 10.33× slower
|
|
303
|
+
! outliers-detected
|
|
304
|
+
stats/timingWarnings (1e3 samples) 47.3 µs 88.5 µs…510.4 µs 43.0 µs…14383.1 µs 1.91× slower
|
|
305
|
+
! outliers-detected
|
|
306
|
+
|
|
307
|
+
Warnings:
|
|
308
|
+
stats/computeTimingStats (1e3 samples): 2455 outlier(s) detected (2108 severe, 347 mild).
|
|
309
|
+
stats/computeTimingStats (1e4 samples): 215 outlier(s) detected (70 severe, 145 mild).
|
|
310
|
+
stats/timingWarnings (1e3 samples): 541 outlier(s) detected (191 severe, 350 mild).
|
|
211
311
|
```
|
|
212
312
|
|
|
313
|
+
Tasks with a `group()` print the group name once, indented; ungrouped tasks and
|
|
314
|
+
subprocess commands print flat.
|
|
315
|
+
|
|
213
316
|
### `ostia compare`
|
|
214
317
|
|
|
215
318
|
Match workloads by id, rank timing / frame / heap deltas, print a verdict per workload.
|
|
@@ -220,16 +323,63 @@ ostia compare after.json --baseline .ostia/baselines/main.json
|
|
|
220
323
|
```
|
|
221
324
|
|
|
222
325
|
```
|
|
223
|
-
|
|
224
|
-
timing:
|
|
326
|
+
✗ bun fixtures/work.ts
|
|
327
|
+
timing: +11.2% median, 95% CI [+10.0%, +16.4%], p<0.001 (regressed)
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
When both documents carry `git` metadata (see below), `ostia compare` prints a summary
|
|
331
|
+
line above the verdicts:
|
|
225
332
|
|
|
226
|
-
✗ work
|
|
227
|
-
timing: +1249.2% median (regressed)
|
|
228
333
|
```
|
|
334
|
+
base a1b2c3d (main) → cand d4e5f6a (my-opt, dirty)
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
The verdict needs both a confidence interval clear of `timingPct` and a
|
|
338
|
+
significant Mann-Whitney p-value (`thresholds.alpha`, default `0.01`), not
|
|
339
|
+
just a point estimate past the threshold - see
|
|
340
|
+
[Statistics](#statistics-a-real-significance-test-not-a-percentage-threshold)
|
|
341
|
+
below. Comparisons with fewer than 5 samples on either side fall back to the
|
|
342
|
+
old point-estimate rule and carry a `thin-comparison` warning instead.
|
|
343
|
+
|
|
344
|
+
#### Statistics: a real significance test, not a percentage threshold
|
|
345
|
+
|
|
346
|
+
A point estimate past `timingPct` is not enough to call something a
|
|
347
|
+
regression - both documents already carry full sample arrays, so `compare`
|
|
348
|
+
runs two tests instead:
|
|
349
|
+
|
|
350
|
+
- A **bootstrap confidence interval** on the difference of medians:
|
|
351
|
+
resample both sides with replacement `thresholds.bootstrapIterations`
|
|
352
|
+
times (default 2000; each side is randomly subsampled to at most 2000
|
|
353
|
+
samples first, so a many-thousand-sample task doesn't turn a compare into
|
|
354
|
+
a multi-second operation), and report the 2.5th/97.5th percentiles as
|
|
355
|
+
`ci95` (percent of the baseline median). Reproducible: the PRNG seed is
|
|
356
|
+
stored in `Comparison.timing.seed`.
|
|
357
|
+
- A **Mann-Whitney U test** (tie-corrected, normal approximation), reported
|
|
358
|
+
as `pValue` - whether the two sample distributions differ at all, without
|
|
359
|
+
assuming normality the way a t-test would.
|
|
360
|
+
|
|
361
|
+
`regressed` requires `ci95[0] > thresholds.timingPct` (the *whole interval*
|
|
362
|
+
clears the threshold) **and** `pValue < thresholds.alpha`; `improved` is the
|
|
363
|
+
mirror. Otherwise `unchanged`. This is why the earlier example (`+11.2%
|
|
364
|
+
median, 95% CI [+10.0%, +16.4%]`) is a clean regression: even the low end of
|
|
365
|
+
the interval is well past `timingPct`.
|
|
366
|
+
|
|
367
|
+
Both `time()` and `bench()` also stamp `environment` on every document (a
|
|
368
|
+
fixed-cost, deterministic, allocation-free hash loop sampled for ~200ms,
|
|
369
|
+
`noise.floorPct = mad / median`) unless `noiseCheck: false` / `--no-noise-check`
|
|
370
|
+
skips it. `compare` widens the effective threshold to
|
|
371
|
+
`max(thresholds.timingPct, base.environment.noise.floorPct,
|
|
372
|
+
cand.environment.noise.floorPct)` (`Comparison.thresholds.effectiveTimingPct`),
|
|
373
|
+
so a delta smaller than the machine's own jitter right now is never called a
|
|
374
|
+
regression. A `noisy-machine` warning fires when the 1-minute load average is
|
|
375
|
+
already past 75% of available cores at measurement time.
|
|
229
376
|
|
|
230
377
|
### `ostia report`
|
|
231
378
|
|
|
232
|
-
Render a saved `ProfileDocument` without re-running anything.
|
|
379
|
+
Render a saved `ProfileDocument` without re-running anything. `--format` covers
|
|
380
|
+
both the data formats (`table`/`json`/`jsonl`/`markdown`/`minimal`) and the CPU
|
|
381
|
+
visualization formats (`collapsed`/`mermaid`/`speedscope`/`cpuprofile`) - one
|
|
382
|
+
command instead of two.
|
|
233
383
|
|
|
234
384
|
```sh
|
|
235
385
|
ostia report out.json # table (default)
|
|
@@ -245,7 +395,7 @@ sample (tens of thousands for a fast task), which is tokens a reviewer never rea
|
|
|
245
395
|
Numbers stay in ns so they line up with `compare` deltas and the JSON document.
|
|
246
396
|
|
|
247
397
|
```
|
|
248
|
-
{"task":"diffText()/append at end","group":"diffText()","samples":9282,"mean":50213.4,"median":49871,"stddev":2104.7,"stddevPct":4.19,"min":48120,"max":81002,"relative":1,"warnings":[],"unit":"ns"}
|
|
398
|
+
{"task":"diffText()/append at end","group":"diffText()","samples":9282,"mean":50213.4,"median":49871,"stddev":2104.7,"stddevPct":4.19,"min":48120,"max":81002,"p75":50920,"p99":58011,"mad":1780,"relative":1,"warnings":[],"unit":"ns"}
|
|
249
399
|
{"task":"repaint/4000 chars","group":"repaint","description":"full repaint every keystroke","samples":3,"mean":2.61e9,"median":2.4e9,"stddevPct":15.3,"relative":47800,"warnings":[{"code":"low-sample-count","data":{"samples":3,"target":10}}],"unit":"ns"}
|
|
250
400
|
```
|
|
251
401
|
|
|
@@ -257,39 +407,42 @@ Markdown:
|
|
|
257
407
|
```
|
|
258
408
|
# Profile Report
|
|
259
409
|
|
|
260
|
-
Bun 1.4.
|
|
410
|
+
Bun 1.4.1 · ostia 0.1.0 · darwin/arm64 · 2026-09-05T13:14:50.085Z · a1b2c3d (main)
|
|
261
411
|
|
|
262
412
|
## Timing
|
|
263
413
|
|
|
264
|
-
|
|
|
265
|
-
|
|
266
|
-
| bun -e 1 | 5.
|
|
414
|
+
| Task | Median | Spread (p75…p99) | Mean ± SD | Range | MAD |
|
|
415
|
+
|---|---|---|---|---|---|
|
|
416
|
+
| bun -e 1 | 5.03 ms | 5.42 ms…9.70 ms | 5.29 ms ± 0.84 ms | 4.77 ms…13.4 ms | 0.16 ms |
|
|
267
417
|
```
|
|
268
418
|
|
|
269
|
-
|
|
419
|
+
#### CPU visualization formats
|
|
270
420
|
|
|
271
421
|
Turn CPU evidence into files for other tools. Formats: `collapsed`, `mermaid`,
|
|
272
422
|
`speedscope`, `cpuprofile` (pass-through of a real CDP artifact when present).
|
|
423
|
+
`--measurement <id>` renders only that measurement (default: every CPU
|
|
424
|
+
measurement in the document); `--out-dir PATH` writes files there instead of
|
|
425
|
+
stdout.
|
|
273
426
|
|
|
274
427
|
```sh
|
|
275
|
-
ostia
|
|
276
|
-
ostia
|
|
277
|
-
ostia
|
|
428
|
+
ostia report doc.json --format collapsed
|
|
429
|
+
ostia report doc.json --format mermaid
|
|
430
|
+
ostia report doc.json --format speedscope > flame.json
|
|
278
431
|
```
|
|
279
432
|
|
|
280
433
|
Collapsed stacks (one line per stack; feeds `flamegraph.pl` and friends):
|
|
281
434
|
|
|
282
435
|
```
|
|
283
|
-
(root);(module);hashLoop
|
|
436
|
+
(root);(module);hashLoop 209
|
|
284
437
|
```
|
|
285
438
|
|
|
286
439
|
Mermaid call tree (top N nodes by self time, never the whole profile):
|
|
287
440
|
|
|
288
441
|
```
|
|
289
442
|
graph TD
|
|
290
|
-
n1["(root) (self 0.00ms, total
|
|
291
|
-
n2["(module) (self 0.00ms, total
|
|
292
|
-
n3["hashLoop (self
|
|
443
|
+
n1["(root) (self 0.00ms, total 267.91ms)"]
|
|
444
|
+
n2["(module) (self 0.00ms, total 267.91ms)"]
|
|
445
|
+
n3["hashLoop (self 267.91ms, total 267.91ms)"]
|
|
293
446
|
n1 --> n2
|
|
294
447
|
n2 --> n3
|
|
295
448
|
```
|
|
@@ -304,6 +457,7 @@ ostia ci
|
|
|
304
457
|
ostia ci --full # ignore cache
|
|
305
458
|
ostia ci --baseline main
|
|
306
459
|
ostia ci --export-json out.json
|
|
460
|
+
ostia ci --save-baseline # after a pass, promote today's numbers to the baseline
|
|
307
461
|
```
|
|
308
462
|
|
|
309
463
|
Pass:
|
|
@@ -325,16 +479,36 @@ Fail:
|
|
|
325
479
|
1 affected by this change
|
|
326
480
|
0 cached
|
|
327
481
|
1 executed
|
|
328
|
-
0 passed 1 regressed (+
|
|
482
|
+
0 passed 1 regressed (+1278.7% median on work)
|
|
329
483
|
|
|
330
484
|
Profile CI: ✗
|
|
331
485
|
```
|
|
332
486
|
|
|
333
487
|
Exit codes: `0` pass, `1` regression, `2` harness error (missing config/baseline, spawn failure).
|
|
334
488
|
|
|
335
|
-
#### `ostia.config.json`
|
|
489
|
+
#### `ostia.config.ts` / `ostia.config.json`
|
|
490
|
+
|
|
491
|
+
`loadConfig` looks for `ostia.config.ts` first (Bun imports TypeScript natively), falling
|
|
492
|
+
back to `ostia.config.json`. Both forms are fully supported; pick `.ts` for autocomplete
|
|
493
|
+
and type-checking on every field, via `defineConfig` (an identity function purely for
|
|
494
|
+
typing, the same pattern as Vite/Vitest/ESLint):
|
|
495
|
+
|
|
496
|
+
```ts
|
|
497
|
+
// ostia.config.ts
|
|
498
|
+
import { defineConfig } from "ostia"
|
|
499
|
+
|
|
500
|
+
export default defineConfig({
|
|
501
|
+
baseline: "main",
|
|
502
|
+
thresholds: { timingPct: 5 },
|
|
503
|
+
workloads: [
|
|
504
|
+
{ label: "parse", command: ["bun", "bench/parse.ts"], inputs: ["src/**/*.ts"] },
|
|
505
|
+
{ label: "dogfood-suites", suites: ["bench/*.ts"] },
|
|
506
|
+
],
|
|
507
|
+
})
|
|
508
|
+
```
|
|
336
509
|
|
|
337
510
|
```json
|
|
511
|
+
// ostia.config.json - equivalent, no defineConfig wrapper needed
|
|
338
512
|
{
|
|
339
513
|
"baseline": "main",
|
|
340
514
|
"thresholds": { "timingPct": 5 },
|
|
@@ -343,12 +517,26 @@ Exit codes: `0` pass, `1` regression, `2` harness error (missing config/baseline
|
|
|
343
517
|
"label": "parse",
|
|
344
518
|
"command": ["bun", "bench/parse.ts"],
|
|
345
519
|
"inputs": ["src/**/*.ts"]
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
"label": "dogfood-suites",
|
|
523
|
+
"suites": ["bench/*.ts"]
|
|
346
524
|
}
|
|
347
525
|
]
|
|
348
526
|
}
|
|
349
527
|
```
|
|
350
528
|
|
|
351
|
-
|
|
529
|
+
Each workload is exactly one of `command` (a subprocess timed with `runs`/`warmup`) or
|
|
530
|
+
`suites` (glob patterns, same resolution as `bench`'s own `suites` config, run via
|
|
531
|
+
`bench()`). A `suites` workload gates every task in those files individually - one
|
|
532
|
+
candidate-vs-baseline comparison per task, matched by workload id the same way a `command`
|
|
533
|
+
workload already is, so `ostia ci`'s regression detection covers in-process microbenchmarks,
|
|
534
|
+
not only subprocess commands. Unlike `command` workloads, a `suites` workload always
|
|
535
|
+
executes (there's no cheap way to know a suite file's task ids, and so its per-task cache
|
|
536
|
+
keys, without importing it first) - `inputs`-based cache skipping is `command`-only for now.
|
|
537
|
+
|
|
538
|
+
`inputs` is optional (and, for now, only consulted for `command` workloads). Workloads
|
|
539
|
+
with no `inputs` always rerun (cache fails conservative).
|
|
352
540
|
|
|
353
541
|
Two directory options, both optional: `outDir` (default `node_modules/.cache/ostia`) for
|
|
354
542
|
scratch/cache/artifacts, and `baselineDir` (default `.ostia/baselines`) for baselines. They're
|
|
@@ -359,11 +547,23 @@ independent - `baselineDir` doesn't move just because you override `outDir`.
|
|
|
359
547
|
Baselines are JSON under `.ostia/baselines/` (gitignored). `ostia ci` only needs the file
|
|
360
548
|
on disk; it does not need to be committed.
|
|
361
549
|
|
|
550
|
+
`ostia baseline save [name]` measures every configured workload (the same code path
|
|
551
|
+
`ostia ci` gates against, no comparison) and writes it to `<baselineDir>/<name>.json`
|
|
552
|
+
(default name: config's `"baseline"` field, or `"main"`). `ostia baseline list` shows every
|
|
553
|
+
saved baseline (name, created date, workload count, and git sha/branch when available);
|
|
554
|
+
`ostia baseline show <name> [--format]` renders one (delegates to `ostia report`).
|
|
555
|
+
|
|
556
|
+
Every document stamps `git: { sha, branch, dirty }` (from `git rev-parse` / `git status
|
|
557
|
+
--porcelain` in the process's cwd, 200ms timeout, silently absent outside a repo or
|
|
558
|
+
without `git` installed) - metadata only, never part of any fingerprint or id, so a
|
|
559
|
+
commit or a dirty working tree never orphans a cached run or baseline. Printed in the
|
|
560
|
+
markdown report's header line and `ostia baseline list`.
|
|
561
|
+
|
|
362
562
|
Local branch workflow:
|
|
363
563
|
|
|
364
564
|
```sh
|
|
365
565
|
git checkout master # known-good tip
|
|
366
|
-
|
|
566
|
+
ostia baseline save # -> .ostia/baselines/main.json
|
|
367
567
|
|
|
368
568
|
git checkout -b my-opt
|
|
369
569
|
# ... change code ...
|
|
@@ -374,10 +574,15 @@ The baseline survives branch switches because it is not tracked. Re-seed only wh
|
|
|
374
574
|
intentionally accept a new floor. Seeding on the branch you are guarding compares that
|
|
375
575
|
branch to itself.
|
|
376
576
|
|
|
577
|
+
`ostia ci --save-baseline` folds that re-seed into the gate itself: after a pass (no
|
|
578
|
+
regressions), it writes the just-measured document as the new baseline at the same path
|
|
579
|
+
it just compared against - useful in a CI job that gates every merge to a trunk branch,
|
|
580
|
+
so each green run becomes the next run's floor with no separate step.
|
|
581
|
+
|
|
377
582
|
One-off outside this repo's config:
|
|
378
583
|
|
|
379
584
|
```sh
|
|
380
|
-
ostia
|
|
585
|
+
ostia time --export-json .ostia/baselines/main.json "bun bench.ts"
|
|
381
586
|
ostia ci
|
|
382
587
|
```
|
|
383
588
|
|
|
@@ -391,13 +596,17 @@ The CLI is a thin wrapper around the library. Same `ProfileDocument` either way.
|
|
|
391
596
|
|
|
392
597
|
```ts
|
|
393
598
|
import {
|
|
394
|
-
|
|
599
|
+
time,
|
|
395
600
|
profile,
|
|
396
601
|
bench,
|
|
397
602
|
group,
|
|
398
603
|
task,
|
|
399
604
|
range,
|
|
605
|
+
sweep,
|
|
606
|
+
keep,
|
|
400
607
|
compareDocuments,
|
|
608
|
+
createDocument,
|
|
609
|
+
defineConfig,
|
|
401
610
|
renderers,
|
|
402
611
|
saveDocument,
|
|
403
612
|
loadDocument,
|
|
@@ -405,41 +614,62 @@ import {
|
|
|
405
614
|
import type { ProfileDocument } from "ostia"
|
|
406
615
|
```
|
|
407
616
|
|
|
408
|
-
### `
|
|
617
|
+
### `time(opts)` → `ProfileDocument`
|
|
409
618
|
|
|
410
|
-
Subprocess timing, optional CPU/heap capture. Same behavior as `ostia
|
|
619
|
+
Subprocess timing, optional CPU/heap capture. Same behavior as `ostia time`.
|
|
411
620
|
|
|
412
621
|
```ts
|
|
413
|
-
const doc = await
|
|
622
|
+
const doc = await time({
|
|
414
623
|
commands: ["bun a.ts", "bun b.ts"],
|
|
415
|
-
|
|
624
|
+
samples: 10, // exact trial count
|
|
625
|
+
// budgetMs: 3000, // wall-clock budget instead of an exact count
|
|
626
|
+
// minSamples: 10, // hard floor when samples isn't given
|
|
416
627
|
warmup: 2,
|
|
628
|
+
interleave: true, // default when 2+ commands: round-robins trials across them
|
|
417
629
|
cpu: true,
|
|
418
630
|
heap: false,
|
|
419
631
|
cpuIntervalUs: 200,
|
|
420
632
|
outDir: "node_modules/.cache/ostia", // default; artifacts land under here
|
|
633
|
+
noiseCheck: true, // default; set false to skip the ~200ms noise floor measurement
|
|
421
634
|
})
|
|
422
635
|
```
|
|
423
636
|
|
|
424
|
-
### `profile(fn, opts)` → `{ result,
|
|
637
|
+
### `profile(fn, opts)` → `{ result, measurement, document }`
|
|
425
638
|
|
|
426
639
|
In-process capture. `origin: "jsc"` is the only path that reports JIT tiers
|
|
427
640
|
(LLInt / Baseline / DFG / FTL). Default `origin: "inspector"` writes portable CDP-shaped
|
|
428
|
-
evidence instead.
|
|
641
|
+
evidence instead. `document` is a full `ProfileDocument` (the one workload and
|
|
642
|
+
measurement), so it composes with `renderers.*` or `saveDocument` directly.
|
|
429
643
|
|
|
430
644
|
```ts
|
|
431
|
-
const { result,
|
|
432
|
-
|
|
433
|
-
intervalUs: 100,
|
|
434
|
-
|
|
645
|
+
const { result, measurement, document } = await profile(
|
|
646
|
+
() => hashLoop(8_000_000),
|
|
647
|
+
{ origin: "jsc", intervalUs: 100 },
|
|
648
|
+
)
|
|
435
649
|
|
|
436
|
-
console.log(
|
|
650
|
+
console.log(measurement.jit?.tiers)
|
|
437
651
|
// {
|
|
438
652
|
// llint: 0,
|
|
439
653
|
// baseline: 9,
|
|
440
654
|
// dfg: 37,
|
|
441
655
|
// ftl: 2825,
|
|
442
656
|
// }
|
|
657
|
+
|
|
658
|
+
const { files } = await renderers.collapsed.render(document)
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
### `createDocument(workloads, measurements)` → `ProfileDocument`
|
|
662
|
+
|
|
663
|
+
For composing a document from several `profile()` calls (each of which returns
|
|
664
|
+
just one workload and measurement):
|
|
665
|
+
|
|
666
|
+
```ts
|
|
667
|
+
const a = await profile(() => taskA())
|
|
668
|
+
const b = await profile(() => taskB())
|
|
669
|
+
const document = createDocument(
|
|
670
|
+
[a.document.workloads[0]!, b.document.workloads[0]!],
|
|
671
|
+
[a.measurement, b.measurement],
|
|
672
|
+
)
|
|
443
673
|
```
|
|
444
674
|
|
|
445
675
|
### `group` / `task` / `bench`
|
|
@@ -454,13 +684,71 @@ group("parse", () => {
|
|
|
454
684
|
task("small input", () => parse(smallBuf))
|
|
455
685
|
task("large input", () => parse(largeBuf))
|
|
456
686
|
// Per-task options override the suite-wide time budget / min samples.
|
|
457
|
-
task("full pipeline", () => build(), {
|
|
687
|
+
task("full pipeline", () => build(), { budgetMs: 2000, minSamples: 10 })
|
|
458
688
|
})
|
|
459
689
|
```
|
|
460
690
|
|
|
461
691
|
That is the whole registration surface: `group()` and `task()`. Presentation lives in
|
|
462
692
|
the renderers (`--format`), not in the suite file.
|
|
463
693
|
|
|
694
|
+
All module-scope code in a suite file runs up front, before any task is sampled.
|
|
695
|
+
`{ before, after }` is the hook that runs a task's own setup immediately before its
|
|
696
|
+
sampling and its teardown immediately after - once each, unmeasured, in the task's
|
|
697
|
+
own process (so both work with `isolate`):
|
|
698
|
+
|
|
699
|
+
```ts
|
|
700
|
+
group(
|
|
701
|
+
"parse",
|
|
702
|
+
() => {
|
|
703
|
+
let doc: Document
|
|
704
|
+
task("append", () => doc.append(node), {
|
|
705
|
+
before: () => {
|
|
706
|
+
doc = mountDocument()
|
|
707
|
+
},
|
|
708
|
+
after: () => doc.destroy(),
|
|
709
|
+
})
|
|
710
|
+
},
|
|
711
|
+
{
|
|
712
|
+
// Runs once around the whole group, outside every task's own before/after.
|
|
713
|
+
before: () => setupSharedFixture(),
|
|
714
|
+
after: () => teardownSharedFixture(),
|
|
715
|
+
},
|
|
716
|
+
)
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
There is no per-trial hook (no setup/teardown between individual samples) - that
|
|
720
|
+
would defeat batching, which is how ostia keeps a sub-microsecond task's timer
|
|
721
|
+
overhead down. Reach for `{ gc }` (`Bun.gc(true)` between trials) or `{ isolate }`
|
|
722
|
+
(a fresh process per task) for per-trial concerns instead. Because `before`/`after`
|
|
723
|
+
run once per task, not once per instance, a suite that builds more than one instance
|
|
724
|
+
of something stateful (a mounted UI component, an open connection, a server) still
|
|
725
|
+
has to scope a query to the instance it belongs to, not write it against a
|
|
726
|
+
global/ambient lookup that assumes it's the only one alive - a suite that opens a
|
|
727
|
+
component's menu and queries `getByRole(...)` unscoped, for example, breaks once a
|
|
728
|
+
second instance of that component exists in the document; scope the query with
|
|
729
|
+
something like `within(instance.container)` instead.
|
|
730
|
+
|
|
731
|
+
Task functions may be async (`() => unknown | Promise<unknown>`, same for
|
|
732
|
+
`before`/`after`); `await` on a plain synchronous value still costs a microtask
|
|
733
|
+
turn, so an `async` task function measures a few nanoseconds slower per call than
|
|
734
|
+
the same body written synchronously - immaterial above microsecond cost, worth
|
|
735
|
+
knowing for a task near the timer's resolution floor.
|
|
736
|
+
|
|
737
|
+
Call `keep(value)` on an intermediate value inside a task body - a subcomputation
|
|
738
|
+
whose result the task doesn't return - to pin it against dead-code elimination the
|
|
739
|
+
same way ostia already protects a task's own return value:
|
|
740
|
+
|
|
741
|
+
```ts
|
|
742
|
+
import { keep } from "ostia"
|
|
743
|
+
|
|
744
|
+
task("parse then validate", () => {
|
|
745
|
+
const ast = parse(input)
|
|
746
|
+
keep(ast) // the task returns validate's result; without this, a smart-enough
|
|
747
|
+
// JIT could in principle prove `ast` is otherwise unused and skip building it
|
|
748
|
+
return validate(ast)
|
|
749
|
+
})
|
|
750
|
+
```
|
|
751
|
+
|
|
464
752
|
Both take an optional `description` that flows into the document
|
|
465
753
|
(`Workload.description` / `Workload.groupDescription`) and into `--format minimal`, so
|
|
466
754
|
what a number measures and why travels with the data instead of living only in a
|
|
@@ -479,9 +767,8 @@ group(
|
|
|
479
767
|
)
|
|
480
768
|
```
|
|
481
769
|
|
|
482
|
-
Mark one task per group as the `Relative` reference with `{ baseline: true }
|
|
483
|
-
|
|
484
|
-
task in the group:
|
|
770
|
+
Mark one task per group as the `Relative` reference with `{ baseline: true }`;
|
|
771
|
+
otherwise `Relative` defaults to the fastest task in the group:
|
|
485
772
|
|
|
486
773
|
```ts
|
|
487
774
|
group("parse", () => {
|
|
@@ -490,24 +777,72 @@ group("parse", () => {
|
|
|
490
777
|
})
|
|
491
778
|
```
|
|
492
779
|
|
|
493
|
-
|
|
780
|
+
`task.skip(...)` / `group.skip(...)` register without measuring: the runner never
|
|
781
|
+
samples them, but the document still carries the workload (marked
|
|
782
|
+
`Workload.skipped`), so a renderer prints `- skipped` instead of the task just
|
|
783
|
+
being absent, and `compare` reports it as `unchanged` (with a `skipped` warning)
|
|
784
|
+
rather than silently passing or failing to match a baseline. `task.only(...)` /
|
|
785
|
+
`group.only(...)` restrict a suite file to only the `.only`-marked tasks - `--filter`
|
|
786
|
+
still applies on top - and print a one-line notice (`bench: 2 task(s) selected by
|
|
787
|
+
.only`) to stderr, so a forgotten `.only` doesn't quietly narrow a run in CI:
|
|
788
|
+
|
|
789
|
+
```ts
|
|
790
|
+
group("parse", () => {
|
|
791
|
+
task.only("fast path", () => parse(buf)) // only this task runs this time
|
|
792
|
+
task("slow path", () => parseSlow(buf))
|
|
793
|
+
task.skip("flaky on CI", () => parseFlaky(buf))
|
|
794
|
+
})
|
|
795
|
+
```
|
|
494
796
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
the
|
|
797
|
+
`{ cpu }` / `{ alloc }` on `task()` or `group()` override the suite-wide `bench({ cpu, alloc })`
|
|
798
|
+
/ `--cpu` / `--alloc` default for that task or group, the same pattern as `isolate`/`gc`:
|
|
799
|
+
one extra `phase: "cpu"` measurement (JIT tiers included, from a fixed 200ms window under
|
|
800
|
+
the JSC sampling profiler) and/or one extra `phase: "memstats"` measurement
|
|
801
|
+
(`MemoryEvidence.bytesPerOp`, from a `Bun.gc(true)`-bracketed batch of 100 calls) alongside
|
|
802
|
+
the task's timing numbers, never mixed into them:
|
|
499
803
|
|
|
500
804
|
```ts
|
|
501
|
-
|
|
805
|
+
group("parse", () => {
|
|
806
|
+
task("current impl", () => parse(buf))
|
|
807
|
+
task("candidate impl", () => parseFast(buf), { cpu: true, alloc: true })
|
|
808
|
+
})
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
### `sweep(dims, fn)` → `void`
|
|
812
|
+
|
|
813
|
+
Cartesian product over one or more named dimensions, calling `fn` once per point.
|
|
814
|
+
`task()` calls inside `fn` automatically inherit that point as `Workload.params` -
|
|
815
|
+
a structured alternative to baking the point into the task name, so renderers can
|
|
816
|
+
pivot on it and `compare` matches on the same point across runs instead of just a name:
|
|
817
|
+
|
|
818
|
+
```ts
|
|
819
|
+
import { group, task, range, sweep } from "ostia"
|
|
502
820
|
|
|
503
821
|
group("parse", () => {
|
|
504
|
-
|
|
822
|
+
sweep({ size: range(100, 10_000), impl: ["current", "fast"] }, ({ size, impl }) => {
|
|
505
823
|
const input = buildInput(size) // setup, runs once per point, unmeasured
|
|
506
|
-
task(`${
|
|
507
|
-
}
|
|
824
|
+
task(`${impl}`, () => impls[impl](input))
|
|
825
|
+
})
|
|
508
826
|
})
|
|
509
827
|
```
|
|
510
828
|
|
|
829
|
+
An explicit `{ params }` on a particular `task()` call merges over (and wins
|
|
830
|
+
against) the current sweep point:
|
|
831
|
+
|
|
832
|
+
```ts
|
|
833
|
+
task(`${impl}`, () => impls[impl](input), { params: { size, impl, variant: "warm" } })
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
`--format minimal` includes `params` on every line. The markdown renderer pivots a
|
|
837
|
+
group into a table (rows = first dimension, columns = second) when every task in
|
|
838
|
+
it shares the same two param keys - exactly what the example above produces - and
|
|
839
|
+
otherwise renders params as a `key=value` suffix on the task name.
|
|
840
|
+
|
|
841
|
+
### `range(start, end, multiplier?)` → `number[]`
|
|
842
|
+
|
|
843
|
+
Geometric point generator that feeds `sweep()` (and works standalone): default
|
|
844
|
+
multiplier `8`, always ending on `end` even if the last step overshot it.
|
|
845
|
+
|
|
511
846
|
```ts
|
|
512
847
|
range(100, 10_000) // -> [100, 800, 6400, 10000]
|
|
513
848
|
range(100, 100_000) // -> [100, 800, 6400, 51200, 100000]
|
|
@@ -519,9 +854,11 @@ import { bench } from "ostia"
|
|
|
519
854
|
|
|
520
855
|
const doc = await bench({
|
|
521
856
|
suites: ["suite.ts"],
|
|
522
|
-
|
|
857
|
+
budgetMs: 500,
|
|
858
|
+
// samples: 50, // exact per-task trial count instead of a budget
|
|
523
859
|
minSamples: 50,
|
|
524
860
|
jobs: 1, // suite files at once; > 1 trades fidelity for wall time
|
|
861
|
+
noiseCheck: true, // default; set false to skip the ~200ms noise floor measurement
|
|
525
862
|
})
|
|
526
863
|
```
|
|
527
864
|
|
|
@@ -535,6 +872,8 @@ const diffs = compareDocuments(baselineDoc, candidateDoc, {
|
|
|
535
872
|
frameSelfPct: 10,
|
|
536
873
|
heapTypePct: 10,
|
|
537
874
|
minFrameSelfUs: 1000,
|
|
875
|
+
alpha: 0.01, // Mann-Whitney significance level
|
|
876
|
+
bootstrapIterations: 2000,
|
|
538
877
|
})
|
|
539
878
|
```
|
|
540
879
|
|
|
@@ -545,6 +884,11 @@ await saveDocument(doc, "doc.json")
|
|
|
545
884
|
const loaded: ProfileDocument = await loadDocument("doc.json")
|
|
546
885
|
```
|
|
547
886
|
|
|
887
|
+
### `defineConfig(config)` → `Partial<OstiaConfig>`
|
|
888
|
+
|
|
889
|
+
Identity function purely for typing `ostia.config.ts` - see
|
|
890
|
+
[`ostia.config.ts` / `ostia.config.json`](#ostiaconfigts--ostiaconfigjson) above.
|
|
891
|
+
|
|
548
892
|
### `renderers`
|
|
549
893
|
|
|
550
894
|
Pure functions of a `ProfileDocument`. Each returns `{ text? }` and/or `{ files? }`.
|
|
@@ -563,11 +907,31 @@ Pure functions of a `ProfileDocument`. Each returns `{ text? }` and/or `{ files?
|
|
|
563
907
|
|
|
564
908
|
```ts
|
|
565
909
|
const { text } = await renderers.markdown.render(doc)
|
|
566
|
-
const { files } = await renderers.collapsed.render(doc, {
|
|
910
|
+
const { files } = await renderers.collapsed.render(doc, {
|
|
911
|
+
measurementId: someCpuMeasurementId,
|
|
912
|
+
})
|
|
567
913
|
```
|
|
568
914
|
|
|
569
915
|
Units in the IR are fixed: ns (time), bytes (memory), µs (sampling interval).
|
|
570
916
|
|
|
917
|
+
## Migrating from mitata or hyperfine
|
|
918
|
+
|
|
919
|
+
| mitata / hyperfine | ostia |
|
|
920
|
+
|---|---|
|
|
921
|
+
| `bench("name", fn)` | `task("name", fn)` |
|
|
922
|
+
| `baseline()` | `{ baseline: true }` on a `task()` |
|
|
923
|
+
| `.range(name, start, end, mult)` | `sweep({ dim: range(start, end, mult) }, ...)` |
|
|
924
|
+
| generator setup (`function* () { ...; yield () => fn() }`) | `task(name, fn, { before, after })` |
|
|
925
|
+
| `do_not_optimize(value)` | `keep(value)` |
|
|
926
|
+
| `hyperfine -L var a,b,c cmd-{var}` | `sweep({ var: ["a", "b", "c"] }, ...)` or per-command `params` |
|
|
927
|
+
| `hyperfine --runs N --warmup N` | `ostia time --samples N --warmup N` |
|
|
928
|
+
| `hyperfine --export-json` / `--export-markdown` | `ostia time --export-json PATH` / `--format markdown` |
|
|
929
|
+
|
|
930
|
+
`sweep()`/`range()`/`params` are in-process (`ostia bench`); `hyperfine -L` substitutes into
|
|
931
|
+
a shell command template for a subprocess instead, so a direct port is one literal `ostia time`
|
|
932
|
+
command per substitution value rather than a template - see [`sweep(dims, fn)`](#sweepdims-fn--void)
|
|
933
|
+
and [`ostia.config.ts` / `ostia.config.json`](#ostiaconfigts--ostiaconfigjson) above.
|
|
934
|
+
|
|
571
935
|
## Examples
|
|
572
936
|
|
|
573
937
|
[`examples/`](examples/) has six runnable recipes (they spawn `../../src/cli/main.ts`
|