ostia 0.1.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.
Files changed (5) hide show
  1. package/README.md +439 -0
  2. package/cli.js +151 -0
  3. package/index.d.ts +259 -0
  4. package/index.js +19 -0
  5. package/package.json +18 -0
package/README.md ADDED
@@ -0,0 +1,439 @@
1
+ # ostia
2
+
3
+ ostia is a profiling and benchmarking toolkit for Bun.
4
+
5
+ Use it when you want wall-clock timings, CPU hotspots, heap summaries, or JIT tier
6
+ data in one place, then diff those results or fail CI when something gets slower.
7
+ Everything lands in one schema-versioned JSON document (`ProfileDocument`), so the
8
+ CLI and the library speak the same language.
9
+
10
+ Zero runtime dependencies. Requires Bun ≥ 1.4.
11
+
12
+ ## Install
13
+
14
+ ```sh
15
+ bun add ostia
16
+ ```
17
+
18
+ ## Quick start
19
+
20
+ Compare two commands:
21
+
22
+ ```sh
23
+ ostia run --runs 10 --warmup 2 "bun fixtures/fast.ts" "bun fixtures/slow.ts"
24
+ ```
25
+
26
+ ```
27
+ Command Mean [ms] Min…Max [ms] Relative
28
+ --------------------------------------------------------------------
29
+ bun fixtures/fast.ts 8.413 ± 1.340 7.623…12.365 1.00×
30
+ bun fixtures/slow.ts 21.712 ± 1.157 20.831…24.220 2.62× slower
31
+ ```
32
+
33
+ Find a CPU hotspot (profiler runs as a separate labeled trial, never mixed into the
34
+ timing numbers above):
35
+
36
+ ```sh
37
+ ostia run --runs 5 --cpu --cpu-interval 200 --export-json .ostia/doc.json fixtures/work.ts
38
+ ```
39
+
40
+ ```
41
+ Command Mean [ms] Min…Max [ms]
42
+ ----------------------------------------------------
43
+ bun fixtures/work.ts 275.815 ± 2.853 273.334…281.384
44
+
45
+ CPU capture - bun fixtures/work.ts (instrumented, 200µs interval, diagnostic wall 297.578ms)
46
+ 100.0% 284.19ms self hashLoop
47
+ 0.0% 0.00ms self (root)
48
+ artifact: .ostia/artifacts/<run-id>-cpu.cpuprofile
49
+ ```
50
+
51
+ Gate a change against a local baseline (baselines are gitignored under `.ostia/`):
52
+
53
+ ```sh
54
+ bun run baseline # on known-good: measure ostia.config.json -> .ostia/baselines/main.json
55
+ ostia ci # on your branch: rerun changed workloads, exit 1 on regression
56
+ ```
57
+
58
+ ```
59
+ 2 workloads
60
+ 0 affected by this change
61
+ 2 cached
62
+ 0 executed
63
+ 2 passed 0 regressed
64
+
65
+ Profile CI: ✓
66
+ ```
67
+
68
+ ## What ostia is for
69
+
70
+ - Time subprocesses or in-process functions without a profiler attached to the timing runs.
71
+ - Capture CPU (`--cpu`), heap (`--heap`), or JSC JIT tiers (`profile(..., { origin: "jsc" })`)
72
+ as separate evidence on the same document.
73
+ - Diff two documents (`ostia compare`) or fail CI (`ostia ci`) with exit codes `0` / `1` / `2`.
74
+ - Emit files other tools already understand: collapsed stacks, Mermaid, speedscope JSON,
75
+ raw `.cpuprofile`.
76
+
77
+ ## CLI reference
78
+
79
+ ```
80
+ ostia run <command...> time commands; optional --cpu / --heap capture
81
+ ostia bench <suite.ts...> in-process group()/task() suites (time-budgeted)
82
+ ostia compare <a> <b> diff two ProfileDocuments
83
+ ostia report <document.json> render a saved document
84
+ ostia viz <document.json> render CPU evidence to a file format
85
+ ostia ci run configured workloads vs a baseline, gate regressions
86
+ ```
87
+
88
+ Every subcommand takes `--help` for its full flag list.
89
+
90
+ ### `ostia run`
91
+
92
+ Clean wall-clock timing by default. `--cpu` / `--heap` schedule one extra instrumented
93
+ trial each, labeled separately in the document.
94
+
95
+ ```sh
96
+ ostia run "bun a.ts" "bun b.ts"
97
+ ostia run --runs 25 --warmup 3 --cpu --heap "bun src/server.ts"
98
+ ostia run --format json --export-json out.json "bun a.ts"
99
+ ```
100
+
101
+ Timing table (two commands get a Relative column automatically):
102
+
103
+ ```
104
+ Command Mean [ms] Min…Max [ms] Relative
105
+ --------------------------------------------------------------------
106
+ bun fixtures/fast.ts 8.413 ± 1.340 7.623…12.365 1.00×
107
+ bun fixtures/slow.ts 21.712 ± 1.157 20.831…24.220 2.62× slower
108
+ ```
109
+
110
+ Heap summary (type counts from the snapshot trial):
111
+
112
+ ```
113
+ Command Mean [ms] Min…Max [ms]
114
+ --------------------------------------------------------
115
+ bun fixtures/allocate.ts 27.079 ± 6.844 23.888…91.799
116
+
117
+ Heap snapshot - bun fixtures/allocate.ts (instrumented, 2518 objects, 0.12MB)
118
+ 1369 string
119
+ 426 code
120
+ 321 closure
121
+ 216 object shape
122
+ 104 hidden
123
+ artifact: .ostia/artifacts/<run-id>-heap.heapsnapshot
124
+ ```
125
+
126
+ ### `ostia bench`
127
+
128
+ In-process microbenchmarks registered with `group()` / `task()`. Time budget + min
129
+ samples (mitata-shaped); batches when a call is too fast for `Bun.nanoseconds()`.
130
+
131
+ ```sh
132
+ ostia bench bench/*.ts
133
+ ostia bench --time-budget 500 --min-samples 50 bench/stats.ts
134
+ ```
135
+
136
+ ```
137
+ Command Mean [ms] Min…Max [ms] Relative
138
+ --------------------------------------------------------------------------------------
139
+ stats/computeTimingStats (1e3 samples) 0.014 ± 0.016 0.012…0.598 1.00×
140
+ stats/computeTimingStats (1e4 samples) 0.484 ± 0.052 0.434…0.680 38.22× slower
141
+ stats/timingWarnings (1e3 samples) 0.036 ± 0.020 0.032…0.541 2.82× slower
142
+ ```
143
+
144
+ ### `ostia compare`
145
+
146
+ Match workloads by id, rank timing / frame / heap deltas, print a verdict per workload.
147
+
148
+ ```sh
149
+ ostia compare before.json after.json
150
+ ostia compare after.json --baseline .ostia/baselines/main.json
151
+ ```
152
+
153
+ ```
154
+ ✓ bun fixtures/fast.ts
155
+ timing: -2.9% median (unchanged)
156
+
157
+ ✗ work
158
+ timing: +1249.2% median (regressed)
159
+ ```
160
+
161
+ ### `ostia report`
162
+
163
+ Render a saved `ProfileDocument` without re-running anything.
164
+
165
+ ```sh
166
+ ostia report out.json # table (default)
167
+ ostia report out.json --format markdown
168
+ ostia report out.json --format json
169
+ ostia report out.json --format jsonl
170
+ ```
171
+
172
+ Markdown:
173
+
174
+ ```
175
+ # Profile Report
176
+
177
+ Bun 1.4.0 · ostia 0.1.0 · darwin/arm64 · 2026-09-04T03:46:02.961Z
178
+
179
+ ## Timing
180
+
181
+ | Command | Mean ± SD (ms) | Min…Max (ms) | Median (ms) |
182
+ |---|---|---|---|
183
+ | bun -e 1 | 5.477 ± 0.303 | 5.153…5.882 | 5.396 |
184
+ ```
185
+
186
+ ### `ostia viz`
187
+
188
+ Turn CPU evidence into files for other tools. Formats: `collapsed`, `mermaid`,
189
+ `speedscope`, `cpuprofile` (pass-through of a real CDP artifact when present).
190
+
191
+ ```sh
192
+ ostia viz .ostia/doc.json --format collapsed
193
+ ostia viz .ostia/doc.json --format mermaid
194
+ ostia viz .ostia/doc.json --format speedscope > flame.json
195
+ ```
196
+
197
+ Collapsed stacks (one line per stack; feeds `flamegraph.pl` and friends):
198
+
199
+ ```
200
+ (root);(module);hashLoop 1055
201
+ ```
202
+
203
+ Mermaid call tree (top N nodes by self time, never the whole profile):
204
+
205
+ ```
206
+ graph TD
207
+ n1["(root) (self 0.00ms, total 284.19ms)"]
208
+ n2["(module) (self 0.00ms, total 284.19ms)"]
209
+ n3["hashLoop (self 284.19ms, total 284.19ms)"]
210
+ n1 --> n2
211
+ n2 --> n3
212
+ ```
213
+
214
+ ### `ostia ci`
215
+
216
+ Reads `ostia.config.json`, fingerprints each workload, reruns only what changed, compares
217
+ against a named baseline, exits `1` on regression.
218
+
219
+ ```sh
220
+ ostia ci
221
+ ostia ci --full # ignore cache
222
+ ostia ci --baseline main
223
+ ostia ci --export-json out.json
224
+ ```
225
+
226
+ Pass:
227
+
228
+ ```
229
+ 1 workloads
230
+ 1 affected by this change
231
+ 0 cached
232
+ 1 executed
233
+ 1 passed 0 regressed
234
+
235
+ Profile CI: ✓
236
+ ```
237
+
238
+ Fail:
239
+
240
+ ```
241
+ 1 workloads
242
+ 1 affected by this change
243
+ 0 cached
244
+ 1 executed
245
+ 0 passed 1 regressed (+1249.2% median on work)
246
+
247
+ Profile CI: ✗
248
+ ```
249
+
250
+ Exit codes: `0` pass, `1` regression, `2` harness error (missing config/baseline, spawn failure).
251
+
252
+ #### `ostia.config.json`
253
+
254
+ ```json
255
+ {
256
+ "baseline": "main",
257
+ "thresholds": { "timingPct": 5 },
258
+ "workloads": [
259
+ {
260
+ "label": "parse",
261
+ "command": ["bun", "bench/parse.ts"],
262
+ "inputs": ["src/**/*.ts"]
263
+ }
264
+ ]
265
+ }
266
+ ```
267
+
268
+ `inputs` is optional. Workloads with no `inputs` always rerun (cache fails conservative).
269
+
270
+ #### Baselines (local and CI)
271
+
272
+ Baselines are JSON under `.ostia/baselines/` (gitignored). `ostia ci` only needs the file
273
+ on disk; it does not need to be committed.
274
+
275
+ Local branch workflow:
276
+
277
+ ```sh
278
+ git checkout master # known-good tip
279
+ bun run baseline # -> .ostia/baselines/main.json
280
+
281
+ git checkout -b my-opt
282
+ # ... change code ...
283
+ ostia ci # or: bun run dogfood
284
+ ```
285
+
286
+ The baseline survives branch switches because it is not tracked. Re-seed only when you
287
+ intentionally accept a new floor. Seeding on the branch you are guarding compares that
288
+ branch to itself.
289
+
290
+ One-off outside this repo's config:
291
+
292
+ ```sh
293
+ ostia run --export-json .ostia/baselines/main.json "bun bench.ts"
294
+ ostia ci
295
+ ```
296
+
297
+ On pull requests, CI measures the base branch into that same path, checks out the PR,
298
+ and runs `ostia ci` against it. If the base has no `package.json` / `ostia.config.json`
299
+ yet (empty starter commit), CI seeds from the PR tip instead so dogfood still runs.
300
+
301
+ ## Library API
302
+
303
+ The CLI is a thin wrapper around the library. Same `ProfileDocument` either way.
304
+
305
+ ```ts
306
+ import {
307
+ run,
308
+ profile,
309
+ bench,
310
+ group,
311
+ task,
312
+ compareDocuments,
313
+ renderers,
314
+ saveDocument,
315
+ loadDocument,
316
+ } from "ostia"
317
+ import type { ProfileDocument } from "ostia"
318
+ ```
319
+
320
+ ### `run(opts)` → `ProfileDocument`
321
+
322
+ Subprocess timing, optional CPU/heap capture. Same behavior as `ostia run`.
323
+
324
+ ```ts
325
+ const doc = await run({
326
+ commands: ["bun a.ts", "bun b.ts"],
327
+ runs: 10,
328
+ warmup: 2,
329
+ cpu: true,
330
+ heap: false,
331
+ cpuIntervalUs: 200,
332
+ outDir: ".ostia",
333
+ })
334
+ ```
335
+
336
+ ### `profile(fn, opts)` → `{ result, run }`
337
+
338
+ In-process capture. `origin: "jsc"` is the only path that reports JIT tiers
339
+ (LLInt / Baseline / DFG / FTL). Default `origin: "inspector"` writes portable CDP-shaped
340
+ evidence instead.
341
+
342
+ ```ts
343
+ const { result, run } = await profile(() => hashLoop(8_000_000), {
344
+ origin: "jsc",
345
+ intervalUs: 100,
346
+ })
347
+
348
+ console.log(run.jit?.tiers)
349
+ // {
350
+ // llint: 0,
351
+ // baseline: 9,
352
+ // dfg: 37,
353
+ // ftl: 2825,
354
+ // }
355
+ ```
356
+
357
+ ### `group` / `task` / `bench`
358
+
359
+ Register in-process suites, then run them (same as `ostia bench`):
360
+
361
+ ```ts
362
+ // suite.ts
363
+ import { group, task } from "ostia"
364
+
365
+ group("parse", () => {
366
+ task("small input", () => parse(smallBuf))
367
+ task("large input", () => parse(largeBuf))
368
+ })
369
+ ```
370
+
371
+ ```ts
372
+ // demo.ts
373
+ import { bench } from "ostia"
374
+
375
+ const doc = await bench({
376
+ suites: ["suite.ts"],
377
+ timeBudgetMs: 500,
378
+ minSamples: 50,
379
+ })
380
+ ```
381
+
382
+ ### `compareDocuments(base, cand, thresholds?)` → `Comparison[]`
383
+
384
+ Same matching and thresholds as `ostia compare` / `ostia ci`.
385
+
386
+ ```ts
387
+ const diffs = compareDocuments(baselineDoc, candidateDoc, {
388
+ timingPct: 5,
389
+ frameSelfPct: 10,
390
+ heapTypePct: 10,
391
+ minFrameSelfUs: 1000,
392
+ })
393
+ ```
394
+
395
+ ### `saveDocument` / `loadDocument`
396
+
397
+ ```ts
398
+ await saveDocument(doc, ".ostia/doc.json")
399
+ const loaded: ProfileDocument = await loadDocument(".ostia/doc.json")
400
+ ```
401
+
402
+ ### `renderers`
403
+
404
+ Pure functions of a `ProfileDocument`. Each returns `{ text? }` and/or `{ files? }`.
405
+
406
+ | Name | Output |
407
+ |---|---|
408
+ | `table` | terminal timing / CPU / heap / comparison text |
409
+ | `markdown` | agent- and human-readable report |
410
+ | `json` | pretty JSON document |
411
+ | `jsonl` | one metadata line, then one line per run |
412
+ | `collapsed` | folded stacks (`name;name;name count`) |
413
+ | `mermaid` | top-N call tree |
414
+ | `speedscope` | speedscope.app JSON |
415
+ | `cpuprofile` | verbatim `.cpuprofile` when a CDP artifact exists |
416
+
417
+ ```ts
418
+ const { text } = await renderers.markdown.render(doc)
419
+ const { files } = await renderers.collapsed.render(doc, { runId: someCpuRunId })
420
+ ```
421
+
422
+ Units in the IR are fixed: ns (time), bytes (memory), µs (sampling interval).
423
+
424
+ ## Examples
425
+
426
+ [`examples/`](examples/) has six runnable recipes (they spawn `../../src/cli/main.ts`
427
+ or import `../../src` directly; no install step):
428
+
429
+ - [`compare-two-commands`](examples/compare-two-commands/). Relative timing table.
430
+ - [`find-a-hotspot`](examples/find-a-hotspot/). `--cpu` plus collapsed / Mermaid viz.
431
+ - [`heap-usage`](examples/heap-usage/). `--heap` type breakdown.
432
+ - [`gate-a-regression`](examples/gate-a-regression/). Config, local baseline, and `ostia ci`.
433
+ - [`profile-in-process`](examples/profile-in-process/). `profile(fn, { origin: "jsc" })`.
434
+ - [`benchmark-a-function`](examples/benchmark-a-function/). `bench()` / `group()` / `task()`.
435
+
436
+ ```sh
437
+ cd examples/find-a-hotspot && bun run demo
438
+ bun run examples # all of them from the repo root
439
+ ```
package/cli.js ADDED
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+ function q(e){return JSON.stringify(W(e))}function W(e){if(Array.isArray(e))return e.map(W);if(e!==null&&typeof e==="object"){let n={};for(let t of Object.keys(e).sort())n[t]=W(e[t]);return n}return e}function T(e,...n){let t=Bun.CryptoHasher.hash("sha256",q(n),"hex");return`${e}_${t.slice(0,16)}`}function Ye(e){return e.startsWith("file://")?e.slice(7):e}function K(e,n,t){let s=e.nodes,r=s.length,o=new Map,i=[],a=new Map,c=new Int32Array(r);for(let m=0;m<r;m++){let g=s[m],f=g.callFrame,y=Ye(f.url),C=o.get(f.functionName);if(C===void 0)C=new Map,o.set(f.functionName,C);let N=C.get(y);if(N===void 0)N=i.length,C.set(y,N),i.push({key:T("fr",f.functionName,y),name:f.functionName,url:y||void 0,line:f.lineNumber>=0?f.lineNumber:void 0,col:f.columnNumber>=0?f.columnNumber:void 0});c[m]=N,a.set(g.id,m)}let l=Array(r);for(let m=0;m<r;m++){let g=s[m];l[m]={id:g.id,frameIx:c[a.get(g.id)],children:g.children??[]}}let u=new Float64Array(r),p=new Float64Array(r),d=e.samples,b=e.timeDeltas;for(let m=0;m<d.length;m++){let g=a.get(d[m]);if(g===void 0)continue;u[g]+=b[m]??0,p[g]+=1}let x=new Int32Array(r).fill(-1);for(let m=0;m<r;m++){let g=s[m].children;if(!g)continue;for(let f of g){let y=a.get(f);if(y!==void 0)x[y]=m}}let w=[],R=[];for(let m=r-1;m>=0;m--)if(x[m]===-1)R.push(m);while(R.length>0){let m=R.pop();w.push(m);let g=s[m].children;if(!g)continue;for(let f of g){let y=a.get(f);if(y!==void 0&&x[y]===m)R.push(y)}}let k=new Float64Array(r);for(let m=w.length-1;m>=0;m--){let g=w[m];k[g]+=u[g];let f=x[g];if(f>=0)k[f]+=k[g]}let P=Array(i.length),h=[];for(let m=0;m<r;m++){let g=l[m].frameIx,f=P[g];if(f)f.selfUs+=u[m],f.totalUs+=k[m],f.samples+=p[m];else{let y={frameIx:g,selfUs:u[m],totalUs:k[m],samples:p[m]};P[g]=y,h.push(y)}}return{origin:n,samplingIntervalUs:t,frames:i,nodes:l,totals:h.sort((m,g)=>g.selfUs-m.selfUs),samples:{nodeIds:e.samples,timeDeltasUs:e.timeDeltas}}}function Qe(e,n,t,s){let r=["--cpu-prof","--cpu-prof-dir",n,"--cpu-prof-name",t,"--cpu-prof-interval",String(s)],o=e[0];if(o==="bun"||o?.endsWith("/bun"))return[o,...r,...e.slice(1)];return e}async function pe(e){let n=`${e.artifactDir}/${e.fileName}`,t=e.argv[0],s=t==="bun"||t?.endsWith("/bun"),r=Qe(e.argv,e.artifactDir,e.fileName,e.intervalUs),o=s?e.env:{...process.env,...e.env,BUN_OPTIONS:`--cpu-prof --cpu-prof-dir ${e.artifactDir} --cpu-prof-name ${e.fileName} --cpu-prof-interval ${e.intervalUs}`},i=Bun.nanoseconds(),c=await Bun.spawn(r,{cwd:e.cwd,env:o,stdout:"ignore",stderr:"ignore",stdin:"ignore"}).exited,l=Bun.nanoseconds()-i,u=Bun.file(n);if(!await u.exists())return{diagnosticWallNs:l,exitCode:c,warnings:[{code:"artifact-missing",message:`Expected a .cpuprofile at ${n} after exit ${c}, found nothing. The workload's argv[0] must be a \`bun\` binary for CPU capture.`,data:{artifactPath:n,argv:e.argv}}]};let p=await u.json(),d=K(p,"cpu-prof",e.intervalUs),b=p.samples.length===0?[{code:"empty-profile",message:"CPU capture produced zero samples."}]:[];return{diagnosticWallNs:l,exitCode:c,artifactPath:n,cpu:d,warnings:b}}function me(e,n="heap-prof"){let{node_fields:t,node_types:s}=e.snapshot.meta,r=t.indexOf("type"),o=t.indexOf("self_size"),i=t.length,a=s[0];if(r===-1||o===-1||!Array.isArray(a))return{origin:n,typeCounts:[],objectCount:e.snapshot.node_count};let c=a.length,l=Array(c),u=new Map,p=[],d=0,b=e.nodes,x=b.length;for(let h=0;h<x;h+=i){let m=b[h+r],g=b[h+o]??0;d+=g;let f;if(m>=0&&m<c){if(f=l[m],f===void 0)f={type:a[m],count:0,bytes:0},l[m]=f,p.push(f)}else{let y=`unknown(${m})`;if(f=u.get(y),f===void 0)f={type:y,count:0,bytes:0},u.set(y,f),p.push(f)}f.count++,f.bytes+=g}let w=p.sort((h,m)=>m.count-h.count),R=w.slice(0,20),k=w.slice(20),P=R.map(({type:h,count:m,bytes:g})=>({type:h,count:m,retainedBytes:g}));if(k.length>0){let h=0,m=0;for(let g of k)h+=g.count,m+=g.bytes;P.push({type:"other",count:h,retainedBytes:m})}return{origin:n,heapSizeBytes:d,objectCount:e.snapshot.node_count,typeCounts:P}}function Xe(e,n,t){let s=["--heap-prof","--heap-prof-dir",n,"--heap-prof-name",t],r=e[0];if(r==="bun"||r?.endsWith("/bun"))return[r,...s,...e.slice(1)];return e}async function de(e){let n=`${e.artifactDir}/${e.fileName}`,t=e.argv[0],s=t==="bun"||t?.endsWith("/bun"),r=Xe(e.argv,e.artifactDir,e.fileName),o=s?e.env:{...process.env,...e.env,BUN_OPTIONS:`--heap-prof --heap-prof-dir ${e.artifactDir} --heap-prof-name ${e.fileName}`},i=Bun.nanoseconds(),c=await Bun.spawn(r,{cwd:e.cwd,env:o,stdout:"ignore",stderr:"ignore",stdin:"ignore"}).exited,l=Bun.nanoseconds()-i,u=Bun.file(n);if(!await u.exists())return{diagnosticWallNs:l,exitCode:c,warnings:[{code:"artifact-missing",message:`Expected a heap snapshot at ${n} after exit ${c}, found nothing. The workload's argv[0] must be a \`bun\` binary for heap capture.`,data:{artifactPath:n,argv:e.argv}}]};let p=await u.json(),d=me(p,"heap-prof");return{diagnosticWallNs:l,exitCode:c,artifactPath:n,heap:d,warnings:[]}}import{Session as en}from"inspector/promises";var nn=1000;async function fe(e,n={}){let t=n.intervalUs??nn,s=new en;s.connect();let r=Bun.nanoseconds();try{await s.post("Profiler.enable"),await s.post("Profiler.setSamplingInterval",{interval:t}),await s.post("Profiler.start");let o=await e(),{profile:i}=await s.post("Profiler.stop"),a=Bun.nanoseconds()-r,c=K(i,"inspector",t);return{result:o,cpu:c,diagnosticWallNs:a}}finally{s.disconnect()}}import{profile as rn}from"bun:jsc";var tn=new Map([["LLInt","llint"],["Baseline","baseline"],["DFG","dfg"],["FTL","ftl"]]),ge=4294967295;function he(e,n){let t=n??e.interval*1e6,s=new Map,r=[];function o(f,y,C,N){let D=s.get(f);if(D===void 0)D=new Map,s.set(f,D);let F=y??"",U=D.get(F);if(U===void 0)U=r.length,D.set(F,U),r.push({key:T("fr",f,F),name:f,url:y,line:C,col:N});return U}function i(f){let y=f.line===ge,C=y?void 0:f.line-1,N=y||f.column===ge?void 0:f.column-1;return o(f.name,f.sourceURL,C,N)}let a=o("(root)",void 0,void 0,void 0),c=1,l={id:0,frameIx:a,children:new Map,selfUs:0,samples:0,totalUs:0},u=new Map([[0,l]]),p={llint:0,baseline:0,dfg:0,ftl:0},d=new Map,b=[],x=[];for(let f of e.traces){let y=f.frames,C=l;for(let F=y.length-1;F>=0;F--){let U=i(y[F]),A=C.children.get(U);if(!A)A={id:c++,frameIx:U,children:new Map,selfUs:0,samples:0,totalUs:0},C.children.set(U,A),u.set(A.id,A);C=A}C.selfUs+=t,C.samples+=1,b.push(C.id),x.push(t);let N=y[0],D=N&&tn.get(N.category);if(D){p[D]++;let F=d.get(D)??new Map;F.set(C.frameIx,(F.get(C.frameIx)??0)+1),d.set(D,F)}}function w(f){let y=f.selfUs;for(let C of f.children.values())y+=w(C);return f.totalUs=y,y}w(l);let R=new Map;function k(f){let y=R.get(f.frameIx);if(y)y.selfUs+=f.selfUs,y.totalUs+=f.totalUs,y.samples+=f.samples;else R.set(f.frameIx,{frameIx:f.frameIx,selfUs:f.selfUs,totalUs:f.totalUs,samples:f.samples});for(let C of f.children.values())k(C)}k(l);let P=[...u.values()].map((f)=>({id:f.id,frameIx:f.frameIx,children:[...f.children.values()].map((y)=>y.id)})),h={origin:"jsc-profile",samplingIntervalUs:t,frames:r,nodes:P,totals:[...R.values()].sort((f,y)=>y.selfUs-f.selfUs),samples:{nodeIds:b,timeDeltasUs:x}},m=[...d.entries()].flatMap(([f,y])=>[...y.entries()].sort((C,N)=>N[1]-C[1]).slice(0,3).map(([C,N])=>({tier:f,frameKey:r[C].key,samples:N})));return{cpu:h,jit:{origin:"jsc-profile",tiers:p,topFramesByTier:m}}}var sn=1000;async function be(e,n={}){let t=n.intervalUs??sn,s,r=Bun.nanoseconds(),o=await rn(async()=>(s=await e(),s),t),i=Bun.nanoseconds()-r,{cpu:a,jit:c}=he(o.stackTraces,t);return{result:s,cpu:a,jit:c,diagnosticWallNs:i}}var _="0.1.0";function M(e,n){return{schemaVersion:1,toolVersion:_,bunVersion:Bun.version,platform:{os:process.platform,arch:process.arch},createdAt:new Date().toISOString(),workloads:e,runs:n}}function G(e,n){return{id:T("wl","subprocess",e,process.cwd()),kind:"subprocess",command:e,label:n}}function we(e,n){return{id:T("wl","inprocess",e.name,e.toString()),kind:"inprocess",label:n}}function Z(e){return{id:T("run",e.workload.id,"timing",e.configFingerprint,Bun.version,_),workloadId:e.workload.id,phase:"timing",instrumented:!1,configFingerprint:e.configFingerprint,trials:e.trials,timing:e.timing,warnings:e.warnings,artifacts:[],memory:on(e.trials)}}function on(e){let n=e.map((t)=>t.maxRssBytes).filter((t)=>t!==void 0);if(n.length===0)return;return{origin:"resourceUsage",perTrial:e.map((t)=>({rssBytes:t.maxRssBytes})),maxRssBytes:Math.max(...n)}}function Y(e){return{id:T("run",e.workload.id,e.phase,e.configFingerprint,Bun.version,_),workloadId:e.workload.id,phase:e.phase,instrumented:!0,configFingerprint:e.configFingerprint,trials:[{i:0,wallNs:e.diagnosticWallNs,exitCode:e.exitCode}],diagnosticWallNs:e.diagnosticWallNs,cpu:e.cpu,heap:e.heap,jit:e.jit,warnings:e.warnings,artifacts:e.artifacts}}async function ye(e,n,t){let r=await Bun.file(t).arrayBuffer(),o=new Bun.CryptoHasher("sha256");return o.update(r),{id:T("art",e,n,t),kind:n,path:t,sha256:o.digest("hex"),bytes:r.byteLength}}function H(e){return T("cfg",e)}function re(e){return`${JSON.stringify(W(e),null,2)}
4
+ `}async function E(e,n){await Bun.write(n,re(e))}async function v(e){let n=await Bun.file(e).text();return JSON.parse(n)}async function se(e){let n=Bun.nanoseconds(),t=Bun.spawn(e.argv,{cwd:e.cwd,env:e.env,stdout:"ignore",stderr:"ignore",stdin:"ignore"}),s=await t.exited,r=Bun.nanoseconds(),o=t.resourceUsage?.();return{wallNs:r-n,exitCode:s,userNs:o?Number(o.cpuTime.user)*1000:void 0,systemNs:o?Number(o.cpuTime.system)*1000:void 0,maxRssBytes:o?.maxRSS}}function xe(e){return e.trim().split(/\s+/).filter(Boolean)}function Re(e){if(e.length===0)throw Error("computeTimingStats: samples must be non-empty");let n=e.length,t=ke(e),s=0;for(let h=0;h<n;h++)s+=e[h];let r=s/n,o=L(t,0.5),i=0;for(let h=0;h<n;h++){let m=e[h]-r;i+=m*m}let a=Math.sqrt(i/n),c=t[0],l=t[n-1],u=L(t,0.25),p=L(t,0.75),d=p-u,b=u-1.5*d,x=p+1.5*d,w=u-3*d,R=p+3*d,k=0,P=0;for(let h=0;h<n;h++){let m=e[h];if(m<w||m>R)P++;else if(m<b||m>x)k++}return{unit:"ns",samples:e,mean:r,median:o,stddev:a,min:c,max:l,outliers:{mild:k,severe:P}}}function ke(e){let n=new Float64Array(e.length);return n.set(e),n.sort(),n}function L(e,n){let t=e.length;if(t===1)return e[0];let s=n*(t-1),r=Math.floor(s),o=Math.ceil(s);if(r===o)return e[r];let i=s-r;return e[r]*(1-i)+e[o]*i}var an=5000000,cn=200;function Pe(e,n,t="subprocess"){let s=[],r=e.samples[0];if(r!==void 0){let i=ke(e.samples),a=L(i,0.25),l=L(i,0.75)-a;if(r>e.median+3*l&&l>0)s.push({code:"slow-first-run",message:`First run took ${(r/1e6).toFixed(2)}ms, much slower than the median ${(e.median/1e6).toFixed(2)}ms. Consider more warmup.`,data:{firstNs:r,medianNs:e.median}})}if(e.outliers.mild+e.outliers.severe>0)s.push({code:"outliers-detected",message:`${e.outliers.mild+e.outliers.severe} outlier(s) detected (${e.outliers.severe} severe, ${e.outliers.mild} mild).`,data:e.outliers});if(t==="subprocess"&&e.median<an)s.push({code:"fast-command",message:`Median run time (${(e.median/1e6).toFixed(3)}ms) is very fast; results may be dominated by spawn overhead.`,data:{medianNs:e.median}});if(t==="inprocess"&&e.median<cn)s.push({code:"below-timer-resolution",message:`Median run time (${e.median.toFixed(0)}ns) is close to timer resolution; consider a larger batch size or a coarser operation.`,data:{medianNs:e.median}});let o=n.filter((i)=>i!==void 0&&i!==0);if(o.length>0)s.push({code:"nonzero-exit",message:`${o.length} of ${n.length} trial(s) exited non-zero.`,data:{exitCodes:o}});return s}var un=10,ln=3000000000,pn=3;async function Q(e){let n=e.warmup??pn;for(let u=0;u<n;u++)await se(e);let t=[],s=e.runs??e.minRuns??un,r=e.runs!==void 0?0:e.minTotalNs??ln,o=0,i=0;while(i<s||o<r){let u=await se(e);if(t.push({i,wallNs:u.wallNs,exitCode:u.exitCode,userNs:u.userNs,systemNs:u.systemNs,maxRssBytes:u.maxRssBytes}),o+=u.wallNs,i++,e.runs!==void 0&&i>=e.runs)break}let a=t.map((u)=>u.wallNs),c=Re(a),l=Pe(c,t.map((u)=>u.exitCode));return{trials:t,timing:c,warnings:l}}var mn=new URL("./runner.ts",import.meta.url).pathname,dn=".ostia";async function oe(e){let t=`${e.outDir??dn}/bench-tmp`,s=e.cwd??process.cwd(),r={timeBudgetMs:e.timeBudgetMs,minSamples:e.minSamples,gc:e.gc},o=[],i=[];try{for(let a of e.suites){let c=a.startsWith("/")?a:`${s}/${a}`,l=`${t}/${T("bench-out",c)}.json`,p=await Bun.spawn(["bun",mn,c,l,JSON.stringify(r)],{cwd:s,stdout:"inherit",stderr:"inherit",stdin:"ignore"}).exited;if(p!==0)throw Error(`Bench suite failed: ${a} (runner exited ${p})`);let d=await v(l);o.push(...d.workloads),i.push(...d.runs)}}finally{await Bun.spawn(["rm","-rf",t]).exited}return M(o,i)}var fn=[],X;function gn(e,n){let t=X;X=e;try{n()}finally{X=t}}function hn(e,n){fn.push({groupName:X,name:e,fn:n})}var J={timingPct:5,frameSelfPct:10,heapTypePct:10,minFrameSelfUs:1000};function ee(e,n){if(e===0)return n===0?0:1/0;return(n-e)/e*100}function O(e,n,t){return e.runs.find((s)=>s.workloadId===n&&s.phase===t)}function ie(e,n,t=J){let s=new Set(n.workloads.map((o)=>o.id)),r=[];for(let o of e.workloads){if(!s.has(o.id))continue;let i=ae(e,n,o.id,t);if(i)r.push(i)}return r}function ae(e,n,t,s=J){let r=O(e,t,"timing"),o=O(n,t,"timing"),i=O(e,t,"cpu"),a=O(n,t,"cpu"),c=O(e,t,"heap"),l=O(n,t,"heap"),u=r?.id??i?.id??c?.id,p=o?.id??a?.id??l?.id;if(!u||!p)return;let d=!1,b;if(r?.timing&&o?.timing){let R=ee(r.timing.median,o.timing.median),k=ee(r.timing.mean,o.timing.mean),P=R>s.timingPct?"regressed":R<-s.timingPct?"improved":"unchanged";if(P==="regressed")d=!0;b={medianDeltaPct:R,meanDeltaPct:k,verdict:P}}let x;if(i?.cpu&&a?.cpu){let R=new Map(i.cpu.totals.map((g)=>[i.cpu.frames[g.frameIx].key,g])),k=new Map(a.cpu.totals.map((g)=>[a.cpu.frames[g.frameIx].key,g])),P=new Map(i.cpu.frames.map((g)=>[g.key,g.name])),h=new Map(a.cpu.frames.map((g)=>[g.key,g.name]));x=[...new Set([...R.keys(),...k.keys()])].map((g)=>{let f=R.get(g)?.selfUs??0,y=k.get(g)?.selfUs??0;return{frameKey:g,name:h.get(g)??P.get(g)??g,baseSelfUs:f,candSelfUs:y,deltaPct:ee(f,y)}}).sort((g,f)=>Math.abs(f.deltaPct)-Math.abs(g.deltaPct));for(let g of x)if((g.baseSelfUs>=s.minFrameSelfUs||g.candSelfUs>=s.minFrameSelfUs)&&g.deltaPct>s.frameSelfPct)d=!0}let w;if(c?.heap&&l?.heap){let R=new Map(c.heap.typeCounts.map((h)=>[h.type,h])),k=new Map(l.heap.typeCounts.map((h)=>[h.type,h]));w=[...new Set([...R.keys(),...k.keys()])].map((h)=>{let m=R.get(h),g=k.get(h);return{type:h,baseCount:m?.count??0,candCount:g?.count??0,baseBytes:m?.retainedBytes,candBytes:g?.retainedBytes,deltaPct:ee(m?.count??0,g?.count??0)}}).sort((h,m)=>Math.abs(m.deltaPct)-Math.abs(h.deltaPct));for(let h of w)if(h.deltaPct>s.heapTypePct)d=!0}return{id:T("cmp",u,p),baselineRunId:u,candidateRunId:p,timing:b,frames:x,heapTypes:w,thresholds:s,verdict:d?"fail":"pass"}}function S(e,n){if(n){let t=e.runs.find((s)=>s.id===n);return t?.cpu?[t]:[]}return e.runs.filter((t)=>t.phase==="cpu"&&t.cpu)}function j(e){let n=e.nodes,t=n.length,s=bn(e),r=new Int32Array(t).fill(-1);for(let c=0;c<t;c++)for(let l of n[c].children){let u=s(l);if(u!==-1)r[u]=c}let o=[];for(let c=0;c<t;c++)if(r[c]===-1)o.push(c);let i=[],a=[];for(let c=o.length-1;c>=0;c--)a.push(o[c]);while(a.length>0){let c=a.pop();i.push(c);for(let l of n[c].children){let u=s(l);if(u!==-1&&r[u]===c)a.push(u)}}return{count:t,indexOf:s,parentIx:r,roots:o,order:i}}function bn(e){let n=e.nodes,t=n.length,s=1/0,r=-1/0,o=!0;for(let a=0;a<t;a++){let c=n[a].id;if(!Number.isInteger(c)){o=!1;break}if(c<s)s=c;if(c>r)r=c}if(o&&t>0&&r-s<t*4+64){let a=r-s+1,c=new Int32Array(a).fill(-1);for(let l=0;l<t;l++)c[n[l].id-s]=l;return(l)=>{let u=l-s;return u>=0&&u<a?c[u]:-1}}let i=new Map;for(let a=0;a<t;a++)i.set(n[a].id,a);return(a)=>i.get(a)??-1}function Ce(e,n){let{count:t,indexOf:s,parentIx:r,order:o}=n,i=new Float64Array(t),a=new Float64Array(t),c=e.samples?.nodeIds??[],l=e.samples?.timeDeltasUs??[];for(let p=0;p<c.length;p++){let d=s(c[p]);if(d===-1)continue;i[d]+=l[p]??0,a[d]+=1}let u=new Float64Array(t);for(let p=o.length-1;p>=0;p--){let d=o[p];u[d]+=i[d];let b=r[d];if(b>=0)u[b]+=u[d]}return{selfUs:i,totalUs:u,samples:a}}var Te={name:"collapsed",async render(e,n={}){return{files:S(e,n.runId).map((r)=>{let o=r.cpu,{nodes:i,frames:a}=o,c=j(o),l=Array(c.count);for(let x of c.order){let w=a[i[x].frameIx].name||"(anonymous)",R=c.parentIx[x];l[x]=R===-1?w:`${l[R]};${w}`}let u=new Float64Array(c.count),p=[],d=o.samples?.nodeIds??[];for(let x=0;x<d.length;x++){let w=c.indexOf(d[x]);if(w===-1)continue;if(u[w]++===0)p.push(w)}let b=Array(p.length);for(let x=0;x<p.length;x++){let w=p[x];b[x]=`${l[w]} ${u[w]}`}return{path:`${r.id}.collapsed.txt`,content:b.join(`
5
+ `)+(b.length>0?`
6
+ `:"")}})}}};var $e={name:"cpuprofile",async render(e,n={}){let t=S(e,n.runId),s=[],r=[];for(let o of t){if(o.cpu?.origin!=="cpu-prof"&&o.cpu?.origin!=="inspector"){r.push(`${o.id} (origin ${o.cpu?.origin??"unknown"} has no .cpuprofile artifact to pass through)`);continue}let i=o.artifacts.find((c)=>c.kind==="cpuprofile");if(!i){r.push(`${o.id} (no cpuprofile artifact recorded on this run)`);continue}let a=Bun.file(i.path);if(!await a.exists()){r.push(`${o.id} (artifact missing on disk: ${i.path})`);continue}s.push({path:`${o.id}.cpuprofile`,content:await a.text()})}if(s.length===0&&r.length>0)return{text:`No .cpuprofile artifacts available:
7
+ ${r.map((o)=>` - ${o}`).join(`
8
+ `)}
9
+ `};return{files:s}}};var Ne={name:"json",async render(e){return{text:re(e)}}};var Ie={name:"jsonl",async render(e){let{runs:n,...t}=e;return{text:`${[q(t),...n.map((r)=>q(r))].join(`
10
+ `)}
11
+ `}}};function B(e){return(e/1e6).toFixed(3)}function ne(e){return e?.label??e?.command?.join(" ")??e?.entry?.task??e?.id??"unknown"}var Fe=10,De=10,ve={name:"markdown",async render(e){let n=new Map(e.workloads.map((r)=>[r.id,r])),t=[];t.push("# Profile Report",""),t.push(`Bun ${e.bunVersion} \xB7 ostia ${e.toolVersion} \xB7 ${e.platform.os}/${e.platform.arch} \xB7 ${e.createdAt}`,"");let s=e.runs.filter((r)=>r.phase==="timing"&&r.timing!==void 0);if(s.length>0){t.push("## Timing",""),t.push("| Command | Mean \xB1 SD (ms) | Min\u2026Max (ms) | Median (ms) |","|---|---|---|---|");for(let o of s){let i=ne(n.get(o.workloadId)),a=o.timing;t.push(`| ${i} | ${B(a.mean)} \xB1 ${B(a.stddev)} | ${B(a.min)}\u2026${B(a.max)} | ${B(a.median)} |`)}t.push("");let r=s.filter((o)=>o.warnings.length>0);if(r.length>0){t.push("### Warnings","");for(let o of r){let i=ne(n.get(o.workloadId));for(let a of o.warnings)t.push(`- **${i}**: ${a.message} (\`${a.code}\`)`)}t.push("")}}for(let r of e.runs){if(r.phase!=="cpu"&&r.phase!=="heap")continue;let o=ne(n.get(r.workloadId));if(r.phase==="cpu"){if(t.push(`## CPU capture - ${o}`,""),t.push(`instrumented, diagnostic wall ${B(r.diagnosticWallNs??0)}ms`,""),r.cpu){t.push(`origin: \`${r.cpu.origin}\`, interval: ${r.cpu.samplingIntervalUs}\xB5s`,""),t.push("| Self % | Self (ms) | Total (ms) | Frame |","|---|---|---|---|");let i=r.cpu.totals.reduce((a,c)=>a+c.selfUs,0)||1;for(let a of r.cpu.totals.slice(0,Fe)){let c=r.cpu.frames[a.frameIx],l=(a.selfUs/i*100).toFixed(1);t.push(`| ${l}% | ${(a.selfUs/1000).toFixed(2)} | ${(a.totalUs/1000).toFixed(2)} | ${c?.name||"(anonymous)"} |`)}if(t.push(""),r.jit){let a=r.jit.tiers;t.push(`JIT tiers: LLInt ${a.llint} \xB7 Baseline ${a.baseline} \xB7 DFG ${a.dfg} \xB7 FTL ${a.ftl}`,"")}}}else if(t.push(`## Heap snapshot - ${o}`,""),t.push(`instrumented, diagnostic wall ${B(r.diagnosticWallNs??0)}ms`,""),r.heap){t.push(`${r.heap.objectCount??"?"} objects, ${((r.heap.heapSizeBytes??0)/1e6).toFixed(2)}MB`,""),t.push("| Count | Type |","|---|---|");for(let i of r.heap.typeCounts.slice(0,De))t.push(`| ${i.count} | ${i.type} |`);t.push("")}for(let i of r.artifacts)t.push(`- artifact: \`${i.path}\``);for(let i of r.warnings)t.push(`- ! ${i.message} (\`${i.code}\`)`);if(r.artifacts.length>0||r.warnings.length>0)t.push("")}if(e.comparisons&&e.comparisons.length>0){t.push("## Comparisons","");for(let r of e.comparisons){let o=e.runs.find((a)=>a.id===r.candidateRunId),i=ne(o?n.get(o.workloadId):void 0);if(t.push(`### ${r.verdict==="pass"?"\u2713":"\u2717"} ${i}`,""),r.timing){let a=r.timing.medianDeltaPct>0?"+":"";t.push(`- timing: ${a}${r.timing.medianDeltaPct.toFixed(1)}% median (**${r.timing.verdict}**)`)}for(let a of r.frames?.slice(0,Fe)??[]){if(Math.abs(a.deltaPct)<0.5)continue;let c=a.deltaPct>0?"+":"";t.push(`- frame \`${a.name}\`: ${c}${a.deltaPct.toFixed(1)}% self-time (${(a.baseSelfUs/1000).toFixed(2)}ms \u2192 ${(a.candSelfUs/1000).toFixed(2)}ms)`)}for(let a of r.heapTypes?.slice(0,De)??[]){if(Math.abs(a.deltaPct)<0.5)continue;let c=a.deltaPct>0?"+":"";t.push(`- heap \`${a.type}\`: ${c}${a.deltaPct.toFixed(1)}% count (${a.baseCount} \u2192 ${a.candCount})`)}t.push("")}}return{text:t.join(`
12
+ `)}}};var wn=15;function ce(e){return`n${e}`}function yn(e,n,t){return`${(e||"(anonymous)").replace(/"/g,"'")} (self ${(n/1000).toFixed(2)}ms, total ${(t/1000).toFixed(2)}ms)`}function xn(e,n,t,s){let r=[];if(s<=0)return r;for(let o=0;o<n;o++){if(o===t)continue;let i=e[o];if(r.length===s&&i<=e[r[s-1]])continue;let a=r.length;while(a>0&&e[r[a-1]]<i)a--;if(r.splice(a,0,o),r.length>s)r.pop()}return r}var Ue={name:"mermaid",async render(e,n={}){let t=n.topN??wn;return{files:S(e,n.runId).map((o)=>{let i=o.cpu,{nodes:a,frames:c}=i,l=j(i),{selfUs:u,totalUs:p}=Ce(i,l),{parentIx:d}=l,b=l.roots[0]??-1,x=xn(u,l.count,b,t),w=new Set(b!==-1?[b]:[]),R=[];for(let P of x){R.length=0;for(let h=P;h!==-1;h=d[h])R.push(h);for(let h=R.length-1;h>=0;h--)w.add(R[h])}let k=["graph TD"];for(let P of w){let h=a[P].id;k.push(` ${ce(h)}["${yn(c[a[P].frameIx].name,u[P],p[P])}"]`)}for(let P of w){let h=d[P];if(h!==-1&&w.has(h))k.push(` ${ce(a[h].id)} --> ${ce(a[P].id)}`)}return{path:`${o.id}.mermaid.md`,content:`${k.join(`
13
+ `)}
14
+ `}})}}};var Rn="https://www.speedscope.app/file-format-schema.json";function Se(e){return e?.label??e?.command?.join(" ")??e?.entry?.task??"profile"}var Be={name:"speedscope",async render(e,n={}){let t=S(e,n.runId),s=new Map(e.workloads.map((o)=>[o.id,o]));return{files:t.map((o)=>{let i=o.cpu,{nodes:a}=i,c=j(i),l=i.samples?.nodeIds??[],u=i.samples?.timeDeltasUs??[],p=Array(c.count);for(let w of c.order){let R=c.parentIx[w],k=a[w].frameIx;p[w]=R===-1?[k]:[...p[R],k]}let d=Array(l.length);for(let w=0;w<l.length;w++){let R=c.indexOf(l[w]);d[w]=R===-1?[]:p[R]}let b=0;for(let w=0;w<u.length;w++)b+=u[w];let x={$schema:Rn,exporter:"ostia",name:Se(s.get(o.workloadId)),activeProfileIndex:0,shared:{frames:i.frames.map((w)=>({name:w.name||"(anonymous)",file:w.url,line:w.line!==void 0?w.line+1:void 0}))},profiles:[{type:"sampled",name:Se(s.get(o.workloadId)),unit:"microseconds",startValue:0,endValue:b,samples:d,weights:u}]};return{path:`${o.id}.speedscope.json`,content:`${JSON.stringify(x,null,2)}
15
+ `}})}}};function z(e){return(e/1e6).toFixed(3)}function ue(e){return e.label??e.command?.join(" ")??e.entry?.task??e.id}var Me={name:"table",async render(e){let n=e.runs.filter((p)=>p.phase==="timing"&&p.timing!==void 0),t=new Map(e.workloads.map((p)=>[p.id,p]));if(n.length===0){let p=Ae(e,t);return{text:p.length>0?`${p.join(`
16
+ `)}
17
+ `:`(no timing runs)
18
+ `}}let s=n.map((p)=>{let d=t.get(p.workloadId);return{run:p,workload:d,label:d?ue(d):p.workloadId}}),r=Math.min(...s.map((p)=>p.run.timing.median)),o=s.length>1,i=[],a=Math.max(7,...s.map((p)=>p.label.length)),c=o?`${"Command".padEnd(a)} Mean [ms] Min\u2026Max [ms] Relative`:`${"Command".padEnd(a)} Mean [ms] Min\u2026Max [ms]`;i.push(c),i.push("-".repeat(c.length));for(let{run:p,label:d}of s){let b=p.timing,x=`${z(b.mean)} \xB1 ${z(b.stddev)}`,w=`${z(b.min)}\u2026${z(b.max)}`,R=`${d.padEnd(a)} ${x.padEnd(15)} ${w.padEnd(18)}`;if(o){let k=b.median/r;R+=k===1?" 1.00\xD7":` ${k.toFixed(2)}\xD7 slower`}i.push(R);for(let k of p.warnings)i.push(` ! ${k.message}`)}let l=Pn(e,t);if(l.length>0)i.push(""),i.push(...l);let u=Ae(e,t);if(u.length>0)i.push(""),i.push(...u);return{text:`${i.join(`
19
+ `)}
20
+ `}}};function kn(e,n,t){let s=e.runs.find((o)=>o.id===t),r=s?n.get(s.workloadId):void 0;return r?ue(r):t}function Ae(e,n){if(!e.comparisons||e.comparisons.length===0)return[];let t=[];for(let s of e.comparisons){let r=kn(e,n,s.candidateRunId),o=s.verdict==="pass"?"\u2713":"\u2717";if(t.push(`${o} ${r}`),s.timing){let i=s.timing.medianDeltaPct>0?"+":"";t.push(` timing: ${i}${s.timing.medianDeltaPct.toFixed(1)}% median (${s.timing.verdict})`)}if(s.frames)for(let i of s.frames.slice(0,Ee)){if(Math.abs(i.deltaPct)<0.5)continue;let a=i.deltaPct>0?"+":"";t.push(` frame ${i.name}: ${a}${i.deltaPct.toFixed(1)}% self-time (${(i.baseSelfUs/1000).toFixed(2)}ms -> ${(i.candSelfUs/1000).toFixed(2)}ms)`)}if(s.heapTypes)for(let i of s.heapTypes.slice(0,Oe)){if(Math.abs(i.deltaPct)<0.5)continue;let a=i.deltaPct>0?"+":"";t.push(` heap ${i.type}: ${a}${i.deltaPct.toFixed(1)}% count (${i.baseCount} -> ${i.candCount})`)}}return t}var Ee=5,Oe=5;function Pn(e,n){let t=[];for(let s of e.runs){if(s.phase!=="cpu"&&s.phase!=="heap")continue;let r=n.get(s.workloadId),o=r?ue(r):s.workloadId;if(s.phase==="cpu")if(s.cpu){t.push(`CPU capture - ${o} (instrumented, ${s.cpu.samplingIntervalUs}\xB5s interval, diagnostic wall ${z(s.diagnosticWallNs??0)}ms)`);let i=s.cpu.totals.reduce((a,c)=>a+c.selfUs,0)||1;for(let a of s.cpu.totals.slice(0,Ee)){let c=s.cpu.frames[a.frameIx],l=(a.selfUs/i*100).toFixed(1);t.push(` ${l.padStart(5)}% ${(a.selfUs/1000).toFixed(2).padStart(8)}ms self ${c?.name??"?"}`)}}else t.push(`CPU capture - ${o} (instrumented, no evidence captured)`);else if(s.heap){let i=((s.heap.heapSizeBytes??0)/1e6).toFixed(2);t.push(`Heap snapshot - ${o} (instrumented, ${s.heap.objectCount??"?"} objects, ${i}MB)`);for(let a of s.heap.typeCounts.slice(0,Oe))t.push(` ${String(a.count).padStart(6)} ${a.type}`)}else t.push(`Heap snapshot - ${o} (instrumented, no evidence captured)`);for(let i of s.artifacts)t.push(` artifact: ${i.path}`);for(let i of s.warnings)t.push(` ! ${i.message}`)}return t}var I={table:Me,json:Ne,markdown:ve,jsonl:Ie,collapsed:Te,mermaid:Ue,speedscope:Be,cpuprofile:$e};var Cn=".ostia",le=1000;async function We(e){let n=H({runs:e.runs??null,warmup:e.warmup??null,cpu:e.cpu??!1,heap:e.heap??!1,cpuIntervalUs:e.cpuIntervalUs??le}),s=`${e.outDir??Cn}/artifacts`,r=[],o=[];for(let i of e.commands){let a=Array.isArray(i)?i:xe(i),c=G(a,Array.isArray(i)?void 0:i);r.push(c);let l=await Q({argv:a,cwd:e.cwd,env:e.env,runs:e.runs,warmup:e.warmup}),u=Z({workload:c,configFingerprint:n,trials:l.trials,timing:l.timing,warnings:l.warnings});if(o.push(u),e.cpu){let p=`${u.id}-cpu.cpuprofile`,d=await pe({argv:a,cwd:e.cwd,env:e.env,artifactDir:s,fileName:p,intervalUs:e.cpuIntervalUs??le});o.push(await je({workload:c,phase:"cpu",configFingerprint:n,diagnosticWallNs:d.diagnosticWallNs,exitCode:d.exitCode,cpu:d.cpu,artifactPath:d.artifactPath,artifactKind:"cpuprofile",warnings:d.warnings}))}if(e.heap){let p=`${u.id}-heap.heapsnapshot`,d=await de({argv:a,cwd:e.cwd,env:e.env,artifactDir:s,fileName:p});o.push(await je({workload:c,phase:"heap",configFingerprint:n,diagnosticWallNs:d.diagnosticWallNs,exitCode:d.exitCode,heap:d.heap,artifactPath:d.artifactPath,artifactKind:"heapsnapshot",warnings:d.warnings}))}}return M(r,o)}async function je(e){let n=`${e.workload.id}-${e.phase}-${e.configFingerprint}`,t=e.artifactPath?[await ye(n,e.artifactKind,e.artifactPath)]:[];return Y({workload:e.workload,phase:e.phase,configFingerprint:e.configFingerprint,diagnosticWallNs:e.diagnosticWallNs,exitCode:e.exitCode,cpu:e.cpu,heap:e.heap,warnings:e.warnings,artifacts:t})}async function Yt(e,n={}){let t=we(e),s=H({intervalUs:n.intervalUs??le,origin:n.origin??"inspector"}),r=(l)=>l.samples?.nodeIds.length===0?[{code:"empty-profile",message:"In-process capture produced zero samples."}]:[];if(n.origin==="jsc"){let{result:l,cpu:u,jit:p,diagnosticWallNs:d}=await be(e,n),b=Y({workload:t,phase:"cpu",configFingerprint:s,diagnosticWallNs:d,cpu:u,jit:p,warnings:r(u),artifacts:[]});return{result:l,run:b}}let{result:o,cpu:i,diagnosticWallNs:a}=await fe(e,n),c=Y({workload:t,phase:"cpu",configFingerprint:s,diagnosticWallNs:a,cpu:i,warnings:r(i),artifacts:[]});return{result:o,run:c}}function _e(e){return T("cache",e.workloadId,e.phase,e.configFingerprint,e.bunVersion,e.toolVersion,e.instrumented,e.inputsDigest??null)}async function He(e,n=process.cwd()){if(e.length===0)return;let t=new Set;for(let o of e){let i=new Bun.Glob(o);for await(let a of i.scan({cwd:n,absolute:!1}))t.add(a)}let s=[...t].sort(),r=await Promise.all(s.map(async(o)=>{let i=await Bun.file(`${n}/${o}`).arrayBuffer();return{path:o,sha256:Bun.CryptoHasher.hash("sha256",i,"hex")}}));return T("inputs",r)}function Le(e,n){return`${e}/cache/${n}.json`}async function Je(e,n){let t=Bun.file(Le(e,n));if(!await t.exists())return;return await t.json()}async function ze(e,n,t){await Bun.write(Le(e,n),`${JSON.stringify(t,null,2)}
21
+ `)}var Tn={runs:null,warmup:3,outDir:".ostia",baseline:"main",cpuIntervalUs:1000,thresholds:J,workloads:[]};async function Ve(e="ostia.config.json"){let n=Bun.file(e);if(!await n.exists())return;let t=await n.json();return{...Tn,...t,thresholds:{...J,...t.thresholds??{}}}}function qe(e,n){return`${e.outDir}/baselines/${n??e.baseline}.json`}class te extends Error{path;constructor(e){super(`No baseline document at ${e}. Create one with: ostia run --export-json ${e} <command...>`);this.path=e}}async function Ke(e){let{config:n}=e,t=qe(n,e.baselineName);if(!await Bun.file(t).exists())throw new te(t);let r=await v(t),o=[],i=0,a=0,c=0;for(let b of n.workloads){let x=G(b.command,b.label),w=await He(b.inputs??[]),R=H({runs:n.runs,warmup:n.warmup}),k=_e({workloadId:x.id,phase:"timing",configFingerprint:R,bunVersion:Bun.version,toolVersion:_,instrumented:!1,inputsDigest:w}),P=e.full?void 0:await Je(n.outDir,k),h,m;if(P)h=P,m="cached",a++;else{i++;let g=await Q({argv:b.command,runs:n.runs??void 0,warmup:n.warmup});h=Z({workload:x,configFingerprint:R,trials:g.trials,timing:g.timing,warnings:g.warnings}),await ze(n.outDir,k,h),m="executed",c++}o.push({workload:x,status:m,run:h})}let l=M(o.map((b)=>b.workload),o.map((b)=>b.run)),u=0,p=0,d=0;for(let b of o){let x=ae(r,l,b.workload.id,n.thresholds);if(!x){d++;continue}if(b.comparison=x,x.verdict==="pass")u++;else p++}return l.comparisons=o.map((b)=>b.comparison).filter((b)=>b!==void 0),{document:l,summary:{total:n.workloads.length,affected:i,cached:a,executed:c,passed:u,regressed:p,missingBaseline:d,results:o}}}function Ge(e){let n=[];if(n.push(`${e.total} workloads`),n.push(`${e.affected} affected by this change`),n.push(`${e.cached} cached`),n.push(`${e.executed} executed`),e.missingBaseline>0)n.push(`${e.missingBaseline} skipped (no matching baseline workload)`);let t=e.results.filter((s)=>s.comparison?.verdict==="fail").map((s)=>{let r=s.comparison.timing,o=s.workload.label??s.workload.command?.join(" ")??s.workload.id;return r?`${r.medianDeltaPct>0?"+":""}${r.medianDeltaPct.toFixed(1)}% median on ${o}`:o});return n.push(`${e.passed} passed ${e.regressed} regressed${t.length>0?` (${t.join(", ")})`:""}`),n.push(""),n.push(`Profile CI: ${e.regressed>0?"\u2717":"\u2713"}`),`${n.join(`
22
+ `)}
23
+ `}async function V(e,n){if(e.text)process.stdout.write(e.text);if(!e.files||e.files.length===0)return;if(n)for(let t of e.files){let s=t.path?`${n}/${t.path}`:n;await Bun.write(s,t.content),process.stdout.write(`wrote ${s}
24
+ `)}else if(e.files.length===1)process.stdout.write(e.files[0].content);else for(let t of e.files)process.stdout.write(`--- ${t.path??"(unnamed)"} ---
25
+ ${t.content}
26
+ `)}var $n=`ostia run [flags] <command...>
27
+
28
+ Run one or more commands N times with warmup and report timing statistics.
29
+
30
+ Flags:
31
+ --runs N exact number of timed trials (default: hyperfine-style auto)
32
+ --warmup N warmup trials, discarded (default: 3)
33
+ --cpu capture one instrumented CPU-profile trial (subprocess --cpu-prof)
34
+ --heap capture one instrumented heap-snapshot trial (subprocess --heap-prof)
35
+ --cpu-interval USEC CPU sampling interval in microseconds (default: 1000)
36
+ --out-dir PATH directory for captured artifacts (default: .ostia)
37
+ --export-json PATH write the full ProfileDocument to PATH
38
+ --format FORMAT table | json (default: table)
39
+ --quiet suppress the rendered report (still writes --export-json)
40
+ --help show this message
41
+
42
+ Instrumented runs (--cpu, --heap) are labeled separately from clean timing and never
43
+ mixed into the timing statistics.
44
+
45
+ Examples:
46
+ ostia run "bun ./fixtures/work.ts"
47
+ ostia run --runs 25 --warmup 3 "bun a.ts" "bun b.ts"
48
+ ostia run --cpu --heap "bun src/server.ts"
49
+ ostia run --format json "bun a.ts"
50
+ `,Nn=`ostia bench [flags] <suite.ts...>
51
+
52
+ Run in-process benchmark suites (registered via group()/task()). Each suite file runs
53
+ in its own spawned child process (isolated from CLI startup state).
54
+
55
+ Flags:
56
+ --time-budget MS time budget per task, min-samples permitting (default: 500)
57
+ --min-samples N minimum samples per task (default: 20)
58
+ --gc Bun.gc(true) between trials (default: off - hides allocation cost)
59
+ --out-dir PATH directory for scratch IPC files (default: .ostia)
60
+ --export-json PATH write the full ProfileDocument to PATH
61
+ --format FORMAT table | json (default: table)
62
+ --quiet suppress the rendered report (still writes --export-json)
63
+ --help show this message
64
+
65
+ Suite files register tasks like:
66
+ import { group, task } from "<pkg>"
67
+ group("parse", () => {
68
+ task("small input", () => parse(smallBuf))
69
+ })
70
+
71
+ Examples:
72
+ ostia bench benches/parse.ts
73
+ ostia bench --time-budget 1000 --min-samples 50 benches/*.ts
74
+ `,Ze=`ostia compare <base.json> <candidate.json>
75
+ ostia compare <candidate.json> --baseline <path.json>
76
+
77
+ Compare two ProfileDocuments (matched by workload id) and rank timing/frame/heap deltas.
78
+
79
+ Flags:
80
+ --export-json PATH write the resulting document (with comparisons) to PATH
81
+ --format FORMAT table | json (default: table)
82
+ --quiet suppress the rendered report (still writes --export-json)
83
+ --help show this message
84
+
85
+ Examples:
86
+ ostia compare before.json after.json
87
+ ostia compare after.json --baseline .ostia/baselines/main.json
88
+ `,In=`ostia report <document.json> [--format table|json|markdown|jsonl]
89
+
90
+ Render a saved ProfileDocument.
91
+ `,Fn=`ostia viz <document.json> --format FORMAT [--run <id>] [--out-dir PATH]
92
+
93
+ Render CPU evidence from a saved ProfileDocument as a visualization artifact. Files,
94
+ not a GUI - hand the output to speedscope.app, flamegraph.pl, or
95
+ a Mermaid renderer.
96
+
97
+ Formats:
98
+ ascii ranked self-time table (alias for the table renderer)
99
+ collapsed folded stacks: "root;a;b 42" - flamegraph.pl and most flame tooling
100
+ mermaid call tree, top-15 frames by total time (never the whole profile)
101
+ speedscope sampled profile JSON for speedscope.app
102
+ cpuprofile verbatim .cpuprofile pass-through (cpu-prof/inspector origins only)
103
+
104
+ Flags:
105
+ --run <id> render only this run (default: every CPU run in the document)
106
+ --out-dir PATH write artifacts here instead of stdout
107
+ --help show this message
108
+
109
+ Examples:
110
+ ostia viz run.json --format speedscope --out-dir .ostia/viz
111
+ ostia viz run.json --format collapsed | flamegraph.pl > flame.svg
112
+ `,Dn=`ostia ci [--full] [--baseline NAME]
113
+
114
+ Load ostia.config.json, run configured workloads (reusing cached results when their
115
+ fingerprint is unchanged), compare against the named baseline, and gate on regressions.
116
+
117
+ Flags:
118
+ --full ignore the cache; rerun every configured workload
119
+ --baseline NAME baseline name (default: config's "baseline" field, or "main")
120
+ --export-json PATH write the resulting document (with comparisons) to PATH
121
+ --quiet suppress the rendered report (still writes --export-json)
122
+ --help show this message
123
+
124
+ Exit codes: 0 pass, 1 regression, 2 harness error (missing config/baseline, spawn failure).
125
+ `;function vn(e){let n=[],t,s,r=!1,o=!1,i,a,c,l="table",u=!1,p=!1;for(let d=0;d<e.length;d++){let b=e[d];switch(b){case"--runs":t=Number(e[++d]);break;case"--warmup":s=Number(e[++d]);break;case"--cpu":r=!0;break;case"--heap":o=!0;break;case"--cpu-interval":i=Number(e[++d]);break;case"--out-dir":a=e[++d];break;case"--export-json":c=e[++d];break;case"--format":l=e[++d];break;case"--quiet":u=!0;break;case"--help":case"-h":p=!0;break;default:n.push(b)}}return{commands:n,runs:t,warmup:s,cpu:r,heap:o,cpuIntervalUs:i,outDir:a,exportJson:c,format:l,quiet:u,help:p}}async function Un(e){let n=vn(e);if(n.help||n.commands.length===0)return process.stdout.write($n),n.help?0:2;if(!(n.format in I))return process.stderr.write(`Unknown --format "${n.format}". Expected one of: ${Object.keys(I).join(", ")}
126
+ `),2;let t;try{t=await We({commands:n.commands,runs:n.runs,warmup:n.warmup,cpu:n.cpu,heap:n.heap,cpuIntervalUs:n.cpuIntervalUs,outDir:n.outDir})}catch(r){return process.stderr.write(`Run failed: ${r instanceof Error?r.message:String(r)}
127
+ `),2}if(n.exportJson)await E(t,n.exportJson);if(!n.quiet){let o=await I[n.format].render(t,{});await V(o)}return t.runs.some((r)=>r.trials.some((o)=>o.exitCode!==void 0&&o.exitCode!==0))?1:0}function Sn(e){let n=[],t,s,r=!1,o,i,a="table",c=!1,l=!1;for(let u=0;u<e.length;u++){let p=e[u];switch(p){case"--time-budget":t=Number(e[++u]);break;case"--min-samples":s=Number(e[++u]);break;case"--gc":r=!0;break;case"--out-dir":o=e[++u];break;case"--export-json":i=e[++u];break;case"--format":a=e[++u];break;case"--quiet":c=!0;break;case"--help":case"-h":l=!0;break;default:n.push(p)}}return{suites:n,timeBudgetMs:t,minSamples:s,gc:r,outDir:o,exportJson:i,format:a,quiet:c,help:l}}async function Bn(e){let n=Sn(e);if(n.help||n.suites.length===0)return process.stdout.write(Nn),n.help?0:2;if(!(n.format in I))return process.stderr.write(`Unknown --format "${n.format}". Expected one of: ${Object.keys(I).join(", ")}
128
+ `),2;let t;try{t=await oe({suites:n.suites,timeBudgetMs:n.timeBudgetMs,minSamples:n.minSamples,gc:n.gc,outDir:n.outDir})}catch(s){return process.stderr.write(`Bench failed: ${s instanceof Error?s.message:String(s)}
129
+ `),2}if(n.exportJson)await E(t,n.exportJson);if(!n.quiet){let r=await I[n.format].render(t,{});await V(r)}return 0}function An(e){let n=[],t,s,r="table",o=!1,i=!1;for(let a=0;a<e.length;a++){let c=e[a];switch(c){case"--baseline":t=e[++a];break;case"--export-json":s=e[++a];break;case"--format":r=e[++a];break;case"--quiet":o=!0;break;case"--help":case"-h":i=!0;break;default:n.push(c)}}return{paths:n,baseline:t,exportJson:s,format:r,quiet:o,help:i}}async function Mn(e){let n=An(e);if(n.help)return process.stdout.write(Ze),0;let t,s;if(n.baseline)t=n.baseline,s=n.paths[0];else t=n.paths[0],s=n.paths[1];if(!t||!s)return process.stdout.write(Ze),2;let r,o;try{[r,o]=await Promise.all([v(t),v(s)])}catch(l){return process.stderr.write(`Failed to load documents: ${l instanceof Error?l.message:String(l)}
130
+ `),2}let i=ie(r,o),a={...o,comparisons:i};if(n.exportJson)await E(a,n.exportJson);if(!n.quiet){let u=await I[n.format].render(a,{});await V(u)}return i.some((l)=>l.verdict==="fail")?1:0}function En(e){let n,t="table",s=!1;for(let r=0;r<e.length;r++){let o=e[r];switch(o){case"--format":t=e[++r];break;case"--help":case"-h":s=!0;break;default:n=o}}return{path:n,format:t,help:s}}async function On(e){let n=En(e);if(n.help||!n.path)return process.stdout.write(In),n.help?0:2;let t;try{t=await v(n.path)}catch(o){return process.stderr.write(`Failed to load ${n.path}: ${o instanceof Error?o.message:String(o)}
131
+ `),2}let r=await I[n.format].render(t,{});return await V(r),0}var jn={ascii:"table"};function Wn(e){let n,t,s,r,o=!1;for(let i=0;i<e.length;i++){let a=e[i];switch(a){case"--format":{let c=e[++i]??"";t=jn[c]??c;break}case"--run":s=e[++i];break;case"--out-dir":r=e[++i];break;case"--help":case"-h":o=!0;break;default:n=a}}return{path:n,format:t,runId:s,outDir:r,help:o}}async function _n(e){let n=Wn(e);if(n.help||!n.path||!n.format)return process.stdout.write(Fn),n.help?0:2;if(!(n.format in I))return process.stderr.write(`Unknown --format "${n.format}". Expected one of: ${Object.keys(I).join(", ")}, ascii
132
+ `),2;let t;try{t=await v(n.path)}catch(o){return process.stderr.write(`Failed to load ${n.path}: ${o instanceof Error?o.message:String(o)}
133
+ `),2}let r=await I[n.format].render(t,{runId:n.runId});if(!r.text&&(!r.files||r.files.length===0))return process.stderr.write(n.runId?`No CPU evidence found for run "${n.runId}".
134
+ `:`No CPU evidence found in this document (no cpu-phase runs). Capture some with "ostia run --cpu ...".
135
+ `),2;return await V(r,n.outDir),0}function Hn(e){let n=!1,t,s,r=!1,o=!1;for(let i=0;i<e.length;i++)switch(e[i]){case"--full":n=!0;break;case"--baseline":t=e[++i];break;case"--export-json":s=e[++i];break;case"--quiet":r=!0;break;case"--help":case"-h":o=!0;break}return{full:n,baseline:t,exportJson:s,quiet:r,help:o}}async function Ln(e){let n=Hn(e);if(n.help)return process.stdout.write(Dn),0;let t=await Ve();if(!t)return process.stderr.write(`No ostia.config.json found. "ostia ci" needs configured workloads.
136
+ `),2;if(t.workloads.length===0)return process.stderr.write(`ostia.config.json has no "workloads" configured.
137
+ `),2;let s;try{s=await Ke({config:t,full:n.full,baselineName:n.baseline})}catch(r){if(r instanceof te)return process.stderr.write(`${r.message}
138
+ `),2;return process.stderr.write(`CI run failed: ${r instanceof Error?r.message:String(r)}
139
+ `),2}if(n.exportJson)await E(s.document,n.exportJson);if(!n.quiet)process.stdout.write(Ge(s.summary));return s.summary.regressed>0?1:0}async function Jn(){let[e,...n]=process.argv.slice(2);switch(e){case"run":return Un(n);case"bench":return Bn(n);case"compare":return Mn(n);case"report":return On(n);case"ci":return Ln(n);case"viz":return _n(n);case void 0:case"--help":case"-h":return process.stdout.write(`ostia - Bun-native profile IR engine
140
+
141
+ Commands:
142
+ run Run commands N times and report timing/CPU/heap
143
+ bench Run in-process benchmark suites (group()/task())
144
+ compare Compare two ProfileDocuments
145
+ report Render a saved ProfileDocument
146
+ ci Run configured workloads against a baseline, gate on regressions
147
+ viz Render CPU evidence as collapsed/mermaid/speedscope/cpuprofile
148
+
149
+ Run "ostia <command> --help" for details.
150
+ `),e===void 0?2:0;default:return process.stderr.write(`Unknown subcommand "${e}". Run "ostia --help".
151
+ `),2}}if(import.meta.main)Jn().then((e)=>process.exit(e));
package/index.d.ts ADDED
@@ -0,0 +1,259 @@
1
+ interface RunOptions {
2
+ commands: (string | string[])[];
3
+ runs?: number;
4
+ warmup?: number;
5
+ cwd?: string;
6
+ env?: Record<string, string>;
7
+ cpu?: boolean;
8
+ heap?: boolean;
9
+ cpuIntervalUs?: number;
10
+ outDir?: string;
11
+ }
12
+
13
+ export declare function run(opts: RunOptions): Promise<ProfileDocument>;
14
+
15
+ interface ProfileOptions {
16
+ intervalUs?: number;
17
+ origin?: "inspector" | "jsc";
18
+ }
19
+
20
+ interface ProfileResult<T> {
21
+ result: T;
22
+ run: Run;
23
+ }
24
+
25
+ export declare function profile<T>(fn: () => T | Promise<T>, opts?: ProfileOptions): Promise<ProfileResult<T>>;
26
+
27
+ export interface ProfileDocument {
28
+ schemaVersion: 1;
29
+ toolVersion: string;
30
+ bunVersion: string;
31
+ platform: {
32
+ os: string;
33
+ arch: string;
34
+ };
35
+ createdAt: string;
36
+ workloads: Workload[];
37
+ runs: Run[];
38
+ comparisons?: Comparison[];
39
+ }
40
+
41
+ interface Workload {
42
+ id: string;
43
+ kind: "subprocess" | "inprocess";
44
+ label?: string;
45
+ command?: string[];
46
+ shell?: string;
47
+ entry?: {
48
+ file: string;
49
+ task: string;
50
+ };
51
+ }
52
+
53
+ type Phase = "timing" | "cpu" | "heap" | "memstats";
54
+
55
+ interface Run {
56
+ id: string;
57
+ workloadId: string;
58
+ phase: Phase;
59
+ instrumented: boolean;
60
+ configFingerprint: string;
61
+ trials: Trial[];
62
+ timing?: TimingStats;
63
+ diagnosticWallNs?: number;
64
+ cpu?: CpuEvidence;
65
+ heap?: HeapEvidence;
66
+ memory?: MemoryEvidence;
67
+ jit?: JitTierBreakdown;
68
+ warnings: Warning[];
69
+ artifacts: ArtifactRef[];
70
+ baselineRunId?: string;
71
+ }
72
+
73
+ interface Trial {
74
+ i: number;
75
+ wallNs: number;
76
+ exitCode?: number;
77
+ userNs?: number;
78
+ systemNs?: number;
79
+ maxRssBytes?: number;
80
+ }
81
+
82
+ interface TimingStats {
83
+ unit: "ns";
84
+ samples: number[];
85
+ mean: number;
86
+ median: number;
87
+ stddev: number;
88
+ min: number;
89
+ max: number;
90
+ outliers: {
91
+ mild: number;
92
+ severe: number;
93
+ };
94
+ }
95
+
96
+ interface Frame {
97
+ key: string;
98
+ name: string;
99
+ url?: string;
100
+ line?: number;
101
+ col?: number;
102
+ }
103
+
104
+ interface CallNode {
105
+ id: number;
106
+ frameIx: number;
107
+ children: number[];
108
+ }
109
+
110
+ interface FrameTotal {
111
+ frameIx: number;
112
+ selfUs: number;
113
+ totalUs: number;
114
+ samples: number;
115
+ }
116
+
117
+ interface CpuEvidence {
118
+ origin: "cpu-prof" | "inspector" | "jsc-profile";
119
+ samplingIntervalUs: number;
120
+ frames: Frame[];
121
+ nodes: CallNode[];
122
+ totals: FrameTotal[];
123
+ samples?: {
124
+ nodeIds: number[];
125
+ timeDeltasUs: number[];
126
+ };
127
+ }
128
+
129
+ interface HeapEvidence {
130
+ origin: "heap-prof" | "generateHeapSnapshot" | "heapStats";
131
+ heapSizeBytes?: number;
132
+ objectCount?: number;
133
+ typeCounts: {
134
+ type: string;
135
+ count: number;
136
+ retainedBytes?: number;
137
+ }[];
138
+ snapshotArtifactId?: string;
139
+ }
140
+
141
+ interface MemoryEvidence {
142
+ origin: "resourceUsage" | "memoryUsage" | "heapStats";
143
+ perTrial?: {
144
+ rssBytes?: number;
145
+ heapSizeBytes?: number;
146
+ }[];
147
+ maxRssBytes?: number;
148
+ peakCommitBytes?: number;
149
+ pageFaults?: number;
150
+ }
151
+
152
+ interface JitTierBreakdown {
153
+ origin: "jsc-profile";
154
+ tiers: {
155
+ llint: number;
156
+ baseline: number;
157
+ dfg: number;
158
+ ftl: number;
159
+ };
160
+ topFramesByTier?: {
161
+ tier: string;
162
+ frameKey: string;
163
+ samples: number;
164
+ }[];
165
+ }
166
+
167
+ type WarningCode = "slow-first-run" | "outliers-detected" | "fast-command" | "nonzero-exit" | "instrumented-timing" | "artifact-missing" | "empty-profile" | "below-timer-resolution" | "cache-fallback-rerun";
168
+
169
+ interface Warning {
170
+ code: WarningCode;
171
+ message: string;
172
+ data?: Record<string, unknown>;
173
+ }
174
+
175
+ interface ArtifactRef {
176
+ id: string;
177
+ kind: "cpuprofile" | "cpu-md" | "heapsnapshot" | "heap-md" | "speedscope" | "collapsed" | "mermaid" | "other";
178
+ path: string;
179
+ sha256: string;
180
+ bytes: number;
181
+ }
182
+
183
+ interface Comparison {
184
+ id: string;
185
+ baselineRunId: string;
186
+ candidateRunId: string;
187
+ timing?: {
188
+ medianDeltaPct: number;
189
+ meanDeltaPct: number;
190
+ verdict: "improved" | "regressed" | "unchanged";
191
+ };
192
+ frames?: {
193
+ frameKey: string;
194
+ name: string;
195
+ baseSelfUs: number;
196
+ candSelfUs: number;
197
+ deltaPct: number;
198
+ }[];
199
+ heapTypes?: {
200
+ type: string;
201
+ baseCount: number;
202
+ candCount: number;
203
+ baseBytes?: number;
204
+ candBytes?: number;
205
+ deltaPct: number;
206
+ }[];
207
+ thresholds: {
208
+ timingPct: number;
209
+ frameSelfPct: number;
210
+ heapTypePct: number;
211
+ minFrameSelfUs: number;
212
+ };
213
+ verdict: "pass" | "fail";
214
+ }
215
+
216
+ interface BenchOptions {
217
+ suites: string[];
218
+ timeBudgetMs?: number;
219
+ minSamples?: number;
220
+ gc?: boolean;
221
+ outDir?: string;
222
+ cwd?: string;
223
+ }
224
+
225
+ export declare function bench(opts: BenchOptions): Promise<ProfileDocument>;
226
+
227
+ export declare function group(name: string, fn: () => void): void;
228
+
229
+ export declare function task(name: string, fn: () => unknown | Promise<unknown>): void;
230
+
231
+ interface Thresholds {
232
+ timingPct: number;
233
+ frameSelfPct: number;
234
+ heapTypePct: number;
235
+ minFrameSelfUs: number;
236
+ }
237
+
238
+ export declare function compareDocuments(base: ProfileDocument, cand: ProfileDocument, thresholds?: Thresholds): Comparison[];
239
+
240
+ export declare function saveDocument(doc: ProfileDocument, path: string): Promise<void>;
241
+
242
+ export declare function loadDocument(path: string): Promise<ProfileDocument>;
243
+
244
+ export declare const renderers: Record<FormatName, Renderer<any>>;
245
+
246
+ type FormatName = "table" | "json" | "markdown" | "jsonl" | "collapsed" | "mermaid" | "speedscope" | "cpuprofile";
247
+
248
+ interface RenderResult {
249
+ text?: string;
250
+ files?: {
251
+ path?: string;
252
+ content: string;
253
+ }[];
254
+ }
255
+
256
+ interface Renderer<O = unknown> {
257
+ name: FormatName;
258
+ render(doc: ProfileDocument, options: O): Promise<RenderResult>;
259
+ }
package/index.js ADDED
@@ -0,0 +1,19 @@
1
+ // @bun
2
+ function A(e){return JSON.stringify(W(e))}function W(e){if(Array.isArray(e))return e.map(W);if(e!==null&&typeof e==="object"){let s={};for(let n of Object.keys(e).sort())s[n]=W(e[n]);return s}return e}function C(e,...s){let n=Bun.CryptoHasher.hash("sha256",A(s),"hex");return`${e}_${n.slice(0,16)}`}function Se(e){return e.startsWith("file://")?e.slice(7):e}function _(e,s,n){let r=e.nodes,t=r.length,a=new Map,i=[],o=new Map,c=new Int32Array(t);for(let p=0;p<t;p++){let f=r[p],u=f.callFrame,w=Se(u.url),k=a.get(u.functionName);if(k===void 0)k=new Map,a.set(u.functionName,k);let N=k.get(w);if(N===void 0)N=i.length,k.set(w,N),i.push({key:C("fr",u.functionName,w),name:u.functionName,url:w||void 0,line:u.lineNumber>=0?u.lineNumber:void 0,col:u.columnNumber>=0?u.columnNumber:void 0});c[p]=N,o.set(f.id,p)}let m=Array(t);for(let p=0;p<t;p++){let f=r[p];m[p]={id:f.id,frameIx:c[o.get(f.id)],children:f.children??[]}}let l=new Float64Array(t),d=new Float64Array(t),g=e.samples,x=e.timeDeltas;for(let p=0;p<g.length;p++){let f=o.get(g[p]);if(f===void 0)continue;l[f]+=x[p]??0,d[f]+=1}let R=new Int32Array(t).fill(-1);for(let p=0;p<t;p++){let f=r[p].children;if(!f)continue;for(let u of f){let w=o.get(u);if(w!==void 0)R[w]=p}}let y=[],b=[];for(let p=t-1;p>=0;p--)if(R[p]===-1)b.push(p);while(b.length>0){let p=b.pop();y.push(p);let f=r[p].children;if(!f)continue;for(let u of f){let w=o.get(u);if(w!==void 0&&R[w]===p)b.push(w)}}let T=new Float64Array(t);for(let p=y.length-1;p>=0;p--){let f=y[p];T[f]+=l[f];let u=R[f];if(u>=0)T[u]+=T[f]}let P=Array(i.length),h=[];for(let p=0;p<t;p++){let f=m[p].frameIx,u=P[f];if(u)u.selfUs+=l[p],u.totalUs+=T[p],u.samples+=d[p];else{let w={frameIx:f,selfUs:l[p],totalUs:T[p],samples:d[p]};P[f]=w,h.push(w)}}return{origin:s,samplingIntervalUs:n,frames:i,nodes:m,totals:h.sort((p,f)=>f.selfUs-p.selfUs),samples:{nodeIds:e.samples,timeDeltasUs:e.timeDeltas}}}function Me(e,s,n,r){let t=["--cpu-prof","--cpu-prof-dir",s,"--cpu-prof-name",n,"--cpu-prof-interval",String(r)],a=e[0];if(a==="bun"||a?.endsWith("/bun"))return[a,...t,...e.slice(1)];return e}async function ee(e){let s=`${e.artifactDir}/${e.fileName}`,n=e.argv[0],r=n==="bun"||n?.endsWith("/bun"),t=Me(e.argv,e.artifactDir,e.fileName,e.intervalUs),a=r?e.env:{...process.env,...e.env,BUN_OPTIONS:`--cpu-prof --cpu-prof-dir ${e.artifactDir} --cpu-prof-name ${e.fileName} --cpu-prof-interval ${e.intervalUs}`},i=Bun.nanoseconds(),c=await Bun.spawn(t,{cwd:e.cwd,env:a,stdout:"ignore",stderr:"ignore",stdin:"ignore"}).exited,m=Bun.nanoseconds()-i,l=Bun.file(s);if(!await l.exists())return{diagnosticWallNs:m,exitCode:c,warnings:[{code:"artifact-missing",message:`Expected a .cpuprofile at ${s} after exit ${c}, found nothing. The workload's argv[0] must be a \`bun\` binary for CPU capture.`,data:{artifactPath:s,argv:e.argv}}]};let d=await l.json(),g=_(d,"cpu-prof",e.intervalUs),x=d.samples.length===0?[{code:"empty-profile",message:"CPU capture produced zero samples."}]:[];return{diagnosticWallNs:m,exitCode:c,artifactPath:s,cpu:g,warnings:x}}function ne(e,s="heap-prof"){let{node_fields:n,node_types:r}=e.snapshot.meta,t=n.indexOf("type"),a=n.indexOf("self_size"),i=n.length,o=r[0];if(t===-1||a===-1||!Array.isArray(o))return{origin:s,typeCounts:[],objectCount:e.snapshot.node_count};let c=o.length,m=Array(c),l=new Map,d=[],g=0,x=e.nodes,R=x.length;for(let h=0;h<R;h+=i){let p=x[h+t],f=x[h+a]??0;g+=f;let u;if(p>=0&&p<c){if(u=m[p],u===void 0)u={type:o[p],count:0,bytes:0},m[p]=u,d.push(u)}else{let w=`unknown(${p})`;if(u=l.get(w),u===void 0)u={type:w,count:0,bytes:0},l.set(w,u),d.push(u)}u.count++,u.bytes+=f}let y=d.sort((h,p)=>p.count-h.count),b=y.slice(0,20),T=y.slice(20),P=b.map(({type:h,count:p,bytes:f})=>({type:h,count:p,retainedBytes:f}));if(T.length>0){let h=0,p=0;for(let f of T)h+=f.count,p+=f.bytes;P.push({type:"other",count:h,retainedBytes:p})}return{origin:s,heapSizeBytes:g,objectCount:e.snapshot.node_count,typeCounts:P}}function Be(e,s,n){let r=["--heap-prof","--heap-prof-dir",s,"--heap-prof-name",n],t=e[0];if(t==="bun"||t?.endsWith("/bun"))return[t,...r,...e.slice(1)];return e}async function te(e){let s=`${e.artifactDir}/${e.fileName}`,n=e.argv[0],r=n==="bun"||n?.endsWith("/bun"),t=Be(e.argv,e.artifactDir,e.fileName),a=r?e.env:{...process.env,...e.env,BUN_OPTIONS:`--heap-prof --heap-prof-dir ${e.artifactDir} --heap-prof-name ${e.fileName}`},i=Bun.nanoseconds(),c=await Bun.spawn(t,{cwd:e.cwd,env:a,stdout:"ignore",stderr:"ignore",stdin:"ignore"}).exited,m=Bun.nanoseconds()-i,l=Bun.file(s);if(!await l.exists())return{diagnosticWallNs:m,exitCode:c,warnings:[{code:"artifact-missing",message:`Expected a heap snapshot at ${s} after exit ${c}, found nothing. The workload's argv[0] must be a \`bun\` binary for heap capture.`,data:{artifactPath:s,argv:e.argv}}]};let d=await l.json(),g=ne(d,"heap-prof");return{diagnosticWallNs:m,exitCode:c,artifactPath:s,heap:g,warnings:[]}}import{Session as We}from"inspector/promises";var Oe=1000;async function re(e,s={}){let n=s.intervalUs??Oe,r=new We;r.connect();let t=Bun.nanoseconds();try{await r.post("Profiler.enable"),await r.post("Profiler.setSamplingInterval",{interval:n}),await r.post("Profiler.start");let a=await e(),{profile:i}=await r.post("Profiler.stop"),o=Bun.nanoseconds()-t,c=_(i,"inspector",n);return{result:a,cpu:c,diagnosticWallNs:o}}finally{r.disconnect()}}import{profile as Ae}from"bun:jsc";var Ee=new Map([["LLInt","llint"],["Baseline","baseline"],["DFG","dfg"],["FTL","ftl"]]),se=4294967295;function oe(e,s){let n=s??e.interval*1e6,r=new Map,t=[];function a(u,w,k,N){let I=r.get(u);if(I===void 0)I=new Map,r.set(u,I);let v=w??"",U=I.get(v);if(U===void 0)U=t.length,I.set(v,U),t.push({key:C("fr",u,v),name:u,url:w,line:k,col:N});return U}function i(u){let w=u.line===se,k=w?void 0:u.line-1,N=w||u.column===se?void 0:u.column-1;return a(u.name,u.sourceURL,k,N)}let o=a("(root)",void 0,void 0,void 0),c=1,m={id:0,frameIx:o,children:new Map,selfUs:0,samples:0,totalUs:0},l=new Map([[0,m]]),d={llint:0,baseline:0,dfg:0,ftl:0},g=new Map,x=[],R=[];for(let u of e.traces){let w=u.frames,k=m;for(let v=w.length-1;v>=0;v--){let U=i(w[v]),S=k.children.get(U);if(!S)S={id:c++,frameIx:U,children:new Map,selfUs:0,samples:0,totalUs:0},k.children.set(U,S),l.set(S.id,S);k=S}k.selfUs+=n,k.samples+=1,x.push(k.id),R.push(n);let N=w[0],I=N&&Ee.get(N.category);if(I){d[I]++;let v=g.get(I)??new Map;v.set(k.frameIx,(v.get(k.frameIx)??0)+1),g.set(I,v)}}function y(u){let w=u.selfUs;for(let k of u.children.values())w+=y(k);return u.totalUs=w,w}y(m);let b=new Map;function T(u){let w=b.get(u.frameIx);if(w)w.selfUs+=u.selfUs,w.totalUs+=u.totalUs,w.samples+=u.samples;else b.set(u.frameIx,{frameIx:u.frameIx,selfUs:u.selfUs,totalUs:u.totalUs,samples:u.samples});for(let k of u.children.values())T(k)}T(m);let P=[...l.values()].map((u)=>({id:u.id,frameIx:u.frameIx,children:[...u.children.values()].map((w)=>w.id)})),h={origin:"jsc-profile",samplingIntervalUs:n,frames:t,nodes:P,totals:[...b.values()].sort((u,w)=>w.selfUs-u.selfUs),samples:{nodeIds:x,timeDeltasUs:R}},p=[...g.entries()].flatMap(([u,w])=>[...w.entries()].sort((k,N)=>N[1]-k[1]).slice(0,3).map(([k,N])=>({tier:u,frameKey:t[k].key,samples:N})));return{cpu:h,jit:{origin:"jsc-profile",tiers:d,topFramesByTier:p}}}var _e=1000;async function ie(e,s={}){let n=s.intervalUs??_e,r,t=Bun.nanoseconds(),a=await Ae(async()=>(r=await e(),r),n),i=Bun.nanoseconds()-t,{cpu:o,jit:c}=oe(a.stackTraces,n);return{result:r,cpu:o,jit:c,diagnosticWallNs:i}}var V="0.1.0";function j(e,s){return{schemaVersion:1,toolVersion:V,bunVersion:Bun.version,platform:{os:process.platform,arch:process.arch},createdAt:new Date().toISOString(),workloads:e,runs:s}}function ae(e,s){return{id:C("wl","subprocess",e,process.cwd()),kind:"subprocess",command:e,label:s}}function ce(e,s){return{id:C("wl","inprocess",e.name,e.toString()),kind:"inprocess",label:s}}function ue(e){return{id:C("run",e.workload.id,"timing",e.configFingerprint,Bun.version,V),workloadId:e.workload.id,phase:"timing",instrumented:!1,configFingerprint:e.configFingerprint,trials:e.trials,timing:e.timing,warnings:e.warnings,artifacts:[],memory:je(e.trials)}}function je(e){let s=e.map((n)=>n.maxRssBytes).filter((n)=>n!==void 0);if(s.length===0)return;return{origin:"resourceUsage",perTrial:e.map((n)=>({rssBytes:n.maxRssBytes})),maxRssBytes:Math.max(...s)}}function L(e){return{id:C("run",e.workload.id,e.phase,e.configFingerprint,Bun.version,V),workloadId:e.workload.id,phase:e.phase,instrumented:!0,configFingerprint:e.configFingerprint,trials:[{i:0,wallNs:e.diagnosticWallNs,exitCode:e.exitCode}],diagnosticWallNs:e.diagnosticWallNs,cpu:e.cpu,heap:e.heap,jit:e.jit,warnings:e.warnings,artifacts:e.artifacts}}async function pe(e,s,n){let t=await Bun.file(n).arrayBuffer(),a=new Bun.CryptoHasher("sha256");return a.update(t),{id:C("art",e,s,n),kind:s,path:n,sha256:a.digest("hex"),bytes:t.byteLength}}function K(e){return C("cfg",e)}function q(e){return`${JSON.stringify(W(e),null,2)}
3
+ `}async function Le(e,s){await Bun.write(s,q(e))}async function G(e){let s=await Bun.file(e).text();return JSON.parse(s)}async function Y(e){let s=Bun.nanoseconds(),n=Bun.spawn(e.argv,{cwd:e.cwd,env:e.env,stdout:"ignore",stderr:"ignore",stdin:"ignore"}),r=await n.exited,t=Bun.nanoseconds(),a=n.resourceUsage?.();return{wallNs:t-s,exitCode:r,userNs:a?Number(a.cpuTime.user)*1000:void 0,systemNs:a?Number(a.cpuTime.system)*1000:void 0,maxRssBytes:a?.maxRSS}}function le(e){return e.trim().split(/\s+/).filter(Boolean)}function me(e){if(e.length===0)throw Error("computeTimingStats: samples must be non-empty");let s=e.length,n=de(e),r=0;for(let h=0;h<s;h++)r+=e[h];let t=r/s,a=O(n,0.5),i=0;for(let h=0;h<s;h++){let p=e[h]-t;i+=p*p}let o=Math.sqrt(i/s),c=n[0],m=n[s-1],l=O(n,0.25),d=O(n,0.75),g=d-l,x=l-1.5*g,R=d+1.5*g,y=l-3*g,b=d+3*g,T=0,P=0;for(let h=0;h<s;h++){let p=e[h];if(p<y||p>b)P++;else if(p<x||p>R)T++}return{unit:"ns",samples:e,mean:t,median:a,stddev:o,min:c,max:m,outliers:{mild:T,severe:P}}}function de(e){let s=new Float64Array(e.length);return s.set(e),s.sort(),s}function O(e,s){let n=e.length;if(n===1)return e[0];let r=s*(n-1),t=Math.floor(r),a=Math.ceil(r);if(t===a)return e[t];let i=r-t;return e[t]*(1-i)+e[a]*i}var He=5000000,Je=200;function fe(e,s,n="subprocess"){let r=[],t=e.samples[0];if(t!==void 0){let i=de(e.samples),o=O(i,0.25),m=O(i,0.75)-o;if(t>e.median+3*m&&m>0)r.push({code:"slow-first-run",message:`First run took ${(t/1e6).toFixed(2)}ms, much slower than the median ${(e.median/1e6).toFixed(2)}ms. Consider more warmup.`,data:{firstNs:t,medianNs:e.median}})}if(e.outliers.mild+e.outliers.severe>0)r.push({code:"outliers-detected",message:`${e.outliers.mild+e.outliers.severe} outlier(s) detected (${e.outliers.severe} severe, ${e.outliers.mild} mild).`,data:e.outliers});if(n==="subprocess"&&e.median<He)r.push({code:"fast-command",message:`Median run time (${(e.median/1e6).toFixed(3)}ms) is very fast; results may be dominated by spawn overhead.`,data:{medianNs:e.median}});if(n==="inprocess"&&e.median<Je)r.push({code:"below-timer-resolution",message:`Median run time (${e.median.toFixed(0)}ns) is close to timer resolution; consider a larger batch size or a coarser operation.`,data:{medianNs:e.median}});let a=s.filter((i)=>i!==void 0&&i!==0);if(a.length>0)r.push({code:"nonzero-exit",message:`${a.length} of ${s.length} trial(s) exited non-zero.`,data:{exitCodes:a}});return r}var ze=10,Ve=3000000000,Ke=3;async function ge(e){let s=e.warmup??Ke;for(let l=0;l<s;l++)await Y(e);let n=[],r=e.runs??e.minRuns??ze,t=e.runs!==void 0?0:e.minTotalNs??Ve,a=0,i=0;while(i<r||a<t){let l=await Y(e);if(n.push({i,wallNs:l.wallNs,exitCode:l.exitCode,userNs:l.userNs,systemNs:l.systemNs,maxRssBytes:l.maxRssBytes}),a+=l.wallNs,i++,e.runs!==void 0&&i>=e.runs)break}let o=n.map((l)=>l.wallNs),c=me(o),m=fe(c,n.map((l)=>l.exitCode));return{trials:n,timing:c,warnings:m}}var qe=new URL("./runner.ts",import.meta.url).pathname,Ge=".ostia";async function Ye(e){let n=`${e.outDir??Ge}/bench-tmp`,r=e.cwd??process.cwd(),t={timeBudgetMs:e.timeBudgetMs,minSamples:e.minSamples,gc:e.gc},a=[],i=[];try{for(let o of e.suites){let c=o.startsWith("/")?o:`${r}/${o}`,m=`${n}/${C("bench-out",c)}.json`,d=await Bun.spawn(["bun",qe,c,m,JSON.stringify(t)],{cwd:r,stdout:"inherit",stderr:"inherit",stdin:"ignore"}).exited;if(d!==0)throw Error(`Bench suite failed: ${o} (runner exited ${d})`);let g=await G(m);a.push(...g.workloads),i.push(...g.runs)}}finally{await Bun.spawn(["rm","-rf",n]).exited}return j(a,i)}var Ze=[],H;function Qe(e,s){let n=H;H=e;try{s()}finally{H=n}}function Xe(e,s){Ze.push({groupName:H,name:e,fn:s})}var he={timingPct:5,frameSelfPct:10,heapTypePct:10,minFrameSelfUs:1000};function J(e,s){if(e===0)return s===0?0:1/0;return(s-e)/e*100}function M(e,s,n){return e.runs.find((r)=>r.workloadId===s&&r.phase===n)}function en(e,s,n=he){let r=new Set(s.workloads.map((a)=>a.id)),t=[];for(let a of e.workloads){if(!r.has(a.id))continue;let i=nn(e,s,a.id,n);if(i)t.push(i)}return t}function nn(e,s,n,r=he){let t=M(e,n,"timing"),a=M(s,n,"timing"),i=M(e,n,"cpu"),o=M(s,n,"cpu"),c=M(e,n,"heap"),m=M(s,n,"heap"),l=t?.id??i?.id??c?.id,d=a?.id??o?.id??m?.id;if(!l||!d)return;let g=!1,x;if(t?.timing&&a?.timing){let b=J(t.timing.median,a.timing.median),T=J(t.timing.mean,a.timing.mean),P=b>r.timingPct?"regressed":b<-r.timingPct?"improved":"unchanged";if(P==="regressed")g=!0;x={medianDeltaPct:b,meanDeltaPct:T,verdict:P}}let R;if(i?.cpu&&o?.cpu){let b=new Map(i.cpu.totals.map((f)=>[i.cpu.frames[f.frameIx].key,f])),T=new Map(o.cpu.totals.map((f)=>[o.cpu.frames[f.frameIx].key,f])),P=new Map(i.cpu.frames.map((f)=>[f.key,f.name])),h=new Map(o.cpu.frames.map((f)=>[f.key,f.name]));R=[...new Set([...b.keys(),...T.keys()])].map((f)=>{let u=b.get(f)?.selfUs??0,w=T.get(f)?.selfUs??0;return{frameKey:f,name:h.get(f)??P.get(f)??f,baseSelfUs:u,candSelfUs:w,deltaPct:J(u,w)}}).sort((f,u)=>Math.abs(u.deltaPct)-Math.abs(f.deltaPct));for(let f of R)if((f.baseSelfUs>=r.minFrameSelfUs||f.candSelfUs>=r.minFrameSelfUs)&&f.deltaPct>r.frameSelfPct)g=!0}let y;if(c?.heap&&m?.heap){let b=new Map(c.heap.typeCounts.map((h)=>[h.type,h])),T=new Map(m.heap.typeCounts.map((h)=>[h.type,h]));y=[...new Set([...b.keys(),...T.keys()])].map((h)=>{let p=b.get(h),f=T.get(h);return{type:h,baseCount:p?.count??0,candCount:f?.count??0,baseBytes:p?.retainedBytes,candBytes:f?.retainedBytes,deltaPct:J(p?.count??0,f?.count??0)}}).sort((h,p)=>Math.abs(p.deltaPct)-Math.abs(h.deltaPct));for(let h of y)if(h.deltaPct>r.heapTypePct)g=!0}return{id:C("cmp",l,d),baselineRunId:l,candidateRunId:d,timing:x,frames:R,heapTypes:y,thresholds:r,verdict:g?"fail":"pass"}}function F(e,s){if(s){let n=e.runs.find((r)=>r.id===s);return n?.cpu?[n]:[]}return e.runs.filter((n)=>n.phase==="cpu"&&n.cpu)}function B(e){let s=e.nodes,n=s.length,r=tn(e),t=new Int32Array(n).fill(-1);for(let c=0;c<n;c++)for(let m of s[c].children){let l=r(m);if(l!==-1)t[l]=c}let a=[];for(let c=0;c<n;c++)if(t[c]===-1)a.push(c);let i=[],o=[];for(let c=a.length-1;c>=0;c--)o.push(a[c]);while(o.length>0){let c=o.pop();i.push(c);for(let m of s[c].children){let l=r(m);if(l!==-1&&t[l]===c)o.push(l)}}return{count:n,indexOf:r,parentIx:t,roots:a,order:i}}function tn(e){let s=e.nodes,n=s.length,r=1/0,t=-1/0,a=!0;for(let o=0;o<n;o++){let c=s[o].id;if(!Number.isInteger(c)){a=!1;break}if(c<r)r=c;if(c>t)t=c}if(a&&n>0&&t-r<n*4+64){let o=t-r+1,c=new Int32Array(o).fill(-1);for(let m=0;m<n;m++)c[s[m].id-r]=m;return(m)=>{let l=m-r;return l>=0&&l<o?c[l]:-1}}let i=new Map;for(let o=0;o<n;o++)i.set(s[o].id,o);return(o)=>i.get(o)??-1}function ye(e,s){let{count:n,indexOf:r,parentIx:t,order:a}=s,i=new Float64Array(n),o=new Float64Array(n),c=e.samples?.nodeIds??[],m=e.samples?.timeDeltasUs??[];for(let d=0;d<c.length;d++){let g=r(c[d]);if(g===-1)continue;i[g]+=m[d]??0,o[g]+=1}let l=new Float64Array(n);for(let d=a.length-1;d>=0;d--){let g=a[d];l[g]+=i[g];let x=t[g];if(x>=0)l[x]+=l[g]}return{selfUs:i,totalUs:l,samples:o}}var we={name:"collapsed",async render(e,s={}){return{files:F(e,s.runId).map((t)=>{let a=t.cpu,{nodes:i,frames:o}=a,c=B(a),m=Array(c.count);for(let R of c.order){let y=o[i[R].frameIx].name||"(anonymous)",b=c.parentIx[R];m[R]=b===-1?y:`${m[b]};${y}`}let l=new Float64Array(c.count),d=[],g=a.samples?.nodeIds??[];for(let R=0;R<g.length;R++){let y=c.indexOf(g[R]);if(y===-1)continue;if(l[y]++===0)d.push(y)}let x=Array(d.length);for(let R=0;R<d.length;R++){let y=d[R];x[R]=`${m[y]} ${l[y]}`}return{path:`${t.id}.collapsed.txt`,content:x.join(`
4
+ `)+(x.length>0?`
5
+ `:"")}})}}};var be={name:"cpuprofile",async render(e,s={}){let n=F(e,s.runId),r=[],t=[];for(let a of n){if(a.cpu?.origin!=="cpu-prof"&&a.cpu?.origin!=="inspector"){t.push(`${a.id} (origin ${a.cpu?.origin??"unknown"} has no .cpuprofile artifact to pass through)`);continue}let i=a.artifacts.find((c)=>c.kind==="cpuprofile");if(!i){t.push(`${a.id} (no cpuprofile artifact recorded on this run)`);continue}let o=Bun.file(i.path);if(!await o.exists()){t.push(`${a.id} (artifact missing on disk: ${i.path})`);continue}r.push({path:`${a.id}.cpuprofile`,content:await o.text()})}if(r.length===0&&t.length>0)return{text:`No .cpuprofile artifacts available:
6
+ ${t.map((a)=>` - ${a}`).join(`
7
+ `)}
8
+ `};return{files:r}}};var xe={name:"json",async render(e){return{text:q(e)}}};var Re={name:"jsonl",async render(e){let{runs:s,...n}=e;return{text:`${[A(n),...s.map((t)=>A(t))].join(`
9
+ `)}
10
+ `}}};function D(e){return(e/1e6).toFixed(3)}function z(e){return e?.label??e?.command?.join(" ")??e?.entry?.task??e?.id??"unknown"}var Te=10,Pe=10,ke={name:"markdown",async render(e){let s=new Map(e.workloads.map((t)=>[t.id,t])),n=[];n.push("# Profile Report",""),n.push(`Bun ${e.bunVersion} \xB7 ostia ${e.toolVersion} \xB7 ${e.platform.os}/${e.platform.arch} \xB7 ${e.createdAt}`,"");let r=e.runs.filter((t)=>t.phase==="timing"&&t.timing!==void 0);if(r.length>0){n.push("## Timing",""),n.push("| Command | Mean \xB1 SD (ms) | Min\u2026Max (ms) | Median (ms) |","|---|---|---|---|");for(let a of r){let i=z(s.get(a.workloadId)),o=a.timing;n.push(`| ${i} | ${D(o.mean)} \xB1 ${D(o.stddev)} | ${D(o.min)}\u2026${D(o.max)} | ${D(o.median)} |`)}n.push("");let t=r.filter((a)=>a.warnings.length>0);if(t.length>0){n.push("### Warnings","");for(let a of t){let i=z(s.get(a.workloadId));for(let o of a.warnings)n.push(`- **${i}**: ${o.message} (\`${o.code}\`)`)}n.push("")}}for(let t of e.runs){if(t.phase!=="cpu"&&t.phase!=="heap")continue;let a=z(s.get(t.workloadId));if(t.phase==="cpu"){if(n.push(`## CPU capture - ${a}`,""),n.push(`instrumented, diagnostic wall ${D(t.diagnosticWallNs??0)}ms`,""),t.cpu){n.push(`origin: \`${t.cpu.origin}\`, interval: ${t.cpu.samplingIntervalUs}\xB5s`,""),n.push("| Self % | Self (ms) | Total (ms) | Frame |","|---|---|---|---|");let i=t.cpu.totals.reduce((o,c)=>o+c.selfUs,0)||1;for(let o of t.cpu.totals.slice(0,Te)){let c=t.cpu.frames[o.frameIx],m=(o.selfUs/i*100).toFixed(1);n.push(`| ${m}% | ${(o.selfUs/1000).toFixed(2)} | ${(o.totalUs/1000).toFixed(2)} | ${c?.name||"(anonymous)"} |`)}if(n.push(""),t.jit){let o=t.jit.tiers;n.push(`JIT tiers: LLInt ${o.llint} \xB7 Baseline ${o.baseline} \xB7 DFG ${o.dfg} \xB7 FTL ${o.ftl}`,"")}}}else if(n.push(`## Heap snapshot - ${a}`,""),n.push(`instrumented, diagnostic wall ${D(t.diagnosticWallNs??0)}ms`,""),t.heap){n.push(`${t.heap.objectCount??"?"} objects, ${((t.heap.heapSizeBytes??0)/1e6).toFixed(2)}MB`,""),n.push("| Count | Type |","|---|---|");for(let i of t.heap.typeCounts.slice(0,Pe))n.push(`| ${i.count} | ${i.type} |`);n.push("")}for(let i of t.artifacts)n.push(`- artifact: \`${i.path}\``);for(let i of t.warnings)n.push(`- ! ${i.message} (\`${i.code}\`)`);if(t.artifacts.length>0||t.warnings.length>0)n.push("")}if(e.comparisons&&e.comparisons.length>0){n.push("## Comparisons","");for(let t of e.comparisons){let a=e.runs.find((o)=>o.id===t.candidateRunId),i=z(a?s.get(a.workloadId):void 0);if(n.push(`### ${t.verdict==="pass"?"\u2713":"\u2717"} ${i}`,""),t.timing){let o=t.timing.medianDeltaPct>0?"+":"";n.push(`- timing: ${o}${t.timing.medianDeltaPct.toFixed(1)}% median (**${t.timing.verdict}**)`)}for(let o of t.frames?.slice(0,Te)??[]){if(Math.abs(o.deltaPct)<0.5)continue;let c=o.deltaPct>0?"+":"";n.push(`- frame \`${o.name}\`: ${c}${o.deltaPct.toFixed(1)}% self-time (${(o.baseSelfUs/1000).toFixed(2)}ms \u2192 ${(o.candSelfUs/1000).toFixed(2)}ms)`)}for(let o of t.heapTypes?.slice(0,Pe)??[]){if(Math.abs(o.deltaPct)<0.5)continue;let c=o.deltaPct>0?"+":"";n.push(`- heap \`${o.type}\`: ${c}${o.deltaPct.toFixed(1)}% count (${o.baseCount} \u2192 ${o.candCount})`)}n.push("")}}return{text:n.join(`
11
+ `)}}};var rn=15;function Z(e){return`n${e}`}function sn(e,s,n){return`${(e||"(anonymous)").replace(/"/g,"'")} (self ${(s/1000).toFixed(2)}ms, total ${(n/1000).toFixed(2)}ms)`}function on(e,s,n,r){let t=[];if(r<=0)return t;for(let a=0;a<s;a++){if(a===n)continue;let i=e[a];if(t.length===r&&i<=e[t[r-1]])continue;let o=t.length;while(o>0&&e[t[o-1]]<i)o--;if(t.splice(o,0,a),t.length>r)t.pop()}return t}var $e={name:"mermaid",async render(e,s={}){let n=s.topN??rn;return{files:F(e,s.runId).map((a)=>{let i=a.cpu,{nodes:o,frames:c}=i,m=B(i),{selfUs:l,totalUs:d}=ye(i,m),{parentIx:g}=m,x=m.roots[0]??-1,R=on(l,m.count,x,n),y=new Set(x!==-1?[x]:[]),b=[];for(let P of R){b.length=0;for(let h=P;h!==-1;h=g[h])b.push(h);for(let h=b.length-1;h>=0;h--)y.add(b[h])}let T=["graph TD"];for(let P of y){let h=o[P].id;T.push(` ${Z(h)}["${sn(c[o[P].frameIx].name,l[P],d[P])}"]`)}for(let P of y){let h=g[P];if(h!==-1&&y.has(h))T.push(` ${Z(o[h].id)} --> ${Z(o[P].id)}`)}return{path:`${a.id}.mermaid.md`,content:`${T.join(`
12
+ `)}
13
+ `}})}}};var an="https://www.speedscope.app/file-format-schema.json";function Ne(e){return e?.label??e?.command?.join(" ")??e?.entry?.task??"profile"}var Ce={name:"speedscope",async render(e,s={}){let n=F(e,s.runId),r=new Map(e.workloads.map((a)=>[a.id,a]));return{files:n.map((a)=>{let i=a.cpu,{nodes:o}=i,c=B(i),m=i.samples?.nodeIds??[],l=i.samples?.timeDeltasUs??[],d=Array(c.count);for(let y of c.order){let b=c.parentIx[y],T=o[y].frameIx;d[y]=b===-1?[T]:[...d[b],T]}let g=Array(m.length);for(let y=0;y<m.length;y++){let b=c.indexOf(m[y]);g[y]=b===-1?[]:d[b]}let x=0;for(let y=0;y<l.length;y++)x+=l[y];let R={$schema:an,exporter:"ostia",name:Ne(r.get(a.workloadId)),activeProfileIndex:0,shared:{frames:i.frames.map((y)=>({name:y.name||"(anonymous)",file:y.url,line:y.line!==void 0?y.line+1:void 0}))},profiles:[{type:"sampled",name:Ne(r.get(a.workloadId)),unit:"microseconds",startValue:0,endValue:x,samples:g,weights:l}]};return{path:`${a.id}.speedscope.json`,content:`${JSON.stringify(R,null,2)}
14
+ `}})}}};function E(e){return(e/1e6).toFixed(3)}function Q(e){return e.label??e.command?.join(" ")??e.entry?.task??e.id}var Ie={name:"table",async render(e){let s=e.runs.filter((d)=>d.phase==="timing"&&d.timing!==void 0),n=new Map(e.workloads.map((d)=>[d.id,d]));if(s.length===0){let d=ve(e,n);return{text:d.length>0?`${d.join(`
15
+ `)}
16
+ `:`(no timing runs)
17
+ `}}let r=s.map((d)=>{let g=n.get(d.workloadId);return{run:d,workload:g,label:g?Q(g):d.workloadId}}),t=Math.min(...r.map((d)=>d.run.timing.median)),a=r.length>1,i=[],o=Math.max(7,...r.map((d)=>d.label.length)),c=a?`${"Command".padEnd(o)} Mean [ms] Min\u2026Max [ms] Relative`:`${"Command".padEnd(o)} Mean [ms] Min\u2026Max [ms]`;i.push(c),i.push("-".repeat(c.length));for(let{run:d,label:g}of r){let x=d.timing,R=`${E(x.mean)} \xB1 ${E(x.stddev)}`,y=`${E(x.min)}\u2026${E(x.max)}`,b=`${g.padEnd(o)} ${R.padEnd(15)} ${y.padEnd(18)}`;if(a){let T=x.median/t;b+=T===1?" 1.00\xD7":` ${T.toFixed(2)}\xD7 slower`}i.push(b);for(let T of d.warnings)i.push(` ! ${T.message}`)}let m=un(e,n);if(m.length>0)i.push(""),i.push(...m);let l=ve(e,n);if(l.length>0)i.push(""),i.push(...l);return{text:`${i.join(`
18
+ `)}
19
+ `}}};function cn(e,s,n){let r=e.runs.find((a)=>a.id===n),t=r?s.get(r.workloadId):void 0;return t?Q(t):n}function ve(e,s){if(!e.comparisons||e.comparisons.length===0)return[];let n=[];for(let r of e.comparisons){let t=cn(e,s,r.candidateRunId),a=r.verdict==="pass"?"\u2713":"\u2717";if(n.push(`${a} ${t}`),r.timing){let i=r.timing.medianDeltaPct>0?"+":"";n.push(` timing: ${i}${r.timing.medianDeltaPct.toFixed(1)}% median (${r.timing.verdict})`)}if(r.frames)for(let i of r.frames.slice(0,Ue)){if(Math.abs(i.deltaPct)<0.5)continue;let o=i.deltaPct>0?"+":"";n.push(` frame ${i.name}: ${o}${i.deltaPct.toFixed(1)}% self-time (${(i.baseSelfUs/1000).toFixed(2)}ms -> ${(i.candSelfUs/1000).toFixed(2)}ms)`)}if(r.heapTypes)for(let i of r.heapTypes.slice(0,Fe)){if(Math.abs(i.deltaPct)<0.5)continue;let o=i.deltaPct>0?"+":"";n.push(` heap ${i.type}: ${o}${i.deltaPct.toFixed(1)}% count (${i.baseCount} -> ${i.candCount})`)}}return n}var Ue=5,Fe=5;function un(e,s){let n=[];for(let r of e.runs){if(r.phase!=="cpu"&&r.phase!=="heap")continue;let t=s.get(r.workloadId),a=t?Q(t):r.workloadId;if(r.phase==="cpu")if(r.cpu){n.push(`CPU capture - ${a} (instrumented, ${r.cpu.samplingIntervalUs}\xB5s interval, diagnostic wall ${E(r.diagnosticWallNs??0)}ms)`);let i=r.cpu.totals.reduce((o,c)=>o+c.selfUs,0)||1;for(let o of r.cpu.totals.slice(0,Ue)){let c=r.cpu.frames[o.frameIx],m=(o.selfUs/i*100).toFixed(1);n.push(` ${m.padStart(5)}% ${(o.selfUs/1000).toFixed(2).padStart(8)}ms self ${c?.name??"?"}`)}}else n.push(`CPU capture - ${a} (instrumented, no evidence captured)`);else if(r.heap){let i=((r.heap.heapSizeBytes??0)/1e6).toFixed(2);n.push(`Heap snapshot - ${a} (instrumented, ${r.heap.objectCount??"?"} objects, ${i}MB)`);for(let o of r.heap.typeCounts.slice(0,Fe))n.push(` ${String(o.count).padStart(6)} ${o.type}`)}else n.push(`Heap snapshot - ${a} (instrumented, no evidence captured)`);for(let i of r.artifacts)n.push(` artifact: ${i.path}`);for(let i of r.warnings)n.push(` ! ${i.message}`)}return n}var pn={table:Ie,json:xe,markdown:ke,jsonl:Re,collapsed:we,mermaid:$e,speedscope:Ce,cpuprofile:be};var ln=".ostia",X=1000;async function wt(e){let s=K({runs:e.runs??null,warmup:e.warmup??null,cpu:e.cpu??!1,heap:e.heap??!1,cpuIntervalUs:e.cpuIntervalUs??X}),r=`${e.outDir??ln}/artifacts`,t=[],a=[];for(let i of e.commands){let o=Array.isArray(i)?i:le(i),c=ae(o,Array.isArray(i)?void 0:i);t.push(c);let m=await ge({argv:o,cwd:e.cwd,env:e.env,runs:e.runs,warmup:e.warmup}),l=ue({workload:c,configFingerprint:s,trials:m.trials,timing:m.timing,warnings:m.warnings});if(a.push(l),e.cpu){let d=`${l.id}-cpu.cpuprofile`,g=await ee({argv:o,cwd:e.cwd,env:e.env,artifactDir:r,fileName:d,intervalUs:e.cpuIntervalUs??X});a.push(await De({workload:c,phase:"cpu",configFingerprint:s,diagnosticWallNs:g.diagnosticWallNs,exitCode:g.exitCode,cpu:g.cpu,artifactPath:g.artifactPath,artifactKind:"cpuprofile",warnings:g.warnings}))}if(e.heap){let d=`${l.id}-heap.heapsnapshot`,g=await te({argv:o,cwd:e.cwd,env:e.env,artifactDir:r,fileName:d});a.push(await De({workload:c,phase:"heap",configFingerprint:s,diagnosticWallNs:g.diagnosticWallNs,exitCode:g.exitCode,heap:g.heap,artifactPath:g.artifactPath,artifactKind:"heapsnapshot",warnings:g.warnings}))}}return j(t,a)}async function De(e){let s=`${e.workload.id}-${e.phase}-${e.configFingerprint}`,n=e.artifactPath?[await pe(s,e.artifactKind,e.artifactPath)]:[];return L({workload:e.workload,phase:e.phase,configFingerprint:e.configFingerprint,diagnosticWallNs:e.diagnosticWallNs,exitCode:e.exitCode,cpu:e.cpu,heap:e.heap,warnings:e.warnings,artifacts:n})}async function bt(e,s={}){let n=ce(e),r=K({intervalUs:s.intervalUs??X,origin:s.origin??"inspector"}),t=(m)=>m.samples?.nodeIds.length===0?[{code:"empty-profile",message:"In-process capture produced zero samples."}]:[];if(s.origin==="jsc"){let{result:m,cpu:l,jit:d,diagnosticWallNs:g}=await ie(e,s),x=L({workload:n,phase:"cpu",configFingerprint:r,diagnosticWallNs:g,cpu:l,jit:d,warnings:t(l),artifacts:[]});return{result:m,run:x}}let{result:a,cpu:i,diagnosticWallNs:o}=await re(e,s),c=L({workload:n,phase:"cpu",configFingerprint:r,diagnosticWallNs:o,cpu:i,warnings:t(i),artifacts:[]});return{result:a,run:c}}export{Ye as bench,en as compareDocuments,Qe as group,G as loadDocument,bt as profile,pn as renderers,wt as run,Le as saveDocument,Xe as task};
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "ostia",
3
+ "version": "0.1.0",
4
+ "description": "Fast profiling and benchmarking for Bun.",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./index.d.ts",
9
+ "default": "./index.js"
10
+ }
11
+ },
12
+ "bin": {
13
+ "ostia": "cli.js"
14
+ },
15
+ "main": "./index.js",
16
+ "module": "./index.js",
17
+ "types": "./index.d.ts"
18
+ }