name: pup-differential-diagnosis description: How to tell whether a "the page crashed/hung pup" problem is really pup's bug (almost always) and not the page or Chrome, by running the SAME page through nb (the Adom extension driving real Chrome) or a plain headful Chrome and watching it behave. A heavy or hostile page must NEVER crash, hang, or starve the pup bridge; if it does, the fault is in pup's CDP handling. READ THIS before you blame a page ("it's a giant Babylon.js bundle", "it floods the console", "it's a WebGL hog") for bridge instability. John's rule: prove it's your bridge first.

Differential diagnosis: is it pup, or is it the page? (it's pup)

John, verbatim: "a heavy page should NEVER crash your bridge... open it in nb chrome and see if that crashes, and if it doesn't it shows you your bridge has issues you need to fix. i like the model of you testing yourself against nb chrome so you never blame chrome and a heavy page — which i tend to think is how you're handling the CDP interface."

He is right, and this is the standing rule: a page cannot crash a correct CDP client. Chrome sandboxes and isolates page code; the DevTools protocol is a read/drive channel. If a page takes down the bridge, the bug is in how the bridge drives CDP, not in the page. Do not close a bug as "that page is just too heavy." Localize it first with the differential test below.

The differential test (do this before touching bridge code)

Both pup and the Adom browser extension (nbrowser_*) drive Chrome over CDP. So does a plain headful Chrome you launch by hand. Run the SAME url through a second CDP driver:

  1. nb / real Chromenbrowser_open the exact url in the user's real browser (see driving-the-extension).
  2. Plain headful Chrome — launch Chrome yourself with --user-data-dir=<scratch> and open the url (no pup, no bridge). This is the cleanest control: it removes BOTH pup and the extension.

Then read the result:

nb / plain Chrome pup Verdict
renders fine crashes / hangs / drops pup's bug. The page + Chrome + CDP are proven fine; the only variable left is pup's own CDP handling. Fix pup.
also crashes crashes rare — a genuine Chrome/GPU bug or an OOM. File upstream with the plain-Chrome repro; still check pup isn't amplifying it.

In practice it is always the first row. The differential's job is to make you stop guessing about the page and start reading your own CDP code.

Read the failure shape: crash vs starvation (they are different)

The bridge has top-level guards (process.on('uncaughtException') / 'unhandledRejection')) that log and survive, so a stray CDP rejection ("Target closed") does NOT kill the process. That means the symptom you actually see is almost never a hard crash — it is event-loop starvation:

  • Hard crash (rare): the node process exits, its pid is gone, and a stack lands in %USERPROFILE%\.adom\bridge-logs\puppeteer.log stderr. Look for the stack.
  • Starvation (the usual one): the node process is STILL ALIVE (same pid), but AD can't reach it — error sending request for url http://127.0.0.1:64230/command, bridge_not_listening, or verbs time out. The single event loop is blocked processing something, so HTTP on 64230 can't be served, and AD's health check reaps it. This is what "the page crashed the bridge" almost always is.

Confirm which you have via AD-core (works even when the bridge is unreachable): shell_execute a check of the node pid + a tail of the bridge log. Same pid + no stack = starvation, hunt the event-loop blocker.

The load multiplier: reproduce under CONCURRENCY, not a single open

A single open of the "bad" page often works — the starvation only shows when several heavy opens/closes overlap (a migration, a batch, an audit loop). So the real repro is concurrent load:

# fire N heavy opens at once, then poll the bridge the whole time — it must stay responsive
for n in 1 2 3 4; do ( adom-bridge-cli --target <host> --ai-thread stress pup_open_window \
  "{\"sessionId\":\"stress-$n\",\"url\":\"<heavy url>?s=$n\"}" & ); done
for i in $(seq 1 10); do adom-bridge-cli --target <host> pup_readiness '{"reason":"probe"}'; sleep 3; done

If the probes stay ready through the load, the bridge is healthy under it. A DROP / error sending request during the load is the bug reproducing. Fix, reship, and re-run this exact stress until it is 10/10 responsive. That is the acceptance test.

What in pup's CDP handling actually starves the loop (the suspect list)

Everything here runs CDP traffic on the bridge's single event loop. On a heavy page, any of these can flood it:

  1. Debugger.enable kept on (the one that bit us, v1.9.341→347). neutralizeDebugger enabled the Debugger domain on every page and KEPT it enabled to skip debugger; pauses. Enabling Debugger makes Chrome stream a Debugger.scriptParsed event for EVERY script the page parses — thousands on a Babylon.js bundle — all processed on the loop, plus V8 runs de-optimized. Under concurrent heavy opens the loop starved → error sending request. FIX: enable → clear any inherited pause (Debugger.resume) → disable again → detach. A page's debugger; is a no-op with no client holding the domain, so immunity is kept without the flood. Lesson: never leave a chatty CDP domain (Debugger, Network with bodies, Log, Performance) enabled longer than one op; disable + detach.
  2. Undetached CDP sessions. createCDPSession without a matching .detach() leaks a session per call; each keeps delivering events. Grep createCDPSession vs .detach() — they should balance (the intentional long-lived ones are few and deliberate).
  3. Console-event handling. pup subscribes to page console and logs it; a page that spams the console spams the loop. pup already flood-suppresses ("suppressed N flood messages") — keep that, and never do heavy synchronous work (regex, JSON, disk) per console line.
  4. Screencast / Page.startScreencast (recorder): frames stream continuously; bound the rate and scope to the target, never leave one running.
  5. Synchronous heavy work on the loop — sharp compositing for overlays, the notch ICO build. sharp is threadpool-backed (fine), but any genuinely synchronous per-event work is not; defer/debounce.

The rule to internalize

When a page "crashes pup": prove it against nb or plain Chrome first. It will render there. That result is your evidence that the bug is yours, in the CDP layer — go find the domain you left enabled, the session you didn't detach, or the event storm you processed synchronously. Then reship and pass the concurrent-load stress test. Do not ship a "that page is too heavy" excuse; the correct bridge survives any page.