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

This commit is contained in:
Alexey Martemyanov
2026-09-09 11:21:11 +06:00
parent 666be9f159
commit 3c59befe5f
@@ -661,3 +661,51 @@ No hook; `m_backgroundContentEventListeners` is not exposed. If everything else
6. `cpmBgSwRegistration` / `cpmBgSwState` via the SW-page probe — confirms 1 vs 2. SPI-adjacent (needs the background `WKWebView` reference).
Items 14 are enough to attribute the current stuck pixels; 56 turn the health monitor into a classifier instead of a timer.
## Next steps (2026-09-09)
Ordered so each step de-risks the next. Days are rough effort, not calendar.
### Phase 0 — confirm trunk matches shipping WebKit (½ day, manual, no code)
1. Debug sim persistence: enable "fail CPM background load", load one CMP page (fails), wait 35 s without navigating, load again. Expected: recovers. If it stays broken, shipping WebKit lacks the idle-eviction-on-wake-up path and chain 5/6 ranking changes.
2. Chain 1 repro: launch, wait 15 s, `sudo memory_pressure -S -l critical` for ~5 s, load a CMP page. Expected: no banner handling, `errors` empty, SW inspector target gone, recovery after 35 s idle.
3. Chain 2 repro: `kill -9` DDG's `com.apple.WebKit.Networking`, load a CMP page. Same expectation.
4. Chain 1 revive test: after step 2, in the background `WKWebView` (grab it from `didCreateBackgroundWebView` behind a debug flag) run `navigator.serviceWorker.getRegistration().then(r => r.active.postMessage({type:"ping"}))`, then load a CMP page. If CPM works again, we have a remediation that avoids the reload window.
Outcome: which chains are real on macOS 26.x, and whether `postMessage` revive is viable.
### Phase 1 — attribution in existing pixels (23 days)
Add to `CPMMessagingHealthMonitor` failure/stuck/recovered pixels and to `BrokenSiteReport`:
1. `cpmMemPressureCriticalRecently` / `cpmSecondsSinceCritical` — in-app `DispatchSource.makeMemoryPressureSource`.
2. `cpmNetworkProcessRestarted` — compare `WKWebsiteDataStore.default()._networkProcessIdentifier` against the value recorded at context load (SPI; fall back to OSLogStore crash-line count if SPI is off the table).
3. `cpmContextErrors` (`errors` codes + underlying NSURLError) and `cpmBgViewCreateCount` (count `didCreateBackgroundWebView`).
4. `cpmTabKnownToWebKit` (`context.openTabs.contains(tab)`) and `cpmTabNotFoundLogCount` (OSLogStore, own PID, `category == "Extensions" AND eventMessage CONTAINS "Tab not found"`).
5. `cpmContextLoadedAtCommit`, `cpmMsSinceLastReload`.
Ship behind the existing pixel plumbing; no extension change needed. After one release the stuck pixels split into chains 1 / 2 / 3 / 4 / 5.
### Phase 2 — probes that classify at failure time (23 days, needs `cpm.js` + app)
1. `cpm.js`: publish `globalThis.__ddgCPM = { injectedAt, lifecycle, initSentAt, gotInitResp, lastSendError }`; wrap the `sendMessage` `await` in try/catch to capture `tab not found`.
2. App: on `cpmInitializationFailed`, evaluate `globalThis.__ddgCPM` in `WKContentWorld.world(name: "WebExtension-\(uniqueIdentifier)")` for the failing tab; attach `cpmContentScriptInjected`, `cpmContentScriptLifecycle`, `cpmLastSendError`.
3. App: on `cpmMessagingStuck`, run the SW-page probe on the background web view (`hasRegistration`, `active.state`) and `loadBackgroundContent` with a 2 s timeout; attach `cpmBgSwRegistration`, `cpmBgLoadProbe`.
Outcome: health monitor becomes a classifier; every stuck pixel names its chain.
### Phase 3 — fixes (in order of confidence)
1. **Stable base URL** (½ day): `context.baseURL = URL(string: "webkit-extension://\(identifier)/")` in `WebExtensionLoader.makeContext`. Removes `_renameOrigin` from every load, shrinks the startup miss window (chain 3), and stops migrating extension web storage on every launch. Verify existing `storage.local` is unaffected (it is SQLite, not origin-keyed web storage) and that the first launch after the change performs one final rename from the last random URL.
2. **Self-heal on stuck** (1 day): on `cpmMessagingStuck`, if Phase 0 step 4 works → `postMessage` revive first; else `reloadExtension(identifier:, trigger: .healthRecovery)`. Gate by a 5-minute cooldown; fire `cpmMessagingRecoveredAfterExtensionReload` (already exists).
3. **Content-script retry** (½ day, `cpm.js`): if no `initResp` within ~3 s, resend `init` (max 23 times, backoff). Covers chain 6 and any transient drop, and gives chain 1/2 a natural retry after the 30 s eviction instead of waiting for the next navigation.
4. **Keep the worker alive** (research, 1 day): the SW server only refreshes `needsRunning()` on server-routed events. A background `chrome.alarms` tick does not go through the SW server either. Options: (a) from the SW page, periodic `registration.active.postMessage` keep-alive — would also count as "not idle" for `terminateIdleServiceWorkers`; (b) file a WebKit bug: extension service workers should be exempt from `terminateIdleServiceWorkers`/`terminateServiceWorkers`, or `WebExtensionContext` should observe worker termination and unload/reload the background view. (b) is the real fix; (a) is the interim.
5. **Tab registration audit** (only if Phase 1 shows chain 4 volume): log every `didOpenTab`/`didCloseTab` with tab id in debug builds and diff against `context.openTabs` on stuck.
### WebKit bug reports to file (after Phase 0 confirms)
1. Extension background service worker terminated by `terminateIdleServiceWorkers` / `terminateServiceWorkers` while `WebExtensionContext` keeps `m_backgroundContentIsLoaded == true`; messages resolve `undefined` with no error. Attach chain 1 and 2 repro steps.
2. `_loadServiceWorker` failure path does not schedule `scheduleBackgroundContentToUnload()`, so recovery depends on a second message arriving (minor).
3. Documentation/API request: a delegate callback or notification for background content load/unload and worker start/stop.