[2026-09-09] eagle: work/projects/cpm-web-extension-breakage-findings.md

This commit is contained in:
Alexey Martemyanov
2026-09-09 20:09:12 +06:00
parent df13bd2444
commit a60f8af473
@@ -1072,6 +1072,23 @@ Log confirmation (Network process): `log stream --predicate 'category == "Servic
Production frequency. The mechanism is unguarded in WebKit; what is unknown is how often P1 loses the race on user machines. The pixel data (probe 2) settles that. No fix proposed until the manual repro above is done. Production frequency. The mechanism is unguarded in WebKit; what is unknown is how often P1 loses the race on user machines. The pixel data (probe 2) settles that. No fix proposed until the manual repro above is done.
### ✅ REPRODUCED 2026-09-10: `kill -9 <background WebContent PID>` → CPM dead in all tabs (new tabs too), self-healed after ~1 min
Manual run by Alex: `kill -9` on the process reported by "Print CPM Diagnostics Snapshot" (`background_web_process_pid`). Result: banners stop being handled everywhere, new tabs included; revived on its own about a minute later (matches the 30 s idle-unload → next wake → fresh registration cycle).
This **contradicts** the earlier claim that a dead P1 is cleaned up before any wake. Two candidate sub-mechanisms, both ending in the same state (view alive, `m_backgroundContentIsLoaded` or a pending load, no working SW):
- **A. Stale SW server state at re-registration.** UI process: `didClose``processDidTerminateOrFailedToLaunch``webViewWebContentProcessDidTerminate``unloadBackgroundWebView` (`WebProcessProxy.cpp:1565-1617`, `WebExtensionContextCocoa.mm:2950-2957`); the next queued content-script message immediately creates the new page in a prewarmed process and registers. Network process: cleanup runs from `NetworkConnectionToWebProcess::didClose``unregisterSWConnection``SWServer::removeConnection``~WebSWServerConnection` → unregister clients → `registration->clear()` (`NetworkConnectionToWebProcess.cpp:470-518`, `SWServer.cpp:1368-1378`, `WebSWServerConnection.cpp:96-105`). If P2's `Register` lands first → "directly reusable registration" against the dead worker (same outcome as the idle-unload race above), or "needs updating" against a registration whose page id is the dead page.
- **B. New worker cannot start.** Old registration is gone, but the SW context connection for `webkit-extension://<uuid>` is still being torn down (`m_swContextConnection->stop()`), so the new worker's `createContextConnection` is deferred and the register job does not settle → `_loadServiceWorker` completion never fires → `m_backgroundContentIsLoaded=false`, actions queue, view retained until the 30 s timer → unload → next wake works.
Discriminating evidence to collect on the next run (kill → immediately reload a tab → within 10 s):
1. "Print CPM Diagnostics Snapshot": `background_view_alive`, `background_view_create_count` (2 = new view was created → A or B; 1 = delegate never fired), `background_web_process_pid` (new PID vs 0).
2. Our log: `Background web view created #2 … previousWebProcessAlive=false`, then `webProcessPID=<new>`; any `extension_context_errors` (`background_content_failed_to_load` → load actually failed).
3. `log stream --predicate 'category == "ServiceWorker" || category == "Extensions"'` during the dead minute: "Found directly reusable registration" / "needs updating" / "No existing registration … constructing a new one", and whether the register job ever resolves.
4. Stuck pixel / probe 2 (`background_sw_registration`) if the health monitor fires within the window.
Production relevance: any death of the background WebContent process — WebKit's own memory kill (`ExceededMemoryLimit`), jetsam on low-memory Macs, a JSC/WebCore crash in the worker — now maps to "CPM dead everywhere until 30 s of silence". `background_view_create_count` + `network_process_restarted=false` + `previousWebProcessAlive=false` in the pixel will measure how often this happens in the field.
### Side note checked (low value) ### Side note checked (low value)
`cpm.js` sends `init` inside the `AutoConsent` constructor and only afterwards calls `chrome.runtime.onMessage.addListener` (`cpm.js:3392-3401`). `tabs.sendMessage(initResp)` requires that listener to be registered in `m_eventListenerFrames` (`WebExtensionContextAPITabsCocoa.mm:483-497`). Both IPCs travel on the same connection and the background round trip is far slower, so this is not a realistic path — noted so nobody re-derives it. `cpm.js` sends `init` inside the `AutoConsent` constructor and only afterwards calls `chrome.runtime.onMessage.addListener` (`cpm.js:3392-3401`). `tabs.sendMessage(initResp)` requires that listener to be registered in `m_eventListenerFrames` (`WebExtensionContextAPITabsCocoa.mm:483-497`). Both IPCs travel on the same connection and the background round trip is far slower, so this is not a realistic path — noted so nobody re-derives it.