---
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 Chrome** — `nbrowser_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**:

```bash
# 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.
