From 2292790c5cef54b1ddf897a0207b0ef138abb8e5 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Wed, 9 Sep 2026 12:06:58 +0600 Subject: [PATCH] [2026-09-09] eagle: work/projects/cpm-web-extension-breakage-findings.md --- .../cpm-web-extension-breakage-findings.md | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/work/projects/cpm-web-extension-breakage-findings.md b/work/projects/cpm-web-extension-breakage-findings.md index 6356f5c7..85d102a8 100644 --- a/work/projects/cpm-web-extension-breakage-findings.md +++ b/work/projects/cpm-web-extension-breakage-findings.md @@ -709,3 +709,62 @@ Outcome: health monitor becomes a classifier; every stuck pixel names its chain. 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. + + +## Status 2026-09-09 (evening) + +### Manual checks on shipping WebKit (macOS 26.x) + +| Check | Result | Meaning | +|---|---|---| +| Debug sim, wait 35 s idle, navigate | **recovered** | Idle eviction in `wakeUpBackgroundContentIfNecessary` is present in shipping WebKit; retained-failed-view is self-healing, as traced. | +| `sudo memory_pressure -S -l critical`, then CMP page | **CPM kept working** | Chain 1 not reproduced. Either `memoryPressureStatusChangedForProcess → TerminateIdleServiceWorkers` is not in this WebKit (it is under `ENABLE(WEB_PROCESS_SUSPENSION_DELAY)`, recent), or `-S` did not reach the WebContent process. Verify with `log stream --predicate 'process CONTAINS "WebKit.Networking" AND eventMessage CONTAINS "terminateIdleServiceWorkers"'`; until then chain 1 is downgraded to "future macOS". | +| Network process kill | pending | Use Debug → Web Extensions → "Terminate WebKit Network Process" (added, calls `_terminateNetworkProcess`) or lldb `[[WKWebsiteDataStore defaultDataStore] _networkProcessIdentifier]`. WebKit XPC processes are launchd children (ppid 1), so they cannot be found by parent PID. | + +### Implemented (branch `cpm-breakage-pixel`, not yet compiled here — build before trusting) + +`SharedPackages/WebExtensions`: +- `Autoconsent/CPMMessagingDiagnostics.swift` — the fact struct and its bucketed, PII-free `pixelParameters`. +- `Autoconsent/CPMMessagingDiagnosticsRecorder.swift` — `CPMMessagingDiagnosticsProviding` + recorder: critical-memory-pressure dispatch source; Network-process PID at context load vs now (`_networkProcessIdentifier` via guarded KVC); `errorsDidUpdateNotification` observer; background web view create count, current view (weak), leak count via `onDeinit` and a weak list, `_webProcessIdentifier` liveness; `openTabs` membership; probes: `loadBackgroundContent` with timeout, `navigator.serviceWorker.getRegistration()` in the background page, `globalThis.__ddgCPM` in `WKContentWorld.world(name: "WebExtension-")`. +- `CPMMessagingHealthMonitor.diagnosticsProvider` — failure pixels get content-script probe + snapshot; stuck pixels also get background probes. Without a provider behaviour is unchanged (tests rely on that). +- `WebExtensionPixelEvent.cpmInitializationFailed/.cpmMessagingStuck` carry `diagnostics:`; `CPMWebExtensionPixelMetadata.parameters` renders them. +- `WebExtensionManager`: owns the recorder, feeds `willLoad`, unload (uninstall / unloadAll / reload) and the private `didCreateBackgroundWebView` delegate into it. + +`macOS`: +- `WebExtensionManagerFactory` creates the recorder with a `Tab.uuid` → `Tab` resolver (`WindowControllersManager.loadedTab(withUUID:)`, new). +- Debug menu: "Terminate WebKit Network Process", "Print CPM Diagnostics Snapshot". + +Tests: `CPMMessagingDiagnosticsTests` (bucketing, sanitization, descriptors, recorder lifecycle), `CPMMessagingHealthMonitorDiagnosticsTests` (async attachment, probe levels). + +### Pixel parameters now attached + +`extension_context_loaded`, `memory_pressure_critical` (none/under_1_min/under_5_min/under_30_min/over_30_min), `network_process_restarted`, `extension_context_errors` (`background_failed_to_load:NSURLErrorDomain:-1100` style, or `none`), `background_view_create_count` (0/1/2/3_to_5/over_5), `background_view_alive`, `background_view_leaked_count`, `background_web_process_alive`, `tab_known_to_webkit`, `content_script` (present/marker_absent/unavailable), `content_script_lifecycle`, `content_script_init_response`, `content_script_send_error` (none/tab_not_found/extension_id_mismatch/other); stuck only: `background_load_probe` (loaded/failed:/timed_out/unavailable), `background_sw_registration` (present/missing), `background_sw_state`. + +### Required extension change (probe 1 reads `marker_absent` until this ships) + +In the autoconsent content script (`cpm.js` source, `shared/js/cpm.js` in the extension repo), publish the marker on the isolated-world global: + +```js +globalThis.__ddgCPM = { injectedAt: Date.now(), lifecycle: 'created', receivedInitResponse: false, lastSendError: null }; + +const consent = new AutoConsent(async (msg) => { + try { + await chrome.runtime.sendMessage({ messageType: 'autoconsent', autoconsentPayload: msg }); + } catch (e) { + globalThis.__ddgCPM.lastSendError = String(e && e.message || e); + } +}); +chrome.runtime.onMessage.addListener((message) => { + if (message && message.type === 'initResp') globalThis.__ddgCPM.receivedInitResponse = true; + return Promise.resolve(consent.receiveMessageCallback(message)); +}); +// in AutoConsent.updateState({ lifecycle }): globalThis.__ddgCPM.lifecycle = lifecycle; +``` + +Keys read by the recorder: `lifecycle` (string), `receivedInitResponse` (bool), `lastSendError` (string|null). Values are sanitized/classified before reaching the pixel. + +### Not done on purpose + +- OSLogStore-based counters (per request). +- iOS wiring (factory passes no recorder; behaviour unchanged there). +- Breakage-report fields — `CPMMessagingDiagnosticsRecorder.snapshot().pixelParameters` is ready to be merged into `CookieConsentInfo` when wanted.