gm-plugkit 2.0.1631 → 2.0.1632

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.
@@ -24,13 +24,17 @@ url=<url>\n<expression>
24
24
  timeout=<ms>\n<expression>
25
25
  capture\n<expression>
26
26
  profile\n<expression>
27
+ profile interval=<us> topN=<n>\n<expression>
28
+ trace\n<expression>
27
29
  ```
28
30
 
29
31
  **Open on the page you want to test, not a blank one.** A bare `https://...` URL body navigates the session straight to that page and returns `{url, title}` -- the simplest "show me this page." `url=<url>\n<expression>` navigates first, then runs your expression on the loaded page, so the global/DOM you assert is already there in one dispatch instead of a blank surface you must `page.goto` yourself. `url=` composes with `timeout=` and `capture` -- stack the prefix lines in order `timeout=`, then `url=`, then `capture`, the expression last; the prepended `page.goto` rides inside the capture so its navigation console/network is captured too. A bare expression with no `url=`/bare-URL prefix runs against whatever the session is already on -- a never-navigated session is on `about:blank`, so the expression evaluates an empty page and the envelope comes back with `landed_on_blank: true` and a `hint` telling you to add `url=`; navigate first and the surprise never happens. `session new` returns the id you carry. (`session close` and `session kill` are aliases.) Default per-eval timeout 120000ms; operations that legitimately exceed it prefix `timeout=<ms>\n` (wrapper clamps to 120000ms). The response carries `timeout_ms_used`; `browser.runner-timeout` fires at the cap -- read `stderr`, narrow or raise, never retry blind at the same budget.
30
32
 
31
33
  **`capture\n<expression>` is the zero-boilerplate debug path -- prefer it.** Prefix your script with `capture` (or `profile`) on its own line and the wrapper auto-attaches `page.on('console'|'pageerror'|'requestfinished')` before your code runs, runs your script in an async wrapper (your top-level `await`/`return` work unchanged), and returns `{result: <your return>, debug: {console, pageErrors, network, performance}}` -- page console logs, uncaught errors, per-request network timing, and navigation performance, captured for free. Combine with timeout via `timeout=<ms>\ncapture\n<expr>`. Use the bare expression only when you do not want the capture overhead.
32
34
 
33
- **`profile\n<expression>` is the bottom-up CPU profiler -- worst-20 culprits by file location across init and code-execution.** Prefix your script with `profile` on its own line: the wrapper opens a CDP `Profiler` (`newCDPSession` + `Profiler.start` BEFORE the prepended `page.goto`, so navigation, script-parse, and init are sampled, not only steady-state), runs your script, `Profiler.stop`s, and aggregates the v8 CPU profile into `{result, profile: {timeframe: {start_us, end_us, total_us, sample_count}, culprits: [{location, function, self_us, self_pct, hits}]}, profile_error, debug: {...}}`. `culprits` is the bottom-up self-time ranking capped at the worst 20 `url:line` locations; `timeframe` is the capture window in microseconds. Composes with `url=`/`timeout=` in the same prefix order. Page scripts loaded from `.js` files carry real `file:line`; `page.evaluate` anonymous frames bucket to `(program)`/`(native)`. On a CDP failure `profile` is `null` with `profile_error` set and your `result` still returns. The identical `{timeframe, culprits}` shape comes back from `exec_js` with `opts.profile:true`, so the cli and browser bottom-up views read the same.
35
+ **`profile\n<expression>` is the bottom-up CPU profiler -- worst-20 culprits by file location across init and code-execution.** Prefix your script with `profile` on its own line: the wrapper opens a CDP `Profiler` (`newCDPSession` + `Profiler.start` BEFORE the prepended `page.goto`, so navigation, script-parse, and init are sampled, not only steady-state), runs your script, `Profiler.stop`s, and aggregates the v8 CPU profile into `{result, profile: {timeframe: {start_us, end_us, total_us, sample_count}, culprits: [{location, function, self_us, self_pct, hits}]}, profile_error, debug: {...}}`. `culprits` is the bottom-up self-time ranking capped at the worst 20 `url:line` locations; `timeframe` is the capture window in microseconds. Composes with `url=`/`timeout=` in the same prefix order. Page scripts loaded from `.js` files carry real `file:line`; `page.evaluate` anonymous frames bucket to `(program)`/`(native)`. On a CDP failure `profile` is `null` with `profile_error` set and your `result` still returns. The identical `{timeframe, culprits}` shape comes back from `exec_js` with `opts.profile:true`, so the cli and browser bottom-up views read the same. `profile` also returns `wall_vs_cpu: {wall_us, cpu_self_us, offcpu_us}` -- the CPU sampler measures only on-CPU JS, so `offcpu_us` is the time it cannot see. Tune the sampler with `interval=<us>` and the culprit count with `topN=<n>` stacked after the mode word (`profile interval=50 topN=40`), symmetric with `exec_js` `opts.sampleIntervalUs`/`opts.profileTopN`.
36
+
37
+ **`trace\n<expression>` catches GPU activity the CPU sampler is structurally blind to.** A V8 CPU profiler samples on-CPU JS call stacks only; GPU-process work -- compositor, raster, draw, WebGL/canvas -- never appears in it. `trace` opens a CDP `Tracing` session over wrapper-controlled categories (`gpu`, `viz`, `cc`, `blink`, `devtools.timeline`), runs your script, ends tracing, and returns `{result, trace: {wall_us, gpu_us, viz_us, cc_us, raster_us, event_count, complete, by_category}, trace_error, debug: {...}}`. `gpu_us`/`viz_us`/`cc_us` are wall-clock microseconds of GPU-process activity summed from the trace; `by_category` is the bounded top-15 category rollup (raw events never returned). When wall greatly exceeds CPU self-time, `trace` is how you attribute the gap to the GPU rather than guessing. `debug.performance` carries paint/frame metrics (`first_contentful_paint_ms`, `largest_contentful_paint_ms`, `cumulative_layout_shift`, `longtasks`, `fps`) for client-side render jank. `tracingComplete` is bounded by a timeout; on a CDP failure `trace_error` is set and `result`/`debug` still return.
34
38
 
35
39
  ## Envelope
36
40
 
@@ -38,7 +38,7 @@ First emit = closure of the transform; scaffold + IOU externalizes residual cost
38
38
 
39
39
  Data first -- get the structures and their invariants right and the code writes itself; convoluted control flow means the data model is wrong, so fix the model. Make invalid state unrepresentable -- pass parameters over hidden globals, encode the constraint in the type/shape so the bad combination cannot be constructed. Reason from physical constraints (latency, bandwidth, memory, coordination, the worst node) before designing within them. Keep the spine flat, each unit single-focus and understandable at its call site. Make misuse structurally impossible, not documented-against. Optimize the worst case, not the average; design every failure path explicitly (full -> degraded -> safe-fail -> explicit-error), never a silent catastrophic mode. Measure, do not assume -- profile before optimizing, implement both and compare on real input when in genuine dispute. When a change regresses something that worked, revert first and investigate second: restore green, then diagnose from a known-good base. Fail fast and loud over limping on bad state.
40
40
 
41
- **Process of elimination is the debugging paradigm on every surface, and manual labour against real services is how you witness.** This is thinking-in-code at its sharpest: each candidate cause is a hypothesis, and you test the hypothesis by running it, not by reasoning around it. Never guess-and-restart, a/b-test, or shotgun variants: enumerate the candidate causes as mutables, then eliminate each by a witness read against REAL input -- `exec_js` against the real service, `codesearch`/`Read` against the real source, the `browser` verb's `page.evaluate` against a `window.*` global on the live page. Each elimination reveals the next mutable; record it and keep going until one cause survives every other's refutation. Reading the live runtime once observes more than a hundred blind restarts. Profile on the real surface, not from intuition: wrap the suspect node and read the live numbers. In node, `exec_js` carries `duration_ms` for free, surfaces your own timing and `process.memoryUsage()` on stdout, and lands the thrown-error `stack` on stderr -- read both channels (numbers on stdout, stack on stderr). In the browser, a body prefixed `capture\n<script>` auto-returns `{result, debug:{console, pageErrors, network, performance}}` with zero boilerplate. When the slow node is not obvious, sample it bottom-up: `exec_js` with `opts.profile:true` and the browser `profile\n<script>` prefix both return `{result, profile:{timeframe:{start_us,end_us,total_us,sample_count}, culprits:[{location,function,self_us,self_pct,hits}]}}` -- the worst-20 `file:line` by self-time across init and code-execution, identical shape on both surfaces, so the culprit ranking points straight at the line to fix. Profile to LOCATE the slow/broken node, then eliminate hypotheses by live measurement. Verification is the same labour: run the real thing and witness the real output (the single mock-free `test.js`, the live page, the real service), never an automated unit/mock harness standing in for the real-services witness. Apparent tooling failure is part of this -- it is your mechanical self-recovery by elimination, never a question for the user.
41
+ **Process of elimination is the debugging paradigm on every surface, and manual labour against real services is how you witness.** This is thinking-in-code at its sharpest: each candidate cause is a hypothesis, and you test the hypothesis by running it, not by reasoning around it. Never guess-and-restart, a/b-test, or shotgun variants: enumerate the candidate causes as mutables, then eliminate each by a witness read against REAL input -- `exec_js` against the real service, `codesearch`/`Read` against the real source, the `browser` verb's `page.evaluate` against a `window.*` global on the live page. Each elimination reveals the next mutable; record it and keep going until one cause survives every other's refutation. Reading the live runtime once observes more than a hundred blind restarts. Profile on the real surface, not from intuition: wrap the suspect node and read the live numbers. In node, `exec_js` carries `duration_ms` for free, surfaces your own timing and `process.memoryUsage()` on stdout, and lands the thrown-error `stack` on stderr -- read both channels (numbers on stdout, stack on stderr). In the browser, a body prefixed `capture\n<script>` auto-returns `{result, debug:{console, pageErrors, network, performance}}` with zero boilerplate. When the slow node is not obvious, sample it bottom-up: `exec_js` with `opts.profile:true` and the browser `profile\n<script>` prefix both return `{result, profile:{timeframe:{start_us,end_us,total_us,sample_count}, culprits:[{location,function,self_us,self_pct,hits}]}}` -- the worst-N `file:line` by self-time across init and code-execution, identical shape on both surfaces, so the culprit ranking points straight at the line to fix. Both also return `mem` (rss/heap/delta) and `wall_vs_cpu:{wall_us, offcpu_us}` -- the sampler sees only on-CPU JS, so a large `offcpu_us` means the time is going to IO, async wait, or the GPU, not the JS you can see; tune with `opts.sampleIntervalUs`/`opts.profileTopN` (cli) or `interval=`/`topN=` (browser). The CPU sampler is structurally blind to GPU activity -- when wall greatly exceeds CPU self-time on a render/canvas/WebGL surface, the browser `trace\n<script>` prefix opens CDP Tracing and returns `trace:{wall_us, gpu_us, viz_us, cc_us, by_category}`, the wall-clock GPU-process time the profiler cannot show. Profile to LOCATE the slow/broken node, then eliminate hypotheses by live measurement. Verification is the same labour: run the real thing and witness the real output (the single mock-free `test.js`, the live page, the real service), never an automated unit/mock harness standing in for the real-services witness. Apparent tooling failure is part of this -- it is your mechanical self-recovery by elimination, never a question for the user.
42
42
 
43
43
  ## Memorize
44
44
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-plugkit",
3
- "version": "2.0.1631",
3
+ "version": "2.0.1632",
4
4
  "description": "Bootstrap and daemon-spawn tool for gm plugkit binary. Downloads the correct platform binary, verifies SHA256, and starts the spool watcher daemon. Includes plugkit-wasm-wrapper for WASM-based spool watching.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -1989,7 +1989,12 @@ function makeHostFunctions(instanceRef) {
1989
1989
  });
1990
1990
  }
1991
1991
  const timeoutMs = rawTimeout;
1992
- const wantProfile = opts.profile === true && (lang === 'nodejs' || lang === 'js' || lang === undefined);
1992
+ const isJsLang = lang === 'nodejs' || lang === 'js' || lang === undefined;
1993
+ const wantProfile = opts.profile === true && isJsLang;
1994
+ const profileSkipped = opts.profile === true && !isJsLang
1995
+ ? { reason: `profile requested but lang=${lang} is not js/nodejs; CPU profiling only supported on the node surface`, lang }
1996
+ : null;
1997
+ const profileTopN = Number.isFinite(opts.profileTopN) && opts.profileTopN > 0 ? Math.floor(opts.profileTopN) : 20;
1993
1998
  let profileUserFile = null;
1994
1999
  let cmd, args;
1995
2000
  if (lang === 'nodejs' || lang === 'js') {
@@ -1998,21 +2003,32 @@ function makeHostFunctions(instanceRef) {
1998
2003
  fs.writeFileSync(profileUserFile, `module.exports = (async () => {\n${code}\n});`, 'utf-8');
1999
2004
  const runnerCode = `${AGGREGATE_CPU_PROFILE_SRC}\n`
2000
2005
  + `const __inspector = require('inspector');\n`
2006
+ + `const { performance: __perf } = require('perf_hooks');\n`
2001
2007
  + `const __session = new __inspector.Session();\n`
2002
2008
  + `__session.connect();\n`
2003
2009
  + `const __post = (m, p) => new Promise((res, rej) => __session.post(m, p || {}, (e, r) => e ? rej(e) : res(r)));\n`
2004
2010
  + `(async () => {\n`
2005
- + ` let __profile = null, __profileError = null, __userResult = null, __userError = null;\n`
2011
+ + ` let __profile = null, __profileError = null, __userResult = null, __userError = null, __wallMs = 0;\n`
2012
+ + ` const __memBefore = process.memoryUsage();\n`
2006
2013
  + ` try {\n`
2007
2014
  + ` await __post('Profiler.enable');\n`
2008
2015
  + ` await __post('Profiler.setSamplingInterval', { interval: ${Number.isFinite(opts.sampleIntervalUs) && opts.sampleIntervalUs > 0 ? Math.floor(opts.sampleIntervalUs) : 100} });\n`
2009
2016
  + ` await __post('Profiler.start');\n`
2017
+ + ` const __w0 = __perf.now();\n`
2010
2018
  + ` try { __userResult = await require(${JSON.stringify(profileUserFile)})(); } catch (ue) { __userError = String(ue && ue.stack || ue); }\n`
2019
+ + ` __wallMs = Math.round((__perf.now() - __w0) * 1000) / 1000;\n`
2011
2020
  + ` const __r = await __post('Profiler.stop');\n`
2012
2021
  + ` __profile = __r && __r.profile || null;\n`
2013
2022
  + ` } catch (pe) { __profileError = String(pe && pe.message || pe); }\n`
2014
- + ` const __agg = __profile ? aggregateCpuProfile(__profile) : { timeframe: null, culprits: [] };\n`
2015
- + ` process.stdout.write('__GM_PROFILE__' + JSON.stringify({ result: __userResult, user_error: __userError, profile: __agg, profile_error: __profileError }));\n`
2023
+ + ` const __memAfter = process.memoryUsage();\n`
2024
+ + ` const __agg = __profile ? aggregateCpuProfile(__profile, ${profileTopN}) : { timeframe: null, culprits: [] };\n`
2025
+ + ` const __userFile = ${JSON.stringify('file:///' + profileUserFile.replace(/\\/g, '/'))};\n`
2026
+ + ` const __cpuTotalUs = __agg.timeframe ? __agg.timeframe.total_us : 0;\n`
2027
+ + ` const __cpuUserUs = (__agg.culprits || []).filter(c => c.location && c.location.indexOf(__userFile) === 0).reduce((a, c) => a + c.self_us, 0);\n`
2028
+ + ` const __wallUs = Math.round(__wallMs * 1000);\n`
2029
+ + ` const __mem = { rss_mb: Math.round(__memAfter.rss/10485.76)/100, heapUsed_mb: Math.round(__memAfter.heapUsed/10485.76)/100, heapUsed_delta_mb: Math.round((__memAfter.heapUsed-__memBefore.heapUsed)/10485.76)/100, external_mb: Math.round(__memAfter.external/10485.76)/100 };\n`
2030
+ + ` const __wallVsCpu = { wall_us: __wallUs, cpu_user_self_us: __cpuUserUs, cpu_total_sampled_us: __cpuTotalUs, offcpu_us: Math.max(0, __wallUs - __cpuUserUs), note: 'offcpu_us = inner wall minus on-CPU user-code JS self time = IO/async/GPU/idle the CPU sampler is blind to; cpu_total_sampled_us includes node-init/inspector overhead' };\n`
2031
+ + ` process.stdout.write('__GM_PROFILE__' + JSON.stringify({ result: __userResult, user_error: __userError, profile: __agg, profile_error: __profileError, mem: __mem, wall_vs_cpu: __wallVsCpu }));\n`
2016
2032
  + ` __session.disconnect();\n`
2017
2033
  + `})();\n`;
2018
2034
  cmd = process.execPath; args = ['-e', runnerCode];
@@ -2043,6 +2059,8 @@ function makeHostFunctions(instanceRef) {
2043
2059
  profile: parsed ? parsed.profile : { timeframe: null, culprits: [] },
2044
2060
  profile_error: parsed ? parsed.profile_error : 'profile sentinel not found in stdout',
2045
2061
  user_error: parsed ? parsed.user_error : null,
2062
+ mem: parsed ? parsed.mem : null,
2063
+ wall_vs_cpu: parsed ? parsed.wall_vs_cpu : null,
2046
2064
  });
2047
2065
  }
2048
2066
  return writeWasmJson(instanceRef.value, {
@@ -2052,6 +2070,7 @@ function makeHostFunctions(instanceRef) {
2052
2070
  exit_code: result.status === null ? -1 : result.status,
2053
2071
  timed_out: result.signal === 'SIGTERM',
2054
2072
  duration_ms: Date.now() - __execT0,
2073
+ ...(profileSkipped ? { profile_skipped: profileSkipped } : {}),
2055
2074
  });
2056
2075
  } catch (e) {
2057
2076
  return writeWasmJson(instanceRef.value, { ok: false, error: e.message });
@@ -2183,7 +2202,12 @@ function makeHostFunctions(instanceRef) {
2183
2202
  const gotoPrefix = startUrl
2184
2203
  ? `await page.goto(${JSON.stringify(startUrl)},{waitUntil:'load',timeout:${navTimeout}});\n`
2185
2204
  : '';
2186
- const modeMatch = evalBody.match(/^(capture|profile)[ \t]*\n([\s\S]*)$/);
2205
+ const modeMatch = evalBody.match(/^(capture|profile|trace)((?:[ \t]+(?:interval|topN)=\d+)*)[ \t]*\n([\s\S]*)$/);
2206
+ const modeOpts = modeMatch ? modeMatch[2] : '';
2207
+ const __intervalM = modeOpts.match(/interval=(\d+)/);
2208
+ const __topNM = modeOpts.match(/topN=(\d+)/);
2209
+ const sampleIntervalUs = __intervalM && parseInt(__intervalM[1], 10) > 0 ? parseInt(__intervalM[1], 10) : 100;
2210
+ const profileTopNBrowser = __topNM && parseInt(__topNM[1], 10) > 0 ? parseInt(__topNM[1], 10) : 20;
2187
2211
  const debugSetup = `const __logs=[],__errs=[],__net=[];\n`
2188
2212
  + `try{page.on('console',m=>{try{__logs.push({type:m.type(),text:m.text()});}catch(_){}});`
2189
2213
  + `page.on('pageerror',e=>{try{__errs.push({type:'pageerror',msg:String(e&&e.message||e)});}catch(_){}});`
@@ -2192,25 +2216,48 @@ function makeHostFunctions(instanceRef) {
2192
2216
  + `page.on('requestfailed',r=>{try{const err=r.failure();__errs.push({type:'fetch',msg:String(err&&err.errorText||'request failed'),url:String(r.url()).slice(0,120)});}catch(_){}});`
2193
2217
  + `page.evaluateOnNewDocument(()=>{window.__gmErrors=[];window.onerror=(msg,src,line,col,err)=>{try{window.__gmErrors.push({type:'error',msg:String(msg),src:String(src).slice(0,80),line,col,stack:String(err&&err.stack||'')});}catch(_){};return false;};window.onunhandledrejection=(e)=>{try{window.__gmErrors.push({type:'unhandledRejection',msg:String(e.reason&&e.reason.message||e.reason),stack:String(e.reason&&e.reason.stack||'')});}catch(_){}};});`
2194
2218
  + `}catch(_){}\n`;
2195
- const perfRead = `let __perf=null;try{__perf=await page.evaluate(()=>{const n=performance.getEntriesByType('navigation')[0];return n?{load_ms:Math.round(n.loadEventEnd||0),dcl_ms:Math.round(n.domContentLoadedEventEnd||0),resources:performance.getEntriesByType('resource').length,now:Math.round(performance.now())}:null;});}catch(_){}\n`;
2219
+ const perfRead = `let __perf=null;try{__perf=await page.evaluate(async()=>{const n=performance.getEntriesByType('navigation')[0];const paints={};for(const p of performance.getEntriesByType('paint')){paints[p.name]=Math.round(p.startTime);}let lcp=0;try{const le=performance.getEntriesByType('largest-contentful-paint');if(le.length)lcp=Math.round(le[le.length-1].startTime);}catch(_){}let cls=0;try{for(const ls of performance.getEntriesByType('layout-shift')){if(!ls.hadRecentInput)cls+=ls.value;}}catch(_){}let longtasks=0;try{longtasks=performance.getEntriesByType('longtask').length;}catch(_){}const fps=await new Promise(res=>{let f=0;const s=performance.now();function tick(){f++;if(performance.now()-s>=500)return res(Math.round(f/((performance.now()-s)/1000)));requestAnimationFrame(tick);}requestAnimationFrame(tick);});return{load_ms:n?Math.round(n.loadEventEnd||0):0,dcl_ms:n?Math.round(n.domContentLoadedEventEnd||0):0,resources:performance.getEntriesByType('resource').length,now:Math.round(performance.now()),first_paint_ms:paints['first-paint']||0,first_contentful_paint_ms:paints['first-contentful-paint']||0,largest_contentful_paint_ms:lcp,cumulative_layout_shift:Math.round(cls*1000)/1000,longtasks,fps};});}catch(_){}\n`;
2196
2220
  const blankProbe = startUrl ? '' : `try{const __u=page.url();if(__u==='about:blank'||__u===''){console.error('__GM_BLANK__');}}catch(_){}\n`;
2197
2221
  if (modeMatch && modeMatch[1] === 'profile') {
2198
- const userScript = modeMatch[2];
2199
- const intervalUs = 100;
2222
+ const userScript = modeMatch[3];
2223
+ const intervalUs = sampleIntervalUs;
2200
2224
  evalBody = debugSetup
2201
2225
  + `let __profile=null,__profileError=null;\n`
2202
2226
  + `let __cdp=null;\n`
2203
2227
  + `try{__cdp=await page.context().newCDPSession(page);await __cdp.send('Profiler.enable');await __cdp.send('Profiler.setSamplingInterval',{interval:${intervalUs}});await __cdp.send('Profiler.start');}catch(e){__profileError=String(e&&e.message||e);__cdp=null;}\n`
2228
+ + `const __wallT0=Date.now();\n`
2204
2229
  + `const __result = await (async () => {\n${blankProbe}${gotoPrefix}try{${userScript}}catch(e){__errs.push({type:'exec',msg:String(e&&e.message||e),stack:String(e&&e.stack||'')});throw e;}\n})();\n`
2230
+ + `const __wallUs=(Date.now()-__wallT0)*1000;\n`
2205
2231
  + `if(__cdp){try{const __r=await __cdp.send('Profiler.stop');__profile=__r&&__r.profile||null;}catch(e){__profileError=String(e&&e.message||e);}}\n`
2206
2232
  + `const __wmErrors=await page.evaluate(()=>window.__gmErrors||[]);\n`
2207
2233
  + perfRead
2208
2234
  + AGGREGATE_CPU_PROFILE_SRC + `\n`
2209
- + `const __agg = __profile ? aggregateCpuProfile(__profile) : {timeframe:null,culprits:[]};\n`
2235
+ + `const __agg = __profile ? aggregateCpuProfile(__profile, ${profileTopNBrowser}) : {timeframe:null,culprits:[]};\n`
2236
+ + `const __cpuUs=__agg.timeframe?__agg.timeframe.total_us:0;\n`
2237
+ + `const __wallVsCpu={wall_us:__wallUs,cpu_self_us:__cpuUs,offcpu_us:Math.max(0,__wallUs-__cpuUs),note:'offcpu_us = wall minus on-CPU JS self time = GPU/compositor/raster/IO/idle the CPU sampler is blind to; use trace mode to attribute GPU activity'};\n`
2210
2238
  + `const __allErrors=[...__errs,...__wmErrors];\n`
2211
- + `return {result:__result,profile:__agg,profile_error:__profileError,debug:{console:__logs,pageErrors:__allErrors,network:__net.slice(0,30),performance:__perf}};`;
2239
+ + `return {result:__result,profile:__agg,profile_error:__profileError,wall_vs_cpu:__wallVsCpu,debug:{console:__logs,pageErrors:__allErrors,network:__net.slice(0,30),performance:__perf}};`;
2240
+ } else if (modeMatch && modeMatch[1] === 'trace') {
2241
+ const userScript = modeMatch[3];
2242
+ evalBody = debugSetup
2243
+ + `let __traceEvents=[],__traceError=null,__cdp=null,__traceComplete=false;\n`
2244
+ + `const __traceCats=['gpu','disabled-by-default-gpu.service','viz','cc','blink','devtools.timeline','toplevel','rail'];\n`
2245
+ + `try{__cdp=await page.context().newCDPSession(page);__cdp.on('Tracing.dataCollected',p=>{if(p&&p.value)__traceEvents.push(...p.value);});await __cdp.send('Tracing.start',{traceConfig:{includedCategories:__traceCats},transferMode:'ReportEvents',bufferUsageReportingInterval:0});}catch(e){__traceError='start:'+String(e&&e.message||e);__cdp=null;}\n`
2246
+ + `const __wallT0=Date.now();\n`
2247
+ + `const __result = await (async () => {\n${blankProbe}${gotoPrefix}try{${userScript}}catch(e){__errs.push({type:'exec',msg:String(e&&e.message||e),stack:String(e&&e.stack||'')});throw e;}\n})();\n`
2248
+ + `const __wallUs=(Date.now()-__wallT0)*1000;\n`
2249
+ + `if(__cdp){const __done=new Promise(res=>{__cdp.once('Tracing.tracingComplete',()=>res(true));setTimeout(()=>res(false),Math.min(${Math.min(navTimeout, 10000)},10000));});try{await __cdp.send('Tracing.end');}catch(e){__traceError=(__traceError||'')+' end:'+String(e&&e.message||e);}__traceComplete=await __done;}\n`
2250
+ + `const __wmErrors=await page.evaluate(()=>window.__gmErrors||[]);\n`
2251
+ + perfRead
2252
+ + `const __byCat={};let __minTs=Infinity,__maxTs=-Infinity;for(const ev of __traceEvents){if(typeof ev.ts==='number'){__minTs=Math.min(__minTs,ev.ts);if(typeof ev.dur==='number')__maxTs=Math.max(__maxTs,ev.ts+ev.dur);}if(typeof ev.dur==='number'&&ev.dur>0){const c=ev.cat||'?';__byCat[c]=(__byCat[c]||0)+ev.dur;}}\n`
2253
+ + `const __sum=(re)=>Object.entries(__byCat).filter(([k])=>re.test(k)).reduce((a,[,v])=>a+v,0);\n`
2254
+ + `const __gpuUs=__sum(/gpu|graphics\\.pipeline/),__vizUs=__sum(/viz/),__ccUs=__sum(/\\bcc\\b/),__rasterUs=__sum(/raster/);\n`
2255
+ + `const __topCats=Object.entries(__byCat).sort((a,b)=>b[1]-a[1]).slice(0,15).map(([cat,us])=>({cat,wall_us:us}));\n`
2256
+ + `const __spanUs=(isFinite(__minTs)&&__maxTs>0)?(__maxTs-__minTs):0;\n`
2257
+ + `const __allErrors=[...__errs,...__wmErrors];\n`
2258
+ + `return {result:__result,trace:{wall_us:__wallUs,trace_span_us:__spanUs,event_count:__traceEvents.length,complete:__traceComplete,gpu_us:__gpuUs,viz_us:__vizUs,cc_us:__ccUs,raster_us:__rasterUs,offcpu_note:'gpu_us/viz_us/cc_us are wall-clock GPU-process activity (compositor/raster/draw) captured via CDP Tracing -- the CPU sampler cannot see these',by_category:__topCats},trace_error:__traceError,debug:{console:__logs,pageErrors:__allErrors,network:__net.slice(0,30),performance:__perf}};`;
2212
2259
  } else if (modeMatch && modeMatch[1] === 'capture') {
2213
- const userScript = modeMatch[2];
2260
+ const userScript = modeMatch[3];
2214
2261
  evalBody = debugSetup
2215
2262
  + `const __result = await (async () => {\n${blankProbe}${gotoPrefix}try{${userScript}}catch(e){__errs.push({type:'exec',msg:String(e&&e.message||e),stack:String(e&&e.stack||'')});throw e;}\n})();\n`
2216
2263
  + `const __wmErrors=await page.evaluate(()=>window.__gmErrors||[]);\n`