ostia 0.1.7 → 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 +438 -107
- package/chunk-qbt85kmg.js +4 -0
- package/chunk-r5b8vf8w.js +21 -0
- package/cli.js +132 -62
- package/index.d.ts +296 -33
- package/index.js +1 -1
- package/package.json +1 -1
- package/runner.ts +5 -4
- package/chunk-3qbcj1m9.js +0 -21
- package/chunk-zrck2q0b.js +0 -4
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,6 +237,22 @@ 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
|
|
@@ -223,13 +294,25 @@ shell's environment.
|
|
|
223
294
|
|
|
224
295
|
|
|
225
296
|
```
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
stats
|
|
229
|
-
stats/computeTimingStats (
|
|
230
|
-
|
|
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).
|
|
231
311
|
```
|
|
232
312
|
|
|
313
|
+
Tasks with a `group()` print the group name once, indented; ungrouped tasks and
|
|
314
|
+
subprocess commands print flat.
|
|
315
|
+
|
|
233
316
|
### `ostia compare`
|
|
234
317
|
|
|
235
318
|
Match workloads by id, rank timing / frame / heap deltas, print a verdict per workload.
|
|
@@ -240,16 +323,63 @@ ostia compare after.json --baseline .ostia/baselines/main.json
|
|
|
240
323
|
```
|
|
241
324
|
|
|
242
325
|
```
|
|
243
|
-
|
|
244
|
-
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:
|
|
245
332
|
|
|
246
|
-
✗ work
|
|
247
|
-
timing: +1249.2% median (regressed)
|
|
248
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.
|
|
249
376
|
|
|
250
377
|
### `ostia report`
|
|
251
378
|
|
|
252
|
-
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.
|
|
253
383
|
|
|
254
384
|
```sh
|
|
255
385
|
ostia report out.json # table (default)
|
|
@@ -265,7 +395,7 @@ sample (tens of thousands for a fast task), which is tokens a reviewer never rea
|
|
|
265
395
|
Numbers stay in ns so they line up with `compare` deltas and the JSON document.
|
|
266
396
|
|
|
267
397
|
```
|
|
268
|
-
{"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"}
|
|
269
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"}
|
|
270
400
|
```
|
|
271
401
|
|
|
@@ -277,39 +407,42 @@ Markdown:
|
|
|
277
407
|
```
|
|
278
408
|
# Profile Report
|
|
279
409
|
|
|
280
|
-
Bun 1.4.
|
|
410
|
+
Bun 1.4.1 · ostia 0.1.0 · darwin/arm64 · 2026-09-05T13:14:50.085Z · a1b2c3d (main)
|
|
281
411
|
|
|
282
412
|
## Timing
|
|
283
413
|
|
|
284
|
-
|
|
|
285
|
-
|
|
286
|
-
| 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 |
|
|
287
417
|
```
|
|
288
418
|
|
|
289
|
-
|
|
419
|
+
#### CPU visualization formats
|
|
290
420
|
|
|
291
421
|
Turn CPU evidence into files for other tools. Formats: `collapsed`, `mermaid`,
|
|
292
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.
|
|
293
426
|
|
|
294
427
|
```sh
|
|
295
|
-
ostia
|
|
296
|
-
ostia
|
|
297
|
-
ostia
|
|
428
|
+
ostia report doc.json --format collapsed
|
|
429
|
+
ostia report doc.json --format mermaid
|
|
430
|
+
ostia report doc.json --format speedscope > flame.json
|
|
298
431
|
```
|
|
299
432
|
|
|
300
433
|
Collapsed stacks (one line per stack; feeds `flamegraph.pl` and friends):
|
|
301
434
|
|
|
302
435
|
```
|
|
303
|
-
(root);(module);hashLoop
|
|
436
|
+
(root);(module);hashLoop 209
|
|
304
437
|
```
|
|
305
438
|
|
|
306
439
|
Mermaid call tree (top N nodes by self time, never the whole profile):
|
|
307
440
|
|
|
308
441
|
```
|
|
309
442
|
graph TD
|
|
310
|
-
n1["(root) (self 0.00ms, total
|
|
311
|
-
n2["(module) (self 0.00ms, total
|
|
312
|
-
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)"]
|
|
313
446
|
n1 --> n2
|
|
314
447
|
n2 --> n3
|
|
315
448
|
```
|
|
@@ -324,6 +457,7 @@ ostia ci
|
|
|
324
457
|
ostia ci --full # ignore cache
|
|
325
458
|
ostia ci --baseline main
|
|
326
459
|
ostia ci --export-json out.json
|
|
460
|
+
ostia ci --save-baseline # after a pass, promote today's numbers to the baseline
|
|
327
461
|
```
|
|
328
462
|
|
|
329
463
|
Pass:
|
|
@@ -345,16 +479,36 @@ Fail:
|
|
|
345
479
|
1 affected by this change
|
|
346
480
|
0 cached
|
|
347
481
|
1 executed
|
|
348
|
-
0 passed 1 regressed (+
|
|
482
|
+
0 passed 1 regressed (+1278.7% median on work)
|
|
349
483
|
|
|
350
484
|
Profile CI: ✗
|
|
351
485
|
```
|
|
352
486
|
|
|
353
487
|
Exit codes: `0` pass, `1` regression, `2` harness error (missing config/baseline, spawn failure).
|
|
354
488
|
|
|
355
|
-
#### `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
|
+
```
|
|
356
509
|
|
|
357
510
|
```json
|
|
511
|
+
// ostia.config.json - equivalent, no defineConfig wrapper needed
|
|
358
512
|
{
|
|
359
513
|
"baseline": "main",
|
|
360
514
|
"thresholds": { "timingPct": 5 },
|
|
@@ -363,12 +517,26 @@ Exit codes: `0` pass, `1` regression, `2` harness error (missing config/baseline
|
|
|
363
517
|
"label": "parse",
|
|
364
518
|
"command": ["bun", "bench/parse.ts"],
|
|
365
519
|
"inputs": ["src/**/*.ts"]
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
"label": "dogfood-suites",
|
|
523
|
+
"suites": ["bench/*.ts"]
|
|
366
524
|
}
|
|
367
525
|
]
|
|
368
526
|
}
|
|
369
527
|
```
|
|
370
528
|
|
|
371
|
-
|
|
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).
|
|
372
540
|
|
|
373
541
|
Two directory options, both optional: `outDir` (default `node_modules/.cache/ostia`) for
|
|
374
542
|
scratch/cache/artifacts, and `baselineDir` (default `.ostia/baselines`) for baselines. They're
|
|
@@ -379,11 +547,23 @@ independent - `baselineDir` doesn't move just because you override `outDir`.
|
|
|
379
547
|
Baselines are JSON under `.ostia/baselines/` (gitignored). `ostia ci` only needs the file
|
|
380
548
|
on disk; it does not need to be committed.
|
|
381
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
|
+
|
|
382
562
|
Local branch workflow:
|
|
383
563
|
|
|
384
564
|
```sh
|
|
385
565
|
git checkout master # known-good tip
|
|
386
|
-
|
|
566
|
+
ostia baseline save # -> .ostia/baselines/main.json
|
|
387
567
|
|
|
388
568
|
git checkout -b my-opt
|
|
389
569
|
# ... change code ...
|
|
@@ -394,10 +574,15 @@ The baseline survives branch switches because it is not tracked. Re-seed only wh
|
|
|
394
574
|
intentionally accept a new floor. Seeding on the branch you are guarding compares that
|
|
395
575
|
branch to itself.
|
|
396
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
|
+
|
|
397
582
|
One-off outside this repo's config:
|
|
398
583
|
|
|
399
584
|
```sh
|
|
400
|
-
ostia
|
|
585
|
+
ostia time --export-json .ostia/baselines/main.json "bun bench.ts"
|
|
401
586
|
ostia ci
|
|
402
587
|
```
|
|
403
588
|
|
|
@@ -411,13 +596,17 @@ The CLI is a thin wrapper around the library. Same `ProfileDocument` either way.
|
|
|
411
596
|
|
|
412
597
|
```ts
|
|
413
598
|
import {
|
|
414
|
-
|
|
599
|
+
time,
|
|
415
600
|
profile,
|
|
416
601
|
bench,
|
|
417
602
|
group,
|
|
418
603
|
task,
|
|
419
604
|
range,
|
|
605
|
+
sweep,
|
|
606
|
+
keep,
|
|
420
607
|
compareDocuments,
|
|
608
|
+
createDocument,
|
|
609
|
+
defineConfig,
|
|
421
610
|
renderers,
|
|
422
611
|
saveDocument,
|
|
423
612
|
loadDocument,
|
|
@@ -425,41 +614,62 @@ import {
|
|
|
425
614
|
import type { ProfileDocument } from "ostia"
|
|
426
615
|
```
|
|
427
616
|
|
|
428
|
-
### `
|
|
617
|
+
### `time(opts)` → `ProfileDocument`
|
|
429
618
|
|
|
430
|
-
Subprocess timing, optional CPU/heap capture. Same behavior as `ostia
|
|
619
|
+
Subprocess timing, optional CPU/heap capture. Same behavior as `ostia time`.
|
|
431
620
|
|
|
432
621
|
```ts
|
|
433
|
-
const doc = await
|
|
622
|
+
const doc = await time({
|
|
434
623
|
commands: ["bun a.ts", "bun b.ts"],
|
|
435
|
-
|
|
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
|
|
436
627
|
warmup: 2,
|
|
628
|
+
interleave: true, // default when 2+ commands: round-robins trials across them
|
|
437
629
|
cpu: true,
|
|
438
630
|
heap: false,
|
|
439
631
|
cpuIntervalUs: 200,
|
|
440
632
|
outDir: "node_modules/.cache/ostia", // default; artifacts land under here
|
|
633
|
+
noiseCheck: true, // default; set false to skip the ~200ms noise floor measurement
|
|
441
634
|
})
|
|
442
635
|
```
|
|
443
636
|
|
|
444
|
-
### `profile(fn, opts)` → `{ result,
|
|
637
|
+
### `profile(fn, opts)` → `{ result, measurement, document }`
|
|
445
638
|
|
|
446
639
|
In-process capture. `origin: "jsc"` is the only path that reports JIT tiers
|
|
447
640
|
(LLInt / Baseline / DFG / FTL). Default `origin: "inspector"` writes portable CDP-shaped
|
|
448
|
-
evidence instead.
|
|
641
|
+
evidence instead. `document` is a full `ProfileDocument` (the one workload and
|
|
642
|
+
measurement), so it composes with `renderers.*` or `saveDocument` directly.
|
|
449
643
|
|
|
450
644
|
```ts
|
|
451
|
-
const { result,
|
|
452
|
-
|
|
453
|
-
intervalUs: 100,
|
|
454
|
-
|
|
645
|
+
const { result, measurement, document } = await profile(
|
|
646
|
+
() => hashLoop(8_000_000),
|
|
647
|
+
{ origin: "jsc", intervalUs: 100 },
|
|
648
|
+
)
|
|
455
649
|
|
|
456
|
-
console.log(
|
|
650
|
+
console.log(measurement.jit?.tiers)
|
|
457
651
|
// {
|
|
458
652
|
// llint: 0,
|
|
459
653
|
// baseline: 9,
|
|
460
654
|
// dfg: 37,
|
|
461
655
|
// ftl: 2825,
|
|
462
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
|
+
)
|
|
463
673
|
```
|
|
464
674
|
|
|
465
675
|
### `group` / `task` / `bench`
|
|
@@ -474,25 +684,70 @@ group("parse", () => {
|
|
|
474
684
|
task("small input", () => parse(smallBuf))
|
|
475
685
|
task("large input", () => parse(largeBuf))
|
|
476
686
|
// Per-task options override the suite-wide time budget / min samples.
|
|
477
|
-
task("full pipeline", () => build(), {
|
|
687
|
+
task("full pipeline", () => build(), { budgetMs: 2000, minSamples: 10 })
|
|
478
688
|
})
|
|
479
689
|
```
|
|
480
690
|
|
|
481
691
|
That is the whole registration surface: `group()` and `task()`. Presentation lives in
|
|
482
692
|
the renderers (`--format`), not in the suite file.
|
|
483
693
|
|
|
484
|
-
All module-scope code in a suite file runs up front, before any task is sampled
|
|
485
|
-
|
|
486
|
-
its teardown immediately after, the
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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
|
+
```
|
|
496
751
|
|
|
497
752
|
Both take an optional `description` that flows into the document
|
|
498
753
|
(`Workload.description` / `Workload.groupDescription`) and into `--format minimal`, so
|
|
@@ -512,9 +767,8 @@ group(
|
|
|
512
767
|
)
|
|
513
768
|
```
|
|
514
769
|
|
|
515
|
-
Mark one task per group as the `Relative` reference with `{ baseline: true }
|
|
516
|
-
|
|
517
|
-
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:
|
|
518
772
|
|
|
519
773
|
```ts
|
|
520
774
|
group("parse", () => {
|
|
@@ -523,24 +777,72 @@ group("parse", () => {
|
|
|
523
777
|
})
|
|
524
778
|
```
|
|
525
779
|
|
|
526
|
-
|
|
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
|
+
```
|
|
527
796
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
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:
|
|
532
803
|
|
|
533
804
|
```ts
|
|
534
|
-
|
|
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"
|
|
535
820
|
|
|
536
821
|
group("parse", () => {
|
|
537
|
-
|
|
822
|
+
sweep({ size: range(100, 10_000), impl: ["current", "fast"] }, ({ size, impl }) => {
|
|
538
823
|
const input = buildInput(size) // setup, runs once per point, unmeasured
|
|
539
|
-
task(`${
|
|
540
|
-
}
|
|
824
|
+
task(`${impl}`, () => impls[impl](input))
|
|
825
|
+
})
|
|
541
826
|
})
|
|
542
827
|
```
|
|
543
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
|
+
|
|
544
846
|
```ts
|
|
545
847
|
range(100, 10_000) // -> [100, 800, 6400, 10000]
|
|
546
848
|
range(100, 100_000) // -> [100, 800, 6400, 51200, 100000]
|
|
@@ -552,9 +854,11 @@ import { bench } from "ostia"
|
|
|
552
854
|
|
|
553
855
|
const doc = await bench({
|
|
554
856
|
suites: ["suite.ts"],
|
|
555
|
-
|
|
857
|
+
budgetMs: 500,
|
|
858
|
+
// samples: 50, // exact per-task trial count instead of a budget
|
|
556
859
|
minSamples: 50,
|
|
557
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
|
|
558
862
|
})
|
|
559
863
|
```
|
|
560
864
|
|
|
@@ -568,6 +872,8 @@ const diffs = compareDocuments(baselineDoc, candidateDoc, {
|
|
|
568
872
|
frameSelfPct: 10,
|
|
569
873
|
heapTypePct: 10,
|
|
570
874
|
minFrameSelfUs: 1000,
|
|
875
|
+
alpha: 0.01, // Mann-Whitney significance level
|
|
876
|
+
bootstrapIterations: 2000,
|
|
571
877
|
})
|
|
572
878
|
```
|
|
573
879
|
|
|
@@ -578,6 +884,11 @@ await saveDocument(doc, "doc.json")
|
|
|
578
884
|
const loaded: ProfileDocument = await loadDocument("doc.json")
|
|
579
885
|
```
|
|
580
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
|
+
|
|
581
892
|
### `renderers`
|
|
582
893
|
|
|
583
894
|
Pure functions of a `ProfileDocument`. Each returns `{ text? }` and/or `{ files? }`.
|
|
@@ -596,11 +907,31 @@ Pure functions of a `ProfileDocument`. Each returns `{ text? }` and/or `{ files?
|
|
|
596
907
|
|
|
597
908
|
```ts
|
|
598
909
|
const { text } = await renderers.markdown.render(doc)
|
|
599
|
-
const { files } = await renderers.collapsed.render(doc, {
|
|
910
|
+
const { files } = await renderers.collapsed.render(doc, {
|
|
911
|
+
measurementId: someCpuMeasurementId,
|
|
912
|
+
})
|
|
600
913
|
```
|
|
601
914
|
|
|
602
915
|
Units in the IR are fixed: ns (time), bytes (memory), µs (sampling interval).
|
|
603
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
|
+
|
|
604
935
|
## Examples
|
|
605
936
|
|
|
606
937
|
[`examples/`](examples/) has six runnable recipes (they spawn `../../src/cli/main.ts`
|