ostia 0.2.9 → 0.2.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -24
- package/ab-runner.ts +6 -4
- package/chunk-5m601yfv.js +15 -0
- package/chunk-82vs1rw1.js +2 -0
- package/chunk-dvqggjkn.js +14 -0
- package/chunk-fc36fd0b.js +3 -0
- package/chunk-pdv5zb3w.js +27 -0
- package/chunk-r6hwb84z.js +25 -0
- package/chunk-scvezzm9.js +229 -0
- package/cli.js +1 -224
- package/index.d.ts +571 -417
- package/index.js +1 -1
- package/package.json +1 -1
- package/runner.ts +5 -16
- package/chunk-cqy5fzae.js +0 -16
- package/chunk-t6q9jbb6.js +0 -25
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import{T,l,q,ae,n,r,me,W,i,Y,Qe,u,E,a,v,L,m,t,O}from"./chunk-5m601yfv.js";import{x,Ke,qe,fe,d,te,an,A,Ye,mn,ge,_,V,J,cn,he,z,Ze,re,K,oe,be,M,h}from"./chunk-pdv5zb3w.js";import{rn,Ge,Ve,Je,ze,on,ee,sn}from"./chunk-scvezzm9.js";import{y}from"./chunk-fc36fd0b.js";import{writeSync as Re}from"fs";import{resolve as xn}from"path";function Ne(o){return n("cache",o.workloadId,o.phase,o.configFingerprint,o.bunVersion,o.toolVersion,o.instrumented,o.inputsDigest??null)}var yn=32;async function De(o,e=process.cwd()){if(o.length===0)return;let s=await z(o,e),c=Array(s.length),p=0,g=async()=>{while(p<s.length){let C=p++,f=s[C],w=await Bun.file(xn(e,f)).arrayBuffer();c[C]={path:f,sha256:Bun.CryptoHasher.hash("sha256",w,"hex")}}};return await Promise.all(Array.from({length:Math.min(yn,s.length)},g)),n("inputs",c)}function Ie(o,e){return`${o}/cache/${e}.json`}async function Be(o,e){try{return await Bun.file(Ie(o,e)).json()}catch{return}}async function _e(o,e,s){await Bun.write(Ie(o,e),`${JSON.stringify(s,null,2)}
|
|
3
|
+
`)}var Le=600000;class de extends l{}class Ce extends de{path;constructor(o){super(`No baseline document at ${o}. Create one with: ostia baseline save ${o.split("/").pop().replace(/\.json$/,"")}`);this.path=o}}class Ue extends de{path;missing;total;constructor(o,e,s){super(`${e} of ${s} configured workload(s) have no matching row in baseline ${o} (by workload id). Re-seed with: ostia baseline save`);this.path=o;this.missing=e;this.total=s}}async function An(o,e,s,c){let p=await z(e.suites,process.cwd());if(p.length===0)throw new l(`Workload suites ${JSON.stringify(e.suites)} matched no files.`);let g=await re({suites:p,preload:[],bunFlags:[],noiseCheck:!1},o.bench),C=await K({...g,outDir:g.outDir??o.outDir,timeoutMs:e.timeoutMs??g.timeoutMs??Le,signal:c}),f=[];for(let w of C.workloads){let k=C.measurements.find((S)=>S.workloadId===w.id&&S.phase==="timing");if(!k)continue;if(s)k.warnings.push(s);f.push({workload:w,status:"executed",run:k,harnessFailed:!1})}return f}async function $n(o,e,s,c,p){let g=me(e.command,e.label,{prepare:e.prepare,timeSource:e.timeSource}),C=i({samples:o.samples??null,budgetMs:o.budgetMs??null,minSamples:o.minSamples??null,warmup:o.warmup}),f=e.inputs!==void 0&&typeof e.prepare!=="function",w=Ne({workloadId:g.id,phase:"timing",configFingerprint:C,bunVersion:Bun.version,toolVersion:ae,instrumented:!1,inputsDigest:await De(e.inputs??[])}),k=s||!f?void 0:await Be(o.outDir,w),S;if(k)S=k;else{let N=await mn({argv:e.command,samples:o.samples,budgetMs:o.budgetMs,minSamples:o.minSamples,warmup:o.warmup,prepare:e.prepare,timeSource:e.timeSource,timeoutMs:e.timeoutMs??Le,ignoreExitCodes:e.ignoreExitCodes,signal:p});if(S=W({workload:g,configFingerprint:C,trials:N.trials,timing:N.timing,warnings:c?[...N.warnings,c]:N.warnings}),f&&!p?.aborted)await _e(o.outDir,w,S)}return{workload:g,status:k?"cached":"executed",run:S,harnessFailed:ge(S,e.ignoreExitCodes)}}async function ve(o,e,s){let{environment:c,noiseWarning:p}=O(o.noiseCheck),g=[];for(let w of o.workloads){if(s?.aborted)break;if(w.suites)g.push(...await An(o,w,p,s));else g.push(await $n(o,w,e,p,s))}let C=s?.aborted??!1,f=g.at(-1)?.run;if(C&&f&&!f.warnings.some((w)=>w.code==="aborted"))f.warnings=[...f.warnings,oe()];if(g.some((w)=>w.status==="executed"))v(c);return{results:g,environment:c,aborted:C}}async function We(o){let{config:e}=o,s=A(e,o.baselineName);if(!await Bun.file(s).exists())throw new Ce(s);let c=await a(s),{results:p,environment:g,aborted:C}=await ve(e,o.full,o.signal),f=p.map((F)=>({...F})),w=f.filter((F)=>F.status==="executed").length,k=r(f.map((F)=>F.workload),f.map((F)=>F.run),g);if(C)return{document:k,summary:{total:f.length,cached:f.length-w,executed:w,passed:0,regressed:0,missingBaseline:0,failed:0,results:f,unmatched:{baseOnly:[],candOnly:[]}},baseline:c,aborted:C};let S=Ke(c,k,e.thresholds),N=[];for(let F of f){let ie=S.compare(F.workload.id);if(ie)F.comparison=ie,N.push([F.workload.id,ie])}let D=N.map(([,F])=>F),P=f.length-D.length,I=D.filter((F)=>F.verdict==="pass").length,B=e.onMissingBaseline??(P===f.length?"fail":"warn");if(P>0&&B==="fail")throw new Ue(s,P,f.length);let j=S.unmatched();return k.comparisons=D,k.unmatched={baseOnly:j.baseOnly.map((F)=>F.id),candOnly:j.candOnly.map((F)=>F.id)},k.compared=S.compared(N),k.comparisonSummary=qe(D,S.effectiveTimingPct,S.driftPct),{document:k,summary:{total:f.length,cached:f.length-w,executed:w,passed:I,regressed:D.length-I,missingBaseline:P,failed:f.filter((F)=>F.harnessFailed).length,results:f,unmatched:j},baseline:c,aborted:!1}}function je(o){let e=[`${o.total} workloads`,`${o.cached} cached`,`${o.executed} executed`];if(o.missingBaseline>0)e.push(`${o.missingBaseline} skipped (no matching baseline workload)`);if(o.failed>0){let c=o.results.filter((p)=>p.harnessFailed).map((p)=>m(p.workload));e.push(`${o.failed} failed (harness error, a command exited non-zero or produced no samples: ${c.join(", ")})`)}let s=o.results.filter((c)=>c.comparison?.verdict==="fail").map((c)=>{let p=c.comparison.timing,g=m(c.workload);return p?`${t(p.medianDeltaPct)} median on ${g}`:g});return e.push(`${o.passed} passed ${o.regressed} regressed${s.length>0?` (${s.join(", ")})`:""}`,"",`Profile CI: ${o.regressed>0||o.failed>0?"\u2717":"\u2713"}`),`${e.join(`
|
|
4
|
+
`)}
|
|
5
|
+
`}async function He(o,e){let{results:s,environment:c}=await ve(o,!0),p=r(s.map((C)=>C.workload),s.map((C)=>C.run),c),g=A(o,e);return await u(p,g),g}async function Xe(o){let e;try{e=(await Array.fromAsync(new Bun.Glob("*.json").scan({cwd:o.baselineDir}))).map((p)=>p.replace(/\.json$/,"")).sort()}catch{return[]}return(await Promise.all(e.map(async(c)=>{let p=A(o,c);try{let g=await a(p);return{name:c,path:p,createdAt:g.createdAt,toolVersion:g.toolVersion,bunVersion:g.bunVersion,workloads:g.workloads.length,...g.git!==void 0&&{git:g.git}}}catch{return}}))).filter((c)=>c!==void 0)}class H extends Error{}var en=["table","json","jsonl","markdown","minimal"],ke=["collapsed","mermaid","speedscope","cpuprofile"],nn=[...en,...ke];function Sn(o,e,s={}){if(s.allowAuto&&e==="auto")return Ze();let c=s.min??1,p=e?.trim()===""?Number.NaN:Number(e);if(!Number.isInteger(p)||p<c)throw new H(`Invalid ${o} "${e}": expected an integer \u2265 ${c}${s.allowAuto?' (or "auto")':""}`);return p}function Pn(o,e){return e.split(",").map((s)=>{let c=/^\d+$/.test(s)?Number(s):Number.NaN;if(!(c<=255))throw new H(`Invalid ${o} "${e}": expected exit codes 0-255, comma-separated`);return c})}var X=(o,e)=>({dest:o,spec:e}),b={str:(o)=>X(o,{kind:"string"}),int:(o,e=1,s=!1)=>X(o,{kind:"int",min:e,allowAuto:s}),num:(o,e)=>X(o,{kind:"number",min:e}),on:(o)=>X(o,{kind:"bool",value:!0}),off:(o)=>X(o,{kind:"bool",value:!1}),oneOf:(o,e)=>X(o,{kind:"enum",values:e}),list:(o)=>X(o,{kind:"list"}),words:(o)=>X(o,{kind:"words"}),exitCodes:(o)=>X(o,{kind:"exitCodes"})},Oe={format:"table",quiet:!1,help:!1},pe={...Oe,noiseCheck:!0},xe={"--help":b.on("help"),"-h":b.on("help")},ye={...xe,"--export-json":b.str("exportJson"),"--format":b.oneOf("format",en),"--quiet":b.on("quiet")},Te={"--no-noise-check":b.off("noiseCheck")},le={"--config":b.str("config")},Fe={...ye,...Te,"--timeout":b.int("timeoutMs"),"--out-dir":b.str("outDir")},Ee={...Fe,...le,"--filter":b.str("filter"),"--preload":b.list("preload"),"--bun-flags":b.words("bunFlags")};function ne(o,{command:e,flags:s,args:c,positional:p,rest:g}){let C=c;for(let f=0;f<o.length;f++){let w=o[f];if(w==="--"&&g){g(o.slice(f+1),c);break}let k=w.startsWith("--")?w.indexOf("="):-1,S=k>0?w.slice(0,k):w,N=Object.hasOwn(s,S)?s[S]:void 0;if(!N||k>0&&N.spec.kind==="bool"){if(w.startsWith("-")||!p)throw new H(`Unknown flag "${w}" for "ostia ${e}".`);p(w,c);continue}let D=()=>{if(k>0)return w.slice(k+1);if(f+1>=o.length)throw new H(`Missing value for ${S}.`);return o[++f]},{dest:P,spec:I}=N;switch(I.kind){case"string":C[P]=D();break;case"int":C[P]=Sn(S,D(),I);break;case"number":{let B=D(),j=Number(B);if(B===""||!Number.isFinite(j)||j<I.min)throw new H(`Invalid ${S} "${B}": expected a number \u2265 ${I.min}`);C[P]=j;break}case"bool":C[P]=I.value;break;case"list":C[P].push(D());break;case"words":C[P].push(...q(D()));break;case"exitCodes":C[P].push(...k>0?Pn(S,w.slice(k+1)):Array.from({length:255},(B,j)=>j+1));break;case"enum":{let B=D();if(!I.values.includes(B))throw new H(`Invalid ${S} "${B}": expected one of: ${I.values.join(", ")}`);C[P]=B;break}}}return c}var U=(o)=>Bun.write(Bun.stdout,o),tn=(o)=>Bun.write(Bun.stderr,o);class R extends Error{code;data;constructor(o,e,s){super(e);this.code=o;this.data=s}}async function Ae(o,e,s){try{return await s()}catch(c){if(c instanceof R)throw c;if(c instanceof l)throw new R("invalid-flag",`${e}: ${c.message}`);throw new R(o,`${e}: ${T(c)}`)}}var ln=new Set(["minimal","json","jsonl"]);async function vn(){let{isatty:o}=await import("tty");if(!o(2))return!0;let e=process.argv.slice(2);return e.some((s,c)=>s==="--format"&&ln.has(e[c+1]??"")||s.startsWith("--format=")&&ln.has(s.slice(9)))}async function se(o,e,s){if(await tn(e.endsWith(`
|
|
6
|
+
`)?e:`${e}
|
|
7
|
+
`),!await vn())return;await tn(`${JSON.stringify({event:"error",protocolVersion:M,code:o,message:e.split(`
|
|
8
|
+
`)[0],...s&&{data:s}})}
|
|
9
|
+
`)}async function G(o,e){return await U(o),e?0:2}async function ce(o,e){if(o.text)await U(o.text);if(!o.files||o.files.length===0)return;if(e)for(let s of o.files){let c=s.path?`${e}/${s.path}`:e;await Bun.write(c,s.content),await U(`wrote ${c}
|
|
10
|
+
`)}else if(o.files.length===1)await U(o.files[0].content);else for(let s of o.files)await U(`--- ${s.path??"(unnamed)"} ---
|
|
11
|
+
${s.content}
|
|
12
|
+
`)}async function Me(o,{exportJson:e,format:s,quiet:c}){let p=s==="json"&&!c?Y(o):void 0;if(e)if(p!==void 0)await Qe(p,e);else await u(o,e);return p}async function ue(o,e,s={}){let c=await Me(o,e);if(e.quiet)return;if(c!==void 0)await U(c);else await ce(await h[e.format].render(o,s))}var fn=(o)=>o==="--help"||o==="-h";async function $e(o,e){let s=new AbortController,c=()=>s.abort();process.on("SIGINT",c);try{return{result:await Ae("spawn-failed",o,()=>e(s.signal)),aborted:s.signal.aborted}}finally{process.off("SIGINT",c)}}async function Se(o){let e=await an(o);if(!e&&o!==void 0)throw new R("config-missing",`Config file ${o} not found.`);return e}async function Pe(o,e){let s=await Se(e);if(!s){let c=te.join(" or ");throw new R("config-missing",o?`No ${c} found. "${o}" needs configured workloads.`:`No ${c} found.`)}if(o&&s.workloads.length===0){let c=e??await Ye()??te[1];throw new R("config-missing",`${c} has no "workloads" configured.`)}return s}function pn(o){return o.matched===0?2:o.verdict==="fail"?1:0}var On=["ns","us","ms","s"],Tn={...Fe,"--samples":b.int("samples"),"--budget":b.int("budgetMs"),"--min-samples":b.int("minSamples"),"--warmup":b.int("warmup",0),"--no-interleave":b.off("interleave"),"--prepare":b.list("prepare"),"--time-source":b.str("timeSource"),"--time-unit":b.oneOf("timeUnit",On),"--cpu":b.on("cpu"),"--heap":b.on("heap"),"--cpu-interval":b.int("cpuIntervalUs"),"--ignore-failure":b.exitCodes("ignoreExitCodes")};function Fn(o){let e={...pe,commands:[],argvCommand:[],prepare:[],interleave:!0,cpu:!1,heap:!1,ignoreExitCodes:[]};return ne(o,{command:"time",flags:Tn,args:e,positional:(s,c)=>c.commands.push(s),rest:(s,c)=>{c.argvCommand=s}})}async function En(o){let e=Fn(o),s=[...e.commands,...e.argvCommand.length>0?[e.argvCommand]:[]];if(e.help||s.length===0)return G(rn,e.help);if(e.prepare.length>1&&e.prepare.length!==s.length)throw new R("invalid-flag",`--prepare given ${e.prepare.length} times for ${s.length} command(s): give it once (applies to all) or once per command.`);if(e.timeUnit!==void 0&&e.timeSource===void 0)throw new R("invalid-flag","--time-unit only applies with --time-source.");if(e.cpuIntervalUs!==void 0&&!e.cpu)throw new R("invalid-flag","--cpu-interval only applies with --cpu.");if(e.timeSource!==void 0)try{new RegExp(e.timeSource)}catch(k){throw new R("invalid-flag",`Invalid --time-source regex: ${T(k)}`)}let c=e.timeSource!==void 0?{pattern:e.timeSource,unit:e.timeUnit}:void 0,p=s.map((k,S)=>({command:k,prepare:e.prepare.length===1?e.prepare[0]:e.prepare[S]})),{result:g,aborted:C}=await $e("Run failed",(k)=>be({commands:p,timeSource:c,samples:e.samples,budgetMs:e.budgetMs,minSamples:e.minSamples,warmup:e.warmup,interleave:e.interleave,cpu:e.cpu,heap:e.heap,cpuIntervalUs:e.cpuIntervalUs,timeoutMs:e.timeoutMs,ignoreExitCodes:e.ignoreExitCodes,outDir:e.outDir,noiseCheck:e.noiseCheck,signal:k}));if(await ue(g,e),C)return 130;let f=g.workloads.map((k)=>g.measurements.find((S)=>S.workloadId===k.id&&S.phase==="timing")),w=f.some((k)=>k?.timing&&ge(k,e.ignoreExitCodes));if(w||f.some((k)=>!k?.timing)){let k=(N)=>g.measurements.some((D)=>D.warnings.some((P)=>P.code===N)),S=w?"command-failed":k("timeout")?"timeout":k("time-source-no-match")?"time-source-no-match":"command-failed";throw new R(S,"One or more commands failed to produce a clean measurement; see the report above for details.")}return 0}var Mn={...Ee,"--budget":b.int("budgetMs"),"--samples":b.int("samples"),"--min-samples":b.int("minSamples"),"--jobs":b.int("jobs",1,!0),"--gc":b.on("gc"),"--no-gc":b.off("gc"),"--cpu":b.on("cpu"),"--no-cpu":b.off("cpu"),"--cpu-interval":b.int("cpuIntervalUs"),"--alloc":b.on("alloc"),"--no-alloc":b.off("alloc"),"--peak-mem":b.on("peakMem"),"--no-peak-mem":b.off("peakMem"),"--isolate":b.on("isolate"),"--no-isolate":b.off("isolate")};function Rn(o){let e={...pe,suites:[],preload:[],bunFlags:[]};return ne(o,{command:"bench",flags:Mn,args:e,positional:(s,c)=>c.suites.push(s)})}async function Nn(o){let e=Rn(o);if(e.help)return G(Ge,!0);let s=await Se(e.config),c=await re(e,s?.bench);if(c.suites.length===0)return G(Ge,!1);let{result:p,aborted:g}=await $e("Bench failed",(C)=>K({...c,signal:C}));return await ue(p,e),g?130:0}var Dn={...Ee,"--base":b.str("base"),"--base-setup":b.list("baseSetup"),"--rounds":b.int("rounds",3),"--threshold":b.num("thresholdPct",0),"--geomean-threshold":b.num("geomeanThresholdPct",0),"--confirm":b.int("confirm",0),"--alloc":b.on("alloc"),"--no-alloc":b.off("alloc"),"--peak-mem":b.on("peakMem"),"--no-peak-mem":b.off("peakMem"),"--mem-threshold":b.num("memThresholdPct",0),"--keep-trees":b.int("keepTrees"),"--clean":b.on("clean"),"--progress":b.on("progress"),"--no-progress":b.off("progress")};function In(o){switch(o.phase){case"setup":return`[ab] base setup: ${o.command}`;case"measure":return`[ab] suite ${o.suite}/${o.suites} ${o.file} \xB7 pass ${o.pass}/${o.passes} \xB7 task ${o.task}/${o.tasks} ${o.label}`;case"confirm":return`[ab] confirming flagged tasks \xB7 repeat ${o.repeat}/${o.repeats} ${o.label}`;case"memory":return`[ab] memory \xB7 process ${o.run}/${o.runs} ${y(o.side)} ${o.label}`}}function Bn(o){let e=!1;return{onProgress:(s)=>{let c=In(s);if(!o){Re(2,`${c}
|
|
13
|
+
`);return}let p=process.stderr.columns||80;Re(2,`\r\x1B[K${c.slice(0,p-1)}`),e=!0},done:()=>{if(e)Re(2,"\r\x1B[K");e=!1}}}function _n(o){let e={...pe,suites:[],baseSetup:[],preload:[],bunFlags:[],clean:!1};return ne(o,{command:"ab",flags:Dn,args:e,positional:(s,c)=>c.suites.push(s)})}async function Ln(o){let e=_n(o);if(e.help)return G(Ve,!0);let s=await Se(e.config);if(e.clean){let Z=await cn({outDir:e.outDir??s?.bench?.outDir});if(!e.quiet)await Bun.write(Bun.stderr,`Removed ${Z} cached base tree(s).
|
|
14
|
+
`);return 0}let{suites:c,filter:p,preload:g,bunFlags:C,timeoutMs:f,outDir:w,noiseCheck:k}=await re(e,s?.bench);if(c.length===0)return G(Ve,!1);let{isatty:S}=await import("tty"),N=S(2),D=e.progress??(N&&!e.quiet)?Bn(N):void 0,{result:P,aborted:I}=await $e("A/B run failed",async(Z)=>{try{return await he({suites:c,base:e.base,baseSetup:e.baseSetup.length>0?e.baseSetup:s?.ab?.setup,rounds:e.rounds,thresholdPct:e.thresholdPct,geomeanThresholdPct:e.geomeanThresholdPct,confirm:e.confirm,alloc:e.alloc??s?.ab?.alloc,peakMem:e.peakMem??s?.ab?.peakMem,memThresholdPct:e.memThresholdPct??s?.ab?.memThresholdPct,filter:p,preload:g,bunFlags:C,timeoutMs:f,outDir:w,keepTrees:e.keepTrees??s?.ab?.keepTrees,noiseCheck:k,signal:Z,onProgress:D?.onProgress})}catch(Q){if(Q instanceof _)throw new R("invalid-flag",`--base: ${Q.message}`);if(Q instanceof J)throw new R("command-failed",Q.message);if(Q instanceof V)throw new R("suite-failed",Q.message,{side:Q.side});throw Q}finally{D?.done()}}),B=P.ab,j=I?130:pn({matched:B.matched+B.threw,verdict:B.verdict}),F=new Map(P.workloads.map((Z)=>[Z.id,Z])),ie=(Z=[])=>Z.flatMap((Q)=>F.get(Q)??[]);if(await ue(P,e,{protocol:{command:"ab",exitCode:j,unmatched:{baseOnly:ie(P.unmatched?.baseOnly),candOnly:ie(P.unmatched?.candOnly)},...e.exportJson&&{exportedTo:e.exportJson}}}),j===2){let Z=B.newSuites?.length===c.length;await se("no-matches",Z?`No suite exists at ${B.base.ref}; nothing was paired. Use "ostia bench" to time new suites.`:`No task exists both at ${B.base.ref} and in the working tree; nothing was paired.`)}return j}var Un={...ye,...le,"--baseline":b.str("baseline")};function Wn(o){let e={...Oe,paths:[]};return ne(o,{command:"compare",flags:Un,args:e,positional:(s,c)=>c.paths.push(s)})}async function Gn(o){let e=Wn(o);if(e.help)return G(Je,!0);let s=e.baseline?1:2;if(e.paths.length>s)throw new H(e.baseline?`"ostia compare --baseline" takes one candidate path, got ${e.paths.length}.`:`"ostia compare" takes two document paths, got ${e.paths.length}.`);let[c,p]=e.baseline?[e.baseline,e.paths[0]]:e.paths;if(!c||!p)return G(Je,!1);let[g,C]=await Ae("document-load-failed","Failed to load documents",()=>Promise.all([a(c),a(p)])),f=await Se(e.config),w=f?.thresholds??x,k=f?e.config??await Ye()??te[1]:"defaults",S=fe(g,C,w),N={...C,comparisons:S.comparisons,comparisonSummary:S.summary,unmatched:{baseOnly:S.unmatched.baseOnly.map((I)=>I.id),candOnly:S.unmatched.candOnly.map((I)=>I.id)},compared:S.compared};if(!e.quiet&&(e.format==="table"||e.format==="markdown")){if(await U(`thresholds: ${k}
|
|
15
|
+
`),g.git&&C.git)await U(`base ${L(g.git)} \u2192 cand ${L(C.git)}
|
|
16
|
+
`)}let P=pn(S.summary);if(await ue(N,e,{protocol:{command:"compare",exitCode:P,unmatched:S.unmatched,baseGit:g.git,candGit:C.git,...e.exportJson&&{exportedTo:e.exportJson}}}),S.summary.matched===0)await se("no-matches","No workload matched between base and candidate; nothing was compared.");return P}var gn={...xe,"--format":b.oneOf("format",nn),"--measurement":b.str("measurementId"),"--out-dir":b.str("outDir")};function jn(o){return ne(o,{command:"report",flags:gn,args:{format:"table",help:!1},positional:(s,c)=>{if(c.path!==void 0)throw new H(`"ostia report" takes exactly one document path, got "${c.path}" and "${s}".`);c.path=s}})}var un="No CPU evidence in this document; rerun with --cpu (ostia time) or --cpu (ostia bench)";async function hn(o,{format:e,measurementId:s,outDir:c}){let p=ke.includes(e),g=[s!==void 0&&"--measurement",c!==void 0&&"--out-dir"].filter((w)=>w!==!1);if(!p&&g.length>0)throw new H(`${g.join(" and ")} only apply to the visualization formats (${ke.join(", ")}), not --format ${e}.`);let C=await Ae("document-load-failed",`Failed to load ${o}`,()=>a(o));if(p&&!s&&!C.measurements.some((w)=>w.phase==="cpu"))throw new R("no-cpu-evidence",un);let f=await h[e].render(C,{measurementId:s});if(!f.text&&(!f.files||f.files.length===0))throw new R("no-cpu-evidence",s?`No CPU evidence found for measurement "${s}".`:un);return await ce(f,c),0}async function Hn(o){let e=jn(o);if(e.help||!e.path)return G(ze,e.help);return hn(e.path,e)}var Vn={...ye,...Te,...le,"--full":b.on("full"),"--baseline":b.str("baseline"),"--save-baseline":b.on("saveBaseline")};function qn(o){let e={...pe,full:!1,saveBaseline:!1};return ne(o,{command:"ci",flags:Vn,args:e})}var bn=(o)=>new R("baseline-missing",o.message);function Jn(o){if(o instanceof de)throw bn(o);if(o instanceof E)throw new R("document-load-failed",o.message);throw o}async function Kn(o){let e=qn(o);if(e.help)return G(on,!0);let s=await Pe("ostia ci",e.config),c=e.noiseCheck?s:{...s,noiseCheck:!1},p=e.full||e.saveBaseline,{result:g,aborted:C}=await $e("CI run failed",(P)=>We({config:c,full:p,baselineName:e.baseline,signal:P}).catch(Jn)),{document:f,summary:w}=g;if(C)return await ue(f,e),130;let k=e.baseline??c.baseline,S=A(c,k),N=await Me(f,e);if(e.saveBaseline&&w.regressed===0&&w.failed===0)await u(f,S);let D=w.failed>0?2:w.regressed>0?1:0;if(w.failed>0){let P=w.results.filter((I)=>I.harnessFailed).map((I)=>m(I.workload));await se("command-failed",`${w.failed} workload(s) failed (harness error, a command exited non-zero or produced no samples): ${P.join(", ")}`)}if(e.quiet)return D;if(N!==void 0)await U(N);else if(e.format==="table"){if(await U(je(w)),f.comparisons&&f.comparisons.length>0)await ce(await h.table.render(f,{}))}else if(e.format==="markdown")await U(`## ostia ci
|
|
17
|
+
|
|
18
|
+
Baseline: \`${k}\` (\`${S}\`) \xB7 ${w.cached} cached, ${w.executed} executed
|
|
19
|
+
|
|
20
|
+
`),await ce(await h.markdown.render(f,{}));else await ce(await h[e.format].render(f,{protocol:{command:"ci",exitCode:D,unmatched:w.unmatched,baseGit:g.baseline.git,candGit:f.git,baseline:{name:k,path:S},cached:w.cached,executed:w.executed,failed:w.failed,missingBaseline:w.missingBaseline,...e.exportJson&&{exportedTo:e.exportJson}}}));return D}var dn=/^[A-Za-z0-9._-]+$/;function we(o){return new R("invalid-flag",`${o}
|
|
21
|
+
Run 'ostia baseline --help'.`)}function wn(o){if(o.startsWith("-"))throw we(`Invalid baseline name "${o}": names can't start with "-".`);if(!dn.test(o))throw we(`Invalid baseline name "${o}": expected to match ${dn}.`)}var zn={...xe,...le};function Cn(o,e,s){return ne(o,{command:e,flags:zn,args:{help:!1},positional:(c,p)=>{if(!s)throw we(`"ostia ${e}" takes no arguments, got "${c}".`);if(p.name!==void 0)throw we(`"ostia ${e}" takes at most one name argument, got "${p.name}" and "${c}".`);p.name=c}})}async function Zn(o){let{name:e,help:s,config:c}=Cn(o,"baseline save",!0);if(s)return G(ee,!0);if(e!==void 0)wn(e);let p=await Pe("ostia baseline save",c);return await U(`Wrote ${await He(p,e)}
|
|
22
|
+
`),0}async function Yn(o){let{help:e,config:s}=Cn(o,"baseline list",!1);if(e)return G(ee,!0);let c=await Pe(void 0,s),p=await Xe(c);if(p.length===0)return await U(`No baselines found in ${c.baselineDir}.
|
|
23
|
+
`),0;for(let g of p){let C=g.git?` ${L(g.git)}`:"";await U(`${g.name} ${g.workloads} workloads created ${g.createdAt} toolVersion ${g.toolVersion}${C}
|
|
24
|
+
`)}return 0}async function Qn(o){let[e,...s]=o;if(!e||fn(e))return G(ee,!!e);wn(e);let c=ne(s,{command:"baseline show",flags:{...gn,...le},args:{format:"table",help:!1},positional:(C)=>{throw we(`"ostia baseline show" takes one name, got "${e}" and "${C}".`)}});if(c.help)return G(ze,!0);let p=await Pe(void 0,c.config),g=A(p,e);if(!await Bun.file(g).exists())throw bn(new Ce(g));return hn(g,c)}async function kn(o,e,s,c,p,g=""){if(o===void 0||fn(o))return G(c,o!==void 0);let C=Object.hasOwn(s,o)?s[o]:void 0;try{if(!C)throw new R("invalid-flag",p(o));return await C(e)}catch(f){if(f instanceof R)await se(f.code,f.message,f.data);else if(f instanceof d)await se("config-invalid",f.message);else if(f instanceof H)await se("invalid-flag",`${f.message}
|
|
25
|
+
Run 'ostia ${g}${o} --help'.`);else await se("internal",`Internal error: ${f instanceof Error?f.stack??f.message:String(f)}`);return 2}}function Xn(o){let[e,...s]=o;return kn(e,s,{save:Zn,list:Yn,show:Qn},ee,(c)=>`Unknown "ostia baseline ${c}". Run "ostia baseline --help".`,"baseline ")}function zt(){let[o,...e]=process.argv.slice(2);return kn(o,e,{time:En,bench:Nn,ab:Ln,compare:Gn,report:Hn,ci:Kn,baseline:Xn},sn,(s)=>`Unknown subcommand "${s}". Run "ostia --help".`)}export{zt as main};
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
var rn=`ostia time [flags] <command...>
|
|
3
|
+
ostia time [flags] -- <argv...>
|
|
4
|
+
|
|
5
|
+
Time commands as subprocesses. Each <command> is whitespace-split into argv (no shell);
|
|
6
|
+
everything after -- is one more command's argv, verbatim.
|
|
7
|
+
|
|
8
|
+
Flags:
|
|
9
|
+
--samples N exact trials per command (default: ~3s budget, at least 10)
|
|
10
|
+
--budget MS sampling time budget per command
|
|
11
|
+
--min-samples N floor on trials when --samples isn't given
|
|
12
|
+
--warmup N discarded warmup trials (default: 3)
|
|
13
|
+
--no-interleave run commands one after another instead of round-robin
|
|
14
|
+
--prepare CMD run before every trial, unmeasured; once, or once per command
|
|
15
|
+
--time-source REGEX take each trial's time from capture group 1 in the command's output
|
|
16
|
+
--time-unit UNIT unit of --time-source: ns | us | ms | s (default: ms); needs --time-source
|
|
17
|
+
--cpu capture one extra CPU-profile trial
|
|
18
|
+
--heap capture one extra heap-snapshot trial
|
|
19
|
+
--cpu-interval US CPU sampling interval (default: 1000); needs --cpu
|
|
20
|
+
--timeout MS kill a trial or --prepare hook after MS
|
|
21
|
+
--ignore-failure[=CODE,...] treat these exit codes (bare: all) as success
|
|
22
|
+
--out-dir PATH artifact directory (default: node_modules/.cache/ostia)
|
|
23
|
+
--no-noise-check skip the ~200ms noise-floor measurement
|
|
24
|
+
--export-json PATH write the ProfileDocument to PATH
|
|
25
|
+
--format FORMAT table | json | jsonl | markdown | minimal (default: table)
|
|
26
|
+
--quiet don't print the report
|
|
27
|
+
--help show this message
|
|
28
|
+
|
|
29
|
+
A command stops at its first non-ignored non-zero exit.
|
|
30
|
+
Exit codes: 0 ok, 2 a command failed or produced no samples (or a bad flag), 130 Ctrl-C.
|
|
31
|
+
|
|
32
|
+
Examples:
|
|
33
|
+
ostia time "bun a.ts" "bun b.ts"
|
|
34
|
+
ostia time --samples 25 --cpu --heap "bun src/server.ts"
|
|
35
|
+
ostia time --prepare "rm -rf dist" "bun build.ts"
|
|
36
|
+
ostia time --time-source "built in (\\d+)ms" "bun build.ts"
|
|
37
|
+
ostia time -- bun -e "console.log('a b')"
|
|
38
|
+
`,Ge=`ostia bench [flags] <suite.ts...>
|
|
39
|
+
|
|
40
|
+
Run in-process group()/task() suites, each file in its own child process. With no files,
|
|
41
|
+
uses ostia.config's "bench" section; each flag overrides its config field.
|
|
42
|
+
|
|
43
|
+
Flags:
|
|
44
|
+
--budget MS sampling budget per task (default: 500)
|
|
45
|
+
--samples N exact trials per task (ignores --budget)
|
|
46
|
+
--min-samples N floor on trials (default: cost-aware, 3-20)
|
|
47
|
+
--jobs N|auto suite files at once (default: 1; >1 adds noise)
|
|
48
|
+
--isolate one process per task, not per file (--no-isolate to undo config)
|
|
49
|
+
--gc Bun.gc(true) between trials (--no-gc)
|
|
50
|
+
--cpu extra per-task CPU profile with JIT tiers, ~2,000 samples (--no-cpu)
|
|
51
|
+
--cpu-interval US CPU sampling interval (default: 100)
|
|
52
|
+
--alloc extra per-task retained-heap-per-call measurement: what calls keep
|
|
53
|
+
alive after a full GC, not what they allocate (--no-alloc)
|
|
54
|
+
--peak-mem extra per-task RSS rise during the task's first call, garbage
|
|
55
|
+
included; median of 3 fresh processes (--no-peak-mem)
|
|
56
|
+
--filter REGEX only tasks whose "group/name" matches
|
|
57
|
+
--preload PATH import before each suite file (repeatable, in order)
|
|
58
|
+
--bun-flags FLAGS extra flags for the bun process running each suite (repeatable)
|
|
59
|
+
--timeout MS kill a suite (or isolated task) process after MS
|
|
60
|
+
--out-dir PATH scratch directory (default: node_modules/.cache/ostia)
|
|
61
|
+
--config PATH read this config file instead of ostia.config.ts/.json
|
|
62
|
+
--no-noise-check skip the ~200ms noise-floor measurement
|
|
63
|
+
--export-json PATH write the ProfileDocument to PATH
|
|
64
|
+
--format FORMAT table | json | jsonl | markdown | minimal (default: table)
|
|
65
|
+
--quiet don't print the report
|
|
66
|
+
--help show this message
|
|
67
|
+
|
|
68
|
+
Per-task/per-group options ({ budgetMs, minSamples, gc, isolate, cpu, alloc, peakMem }) override
|
|
69
|
+
the suite-wide defaults. Exit codes: 0 ok, 2 a suite failed (or a bad flag), 130 Ctrl-C.
|
|
70
|
+
|
|
71
|
+
Examples:
|
|
72
|
+
ostia bench bench/*.ts
|
|
73
|
+
ostia bench bench/*.ts --filter parse --cpu --alloc
|
|
74
|
+
ostia bench --preload ./bench/dom-setup.ts --bun-flags="--conditions=browser" bench/*.ts
|
|
75
|
+
`,Ve=`ostia ab [flags] <suite.ts...>
|
|
76
|
+
|
|
77
|
+
Pair every task on a git ref's committed tree (base) against the working tree (candidate), in
|
|
78
|
+
one process, alternating ~10ms batches, and gate on the per-round time ratio. Drift between
|
|
79
|
+
the two sides cancels within a round. Each suite runs twice, once with each side first, and
|
|
80
|
+
the rounds are pooled. With no files, uses ostia.config's "bench" suites.
|
|
81
|
+
|
|
82
|
+
Flags:
|
|
83
|
+
--base REF git ref for the base side (default: HEAD)
|
|
84
|
+
--base-setup CMD shell command run once in a freshly extracted base tree, e.g. to build
|
|
85
|
+
gitignored files the suites import (repeatable, in order)
|
|
86
|
+
--rounds N base/candidate rounds per task (default: 15)
|
|
87
|
+
--threshold PCT flag a task whose median ratio moves past PCT (default: 10)
|
|
88
|
+
--geomean-threshold PCT fail when the geometric mean of all ratios is slower than PCT
|
|
89
|
+
(default: 1.5)
|
|
90
|
+
--confirm N re-measure each flagged task in N fresh processes (default: 2; 0 skips)
|
|
91
|
+
--alloc also compare retained heap per call on each side (--no-alloc)
|
|
92
|
+
--peak-mem also compare each side's first-call RSS rise (--no-peak-mem); both
|
|
93
|
+
run in 3 fresh processes per side, and the median counts
|
|
94
|
+
--mem-threshold PCT flag a memory reading that moves past PCT of the base's and past
|
|
95
|
+
its noise floor (default: 10)
|
|
96
|
+
--filter REGEX only tasks whose "group/name" matches
|
|
97
|
+
--preload PATH import before each suite file (repeatable, in order)
|
|
98
|
+
--bun-flags FLAGS extra flags for the bun process running each suite (repeatable)
|
|
99
|
+
--timeout MS kill a suite (or repeat) process after MS
|
|
100
|
+
--out-dir PATH scratch directory and base-tree cache (default: node_modules/.cache/ostia)
|
|
101
|
+
--keep-trees N base trees to keep cached, least recently used go first (default: 5)
|
|
102
|
+
--clean remove every cached base tree, then exit
|
|
103
|
+
--config PATH read this config file instead of ostia.config.ts/.json
|
|
104
|
+
--no-noise-check skip the ~200ms noise-floor measurement
|
|
105
|
+
--export-json PATH write the ProfileDocument to PATH
|
|
106
|
+
--format FORMAT table | json | jsonl | markdown | minimal (default: table)
|
|
107
|
+
--progress print progress to stderr (default: when stderr is a terminal and not
|
|
108
|
+
--quiet); --no-progress turns it off
|
|
109
|
+
--quiet don't print the report
|
|
110
|
+
--help show this message
|
|
111
|
+
|
|
112
|
+
The base tree is cached per commit (and --base-setup commands) under --out-dir. Setup runs
|
|
113
|
+
with the project's node_modules linked in, and OSTIA_AB_SHA and OSTIA_AB_CANDIDATE_DIR (the
|
|
114
|
+
working tree) set. A flagged task counts only when every fresh-process repeat flags it the
|
|
115
|
+
same way. A task that throws isn't timed; one that throws on the candidate side only fails
|
|
116
|
+
the run. A task whose suite file and output both changed reads "not comparable" and stays
|
|
117
|
+
out of the verdict. Exit codes: 0 pass, 1 a confirmed regression, a candidate-only throw,
|
|
118
|
+
the geomean over its threshold or a memory regression, 2 nothing paired or a harness error (not a git repo,
|
|
119
|
+
unknown ref, a failed setup or suite), 130 Ctrl-C.
|
|
120
|
+
|
|
121
|
+
Examples:
|
|
122
|
+
ostia ab bench/parse.bench.ts
|
|
123
|
+
ostia ab bench/*.ts --base origin/main --rounds 21
|
|
124
|
+
ostia ab bench/*.ts --filter parse --format minimal
|
|
125
|
+
ostia ab bench/*.ts --base-setup "bun scripts/generate.ts"
|
|
126
|
+
ostia ab bench/paint.bench.ts --alloc --peak-mem
|
|
127
|
+
`,Je=`ostia compare <base.json> <candidate.json>
|
|
128
|
+
ostia compare <candidate.json> --baseline <base.json>
|
|
129
|
+
|
|
130
|
+
Compare two ProfileDocuments by workload id (or a task's former names, from its
|
|
131
|
+
"aliases"). Gates on ostia.config's "thresholds" when present (the same ones "ostia ci"
|
|
132
|
+
uses), else the defaults, widened by machine noise and drift. Tasks on only one side are
|
|
133
|
+
listed under "Not compared"; with --format json, "compared" holds one row per task.
|
|
134
|
+
|
|
135
|
+
Flags:
|
|
136
|
+
--baseline PATH the base document, when only the candidate is positional
|
|
137
|
+
--config PATH read thresholds from this config file instead of ostia.config.ts/.json
|
|
138
|
+
--export-json PATH write the candidate document with comparisons to PATH
|
|
139
|
+
--format FORMAT table | json | jsonl | markdown | minimal (default: table)
|
|
140
|
+
--quiet don't print the report
|
|
141
|
+
--help show this message
|
|
142
|
+
|
|
143
|
+
Exactly two document paths (one with --baseline); anything more is an error.
|
|
144
|
+
Exit codes: 0 pass, 1 a workload regressed, 2 nothing matched or a harness error.
|
|
145
|
+
`,ze=`ostia report <document.json> [flags]
|
|
146
|
+
|
|
147
|
+
Render a saved ProfileDocument. Files, not a GUI for the visualization formats -
|
|
148
|
+
hand the output to speedscope.app, flamegraph.pl, or a Mermaid renderer.
|
|
149
|
+
|
|
150
|
+
Formats:
|
|
151
|
+
table terminal timing/CPU/heap/comparison text (default)
|
|
152
|
+
json pretty JSON document
|
|
153
|
+
jsonl one metadata line, then one line per measurement
|
|
154
|
+
markdown agent- and human-readable report
|
|
155
|
+
minimal one compact JSON object per timing measurement, for LLM/CI consumption
|
|
156
|
+
collapsed folded stacks: "root;a;b 42" - flamegraph.pl and most flame tooling
|
|
157
|
+
mermaid call tree, top-15 frames by total time (never the whole profile)
|
|
158
|
+
speedscope sampled profile JSON for speedscope.app
|
|
159
|
+
cpuprofile verbatim .cpuprofile pass-through (cpu-prof/inspector origins only)
|
|
160
|
+
|
|
161
|
+
Flags:
|
|
162
|
+
--format FORMAT one of the formats above (default: table)
|
|
163
|
+
--measurement <id> visualization formats only: render only this measurement
|
|
164
|
+
(default: every CPU measurement in the document)
|
|
165
|
+
--out-dir PATH visualization formats only: write files here instead of stdout
|
|
166
|
+
--help show this message
|
|
167
|
+
|
|
168
|
+
Examples:
|
|
169
|
+
ostia report doc.json
|
|
170
|
+
ostia report doc.json --format markdown
|
|
171
|
+
ostia report doc.json --format speedscope --out-dir node_modules/.cache/ostia/viz
|
|
172
|
+
ostia report doc.json --format collapsed | flamegraph.pl > flame.svg
|
|
173
|
+
`,on=`ostia ci [flags]
|
|
174
|
+
|
|
175
|
+
Run ostia.config's workloads (reusing cached runs whose declared inputs are unchanged),
|
|
176
|
+
compare against a saved baseline, and gate on regressions.
|
|
177
|
+
|
|
178
|
+
Flags:
|
|
179
|
+
--full ignore the cache
|
|
180
|
+
--baseline NAME baseline to compare against (default: config "baseline", or "main")
|
|
181
|
+
--save-baseline after a pass, save this run as the new baseline (always measures
|
|
182
|
+
fresh: the cache is not used)
|
|
183
|
+
--config PATH read this config file instead of ostia.config.ts/.json
|
|
184
|
+
--no-noise-check skip the ~200ms noise-floor measurement
|
|
185
|
+
--export-json PATH write the candidate document with comparisons to PATH
|
|
186
|
+
--format FORMAT table | json | jsonl | markdown | minimal (default: table)
|
|
187
|
+
--quiet don't print the report
|
|
188
|
+
--help show this message
|
|
189
|
+
|
|
190
|
+
Exit codes: 0 pass, 1 regression, 2 harness error (missing config/baseline, a workload
|
|
191
|
+
failed or produced no samples, or an onMissingBaseline "fail" mismatch), 130 Ctrl-C (the
|
|
192
|
+
partial document is exported; nothing is compared or saved).
|
|
193
|
+
`,ee=`ostia baseline <save|list|show> [args]
|
|
194
|
+
|
|
195
|
+
Manage the baseline ProfileDocuments "ostia ci" gates against and "ostia compare --baseline"
|
|
196
|
+
reads.
|
|
197
|
+
|
|
198
|
+
Flags (every subcommand): --config PATH reads that config file instead of
|
|
199
|
+
ostia.config.ts/.json.
|
|
200
|
+
|
|
201
|
+
Subcommands:
|
|
202
|
+
save [name] measure every configured workload (same code path as "ostia ci",
|
|
203
|
+
no comparison) and write it to <baselineDir>/<name>.json
|
|
204
|
+
(default name: config's "baseline" field, or "main")
|
|
205
|
+
list list saved baselines: name, created date, workload count
|
|
206
|
+
(takes no arguments)
|
|
207
|
+
show <name> [flags] render a saved baseline, like "ostia report" (same
|
|
208
|
+
--format/--measurement/--out-dir flags)
|
|
209
|
+
|
|
210
|
+
Examples:
|
|
211
|
+
ostia baseline save
|
|
212
|
+
ostia baseline save my-feature
|
|
213
|
+
ostia baseline list
|
|
214
|
+
ostia baseline show main
|
|
215
|
+
ostia baseline show main --format markdown
|
|
216
|
+
`,sn=`ostia - profiling and benchmarking for Bun
|
|
217
|
+
|
|
218
|
+
Commands:
|
|
219
|
+
time Time commands N times and report timing/CPU/heap
|
|
220
|
+
bench Run in-process benchmark suites (group()/task())
|
|
221
|
+
ab Pair suites against a git ref in one process, gate on regressions
|
|
222
|
+
compare Compare two ProfileDocuments
|
|
223
|
+
report Render a saved ProfileDocument (table/json/markdown/collapsed/mermaid/speedscope/...)
|
|
224
|
+
ci Run configured workloads against a baseline, gate on regressions
|
|
225
|
+
baseline Manage baseline ProfileDocuments (save/list/show)
|
|
226
|
+
|
|
227
|
+
Run "ostia <command> --help" for details.
|
|
228
|
+
`;
|
|
229
|
+
export{rn,Ge,Ve,Je,ze,on,ee,sn};
|
package/cli.js
CHANGED
|
@@ -1,226 +1,3 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
|
-
|
|
4
|
-
`)}var $e=600000;class me extends Error{path;constructor(r){super(`No baseline document at ${r}. Create one with: ostia baseline save ${r.split("/").pop().replace(/\.json$/,"")}`);this.path=r}}class de extends Error{path;missing;total;constructor(r,n,c){super(`${n} of ${c} configured workload(s) have no matching row in baseline ${r} (by workload id). Re-seed with: ostia baseline save`);this.path=r;this.missing=n;this.total=c}}function qe(r,n,c){if(r)return r;return n===c?"fail":"warn"}async function ge(r,n){let c=r.noiseCheck===!1?void 0:u(),p=c?m(c):void 0,h=[];for(let g of r.workloads){if(g.suites){let F=await U(g.suites,process.cwd()),K=await z({suites:F,preload:[],bunFlags:[],noiseCheck:!1},r.bench),Z=await D({...K,outDir:K.outDir??r.outDir,timeoutMs:K.timeoutMs??$e});for(let X of Z.workloads){let E=Z.measurements.find((te)=>te.workloadId===X.id&&te.phase==="timing");if(!E)continue;if(p)E.warnings.push(p);h.push({workload:X,status:"executed",run:E,harnessFailed:!1})}continue}let x=_(g.command,g.label,{prepare:g.prepare,timeSource:g.timeSource}),b=await ye(g.inputs??[]),k=i({samples:r.samples??null,budgetMs:r.budgetMs??null,minSamples:r.minSamples??null,warmup:r.warmup}),q=g.inputs!==void 0&&typeof g.prepare!=="function",j=ve({workloadId:x.id,phase:"timing",configFingerprint:k,bunVersion:Bun.version,toolVersion:S,instrumented:!1,inputsDigest:b}),M=n||!q?void 0:await xe(r.outDir,j),P,y;if(M)P=M,y="cached";else{let F=await le({argv:g.command,samples:r.samples,budgetMs:r.budgetMs,minSamples:r.minSamples,warmup:r.warmup,prepare:g.prepare,timeSource:g.timeSource,timeoutMs:g.timeoutMs??$e,ignoreExitCodes:g.ignoreExitCodes});if(P=C({workload:x,configFingerprint:k,trials:F.trials,timing:F.timing,warnings:p?[...F.warnings,p]:F.warnings}),q)await Pe(r.outDir,j,P);y="executed"}h.push({workload:x,status:y,run:P,harnessFailed:J(P,g.ignoreExitCodes)})}return{results:h,environment:c}}async function Ae(r){let{config:n}=r,c=l(n,r.baselineName);if(!await Bun.file(c).exists())throw new me(c);let h=await o(c),{results:g,environment:x}=await ge(n,r.full),b=g.map((E)=>({...E})),k=b.filter((E)=>E.status==="executed").length,q=b.length-k,j=b.filter((E)=>E.harnessFailed).length,M=s(b.map((E)=>E.workload),b.map((E)=>E.run),x),P=0,y=0,F=0,K=re(h,M,n.thresholds);for(let E of b){let te=K.compare(E.workload.id);if(!te){F++;continue}if(E.comparison=te,te.verdict==="pass")P++;else y++}if(F>0){if(qe(n.onMissingBaseline,F,b.length)==="fail")throw new de(c,F,b.length)}let Z=b.map((E)=>E.comparison).filter((E)=>E!==void 0);M.comparisons=Z;let X=K.unmatched();return M.unmatched={baseOnly:X.baseOnly.map((E)=>E.id),candOnly:X.candOnly.map((E)=>E.id)},M.comparisonSummary=se(Z,K.effectiveTimingPct),{document:M,summary:{total:b.length,cached:q,executed:k,passed:P,regressed:y,missingBaseline:F,failed:j,results:b,unmatched:X},baseline:h}}function Me(r){let n=[];if(n.push(`${r.total} workloads`),n.push(`${r.cached} cached`),n.push(`${r.executed} executed`),r.missingBaseline>0)n.push(`${r.missingBaseline} skipped (no matching baseline workload)`);if(r.failed>0){let p=r.results.filter((h)=>h.harnessFailed).map((h)=>t(h.workload));n.push(`${r.failed} failed (harness error, a command exited non-zero or produced no samples: ${p.join(", ")})`)}let c=r.results.filter((p)=>p.comparison?.verdict==="fail").map((p)=>{let h=p.comparison.timing,g=t(p.workload);return h?`${h.medianDeltaPct>0?"+":""}${h.medianDeltaPct.toFixed(1)}% median on ${g}`:g});return n.push(`${r.passed} passed ${r.regressed} regressed${c.length>0?` (${c.join(", ")})`:""}`),n.push(""),n.push(`Profile CI: ${r.regressed>0||r.failed>0?"\u2717":"\u2713"}`),`${n.join(`
|
|
5
|
-
`)}
|
|
6
|
-
`}async function Se(r,n){let{results:c,environment:p}=await ge(r,!1),h=s(c.map((x)=>x.workload),c.map((x)=>x.run),p),g=l(r,n);return await a(h,g),g}async function Fe(r){let n=[];try{let p=new Bun.Glob("*.json");for await(let h of p.scan({cwd:r.baselineDir,absolute:!1}))n.push(h.replace(/\.json$/,""))}catch{return[]}return n.sort(),(await Promise.all(n.map(async(p)=>{let h=l(r,p);try{let g=await o(h);return{name:p,path:h,createdAt:g.createdAt,toolVersion:g.toolVersion,bunVersion:g.bunVersion,workloads:g.workloads.length,...g.git!==void 0&&{git:g.git}}}catch{return}}))).filter((p)=>p!==void 0)}function Y(r){return r instanceof Error?r.message:String(r)}var B=(r)=>Bun.write(Bun.stdout,r),Te=(r)=>Bun.write(Bun.stderr,r);class Q extends Error{}var Ie=[],Ee=new Set(["minimal","json","jsonl"]);async function Le(r){let{isatty:n}=await import("tty");if(!n(2))return!0;return r.some((c,p)=>c==="--format"&&Ee.has(r[p+1]??"")||c.startsWith("--format=")&&Ee.has(c.slice(9)))}async function T(r,n,c){if(await Te(n.endsWith(`
|
|
7
|
-
`)?n:`${n}
|
|
8
|
-
`),!await Le(Ie))return;await Te(`${JSON.stringify({event:"error",protocolVersion:w,code:r,message:n.split(`
|
|
9
|
-
`)[0],...c&&{data:c}})}
|
|
10
|
-
`)}async function L(r,n){return await B(r),n?0:2}async function Je(r,n){return await T("invalid-flag",`${r.message}
|
|
11
|
-
Run 'ostia ${n} --help'.`),2}function Ge(r,n,c={}){if(c.allowAuto&&n==="auto")return ie();let p=c.min??1,h=Number(n);if(!Number.isInteger(h)||h<p)throw new Q(`Invalid ${r} "${n}": expected an integer \u2265 ${p}${c.allowAuto?' (or "auto")':""}`);return h}var Ve=["ns","us","ms","s"],ne=["table","json","jsonl","markdown","minimal"],we=["collapsed","mermaid","speedscope","cpuprofile"],ze=[...ne,...we],be=`No CPU evidence in this document; rerun with --cpu (ostia time) or --cpu (ostia bench)
|
|
12
|
-
`;function Ne(r){return r.measurements.some((n)=>n.phase==="cpu")}async function pe(r,n,c={}){if(n.format==="json"&&n.exportJson){let p=I(r);if(await ee(p,n.exportJson),!n.quiet)await B(p);return 0}if(n.exportJson)await a(r,n.exportJson);if(n.quiet)return 0;if(we.includes(n.format)&&!Ne(r))return await T("no-cpu-evidence",be),2;return await ue(await d[n.format].render(r,c)),0}async function fe(r){let n=await W();if(!n){await T("config-missing",r?`No ostia.config.json found. "${r}" needs configured workloads.`:"No ostia.config.json found.");return}if(r&&n.workloads.length===0){let c=await oe()??"ostia.config.json";await T("config-missing",`${c} has no "workloads" configured.`);return}return n}async function ue(r,n){if(r.text)await B(r.text);if(!r.files||r.files.length===0)return;if(n)for(let c of r.files){let p=c.path?`${n}/${c.path}`:n;await Bun.write(p,c.content),await B(`wrote ${p}
|
|
13
|
-
`)}else if(r.files.length===1)await B(r.files[0].content);else for(let c of r.files)await B(`--- ${c.path??"(unnamed)"} ---
|
|
14
|
-
${c.content}
|
|
15
|
-
`)}var Ke=`ostia time [flags] <command...>
|
|
16
|
-
ostia time [flags] -- <argv...>
|
|
17
|
-
|
|
18
|
-
Time commands as subprocesses. Each <command> is whitespace-split into argv (no shell);
|
|
19
|
-
everything after -- is one more command's argv, verbatim.
|
|
20
|
-
|
|
21
|
-
Flags:
|
|
22
|
-
--samples N exact trials per command (default: ~3s budget, at least 10)
|
|
23
|
-
--budget MS sampling time budget per command
|
|
24
|
-
--min-samples N floor on trials when --samples isn't given
|
|
25
|
-
--warmup N discarded warmup trials (default: 3)
|
|
26
|
-
--no-interleave run commands one after another instead of round-robin
|
|
27
|
-
--prepare CMD run before every trial, unmeasured; once, or once per command
|
|
28
|
-
--time-source REGEX take each trial's time from capture group 1 in the command's output
|
|
29
|
-
--time-unit UNIT unit of --time-source: ns | us | ms | s (default: ms)
|
|
30
|
-
--cpu capture one extra CPU-profile trial
|
|
31
|
-
--heap capture one extra heap-snapshot trial
|
|
32
|
-
--cpu-interval US CPU sampling interval (default: 1000)
|
|
33
|
-
--timeout MS kill a trial or --prepare hook after MS
|
|
34
|
-
--ignore-failure[=CODE,...] treat these exit codes (bare: all) as success
|
|
35
|
-
--out-dir PATH artifact directory (default: node_modules/.cache/ostia)
|
|
36
|
-
--no-noise-check skip the ~200ms noise-floor measurement
|
|
37
|
-
--export-json PATH write the ProfileDocument to PATH
|
|
38
|
-
--format FORMAT table | json | jsonl | markdown | minimal (default: table)
|
|
39
|
-
--quiet don't print the report
|
|
40
|
-
--help show this message
|
|
41
|
-
|
|
42
|
-
A command stops at its first non-ignored non-zero exit.
|
|
43
|
-
Exit codes: 0 ok, 2 a command failed or produced no samples (or a bad flag), 130 Ctrl-C.
|
|
44
|
-
|
|
45
|
-
Examples:
|
|
46
|
-
ostia time "bun a.ts" "bun b.ts"
|
|
47
|
-
ostia time --samples 25 --cpu --heap "bun src/server.ts"
|
|
48
|
-
ostia time --prepare "rm -rf dist" "bun build.ts"
|
|
49
|
-
ostia time --time-source "built in (\\d+)ms" "bun build.ts"
|
|
50
|
-
ostia time -- bun -e "console.log('a b')"
|
|
51
|
-
`,Xe=Array.from({length:255},(r,n)=>n+1),De=`ostia bench [flags] <suite.ts...>
|
|
52
|
-
|
|
53
|
-
Run in-process group()/task() suites, each file in its own child process. With no files,
|
|
54
|
-
uses ostia.config's "bench" section; each flag overrides its config field.
|
|
55
|
-
|
|
56
|
-
Flags:
|
|
57
|
-
--budget MS sampling budget per task (default: 500)
|
|
58
|
-
--samples N exact trials per task (ignores --budget)
|
|
59
|
-
--min-samples N floor on trials (default: cost-aware, 3-20)
|
|
60
|
-
--jobs N|auto suite files at once (default: 1; >1 adds noise)
|
|
61
|
-
--isolate one process per task, not per file (--no-isolate to undo config)
|
|
62
|
-
--gc Bun.gc(true) between trials (--no-gc)
|
|
63
|
-
--cpu extra per-task CPU profile with JIT tiers, ~2,000 samples (--no-cpu)
|
|
64
|
-
--cpu-interval US CPU sampling interval (default: 100)
|
|
65
|
-
--alloc extra per-task retained-heap-per-call measurement: what calls keep
|
|
66
|
-
alive after a full GC, not what they allocate (--no-alloc)
|
|
67
|
-
--peak-mem extra per-task RSS rise during the task's first call, garbage
|
|
68
|
-
included; median of 3 fresh processes (--no-peak-mem)
|
|
69
|
-
--filter REGEX only tasks whose "group/name" matches
|
|
70
|
-
--preload PATH import before each suite file (repeatable, in order)
|
|
71
|
-
--bun-flags FLAGS extra flags for the bun process running each suite (repeatable)
|
|
72
|
-
--timeout MS kill a suite (or isolated task) process after MS
|
|
73
|
-
--out-dir PATH scratch directory (default: node_modules/.cache/ostia)
|
|
74
|
-
--no-noise-check skip the ~200ms noise-floor measurement
|
|
75
|
-
--export-json PATH write the ProfileDocument to PATH
|
|
76
|
-
--format FORMAT table | json | jsonl | markdown | minimal (default: table)
|
|
77
|
-
--quiet don't print the report
|
|
78
|
-
--help show this message
|
|
79
|
-
|
|
80
|
-
Per-task/per-group options ({ budgetMs, minSamples, gc, isolate, cpu, alloc, peakMem }) override
|
|
81
|
-
the suite-wide defaults. Exit codes: 0 ok, 2 a suite failed (or a bad flag), 130 Ctrl-C.
|
|
82
|
-
|
|
83
|
-
Examples:
|
|
84
|
-
ostia bench bench/*.ts
|
|
85
|
-
ostia bench bench/*.ts --filter parse --cpu --alloc
|
|
86
|
-
ostia bench --preload ./bench/dom-setup.ts --bun-flags="--conditions=browser" bench/*.ts
|
|
87
|
-
`,Re=`ostia ab [flags] <suite.ts...>
|
|
88
|
-
|
|
89
|
-
Pair every task on a git ref's committed tree (base) against the working tree (candidate), in
|
|
90
|
-
one process, alternating ~10ms batches, and gate on the per-round time ratio. Drift between
|
|
91
|
-
the two sides cancels within a round. With no files, uses ostia.config's "bench" suites.
|
|
92
|
-
|
|
93
|
-
Flags:
|
|
94
|
-
--base REF git ref for the base side (default: HEAD)
|
|
95
|
-
--rounds N base/candidate rounds per task (default: 15)
|
|
96
|
-
--threshold PCT flag a task whose median ratio moves past PCT (default: 10)
|
|
97
|
-
--geomean-threshold PCT fail when the geometric mean of all ratios is slower than PCT
|
|
98
|
-
(default: 1.5)
|
|
99
|
-
--confirm N re-measure each flagged task in N fresh processes (default: 2; 0 skips)
|
|
100
|
-
--filter REGEX only tasks whose "group/name" matches
|
|
101
|
-
--preload PATH import before each suite file (repeatable, in order)
|
|
102
|
-
--bun-flags FLAGS extra flags for the bun process running each suite (repeatable)
|
|
103
|
-
--timeout MS kill a suite (or repeat) process after MS
|
|
104
|
-
--out-dir PATH scratch directory and base-tree cache (default: node_modules/.cache/ostia)
|
|
105
|
-
--no-noise-check skip the ~200ms noise-floor measurement
|
|
106
|
-
--export-json PATH write the ProfileDocument to PATH
|
|
107
|
-
--format FORMAT table | json | jsonl | markdown | minimal (default: table)
|
|
108
|
-
--quiet don't print the report
|
|
109
|
-
--help show this message
|
|
110
|
-
|
|
111
|
-
A flagged task counts only when every fresh-process repeat flags it the same way. Exit codes:
|
|
112
|
-
0 pass, 1 a confirmed regression or the geomean over its threshold, 2 nothing paired or a
|
|
113
|
-
harness error (not a git repo, unknown ref, a suite failed), 130 Ctrl-C.
|
|
114
|
-
|
|
115
|
-
Examples:
|
|
116
|
-
ostia ab bench/parse.bench.ts
|
|
117
|
-
ostia ab bench/*.ts --base origin/main --rounds 21
|
|
118
|
-
ostia ab bench/*.ts --filter parse --format minimal
|
|
119
|
-
`,Oe=`ostia compare <base.json> <candidate.json>
|
|
120
|
-
ostia compare <candidate.json> --baseline <base.json>
|
|
121
|
-
|
|
122
|
-
Compare two ProfileDocuments by workload id. Gates on ostia.config's "thresholds" when
|
|
123
|
-
present (the same ones "ostia ci" uses), else the defaults.
|
|
124
|
-
|
|
125
|
-
Flags:
|
|
126
|
-
--baseline PATH the base document, when only the candidate is positional
|
|
127
|
-
--export-json PATH write the candidate document with comparisons to PATH
|
|
128
|
-
--format FORMAT table | json | jsonl | markdown | minimal (default: table)
|
|
129
|
-
--quiet don't print the report
|
|
130
|
-
--help show this message
|
|
131
|
-
|
|
132
|
-
Exit codes: 0 pass, 1 a workload regressed, 2 nothing matched or a harness error.
|
|
133
|
-
`,Ze=`ostia report <document.json> [flags]
|
|
134
|
-
|
|
135
|
-
Render a saved ProfileDocument. Files, not a GUI for the visualization formats -
|
|
136
|
-
hand the output to speedscope.app, flamegraph.pl, or a Mermaid renderer.
|
|
137
|
-
|
|
138
|
-
Formats:
|
|
139
|
-
table terminal timing/CPU/heap/comparison text (default)
|
|
140
|
-
json pretty JSON document
|
|
141
|
-
jsonl one metadata line, then one line per measurement
|
|
142
|
-
markdown agent- and human-readable report
|
|
143
|
-
minimal one compact JSON object per timing measurement, for LLM/CI consumption
|
|
144
|
-
collapsed folded stacks: "root;a;b 42" - flamegraph.pl and most flame tooling
|
|
145
|
-
mermaid call tree, top-15 frames by total time (never the whole profile)
|
|
146
|
-
speedscope sampled profile JSON for speedscope.app
|
|
147
|
-
cpuprofile verbatim .cpuprofile pass-through (cpu-prof/inspector origins only)
|
|
148
|
-
|
|
149
|
-
Flags:
|
|
150
|
-
--format FORMAT one of the formats above (default: table)
|
|
151
|
-
--measurement <id> for the visualization formats: render only this measurement
|
|
152
|
-
(default: every CPU measurement in the document)
|
|
153
|
-
--out-dir PATH write visualization files here instead of stdout
|
|
154
|
-
--help show this message
|
|
155
|
-
|
|
156
|
-
Examples:
|
|
157
|
-
ostia report doc.json
|
|
158
|
-
ostia report doc.json --format markdown
|
|
159
|
-
ostia report doc.json --format speedscope --out-dir node_modules/.cache/ostia/viz
|
|
160
|
-
ostia report doc.json --format collapsed | flamegraph.pl > flame.svg
|
|
161
|
-
`,Qe=`ostia ci [flags]
|
|
162
|
-
|
|
163
|
-
Run ostia.config's workloads (reusing cached runs whose declared inputs are unchanged),
|
|
164
|
-
compare against a saved baseline, and gate on regressions.
|
|
165
|
-
|
|
166
|
-
Flags:
|
|
167
|
-
--full ignore the cache
|
|
168
|
-
--baseline NAME baseline to compare against (default: config "baseline", or "main")
|
|
169
|
-
--save-baseline after a pass, save this run as the new baseline
|
|
170
|
-
--no-noise-check skip the ~200ms noise-floor measurement
|
|
171
|
-
--export-json PATH write the candidate document with comparisons to PATH
|
|
172
|
-
--format FORMAT table | json | jsonl | markdown | minimal (default: table)
|
|
173
|
-
--quiet don't print the report
|
|
174
|
-
--help show this message
|
|
175
|
-
|
|
176
|
-
Exit codes: 0 pass, 1 regression, 2 harness error (missing config/baseline, a workload
|
|
177
|
-
failed or produced no samples, or an onMissingBaseline "fail" mismatch).
|
|
178
|
-
`,he=`ostia baseline <save|list|show> [args]
|
|
179
|
-
|
|
180
|
-
Manage the baseline ProfileDocuments "ostia ci" gates against and "ostia compare --baseline"
|
|
181
|
-
reads.
|
|
182
|
-
|
|
183
|
-
Subcommands:
|
|
184
|
-
save [name] measure every configured workload (same code path as "ostia ci",
|
|
185
|
-
no comparison) and write it to <baselineDir>/<name>.json
|
|
186
|
-
(default name: config's "baseline" field, or "main")
|
|
187
|
-
list list saved baselines: name, created date, workload count
|
|
188
|
-
show <name> [flags] render a saved baseline; delegates to "ostia report" (same
|
|
189
|
-
--format/--measurement/--out-dir flags)
|
|
190
|
-
|
|
191
|
-
Examples:
|
|
192
|
-
ostia baseline save
|
|
193
|
-
ostia baseline save my-feature
|
|
194
|
-
ostia baseline list
|
|
195
|
-
ostia baseline show main
|
|
196
|
-
ostia baseline show main --format markdown
|
|
197
|
-
`;function ce(r,n,c,p,h,g){let x=p;for(let b=0;b<r.length;b++){let k=r[b],q=g?.(k,b,p);if(q!==void 0){b=q;continue}let j=k.startsWith("--")?k.indexOf("="):-1,M=j>0?k.slice(0,j):k,P=c[M];if(!P||j>0&&P.spec.kind==="bool"){if(k.startsWith("-"))throw new Q(`Unknown flag "${k}" for "ostia ${n}".`);h(k,p);continue}let y=()=>j>0?k.slice(j+1):r[++b];switch(P.spec.kind){case"string":x[P.dest]=y();break;case"int":x[P.dest]=Ge(M,y(),P.spec);break;case"number":{let F=y(),K=Number(F);if(F===void 0||F===""||!Number.isFinite(K)||K<P.spec.min)throw new Q(`Invalid ${M} "${F}": expected a number \u2265 ${P.spec.min}`);x[P.dest]=K;break}case"bool":x[P.dest]=P.spec.value;break;case"list":x[P.dest].push(y()??"");break;case"enum":{let F=y();if(!P.spec.values.includes(F))throw new Q(`Invalid ${M} "${F}": expected one of: ${P.spec.values.join(", ")}`);x[P.dest]=F;break}}}return p}var Ye={"--samples":{dest:"samples",spec:{kind:"int",min:1}},"--budget":{dest:"budgetMs",spec:{kind:"int",min:1}},"--min-samples":{dest:"minSamples",spec:{kind:"int",min:1}},"--warmup":{dest:"warmup",spec:{kind:"int",min:0}},"--no-interleave":{dest:"interleave",spec:{kind:"bool",value:!1}},"--prepare":{dest:"prepare",spec:{kind:"list"}},"--time-source":{dest:"timeSource",spec:{kind:"string"}},"--time-unit":{dest:"timeUnit",spec:{kind:"enum",values:Ve}},"--cpu":{dest:"cpu",spec:{kind:"bool",value:!0}},"--heap":{dest:"heap",spec:{kind:"bool",value:!0}},"--cpu-interval":{dest:"cpuIntervalUs",spec:{kind:"int",min:1}},"--timeout":{dest:"timeoutMs",spec:{kind:"int",min:1}},"--out-dir":{dest:"outDir",spec:{kind:"string"}},"--no-noise-check":{dest:"noiseCheck",spec:{kind:"bool",value:!1}},"--export-json":{dest:"exportJson",spec:{kind:"string"}},"--format":{dest:"format",spec:{kind:"enum",values:ne}},"--quiet":{dest:"quiet",spec:{kind:"bool",value:!0}},"--help":{dest:"help",spec:{kind:"bool",value:!0}},"-h":{dest:"help",spec:{kind:"bool",value:!0}}};function et(r){return ce(r,"time",Ye,{commands:[],prepare:[],interleave:!0,cpu:!1,heap:!1,ignoreExitCodes:[],noiseCheck:!0,format:"table",quiet:!1,help:!1},(c,p)=>p.commands.push(c),(c,p,h)=>{if(c==="--")return h.argvCommand=r.slice(p+1),r.length;if(c==="--ignore-failure"||c.startsWith("--ignore-failure=")){let g=c.startsWith("--ignore-failure=")?c.slice(17):void 0;return h.ignoreExitCodes.push(...g===void 0?Xe:g.split(",").map(Number)),p}return})}async function ke(r){let n=new AbortController,c=()=>n.abort();process.on("SIGINT",c);try{return{result:await r(n.signal),aborted:n.signal.aborted}}finally{process.off("SIGINT",c)}}async function tt(r){let n=et(r),c=(n.argvCommand?.length??0)>0;if(n.help||n.commands.length===0&&!c)return L(Ke,n.help);let p=n.commands.length+(c?1:0);if(n.prepare.length>1&&n.prepare.length!==p)return await T("invalid-flag",`--prepare given ${n.prepare.length} times for ${p} command(s): give it once (applies to all) or once per command.`),2;if(n.timeSource!==void 0)try{new RegExp(n.timeSource)}catch(y){return await T("invalid-flag",`Invalid --time-source regex: ${Y(y)}`),2}let h=n.timeSource!==void 0?{pattern:n.timeSource,unit:n.timeUnit}:void 0,g=(y)=>n.prepare.length===1?n.prepare[0]:n.prepare[y],x=n.commands.map((y,F)=>({command:y,prepare:g(F)}));if(c)x.push({command:n.argvCommand,prepare:g(n.commands.length)});let b,k;try{({result:b,aborted:k}=await ke((y)=>ae({commands:x,timeSource:h,samples:n.samples,budgetMs:n.budgetMs,minSamples:n.minSamples,warmup:n.warmup,interleave:n.interleave,cpu:n.cpu,heap:n.heap,cpuIntervalUs:n.cpuIntervalUs,timeoutMs:n.timeoutMs,ignoreExitCodes:n.ignoreExitCodes,outDir:n.outDir,noiseCheck:n.noiseCheck,signal:y})))}catch(y){return await T("spawn-failed",`Run failed: ${Y(y)}`),2}let q=await pe(b,n);if(q!==0)return q;if(k)return 130;let j=b.workloads.map((y)=>b.measurements.find((F)=>F.workloadId===y.id&&F.phase==="timing")??{trials:[],timing:void 0}),M=j.some((y)=>!y.timing),P=j.some((y)=>y.timing&&J(y,n.ignoreExitCodes));if(P||M){let y=b.measurements.some((Z)=>Z.warnings.some((X)=>X.code==="timeout")),F=b.measurements.some((Z)=>Z.warnings.some((X)=>X.code==="time-source-no-match"));return await T(P?"command-failed":y?"timeout":F?"time-source-no-match":"command-failed","One or more commands failed to produce a clean measurement; see the report above for details."),2}return 0}var nt={"--budget":{dest:"budgetMs",spec:{kind:"int",min:1}},"--samples":{dest:"samples",spec:{kind:"int",min:1}},"--min-samples":{dest:"minSamples",spec:{kind:"int",min:1}},"--jobs":{dest:"jobs",spec:{kind:"int",min:1,allowAuto:!0}},"--gc":{dest:"gc",spec:{kind:"bool",value:!0}},"--no-gc":{dest:"gc",spec:{kind:"bool",value:!1}},"--cpu":{dest:"cpu",spec:{kind:"bool",value:!0}},"--no-cpu":{dest:"cpu",spec:{kind:"bool",value:!1}},"--cpu-interval":{dest:"cpuIntervalUs",spec:{kind:"int",min:1}},"--alloc":{dest:"alloc",spec:{kind:"bool",value:!0}},"--no-alloc":{dest:"alloc",spec:{kind:"bool",value:!1}},"--peak-mem":{dest:"peakMem",spec:{kind:"bool",value:!0}},"--no-peak-mem":{dest:"peakMem",spec:{kind:"bool",value:!1}},"--filter":{dest:"filter",spec:{kind:"string"}},"--isolate":{dest:"isolate",spec:{kind:"bool",value:!0}},"--no-isolate":{dest:"isolate",spec:{kind:"bool",value:!1}},"--preload":{dest:"preload",spec:{kind:"list"}},"--timeout":{dest:"timeoutMs",spec:{kind:"int",min:1}},"--out-dir":{dest:"outDir",spec:{kind:"string"}},"--no-noise-check":{dest:"noiseCheck",spec:{kind:"bool",value:!1}},"--export-json":{dest:"exportJson",spec:{kind:"string"}},"--format":{dest:"format",spec:{kind:"enum",values:ne}},"--quiet":{dest:"quiet",spec:{kind:"bool",value:!0}},"--help":{dest:"help",spec:{kind:"bool",value:!0}},"-h":{dest:"help",spec:{kind:"bool",value:!0}}};function st(r){return ce(r,"bench",nt,{suites:[],preload:[],bunFlags:[],noiseCheck:!0,format:"table",quiet:!1,help:!1},(c,p)=>p.suites.push(c),Be(r))}function Be(r){return(n,c,p)=>{if(n==="--bun-flags")return p.bunFlags.push(...R(r[c+1]??"")),c+1;if(n.startsWith("--bun-flags="))return p.bunFlags.push(...R(n.slice(12))),c;return}}async function ot(r){let n=st(r);if(n.help)return L(De,!0);let c=await W(),p=await z(n,c?.bench);if(p.suites.length===0)return L(De,!1);if(p.jobs!==void 0&&!(p.jobs>=1))return await T("invalid-flag",'--jobs expects a positive integer or "auto".'),2;let h,g;try{({result:h,aborted:g}=await ke((b)=>D({...p,signal:b})))}catch(b){return await T("spawn-failed",`Bench failed: ${Y(b)}`),2}let x=await pe(h,n);if(x!==0)return x;return g?130:0}var at={"--base":{dest:"base",spec:{kind:"string"}},"--rounds":{dest:"rounds",spec:{kind:"int",min:3}},"--threshold":{dest:"thresholdPct",spec:{kind:"number",min:0}},"--geomean-threshold":{dest:"geomeanThresholdPct",spec:{kind:"number",min:0}},"--confirm":{dest:"confirm",spec:{kind:"int",min:0}},"--filter":{dest:"filter",spec:{kind:"string"}},"--preload":{dest:"preload",spec:{kind:"list"}},"--timeout":{dest:"timeoutMs",spec:{kind:"int",min:1}},"--out-dir":{dest:"outDir",spec:{kind:"string"}},"--no-noise-check":{dest:"noiseCheck",spec:{kind:"bool",value:!1}},"--export-json":{dest:"exportJson",spec:{kind:"string"}},"--format":{dest:"format",spec:{kind:"enum",values:ne}},"--quiet":{dest:"quiet",spec:{kind:"bool",value:!0}},"--help":{dest:"help",spec:{kind:"bool",value:!0}},"-h":{dest:"help",spec:{kind:"bool",value:!0}}};function it(r){return ce(r,"ab",at,{suites:[],preload:[],bunFlags:[],noiseCheck:!0,format:"table",quiet:!1,help:!1},(c,p)=>p.suites.push(c),Be(r))}async function rt(r){let n=it(r);if(n.help)return L(Re,!0);let c=(await W())?.bench,p=n.suites.length>0?n.suites:c?.suites?await U(c.suites,process.cwd()):[];if(p.length===0)return L(Re,!1);let h,g;try{({result:h,aborted:g}=await ke((M)=>G({suites:p,base:n.base,rounds:n.rounds,thresholdPct:n.thresholdPct,geomeanThresholdPct:n.geomeanThresholdPct,confirm:n.confirm,filter:n.filter??c?.filter,preload:n.preload.length>0?n.preload:c?.preload??[],bunFlags:n.bunFlags.length>0?n.bunFlags:c?.bunFlags??[],timeoutMs:n.timeoutMs??c?.timeoutMs,outDir:n.outDir??c?.outDir,noiseCheck:n.noiseCheck,signal:M})))}catch(M){if(M instanceof A)return await T("invalid-flag",`--base: ${M.message}`),2;return await T("spawn-failed",`A/B run failed: ${Y(M)}`),2}let x=h.ab,b=g?130:x.matched===0?2:x.verdict==="fail"?1:0,k=new Map(h.workloads.map((M)=>[M.id,M])),q=(M=[])=>M.flatMap((P)=>k.get(P)??[]),j=await pe(h,n,{protocol:{command:"ab",exitCode:b,unmatched:{baseOnly:q(h.unmatched?.baseOnly),candOnly:q(h.unmatched?.candOnly)},...n.exportJson&&{exportedTo:n.exportJson}}});if(j!==0)return j;if(b===2)await T("no-matches",`No task exists both at ${x.base.ref} and in the working tree; nothing was paired.`);return b}var lt={"--baseline":{dest:"baseline",spec:{kind:"string"}},"--export-json":{dest:"exportJson",spec:{kind:"string"}},"--format":{dest:"format",spec:{kind:"enum",values:ne}},"--quiet":{dest:"quiet",spec:{kind:"bool",value:!0}},"--help":{dest:"help",spec:{kind:"bool",value:!0}},"-h":{dest:"help",spec:{kind:"bool",value:!0}}};function ct(r){return ce(r,"compare",lt,{paths:[],format:"table",quiet:!1,help:!1},(c,p)=>p.paths.push(c))}async function ut(){let r=await W();if(!r)return{thresholds:f,source:"defaults"};return{thresholds:r.thresholds,source:await oe()??"ostia.config.json"}}async function mt(r){let n=ct(r);if(n.help)return L(Oe,!0);let c,p;if(n.baseline)c=n.baseline,p=n.paths[0];else c=n.paths[0],p=n.paths[1];if(!c||!p)return L(Oe,!1);let h,g;try{[h,g]=await Promise.all([o(c),o(p)])}catch(y){return await T("document-load-failed",`Failed to load documents: ${Y(y)}`),2}let{thresholds:x,source:b}=await ut(),k=H(h,g,x),q={...g,comparisons:k.comparisons,comparisonSummary:k.summary,unmatched:{baseOnly:k.unmatched.baseOnly.map((y)=>y.id),candOnly:k.unmatched.candOnly.map((y)=>y.id)}},j=n.format==="table"||n.format==="markdown";if(!n.quiet&&j){if(await B(`thresholds: ${b}
|
|
198
|
-
`),h.git&&g.git)await B(`base ${v(h.git)} \u2192 cand ${v(g.git)}
|
|
199
|
-
`);if(n.format==="table"&&k.summary.effectiveTimingPct>x.timingPct)await B(`threshold ${x.timingPct}% (widened to ${k.summary.effectiveTimingPct.toFixed(1)}% by noise floor)
|
|
200
|
-
`)}let M=k.summary.matched===0?2:k.summary.verdict==="fail"?1:0,P=await pe(q,n,{protocol:{command:"compare",exitCode:M,unmatched:k.unmatched,baseGit:h.git,candGit:g.git,...n.exportJson&&{exportedTo:n.exportJson}}});if(P!==0)return P;if(!n.quiet&&(n.format==="table"||n.format==="markdown")&&(k.unmatched.baseOnly.length>0||k.unmatched.candOnly.length>0))await B(dt(k.unmatched,n.format));if(k.summary.matched===0)await T("no-matches","No workload matched between base and candidate; nothing was compared.");return M}function dt(r,n){let c=(h)=>h.map((g)=>t(g)).join(", ");if(n==="markdown"){let h=["### Unmatched",""];if(r.baseOnly.length>0)h.push(`- baseline only: ${c(r.baseOnly)}`);if(r.candOnly.length>0)h.push(`- candidate only: ${c(r.candOnly)}`);return h.push(""),`${h.join(`
|
|
201
|
-
`)}
|
|
202
|
-
`}let p=["","Unmatched:"];if(r.baseOnly.length>0)p.push(` baseline only: ${c(r.baseOnly)}`);if(r.candOnly.length>0)p.push(` candidate only: ${c(r.candOnly)}`);return`${p.join(`
|
|
203
|
-
`)}
|
|
204
|
-
`}var pt={"--format":{dest:"format",spec:{kind:"enum",values:ze}},"--measurement":{dest:"measurementId",spec:{kind:"string"}},"--out-dir":{dest:"outDir",spec:{kind:"string"}},"--help":{dest:"help",spec:{kind:"bool",value:!0}},"-h":{dest:"help",spec:{kind:"bool",value:!0}}};function ft(r){return ce(r,"report",pt,{format:"table",help:!1},(c,p)=>{if(p.path!==void 0)throw new Q(`"ostia report" takes exactly one document path, got "${p.path}" and "${c}".`);p.path=c})}async function _e(r){let n=ft(r);if(n.help||!n.path)return L(Ze,n.help);let c;try{c=await o(n.path)}catch(g){return await T("document-load-failed",`Failed to load ${n.path}: ${Y(g)}`),2}if(we.includes(n.format)&&!n.measurementId&&!Ne(c))return await T("no-cpu-evidence",be),2;let h=await d[n.format].render(c,{measurementId:n.measurementId});if(!h.text&&(!h.files||h.files.length===0))return await T("no-cpu-evidence",n.measurementId?`No CPU evidence found for measurement "${n.measurementId}".`:be),2;return await ue(h,n.outDir),0}var ht={"--full":{dest:"full",spec:{kind:"bool",value:!0}},"--baseline":{dest:"baseline",spec:{kind:"string"}},"--save-baseline":{dest:"saveBaseline",spec:{kind:"bool",value:!0}},"--export-json":{dest:"exportJson",spec:{kind:"string"}},"--format":{dest:"format",spec:{kind:"enum",values:ne}},"--no-noise-check":{dest:"noiseCheck",spec:{kind:"bool",value:!1}},"--quiet":{dest:"quiet",spec:{kind:"bool",value:!0}},"--help":{dest:"help",spec:{kind:"bool",value:!0}},"-h":{dest:"help",spec:{kind:"bool",value:!0}}};function gt(r){return ce(r,"ci",ht,{full:!1,saveBaseline:!1,format:"table",quiet:!1,help:!1,noiseCheck:!0},(c)=>{throw new Q(`Unknown flag "${c}" for "ostia ci".`)})}async function bt(r){let n=gt(r);if(n.help)return L(Qe,!0);let c=await fe("ostia ci");if(!c)return 2;let p={...c,...!n.noiseCheck&&{noiseCheck:!1}},h;try{h=await Ae({config:p,full:n.full,baselineName:n.baseline})}catch(b){if(b instanceof me||b instanceof de)return await T("baseline-missing",b.message),2;if(b instanceof O)return await T("document-load-failed",b.message),2;return await T("spawn-failed",`CI run failed: ${Y(b)}`),2}let g=n.format==="json"&&!n.quiet?I(h.document):void 0;if(n.exportJson)if(g)await ee(g,n.exportJson);else await a(h.document,n.exportJson);if(n.saveBaseline&&h.summary.regressed===0&&h.summary.failed===0)await a(h.document,l(p,n.baseline));let x=h.summary.failed>0?2:h.summary.regressed>0?1:0;if(h.summary.failed>0){let b=h.summary.results.filter((k)=>k.harnessFailed).map((k)=>t(k.workload));await T("command-failed",`${h.summary.failed} workload(s) failed (harness error, a command exited non-zero or produced no samples): ${b.join(", ")}`)}if(!n.quiet){let b=n.baseline??p.baseline;if(n.format==="table"){if(await B(Me(h.summary)),h.document.comparisons&&h.document.comparisons.length>0)await ue(await d.table.render(h.document,{}))}else if(g!==void 0)await B(g);else if(n.format==="markdown")await B(`## ostia ci
|
|
205
|
-
|
|
206
|
-
Baseline: \`${b}\` (\`${l(p,n.baseline)}\`) \xB7 ${h.summary.cached} cached, ${h.summary.executed} executed
|
|
207
|
-
|
|
208
|
-
`),await ue(await d.markdown.render(h.document,{}));else await ue(await d[n.format].render(h.document,{protocol:{command:"ci",exitCode:x,unmatched:h.summary.unmatched,baseGit:h.baseline.git,candGit:h.document.git,baseline:{name:b,path:l(p,n.baseline)},cached:h.summary.cached,executed:h.summary.executed,failed:h.summary.failed,missingBaseline:h.summary.missingBaseline,...n.exportJson&&{exportedTo:n.exportJson}}}))}return x}var je=/^[A-Za-z0-9._-]+$/;function He(r){if(r.startsWith("-"))return`Invalid baseline name "${r}": names can't start with "-".`;if(!je.test(r))return`Invalid baseline name "${r}": expected to match ${je}.`;return}async function wt(r){if(r.includes("--help")||r.includes("-h"))return L(he,!0);if(r.length>1)return await T("invalid-flag",`"ostia baseline save" takes at most one name argument, got ${r.length}.
|
|
209
|
-
Run 'ostia baseline --help'.`),2;let n=r[0];if(n!==void 0){let h=He(n);if(h)return await T("invalid-flag",`${h}
|
|
210
|
-
Run 'ostia baseline --help'.`),2}let c=await fe("ostia baseline save");if(!c)return 2;let p=await Se(c,n);return await B(`Wrote ${p}
|
|
211
|
-
`),0}async function kt(r){if(r.includes("--help")||r.includes("-h"))return L(he,!0);let n=await fe();if(!n)return 2;let c=await Fe(n);if(c.length===0)return await B(`No baselines found in ${n.baselineDir}.
|
|
212
|
-
`),0;for(let p of c){let h=p.git?` ${v(p.git)}`:"";await B(`${p.name} ${p.workloads} workloads created ${p.createdAt} toolVersion ${p.toolVersion}${h}
|
|
213
|
-
`)}return 0}async function vt(r){let[n,...c]=r;if(!n||n==="--help"||n==="-h")return L(he,!!n);let p=He(n);if(p)return await T("invalid-flag",`${p}
|
|
214
|
-
Run 'ostia baseline --help'.`),2;let h=await fe();if(!h)return 2;return _e([l(h,n),...c])}async function We(r,n,c,p,h,g=""){if(r===void 0||r==="--help"||r==="-h")return L(p,r!==void 0);let x=c[r];if(!x)return await T("invalid-flag",h(r)),2;try{return await x(n)}catch(b){if(b instanceof N)return await T("config-invalid",b.message),2;if(b instanceof Q)return Je(b,`${g}${r}`);return await T("internal",`Internal error: ${b instanceof Error?b.stack??b.message:String(b)}`),2}}async function yt(r){let[n,...c]=r;return We(n,c,{save:wt,list:kt,show:vt},he,(p)=>`Unknown "ostia baseline ${p}". Run "ostia baseline --help".`,"baseline ")}var Ct=`ostia - profiling and benchmarking for Bun
|
|
215
|
-
|
|
216
|
-
Commands:
|
|
217
|
-
time Time commands N times and report timing/CPU/heap
|
|
218
|
-
bench Run in-process benchmark suites (group()/task())
|
|
219
|
-
ab Pair suites against a git ref in one process, gate on regressions
|
|
220
|
-
compare Compare two ProfileDocuments
|
|
221
|
-
report Render a saved ProfileDocument (table/json/markdown/collapsed/mermaid/speedscope/...)
|
|
222
|
-
ci Run configured workloads against a baseline, gate on regressions
|
|
223
|
-
baseline Manage baseline ProfileDocuments (save/list/show)
|
|
224
|
-
|
|
225
|
-
Run "ostia <command> --help" for details.
|
|
226
|
-
`;async function xt(){let[r,...n]=process.argv.slice(2);return Ie=n,We(r,n,{time:tt,bench:ot,ab:rt,compare:mt,report:_e,ci:bt,baseline:yt},Ct,(c)=>`Unknown subcommand "${c}". Run "ostia --help".`)}if(import.meta.main)xt().then((r)=>process.exit(r));export{Q as CliUsageError,Ge as parseIntFlag};
|
|
3
|
+
var t=process.argv[2];if(t===void 0||t==="--help"||t==="-h"){let{MAIN_HELP:i}=await import("./chunk-82vs1rw1.js");await Bun.write(Bun.stdout,i),process.exit(t===void 0?2:0)}var{main:e}=await import("./chunk-r6hwb84z.js");process.exit(await e());
|