@buoy-gg/mcp 7.0.49 → 7.0.50
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/dist/camera-YKUHGME2.mjs +1 -0
- package/dist/chunk-EKM5ZZKU.mjs +139 -0
- package/dist/chunk-QIYTG2A5.mjs +367 -0
- package/dist/chunk-RYFN5J56.mjs +5 -0
- package/dist/cli.js +212 -135
- package/dist/cli.mjs +8 -8
- package/dist/index.d.mts +17 -1
- package/dist/index.d.ts +17 -1
- package/dist/index.js +203 -126
- package/dist/index.mjs +1 -1
- package/dist/webBench-K5MAFWO3.mjs +1 -0
- package/package.json +3 -3
- package/skill/buoy-optimize/SKILL.md +35 -5
- package/dist/chunk-2PTJT2QF.mjs +0 -64
- package/dist/chunk-MLEZPZP3.mjs +0 -369
- package/dist/webBench-LKAOZXHK.mjs +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{ALL_DEVICES as e,BROKER_PORT as o,BROKER_URL as r,DASHBOARD_NAME as t,DEFAULT_ACTION_TIMEOUT_MS as s,DEFAULT_SNAPSHOT_TIMEOUT_MS as i,PACKAGE_NAME as T,SyncClient as n,VERSION as a,buildAutomationConfig as g,compareReportStats as l,createMcpServer as E,estimateBatchMs as R,formatRankingMarkdown as A,getUpdateNotice as S,rankBatchFromIndex as _,registerConsoleTools as c,registerEventsTools as M,registerGenericTools as
|
|
1
|
+
import{ALL_DEVICES as e,BROKER_PORT as o,BROKER_URL as r,DASHBOARD_NAME as t,DEFAULT_ACTION_TIMEOUT_MS as s,DEFAULT_SNAPSHOT_TIMEOUT_MS as i,PACKAGE_NAME as T,SyncClient as n,VERSION as a,buildAutomationConfig as g,compareReportStats as l,createMcpServer as E,estimateBatchMs as R,formatRankingMarkdown as A,getUpdateNotice as S,rankBatchFromIndex as _,registerConsoleTools as c,registerEventsTools as M,registerGenericTools as m,registerJotaiTools as O,registerPerfMonitorTools as p,registerReactQueryTools as u,registerReduxTools as I,registerRoutesTools as d,registerScreenshotTools as C,registerStorageTools as N,registerZustandTools as U,runInit as B}from"./chunk-QIYTG2A5.mjs";import"./chunk-RYFN5J56.mjs";import"./chunk-EKM5ZZKU.mjs";export{e as ALL_DEVICES,o as BROKER_PORT,r as BROKER_URL,t as DASHBOARD_NAME,s as DEFAULT_ACTION_TIMEOUT_MS,i as DEFAULT_SNAPSHOT_TIMEOUT_MS,T as PACKAGE_NAME,n as SyncClient,a as VERSION,g as buildAutomationConfig,l as compareReportStats,E as createMcpServer,R as estimateBatchMs,A as formatRankingMarkdown,S as getUpdateNotice,_ as rankBatchFromIndex,c as registerConsoleTools,M as registerEventsTools,m as registerGenericTools,O as registerJotaiTools,p as registerPerfMonitorTools,u as registerReactQueryTools,I as registerReduxTools,d as registerRoutesTools,C as registerScreenshotTools,N as registerStorageTools,U as registerZustandTools,B as runInit};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{PLAYWRIGHT_MISSING as e,aggregateCase as a,caseUrl as r,cdpDeltas as t,checkBudgets as s,closeCall as c,expectFailureFrom as n,formatWebBenchMarkdown as o,keyMatches as i,locateStepTargets as l,mergeInteractions as m,missedInput as p,oneLine as h,percentile as d,runWebBench as g,shuffled as u,snapRefreshRate as b,summarizeFrameTrace as f,summarizeProbe as k,takeHostLock as B,tiedPairs as I,validateWebBenchOptions as F,webBenchFlags as W}from"./chunk-EKM5ZZKU.mjs";export{e as PLAYWRIGHT_MISSING,a as aggregateCase,r as caseUrl,t as cdpDeltas,s as checkBudgets,c as closeCall,n as expectFailureFrom,o as formatWebBenchMarkdown,i as keyMatches,l as locateStepTargets,m as mergeInteractions,p as missedInput,h as oneLine,d as percentile,g as runWebBench,u as shuffled,b as snapRefreshRate,f as summarizeFrameTrace,k as summarizeProbe,B as takeHostLock,I as tiedPairs,F as validateWebBenchOptions,W as webBenchFlags};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@buoy-gg/mcp",
|
|
3
3
|
"mcpName": "io.github.Buoy-gg/mcp",
|
|
4
|
-
"version": "7.0.
|
|
4
|
+
"version": "7.0.50",
|
|
5
5
|
"buoy": {
|
|
6
6
|
"dashboardAuthVersion": 1
|
|
7
7
|
},
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
"socket.io-client": "^4.8.1",
|
|
36
36
|
"zod": "^3.25.0",
|
|
37
37
|
"ws": "^8.18.0",
|
|
38
|
-
"@buoy-gg/sync-broker": "^7.0.
|
|
39
|
-
"@buoy-gg/license": "^7.0.
|
|
38
|
+
"@buoy-gg/sync-broker": "^7.0.50",
|
|
39
|
+
"@buoy-gg/license": "^7.0.50"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@types/node": "^22.0.0",
|
|
@@ -9,7 +9,7 @@ Use repeatable measurements to investigate the user's workload. Keep a working b
|
|
|
9
9
|
|
|
10
10
|
## Check setup and scope
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
If the target is a web app, you can start with `run_web_benchmark`. It opens the app in its own browser and needs no connected app (see "When the target is a browser"). Otherwise, call `list_devices` and select the intended device. Benchmarking requires a connected app exposing `perf-monitor`, configured account access and Buoy Pro. If a requirement is missing, explain the setup needed. Do not bypass account checks.
|
|
13
13
|
|
|
14
14
|
Infer the target screen, existing harness, rendering libraries and workload from the project and conversation. Ask only for missing details that would change the experiment: workload size, required visual behavior, target devices, performance limits or a time budget. Continue work already authorized without asking the user to approve each routine prune or iteration.
|
|
15
15
|
|
|
@@ -63,21 +63,34 @@ Read it this way:
|
|
|
63
63
|
- p95 frame gap, worst gap and long frames come from the time between frames in that run. A gap over 1.5 frames missed at least one frame.
|
|
64
64
|
- Long tasks % is the share of the run the main thread spent in tasks over 50 ms. It is not CPU use, and it misses many shorter tasks.
|
|
65
65
|
- A metric the browser can't measure shows as —. Safari and Firefox have no long tasks or JS heap. A — can't win or lose.
|
|
66
|
-
- "Why frames were slow" names the scripts behind slow frames, plus style, layout and forced layout time. Start your next idea there.
|
|
67
|
-
- "DOM
|
|
66
|
+
- "Why frames were slow" names the scripts behind slow frames, plus style, layout and forced layout time. Start your next idea there. It only sees frames over 50 ms, and it can't see work in workers.
|
|
67
|
+
- "DOM grew every run" is a hint of a leak, not proof. Confirm it with a heap snapshot after the screen unmounts.
|
|
68
68
|
- "Too close to call" means the top cases overlap across runs. Run more repeats before you pick.
|
|
69
|
-
- A page load table shows up when the batch reloads between cases
|
|
69
|
+
- A page load table shows up only when each case ran on its own fresh page load (the batch reloads between cases with `reloadBetweenCases`). Use it for load-time work.
|
|
70
70
|
|
|
71
71
|
Set up the run:
|
|
72
72
|
|
|
73
73
|
- Keep the tab in front and the window the same size. A run with a hidden tab or a resize is saved as a failure.
|
|
74
74
|
- Rank on a production build when you can. Dev React is slower and can pick a different winner. Put the test page behind its own bench flag, not just `__DEV__`. Keep it out of public builds. If you ranked on a dev build, say so in the report.
|
|
75
75
|
- Keep DevTools closed while ranking. Record the screen size and pixel ratio. A pixel ratio of 3 draws 9 times the canvas pixels of 1.
|
|
76
|
+
- Pass `captureRenders: false` to `run_benchmark_batch` while ranking. Render capture adds work, and the cost differs by variant. Turn it on in a separate pass to explain a slow case.
|
|
76
77
|
|
|
77
78
|
Two tools can run a web batch:
|
|
78
79
|
|
|
79
80
|
- `run_benchmark_batch` runs inside the app's own tab through Bench. Use it when the app has Bench and the user's browser is open.
|
|
80
|
-
- `run_web_benchmark` opens the app in its own headless Chromium. It needs Playwright in the project, but no Buoy setup in the app. Each run gets a fresh
|
|
81
|
+
- `run_web_benchmark` opens the app in its own headless Chromium. It needs Playwright in the project, but no Buoy setup in the app. Each run gets a fresh browser context, and the tab is always in front. It adds Chrome's own main-thread, script, style and layout time. Pass a `readySelector` the page adds once it has drawn.
|
|
82
|
+
|
|
83
|
+
In `run_web_benchmark`, rank on Dropped % first, and only across variants doing the same work. It counts frames Chrome dropped or only partly made that hurt smoothness, from its own count. Deltas are in percentage points (pp). Headless Chrome shows nothing on a screen, so these are frames Chrome produced, not frames a person saw. Shown FPS only counts frames with new content, so an animation that changes 16 times a second needs just 16. Don't compare Shown FPS across variants that update at different rates. Main-thread FPS can be low while nothing drops. That happens when the compositor runs a CSS animation and JavaScript is slow. The animation looks smooth, but taps will feel slow.
|
|
84
|
+
|
|
85
|
+
To test taps, typing or scrolling, pass `steps`. For example: `[{"wait": 300}, {"click": "#add"}, {"type": {"selector": "#search", "text": "lights"}}]`. The report then shows input response for each case. Under 200 ms is good. Click targets must be on screen when the run starts and must not move. Leave at least 300 ms after the last step (a `wait`), so its paint lands inside the run. If not, the report says the input timing is incomplete and won't pass an input budget.
|
|
86
|
+
|
|
87
|
+
End every step list with an `expect` step, like `{"expect": {"selector": "#cart-count", "text": "1"}}`. A run that skipped the work then fails instead of ranking as fast.
|
|
88
|
+
|
|
89
|
+
When the user gave limits, pass them as `budgets`. For "almost no dropped frames, taps under 200 ms", use `{"droppedPct": 2, "inputMs": 200}`. The report says which cases meet every limit. Input and freeze limits use the worst run, and an input limit needs a click, key or typing step. A limit that wasn't measured counts as not met.
|
|
90
|
+
|
|
91
|
+
Mark a case that does different work as a control: `{"name": "empty", "control": true}`. It shows in the table, but it is never the baseline and never a candidate.
|
|
92
|
+
|
|
93
|
+
If the two best cases come out too close to call, pass `extraRoundsWhenClose: 2`. It adds runs only for those two. Pass `outFile` to save the whole result, so you can read it again later.
|
|
81
94
|
|
|
82
95
|
Test in this order, and tell the user which step you are on:
|
|
83
96
|
|
|
@@ -85,6 +98,8 @@ Test in this order, and tell the user which step you are on:
|
|
|
85
98
|
2. The same cases with `cpuSlowdown: [4, 6]`. This slows the CPU only, not the GPU, memory or heat.
|
|
86
99
|
3. A real iPhone in Safari.
|
|
87
100
|
4. A real mid-range Android phone in Chrome.
|
|
101
|
+
|
|
102
|
+
For steps 3 and 4, use the app's own Bench and `run_benchmark_batch`. Start the dev server so other devices can reach it (for Vite, `vite --host`). Open its network URL on the phone, on the same Wi-Fi as this computer. In development, the page finds the broker at its own host on port 42831, so the phone shows up in `list_devices`. Keep the phone awake and the tab in front, or runs fail as hidden. This is a dev build, so say so in the report. A production build on a phone needs `externalSync` settings; see the web install guide.
|
|
88
103
|
5. Desktop Firefox and Safari, if the user supports them.
|
|
89
104
|
|
|
90
105
|
Take a quick look in Safari early, so you don't spend rounds on an idea that breaks there.
|
|
@@ -97,6 +112,21 @@ Ideas to try for heavy drawing on web:
|
|
|
97
112
|
- CSS or Web Animations API transforms. The compositor can run these.
|
|
98
113
|
- List windowing, and `content-visibility` for things off screen.
|
|
99
114
|
|
|
115
|
+
Picking a winner on web:
|
|
116
|
+
|
|
117
|
+
Don't pick a winner when any of these is true:
|
|
118
|
+
|
|
119
|
+
- You can't show both cases do the same work, or you don't know which build was served.
|
|
120
|
+
- Runs failed, frame data is missing, or a case has fewer than 2 good runs.
|
|
121
|
+
- The Dropped % ranges touch or overlap, or the middle values are within 1.5 points.
|
|
122
|
+
- An input limit matters and its timing was incomplete or unknown. A missed or unknown budget can't back a "meets the goal" pick.
|
|
123
|
+
|
|
124
|
+
When you do pick, say all of this with it:
|
|
125
|
+
|
|
126
|
+
- It's a smoothness pick for this build on this desktop setup. Give the Chrome mode, screen size, scale, CPU slowdown, run count, recorded time, Dropped % for each case, their ranges and the gap.
|
|
127
|
+
- Headless Chrome made the frames but showed nothing on a screen. This doesn't prove phone speed.
|
|
128
|
+
- Input times come from the scripted steps, not real users. A missing time is unknown. The tie rule is a guide, not proof.
|
|
129
|
+
|
|
100
130
|
Traps on react-native-web:
|
|
101
131
|
|
|
102
132
|
- `Animated` with `useNativeDriver` still runs on the main thread.
|
package/dist/chunk-2PTJT2QF.mjs
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import{createRequire as Y}from"module";import{join as E}from"path";import{mkdirSync as K,writeFileSync as Q}from"fs";import{pathToFileURL as X}from"url";var Z=`(() => {
|
|
2
|
-
const supported = (t) => {
|
|
3
|
-
try { return (PerformanceObserver.supportedEntryTypes || []).includes(t); } catch { return false; }
|
|
4
|
-
};
|
|
5
|
-
const frames = [], longTasks = [], loaf = [], shifts = [], observers = [];
|
|
6
|
-
const watch = (type, cb) => {
|
|
7
|
-
if (!supported(type)) return false;
|
|
8
|
-
const o = new PerformanceObserver((l) => l.getEntries().forEach(cb));
|
|
9
|
-
o.observe({ type, buffered: false });
|
|
10
|
-
observers.push([o, cb]);
|
|
11
|
-
return true;
|
|
12
|
-
};
|
|
13
|
-
const hasLong = watch("longtask", (e) => longTasks.push({ start: e.startTime, end: e.startTime + e.duration }));
|
|
14
|
-
const hasLoaf = watch("long-animation-frame", (e) => loaf.push({
|
|
15
|
-
startTime: e.startTime,
|
|
16
|
-
duration: e.duration,
|
|
17
|
-
blockingDuration: e.blockingDuration || 0,
|
|
18
|
-
styleAndLayoutStart: e.styleAndLayoutStart || 0,
|
|
19
|
-
scripts: (e.scripts || []).map((s) => {
|
|
20
|
-
const file = s.sourceURL ? s.sourceURL.split("?")[0].split("/").pop() : "";
|
|
21
|
-
const where = s.sourceFunctionName ? s.sourceFunctionName + (file ? " @ " + file : "") : file;
|
|
22
|
-
// Invokers like "FrameRequestCallback" say how a script ran, not which
|
|
23
|
-
// one; add the function and file when the browser gives them.
|
|
24
|
-
const name = s.invoker ? (where ? s.invoker + " \u2192 " + where : s.invoker) : where || "(anonymous)";
|
|
25
|
-
return { duration: s.duration, name, forced: s.forcedStyleAndLayoutDuration || 0 };
|
|
26
|
-
}),
|
|
27
|
-
}));
|
|
28
|
-
const hasShift = watch("layout-shift", (e) => shifts.push({ startTime: e.startTime, value: e.value, hadRecentInput: e.hadRecentInput }));
|
|
29
|
-
let hidden = document.visibilityState === "hidden";
|
|
30
|
-
const onVis = () => { if (document.visibilityState === "hidden") hidden = true; };
|
|
31
|
-
document.addEventListener("visibilitychange", onVis);
|
|
32
|
-
let raf = 0;
|
|
33
|
-
const tick = (t) => { frames.push(t); raf = requestAnimationFrame(tick); };
|
|
34
|
-
raf = requestAnimationFrame(tick);
|
|
35
|
-
const start = performance.now();
|
|
36
|
-
const domStart = document.getElementsByTagName("*").length;
|
|
37
|
-
window.__buoyBench = {
|
|
38
|
-
stop: () => {
|
|
39
|
-
const end = performance.now();
|
|
40
|
-
cancelAnimationFrame(raf);
|
|
41
|
-
document.removeEventListener("visibilitychange", onVis);
|
|
42
|
-
for (const [o, cb] of observers) { o.takeRecords().forEach(cb); o.disconnect(); }
|
|
43
|
-
return {
|
|
44
|
-
start, end, frames,
|
|
45
|
-
longTasks: hasLong ? longTasks : null,
|
|
46
|
-
loaf: hasLoaf ? loaf : null,
|
|
47
|
-
shifts: hasShift ? shifts : null,
|
|
48
|
-
domStart,
|
|
49
|
-
domEnd: document.getElementsByTagName("*").length,
|
|
50
|
-
hidden,
|
|
51
|
-
};
|
|
52
|
-
},
|
|
53
|
-
};
|
|
54
|
-
return true;
|
|
55
|
-
})()`;var ee=`(() => (window.__buoyBench ? window.__buoyBench.stop() : null))()`;var te=`new Promise((resolve) => {
|
|
56
|
-
let n = 0, t0 = 0;
|
|
57
|
-
const tick = (t) => {
|
|
58
|
-
if (!t0) t0 = t;
|
|
59
|
-
n++;
|
|
60
|
-
if (t - t0 >= 1000) resolve(((n - 1) / (t - t0)) * 1000);
|
|
61
|
-
else requestAnimationFrame(tick);
|
|
62
|
-
};
|
|
63
|
-
requestAnimationFrame(tick);
|
|
64
|
-
})`;var w=e=>Math.round(e*10)/10;function C(e,s){if(e.length===0)return null;const t=[...e].sort((r,n)=>r-n);const o=Math.ceil(s/100*t.length)-1;return t[Math.min(t.length-1,Math.max(0,o))]}var ne=[30,60,75,90,120,144,165,240];function R(e){for(const s of ne)if(Math.abs(e-s)/s<=.08)return s;return Math.max(1,Math.round(e))}function D(e,s){let t=s>>>0;const o=()=>{t=t+1831565813>>>0;let n=t;n=Math.imul(n^n>>>15,n|1);n^=n+Math.imul(n^n>>>7,n|61);return((n^n>>>14)>>>0)/4294967296};const r=[...e];for(let n=r.length-1;n>0;n--){const l=Math.floor(o()*(n+1));[r[n],r[l]]=[r[l],r[n]]}return r}function N(e,s,t){const o=new URL(e);const r=s.path??t;if(r)o.pathname=r.startsWith("/")?r:`${o.pathname.replace(/\/$/,"")}/${r}`;for(const[n,l]of Object.entries(s.params??{}))o.searchParams.set(n,l);return o.toString()}function A(e,s){const{start:t,end:o}=e;const r=Math.max(1,o-t);const n=[];for(let u=1;u<e.frames.length;u++)n.push(e.frames[u]-e.frames[u-1]);const l=1e3/s;const a=C(n,95);let i=null;if(e.longTasks){let u=0;for(const d of e.longTasks){const c=Math.min(d.end,o)-Math.max(d.start,t);if(c>0)u+=c}i=w(Math.min(100,u/r*100))}let v=null;let S=null;let M=null;let y=[];if(e.loaf){let u=0;let d=0;let c=0;const f=new Map;for(const h of e.loaf){if(h.startTime<t||h.startTime>o)continue;u+=h.blockingDuration;if(h.styleAndLayoutStart>h.startTime)d+=Math.max(0,h.startTime+h.duration-h.styleAndLayoutStart);for(const m of h.scripts){c+=m.forced;const g=f.get(m.name)??{ms:0,count:0};g.ms+=m.duration;g.count++;f.set(m.name,g)}}v=w(u);S=w(d);M=w(c);y=[...f.entries()].map(([h,m])=>({name:h,ms:w(m.ms),count:m.count})).sort((h,m)=>m.ms-h.ms).slice(0,5)}let $=null;if(e.shifts){let u=0;for(const d of e.shifts)if(!d.hadRecentInput&&d.startTime>=t&&d.startTime<=o)u+=d.value;$=Math.round(u*1e3)/1e3}return{fps:e.frames.length>1?w((e.frames.length-1)/r*1e3):null,frameGapP95Ms:a==null?null:w(a),frameGapMaxMs:n.length?w(Math.max(...n)):null,longFrames:n.filter(u=>u>l*1.5).length,longTaskPct:i,loafBlockingMs:v,styleLayoutMs:S,forcedLayoutMs:M,topScripts:y,layoutShift:$,domNodesStart:e.domStart,domNodesEnd:e.domEnd}}function B(e,s,t){const o=l=>l in e&&l in s?s[l]-e[l]:null;const r=l=>{const a=o(l);return a==null?null:w(a*1e3)};const n=r("TaskDuration");return{mainThreadPct:n==null?null:w(Math.min(100,n/Math.max(1,t)*100)),scriptMs:r("ScriptDuration"),styleMs:r("RecalcStyleDuration"),layoutMs:r("LayoutDuration"),layoutCount:o("LayoutCount"),styleCount:o("RecalcStyleCount"),heapMb:"JSHeapUsedSize"in s?w(s.JSHeapUsedSize/(1024*1024)):null}}function _(e,s,t){const o=[];if(e.fps!=null&&e.fps<s*.95)o.push("Main thread drops frames");if(e.frameGapMaxMs!=null&&e.frameGapMaxMs>=100)o.push(`Freeze of ${Math.round(e.frameGapMaxMs)} ms`);if(e.mainThreadPct!=null&&e.mainThreadPct>=80)o.push("Main thread near full");if(e.longTaskPct!=null&&e.longTaskPct>=20)o.push("Long tasks block the main thread");if(e.forcedLayoutMs!=null&&e.forcedLayoutMs>=50)o.push(`Forced layout in scripts (${Math.round(e.forcedLayoutMs)} ms)`);if(e.layoutShift!=null&&e.layoutShift>=.1)o.push(`Layout shifts (${e.layoutShift})`);if(t)o.push(`DOM grows each run (+${t} nodes)`);return o}function U(e,s){const t=e.filter(a=>!a.failed&&a.fps!=null);const o=[...t].sort((a,i)=>(a.fps??0)-(i.fps??0));const r=o.length?o[Math.floor((o.length-1)/2)]:null;const n=[...t].sort((a,i)=>a.round-i.round).map(a=>a.domNodesEnd).filter(a=>a!=null);let l=null;if(n.length>=3&&n.every((a,i)=>i===0||a>n[i-1])){const a=n[n.length-1]-n[0];if(a>=Math.max(50,n[0]*.05))l=a}return{caseName:e[0]?.caseName??"",cpu:e[0]?.cpu??1,validRuns:t.length,runs:e,median:r,fpsMin:o.length?o[0].fps:null,fpsMax:o.length?o[o.length-1].fps:null,domGrowth:l,flags:r?_(r,s,l):[]}}var G="Playwright isn't installed in this project. Install it, then run again:\n npm i -D playwright && npx playwright install chromium";async function ae(){const e=Y(E(process.cwd(),"noop.js"));for(const s of["playwright","playwright-core"]){try{const t=await import(X(e.resolve(s)).href);const o=t.chromium??t.default?.chromium;if(o)return o}catch{}}throw new Error(G)}function O(e){return e.replace(/\u001b\[[0-9;]*m/g,"").split("\n")[0].trim().slice(0,160)}var j=e=>new Promise(s=>setTimeout(s,e));async function I(e){const{metrics:s}=await e.send("Performance.getMetrics");return Object.fromEntries(s.map(t=>[t.name,t.value]))}async function se(e,s=()=>{}){if(e.cases.length===0)throw new Error("Give at least one case.");const t=Math.max(1,Math.min(10,e.runsPerCase??3));const o=Math.max(500,e.durationMs??5e3);const r=Math.max(0,e.settleMs??1e3);const n=(e.cpuSlowdown?.length?e.cpuSlowdown:[1]).map(u=>Math.max(1,u));let l=e.refreshHz??60;const a={width:e.viewport?.width??1280,height:e.viewport?.height??800,deviceScaleFactor:e.viewport?.deviceScaleFactor??1};const i=[];const v=[];if(t<2)i.push("runsPerCase=1 gives no spread; use 3 or more before choosing.");const S=await ae();const M=await S.launch({headless:!e.headed});const y=[];try{const u=await M.newContext({viewport:{width:a.width,height:a.height},deviceScaleFactor:a.deviceScaleFactor});const d=async(c,f,h,m)=>{const g=await u.newPage();const x=[];g.on("pageerror",p=>x.push(String(p?.message??p)));const T={caseName:c.name,cpu:f,round:h,fps:null,frameGapP95Ms:null,frameGapMaxMs:null,longFrames:0,longTaskPct:null,loafBlockingMs:null,styleLayoutMs:null,forcedLayoutMs:null,topScripts:[],layoutShift:null,domNodesStart:null,domNodesEnd:null,mainThreadPct:null,scriptMs:null,styleMs:null,layoutMs:null,layoutCount:null,styleCount:null,heapMb:null,pageErrors:x};try{const p=await u.newCDPSession(g);await p.send("Performance.enable",{timeDomain:"timeTicks"});if(f>1)await p.send("Emulation.setCPUThrottlingRate",{rate:f});const k=await g.goto(N(e.url,c,e.path),{waitUntil:"load",timeout:3e4});if(k&&k.status()>=400)return{...T,failed:`HTTP ${k.status()}`};if(e.readySelector)await g.waitForSelector(e.readySelector,{state:"attached",timeout:15e3});await j(r);if(m){await p.send("Tracing.start",{transferMode:"ReturnAsStream",categories:["devtools.timeline","disabled-by-default-devtools.timeline","disabled-by-default-devtools.timeline.frame","blink.user_timing","loading","latencyInfo","v8.execute"].join(",")})}const H=await I(p);const z=Date.now();await g.evaluate(Z);await j(o);const P=await g.evaluate(ee);const q=await I(p);const W=Date.now()-z;if(m){const J=new Promise(b=>p.on("Tracing.tracingComplete",V=>b(V.stream)));await p.send("Tracing.end");const F=await J;let L="";for(;;){const b=await p.send("IO.read",{handle:F});L+=b.base64Encoded?Buffer.from(b.data,"base64").toString("utf8"):b.data;if(b.eof)break}await p.send("IO.close",{handle:F});Q(m,L);v.push(m)}if(!P)return{...T,failed:"probe returned nothing (page navigated away?)"};if(P.hidden)return{...T,failed:"page was hidden during the run"};return{...T,...A(P,l),...B(H,q,W)}}catch(p){return{...T,failed:O(p?.message??String(p))}}finally{await g.close().catch(()=>{})}};if(e.refreshHz==null){const c=await u.newPage();try{await c.goto("about:blank");const f=await c.evaluate(te);l=R(f);s(`idle frame rate ${f.toFixed(1)} \u2192 ${l} Hz`)}catch{i.push("Couldn't measure the idle frame rate; frame budgets assume 60 Hz.")}finally{await c.close().catch(()=>{})}}s("warmup load");await d(e.cases[0],1,-1);for(const c of n){for(let f=0;f<t;f++){const h=D(e.cases,2654435769^f*7919+c);for(const m of h){s(`cpu ${c}x \xB7 round ${f+1}/${t} \xB7 ${m.name}`);y.push(await d(m,c,f))}}}if(e.traceDir){K(e.traceDir,{recursive:true});for(const c of e.cases){const f=E(e.traceDir,`${c.name.replace(/[^\w.-]+/g,"_")}.json`);s(`trace \xB7 ${c.name}`);await d(c,n[0],-1,f)}}await u.close()}finally{await M.close().catch(()=>{})}const $=[];for(const u of n){for(const d of e.cases){$.push(U(y.filter(c=>c.cpu===u&&c.caseName===d.name),l))}}if(y.some(u=>u.pageErrors.length))i.push("Some runs threw page errors; check them before trusting those cases.");return{url:e.url,browser:`Chromium ${M.version()}${e.headed?"":" (headless)"}`,viewport:a,refreshHz:l,durationMs:o,runsPerCase:t,results:$,traces:v,warnings:i}}function oe(e){const s=(r,n=1)=>r==null?"\u2014":r.toFixed(n);const t=[];t.push(`### Web benchmark \xB7 ${e.url}`);t.push(`${e.browser} \xB7 ${e.viewport.width}\xD7${e.viewport.height} @${e.viewport.deviceScaleFactor}x \xB7 ${e.runsPerCase} runs \xD7 ${e.durationMs/1e3}s per case, fresh page each run, case order shuffled each round.`);t.push("");t.push(`**No single winner \u2014 weigh the trade-offs.** FPS and frame gaps come from the page's main thread. Main thread % is Chrome's own task time for the run. Desktop numbers, even with CPU slowdown, don't stand in for a real phone's GPU, memory or heat.`);const o=Array.from(new Set(e.results.map(r=>r.cpu)));for(const r of o){t.push("");t.push(r===1?`#### No CPU slowdown`:`#### CPU slowed ${r}x`);t.push("");t.push(`| Case | FPS (range) | p95 gap ms | Worst gap ms | Long frames | Main thread % | Script ms | Style ms | Layout ms | Layouts | DOM nodes | Heap MB | Flags |`);t.push(`|------|------------:|-----------:|-------------:|------------:|--------------:|----------:|---------:|----------:|--------:|----------:|--------:|-------|`);for(const a of e.results.filter(i=>i.cpu===r)){const i=a.median;if(!i){const M=a.runs.find(y=>y.failed)?.failed??"no valid runs";t.push(`| ${a.caseName} | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u274C FAILED (${M}) |`);continue}const v=a.fpsMin!=null&&a.fpsMax!=null&&a.validRuns>1?` (${s(a.fpsMin)}\u2013${s(a.fpsMax)})`:"";const S=a.flags.length?`\u26A0\uFE0F ${a.flags.join("; ")}`:"\u2713 clean";t.push(`| ${a.caseName} | ${s(i.fps)}${v} | ${s(i.frameGapP95Ms)} | ${s(i.frameGapMaxMs)} | ${i.longFrames} | ${s(i.mainThreadPct)} | ${s(i.scriptMs,0)} | ${s(i.styleMs,0)} | ${s(i.layoutMs,0)} | ${s(i.layoutCount,0)} | ${s(i.domNodesEnd,0)} | ${s(i.heapMb)} | ${S} |`)}const n=e.results.filter(a=>a.cpu===r&&a.median?.fps!=null).sort((a,i)=>i.median.fps-a.median.fps);if(n.length>=2&&n[0].validRuns>1&&n[1].validRuns>1&&(n[1].fpsMax??0)>=(n[0].fpsMin??0)){t.push("");t.push(`**Too close to call:** ${n[0].caseName} and ${n[1].caseName} overlap across runs. Use more runs before picking.`)}const l=e.results.filter(a=>a.cpu===r&&a.median&&a.median.topScripts.length).map(a=>`- **${a.caseName}**: ${a.median.topScripts.slice(0,3).map(i=>`${i.name} ${i.ms} ms`).join(", ")}`);if(l.length){t.push("");t.push(`Scripts in slow frames (Long Animation Frames):`);t.push(...l)}}if(e.traces.length){t.push("");t.push(`Chrome traces (open in DevTools \u2192 Performance \u2192 Load profile): ${e.traces.join(", ")}`)}if(e.warnings.length){t.push("");t.push(...e.warnings.map(r=>`\u26A0\uFE0F ${r}`))}return t.join("\n")}export{C as percentile,R as snapRefreshRate,D as shuffled,N as caseUrl,A as summarizeProbe,B as cdpDeltas,_ as webBenchFlags,U as aggregateCase,G as PLAYWRIGHT_MISSING,O as oneLine,se as runWebBench,oe as formatWebBenchMarkdown};
|