From 49ae1c15e7cf9583aff514d1ec403cf4bf771b7b Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Fri, 25 Sep 2026 14:49:20 +0600 Subject: [PATCH] [2026-09-25] eagle: work/projects/cpm-service-worker-architecture-and-diagnostics.md work/projects/cpm-web-extension-breakage-findings.md work/projects/cpm-web-extension-breakage-validation.md --- ...ice-worker-architecture-and-diagnostics.md | 173 ++++++++++++++++++ .../cpm-web-extension-breakage-findings.md | 20 +- .../cpm-web-extension-breakage-validation.md | 1 + 3 files changed, 184 insertions(+), 10 deletions(-) create mode 100644 work/projects/cpm-service-worker-architecture-and-diagnostics.md diff --git a/work/projects/cpm-service-worker-architecture-and-diagnostics.md b/work/projects/cpm-service-worker-architecture-and-diagnostics.md new file mode 100644 index 00000000..24f9c986 --- /dev/null +++ b/work/projects/cpm-service-worker-architecture-and-diagnostics.md @@ -0,0 +1,173 @@ +# CPM Service Worker — architecture, diagnostics, failure gaps + +> **2026-09-25 · source audit, not a new device reproduction.** Start with §4: the five validation priorities. +> Current assessment supersedes the architecture/diagnostics assumptions in [[cpm-web-extension-breakage-findings]]. Milestone status remains in [[cpm-web-extension-breakage-validation]]. +> +> **Answer:** no, we do not measure every useful signal. Existing telemetry describes the extension context and hosting process reasonably well, but cannot establish worker readiness, successful event delivery, native-request progress, or delivery of `initResp` to the current document. Some missing signals are implementable; others are WebKit internals or test-only facilities. + +## 1. What the Service Worker is in CPM + +The bundled extension is MV3, version **2026.9.2**. Its classic service worker is `public/js/background-embedded.js`; `public/js/content-scripts/cpm.js` runs at document start in an isolated world in all matching frames. A background worker is shared by the tabs using that extension context; it is not one worker per tab and not necessarily one across every profile/data store. + +It is the extension's background JavaScript environment: message router, native-settings/configuration access, CPM rule selection and replies. It has a worker global, not the page DOM. CPM uses extension messaging—not the website Service Worker fetch/offline-cache path. + +```text +Page/frame: isolated CPM content script + runtime.sendMessage(init) + → WebKit UI process: resolve tab → check listener/wake background + → background Service Worker: runtime.onMessage + ↔ Swift native-messaging handlers: settings, configuration, dashboard + → tabs.sendMessage(initResp, frameId) + → current page/frame must actually receive and apply the reply + +Network process: SW registrations, jobs, clients and worker-process connections +Hidden WKWebView: host page connecting the extension context to its worker global +``` + +**Important WebKit-specific detail:** the hidden view registers the worker through a generated `navigator.serviceWorker.register(...)` page. For workers backed by such a page, this checkout selects **`WorkerThreadMode::UseMainThread`**. JavaScript executes on the hosting WebContent process's main thread, despite having a separate worker global. Extension bindings are installed by locating that host page **inside the same process**. + +Consequently, these are different states: + +| Observable | What it actually proves | +|---|---| +| `context.isLoaded` | Context is associated with its controller; asynchronous setup may still be pending. | +| Hidden view exists / nonzero PID | A host object/process identifier exists—not a usable worker. | +| SW registration is active | Registration lifecycle state—not live event-loop progress or extension bindings. | +| Background load completed | WebKit's registration/load path settled—not successful CPM round-trip. | +| CPM dashboard response | Background/native path answered—not necessarily `initResp` delivery or consent handling. | + +**Lifecycle:** normal nonpersistent background eviction is scheduled after 30 seconds of idle time. Repeated wake-ups, ports, permission requests or inspection can prevent/delay it. Process termination closes the host; a nonpersistent background is recreated on demand. Failed registration records error 6 and can leave the host and pending actions retained until eviction. A retry only helps if its underlying cause is gone. + +Sources: [background host lifecycle](https://github.com/WebKit/WebKit/blob/0c7e7ad97b9c50e549387914d31ecc2908ae0171/Source/WebKit/UIProcess/Extensions/Cocoa/WebExtensionContextCocoa.mm#L2603), [registration page](https://github.com/WebKit/WebKit/blob/0c7e7ad97b9c50e549387914d31ecc2908ae0171/Source/WebKit/UIProcess/WebPageProxy.cpp#L18459), [main-thread selection](https://github.com/WebKit/WebKit/blob/0c7e7ad97b9c50e549387914d31ecc2908ae0171/Source/WebCore/workers/service/server/SWServerWorker.cpp#L427), [page-dependent worker binding](https://github.com/WebKit/WebKit/blob/0c7e7ad97b9c50e549387914d31ecc2908ae0171/Source/WebCore/workers/service/ServiceWorkerGlobalScope.cpp#L118). + +## 2. What we measure, what is missing + +### Existing coverage: useful but mostly indirect + +| Area | Current signal | Limitation | +|---|---|---| +| Context/errors | `context_loaded`, `context_errors` including immediate underlying domain/code | Errors combine historical and current state. WebKit deduplicates errors; neither array length nor notifications count failures reliably. | +| Background lifecycle | `bg_view_creations`, `bg_view_alive`, `bg_view_leaked_count`, `bg_process_alive`, `bg_process_responsive`, `bg_events` | Object/PID and cached responsiveness are not worker readiness. Timeline records load failures, deaths, responsiveness and retention, not message progress. | +| Routing prerequisites | `tab_in_context`, `handler_registered`, `tab_controller_match`, `tab_has_ext_scripts` | Membership/registration/script metadata do not prove actual injection, handler execution or reply delivery. | +| Environment | `app_version_change`, `app_launch_age`, `critical_memory_age`, `network_restarted` | Correlation, not causation. Network PID comparison is not a worker-connection health check. | +| Experiment/outcome | Cohort, termination/outcome events, init-failure/stuck/recovery events | Current “healthy” means a valid dashboard response. It is not an end-to-end CPM-success metric. | + +Two measurement confounds are source-confirmed: + +- **Intentional retention can count as a leak.** The leak calculation is live weak-tracked views minus one expected current view. During overlap with a replacement, the graveyard's deliberately retained old view can increase `bg_view_leaked_count`. +- **The timeline cannot resolve the 3-second race.** Its first age bucket is 30 seconds; it retains at most 12 events/255 characters. Ordering helps, but local monotonic timestamps and context/view generations are needed for causal timing. + +The recorder's weak “current view” also survives while another owner retains it. Comparing it with WebKit's actual current `_backgroundWebView` would distinguish an intentionally retained, closed host from the context's active host. + +### Useful additions within our control + +| Missing evidence | Proposed measurement | Boundary | +|---|---|---| +| Worker boot / listener readiness | Worker-generated boot + listener-installed marker, associated locally with a context/worker generation | An application protocol addition, not an existing native WebKit readiness callback. Absence alone cannot distinguish failed boot from failed native delivery. | +| Request progress | Host-side arrival/completion/error, outstanding-count and oldest-request-age buckets per allowlisted native method | Independent of the JS diagnostic channel. No URLs, payloads, arbitrary method strings or raw error text in pixels. | +| Actual initialization delivery | Document-scoped init/reply acknowledgement; distinguish background-received, reply-sent and page-acknowledged | Must reject late responses from previous navigation/context generations. Do not rename this metric “consent handled.” | +| Script/load failure phase | Separate current vs previously observed error codes; classify known boot/runtime/native stages | Code 6 is generic load failure. Code 7 is script execution error in this checkout; OS support needs verification. | +| Worker exception contract | Handle the bundle's `autoconsent.extensionException` with bounded, privacy-reviewed categories | **Confirmed mismatch:** bundle emits it; Swift `Method` has no matching case and returns unknown-method failure. | + +The exception mismatch does **not** mean all errors are invisible: WebKit's own error channel may report some of them. Nor will fixing it catch everything: the bundle's extension polyfill can throw **before** its global error/rejection listeners are installed, and the exception notification itself uses the same potentially failing native channel. + +### WebKit diagnostics that are not drop-in production telemetry + +| Facility | What it offers | Why it needs care | +|---|---|---| +| Public `loadBackgroundContent(completionHandler:)` | Explicit attempt to wake/load background | **Active intervention.** Changes idle timing and can repair/perturb the episode. Completion queues behind load and may remain pending on failure. Use only a deliberate debug probe with timeout/generation protection. | +| Private `_backgroundWebView`, `_backgroundContentURL`; context error updates | Current host identity and failure information | Availability-gate SPIs. Do not upload raw URLs/descriptions/stacks. Preserve WebKit's delegate behavior. | +| Worker Inspector + ServiceWorker/Extensions logs | Worker global, exceptions, breakpoints, registration/job/connection/PID evidence | Inspector changes worker lifetime. Reproduce detached; use inspection afterward or in a separate run. This checkout's Inspector ServiceWorker protocol is not Chromium's full lifecycle protocol. | +| `_runningOrTerminatingServiceWorkerCountForTesting`; datastore console callback | Aggregate test count; conditional console forwarding | Count is not CPM-specific. Console forwarding is disabled by default; the enabling call found in this tree is in `ServiceWorkerInternals` testing code. Implementing the datastore delegate alone does not enable a production worker-console stream. | +| SWServer maps/queues, registration-worker-host affinity, listener destinations, queued wake-up actions | The decisive internal state for several failures | No passive public native accessor found. Requires existing internal logs or an instrumented WebKit build; do not invent KVC access to C++ state. | + +**Two tempting probes are invalid:** + +- The generic worker heartbeat is **disabled** for `WorkerMainRunLoop`. It cannot serve as CPM's extra 60-second watchdog. Existing UI-side process-responsiveness monitoring is a different mechanism. +- `evaluateJavaScript` on the hidden view evaluates a frame/document global, not the Service Worker's global. Missing `browser` there does not prove missing worker bindings. A registration query likewise does not prove the worker can answer. + +Sources: [error recording](https://github.com/WebKit/WebKit/blob/0c7e7ad97b9c50e549387914d31ecc2908ae0171/Source/WebKit/UIProcess/Extensions/Cocoa/WebExtensionContextCocoa.mm#L230), [worker script-error callback](https://github.com/WebKit/WebKit/blob/0c7e7ad97b9c50e549387914d31ecc2908ae0171/Source/WebKit/WebProcess/Extensions/Cocoa/WebExtensionControllerProxyCocoa.mm#L110), [active load API implementation](https://github.com/WebKit/WebKit/blob/0c7e7ad97b9c50e549387914d31ecc2908ae0171/Source/WebKit/UIProcess/Extensions/WebExtensionContext.cpp#L1787), [heartbeat exclusion](https://github.com/WebKit/WebKit/blob/0c7e7ad97b9c50e549387914d31ecc2908ae0171/Source/WebCore/workers/service/context/ServiceWorkerThread.cpp#L570), [conditional console forwarding](https://github.com/WebKit/WebKit/blob/0c7e7ad97b9c50e549387914d31ecc2908ae0171/Source/WebCore/workers/service/ServiceWorkerGlobalScope.cpp#L265), [test-only enablement](https://github.com/WebKit/WebKit/blob/0c7e7ad97b9c50e549387914d31ecc2908ae0171/Source/WebCore/testing/ServiceWorkerInternals.cpp#L208). + +## 3. The process-death failure and what graveyard actually changes + +**Historical evidence:** the September 10 investigation recorded forced background-process death causing the all-tabs registration-error loop. This audit checked the supporting source chain; it did not repeat that device test or establish natural production incidence. + +```text +Background process dies + → Network closes SW connection before unregistering that process's clients + → remaining clients cause a connection request referencing the old host page + → if old WebProcessProxy is gone, process selection may create a standalone worker process + → new registration reuses that domain's existing connection + → worker cannot find its host page in this other process + → browser/chrome bindings are not installed + → bundle's initial polyfill throws + → registration fails → error 6 → later retries may repeat +``` + +An existing SW connection is selected by domain/COEP rather than by the new host page's process affinity. Merely creating a fresh background view therefore does not necessarily repair the connection. + +**Graveyard treatment retains the terminated WKWebView and its native proxy object graph.** The old process proxy can still resolve when WebKit services the stale connection request. In this source branch, the initial host-page process lookup does not exclude a terminated proxy. The intended effect is to make that stale request fail/cancel instead of taking the standalone-process fallback while old clients drain. It does **not** revive the worker or fix its JavaScript/native handler. + +The exact winning timing remains an experimental hypothesis, not a WebKit guarantee. Current implementation holds **one** view with a **3-second scheduled release**. A second death replaces it immediately; main-queue blockage/app suspension can delay release beyond three wall-clock seconds. Context transitions also release it. + +**M2 changes the recovery assumption.** The current M5 loader still uses the default changing origin; separate M2 assigns `webkit-extension://com.duckduckgo.web-extension.embedded/`. After M2, extension reload cannot rely on escaping a poisoned connection via a new origin. Fire may also clear data, so its behavior must be tested separately—not assumed equivalent to reload. + +Sources: [connection teardown order](https://github.com/WebKit/WebKit/blob/0c7e7ad97b9c50e549387914d31ecc2908ae0171/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp#L470), [connection recreation](https://github.com/WebKit/WebKit/blob/0c7e7ad97b9c50e549387914d31ecc2908ae0171/Source/WebCore/workers/service/server/SWServer.cpp#L1785), [process selection](https://github.com/WebKit/WebKit/blob/0c7e7ad97b9c50e549387914d31ecc2908ae0171/Source/WebKit/UIProcess/WebProcessPool.cpp#L709), [existing connection reuse](https://github.com/WebKit/WebKit/blob/0c7e7ad97b9c50e549387914d31ecc2908ae0171/Source/WebCore/workers/service/server/SWServer.cpp#L1068). + +## 4. Five validation priorities + +These are **under-tested paths, not five newly proven root causes**. Several broad hypotheses existed in the old note; this audit adds concrete mechanisms and discriminating tests. + +### 1. Stable-origin reload and old-registration teardown — highest integration risk + +**Source-confirmed paths; combined race unproven.** Registration can immediately reuse an existing same-script/type/cache registration without rebinding its host-page identifier in that branch. Old host-client removal separately clears its associated registration. A rapid same-origin replacement may therefore reuse a registration still owned by the old page, which later gets cleared. Separately, the poisoned domain connection can survive reload. + +**Test:** combine M2 with M5+M6; force background death in control/treatment, then test extension reload and Fire separately. Also rapidly unload/reload a healthy context without killing it. Capture register/reuse/unregister order, host/worker PID affinity and page acknowledgement. Require no late old-generation teardown to invalidate the new worker. + +Sources: [reusable registration fast path](https://github.com/WebKit/WebKit/blob/0c7e7ad97b9c50e549387914d31ecc2908ae0171/Source/WebCore/workers/service/server/SWServerJobQueue.cpp#L324), [old page unregister](https://github.com/WebKit/WebKit/blob/0c7e7ad97b9c50e549387914d31ecc2908ae0171/Source/WebCore/workers/service/server/SWServer.cpp#L1526). + +### 2. Main-thread stall, event flood and missing startup deadline + +**Heartbeat exclusion confirmed; production trigger unproven.** A genuine synchronous background stall cannot be detected by the disabled worker heartbeat. Main-thread JS/native work, repeated all-frame init traffic, or a registration that never settles can leave a live-looking host with no progress. Repeated events can prevent idle eviction; retained load actions have no CPM-owned deadline. An unresolved Promise alone is not a blocked event loop. + +**Test:** separately inject a controlled background busy loop, delayed registration and sustained frame churn. Stop the producer and check drain/recovery. Capture process responsiveness, startup age, native outstanding work and page acknowledgements. Test iOS suspend/resume separately from foreground hangs. Ordinary page JS is only relevant if it actually shares the affected process; do not assume it blocks the extension. + +### 3. Native errors/timeouts hidden by the diagnostic transport + +**Contract mismatch and 20-second timeout confirmed; natural stall unproven.** The worker races native requests against 20 seconds, but does not cancel native work. Failure can become `undefined` and disabled settings. Exception/diagnostic notifications travel over that same native channel, and `extensionException` is currently unsupported. Session-storage/configuration operations can also fail before a usable init reply. + +**Test:** independently fail/delay settings, feature and config calls; reject session-storage access; inject a runtime exception. Assert host-side arrival/completion/timeout evidence, bounded outstanding work and explicit failure stages. Repeated frame churn must not leave unlimited old native requests. + +### 4. “Healthy” before response delivery; stale document replies + +**Source-confirmed measurement gap; user-visible race needs reproduction.** The background schedules dashboard stage `init_received` before rule filtering and `tabs.sendMessage(initResp)`. Swift credits a valid dashboard response as healthy without requiring that stage to prove page receipt. The reply send is not awaited/caught here and targets `frameId`, not a document identifier. Navigation while native work is pending can drop the reply or target a newer document. + +**Test:** delay configuration, navigate the same frame, then release the reply; separately reject the reply send or throw during rule filtering. Distinguish “background answered” from “current document initialized.” Require document/generation attribution before claiming end-to-end recovery. + +### 5. Registration failure vs listener-routing failure + +**Both source paths exist; no new production transition demonstrated.** Generic error 6 merges resource/script startup failures with wrong-process binding failure. Separately, persisted listener metadata and live listener-frame destinations can disagree: WebKit can skip wake-up or have no usable receiver despite a loaded-looking context. + +**Test:** independently inject a missing/throwing background resource and a controlled no-listener case; compare cold launch, normal idle eviction and context reload. Capture the load result, boot/listener marker and request arrival. In an instrumented WebKit build inspect listener sets/destinations and pending actions. Do not infer a listener bug from PID/alive alone. + +## 5. What not to conclude; next implementation boundary + +**Deprioritized or disproven explanations:** this WebKit checkout explicitly does **not persist extension-backed SW registrations to disk**, so ordinary website SW-database corruption across relaunch is not the leading explanation. Extension listener-state/session/local storage are separate and still deserve validation. `removeAllUserScripts()` excludes WebExtension scripts. Earlier negative memory-pressure/Network-restart tests remain negative on their tested OS—not universal proof those paths cannot fail. + +**Recommended order:** fix the exception-reporting contract and define request/document-stage evidence first; run priority 1 with stable origin before relying on reload as recovery; then exercise priorities 2–5. Instrumentation must preserve control/treatment behavior, avoid unsolicited wake-ups, keep raw data local and receive normal pixel/privacy review. + +**Scope and evidence:** WebKit checkout `0c7e7ad97b9c50e549387914d31ecc2908ae0171` (2026-08-23), tracked sources clean. App audit used the current dirty `cpm-graveyard-mitigation` worktree (HEAD `4de02983986c47769e4e5ce0d267c23f3ca8d48c`) and inspected M2 separately in `cpm-stable-base-url`. The old `cpm-breakage-pixel` worktree is not the current M5+M6 implementation. Exact behavior/SPI availability must be checked on supported iOS and macOS releases; trunk source is not proof of the shipped OS binary. + +App evidence is in `SharedPackages/WebExtensions/Sources/WebExtensions/`: + +| Source | Relevant evidence | +|---|---| +| `BundledWebExtensions/duckduckgo-embedded-web-extension.zip` | Manifest; background JS lines 64, 2645–2693, 2955–2999, 3300–3347: early polyfill, dashboard-before-reply, native timeout, exception notifications. | +| `Messaging/MessageHandlers/AutoconsentWebExtensionMessageHandler.swift` | Methods 30–41 omit extensionException; 68 rejects unknown method; dashboard response forwarded without requiring initResp acknowledgement. | +| `Autoconsent/CPMMessagingHealthMonitor.swift` | Healthy outcome at 442/680 represents dashboard response, with existing tab/generation safeguards. | +| `Autoconsent/CPMMessagingDiagnostics{,Recorder}.swift` | Pixel coverage, 30-second minimum timeline age bucket, historical error accumulation, leak-count calculation at recorder 506. | +| `Autoconsent/CPMBackgroundWebViewGraveyard.swift` | Dedicated retention entity, one retained view, 3-second scheduling, replacement/reset behavior and experiment outcomes. | + +Additional source: [extension workers are not persisted](https://github.com/WebKit/WebKit/blob/0c7e7ad97b9c50e549387914d31ecc2908ae0171/Source/WebCore/workers/service/server/SWServerWorker.cpp#L561). + +**Validation status:** source tracing and bundle/Swift contract comparison completed. No new device reproduction, build, unit test or app-code change was performed for this documentation audit. diff --git a/work/projects/cpm-web-extension-breakage-findings.md b/work/projects/cpm-web-extension-breakage-findings.md index ac4a3180..d456b9d7 100644 --- a/work/projects/cpm-web-extension-breakage-findings.md +++ b/work/projects/cpm-web-extension-breakage-findings.md @@ -10,6 +10,8 @@ tags: --- # CPM Web Extension Breakage Findings +> **Current architecture/source audit — 2026-09-25:** [[cpm-service-worker-architecture-and-diagnostics]]. Read it for Service Worker architecture, measured/missing diagnostics and five prioritized validation gaps. The investigation below is historical evidence, not a current readiness verdict. In particular: extension workers use the host process’s main thread; their generic SW heartbeat is disabled; dashboard “healthy” does not prove page receipt; and M2’s stable origin requires revalidating reload/Fire recovery. No new device reproduction was performed on September 25. + > Investigation summary as of 2026-09-08. This note distinguishes confirmed code paths from hypotheses that still need production evidence. > > **2026-09-10: two additional app-side tab-lifecycle violations found in production-reachable code** — `Close Other Tabs` sends `didCloseTab` for the one loaded tab it keeps, then puts the same `Tab` object back without `didOpenTab`; `Duplicate Tab` invoked from a popup inserts the original `Tab` into the main window instead of a copy, so closing the popup makes WebKit forget the still-live tab. Both produce a deterministic **single-tab** routing failure: reload does not help, new tabs work, and Fire / extension reload / tab recreation repairs it. The first chain is source-confirmed and has a minimal manual repro below; the popup chain is source-confirmed and still needs a manual run. @@ -35,7 +37,7 @@ The debug-menu simulation enters the third state deliberately. It proves the hea The strongest explanation for the observed **old/specific tabs dead, new tabs fine** shape is now the app violating WebKit's tab lifecycle contract. It has three concrete instances, all ending in the same `m_tabMap` loss. The separate **all tabs including new tabs** shape still points to the background-service-worker lifecycle and must not be conflated with the tab-routing failure. -## Current conclusions (authoritative, 2026-09-10) +## Historical conclusions (2026-09-10; current source audit linked above) Use the scope first when classifying an episode: @@ -55,17 +57,15 @@ Use the scope first when classifying an episode: Explicitly not active production hypotheses: critical-memory-pressure termination (manual test negative), cold-launch/restoration miss (transient and fixed), feature-flag transitions (not a production event), privacy-config scriptlet extension reload (different extension context), `removeAllUserScripts()` (WebExtension scripts are excluded), and a normal page's long-running JavaScript by itself (limited to that page's WebContent thread). -## CPM architecture +## CPM architecture (refreshed 2026-09-25) -The bundled extension is Manifest V3: +The bundled MV3 extension (2026.9.2) runs `public/js/content-scripts/cpm.js` at `document_start` in isolated worlds/all matching frames. Its shared background entry point is the classic Service Worker `public/js/background-embedded.js`. -- Background entry point: `public/js/background-embedded.js` -- CPM content script: `public/js/content-scripts/cpm.js` -- Injection: `document_start`, isolated world, all frames -- Content-to-background transport: `chrome.runtime.sendMessage` -- Background receiver: `runtime.onMessage`, registered by `MessageRouter` +`content script → runtime.sendMessage → WebKit tab/listener routing → background worker → Swift native settings/configuration → tabs.sendMessage(initResp) → page acknowledgement` -The content script sends an `autoconsent` message to the background service worker. The background routes that message and, where required, forwards it through native messaging. +WebKit creates a hidden `WKWebView` whose page registers the worker. In the inspected checkout this extension worker uses **the host WebContent process’s main thread**, with a separate worker global. WebKit must find the host page in that process to install `browser`/`chrome` bindings. The Network process owns registration/jobs/client and connection bookkeeping. This is extension messaging, not a website’s offline/fetch Service Worker. + +Context loaded, host alive, registration active, listener ready and current-document reply received are **different states**. Current diagnostics mostly observe the first two. Dashboard responses can precede `initResp`; the worker heartbeat is disabled for this main-thread mode. Detailed coverage, source links and validation priorities: [[cpm-service-worker-architecture-and-diagnostics]]. ## Normal message path @@ -1216,7 +1216,7 @@ Symptom match: all tabs, new tabs too, survives reload, banners not handled, das **Observation (Alex, 2026-09-10):** with the delegate proxy installed (its lifetime tied to the background `WKWebView`), `kill -9` no longer breaks CPM; without the proxy, or with the proxy tied to the original delegate, it does. Reading: the outcome of step 4 depends on whether the dead P1 still exists as a `WebProcessProxy` when the Network process asks for the context connection — `processForIdentifier` is cleared only in `~WebProcessProxy` (`WebProcessProxy.cpp:488`), which is kept alive by the closed `WebPageProxy`, i.e. by the `WKWebView`. If P1 still resolves: `useProcessForRemoteWorkers(P1)` → send to a dead process → cancelled → Network's retry finds no clients → clean. If P1 is gone: fallback → stray process S → bug. The proxy shifts the view's dealloc by enough to win that race — accidental timing, not a fix. -**Graveyard experiment (implemented 2026-09-10):** `FeatureFlag.cpmBackgroundGraveyardExperiment` (subfeature `webExtensions.cpmBackgroundGraveyard`, cohorts `control`/`treatment`). Treatment: on the termination callback the recorder holds the dead background `WKWebView` strongly for `CPMMessagingDiagnosticsRecorder.graveyardHoldDuration` (3 s), so `processForIdentifier(P1)` keeps resolving while the Network process re-requests; control releases immediately. Pixel: `background_graveyard=control|treatment`. Prediction: treatment never shows `error_background_failed_to_load` after `died_*`; control does. Deterministic A/B: local-override the cohort, `kill -9`, watch `background_events`. +**Graveyard mechanism (refreshed 2026-09-25):** current M5+M6 uses the dedicated `CPMBackgroundWebViewGraveyard`, with control/treatment selected by `cpmBackgroundGraveyardExperiment`. Treatment retains one terminated `WKWebView` and schedules release after 3 seconds. Keeping the native proxy graph alive lets the stale host-page process lookup resolve the terminated proxy instead of falling back to a standalone worker process. The intended effect is cancellation/failure of the stale connection request while clients drain—not revival of the dead worker. This timing benefit is an experimental hypothesis, not a guarantee that treatment never fails. A second death replaces the retained view; app suspension/main-queue blockage can delay scheduled release. Current diagnostics use `background_graveyard_cohort` and `bg_events`, alongside dedicated termination/outcome events. See [[cpm-service-worker-architecture-and-diagnostics]] for the source chain, M2 stable-origin interaction and outcome-measurement limitations. **Delegate proxy flag:** `FeatureFlag.cpmBackgroundDelegateProxy` (subfeature `webExtensions.cpmBackgroundDelegateProxy`, remote-releasable, on by default). The recorder observes `FeatureFlagger.updatesPublisher`; turning it off hands WebKit's original delegate back on the live view (`CPMBackgroundWebViewDelegateProxy.uninstall`), turning it on re-installs. diff --git a/work/projects/cpm-web-extension-breakage-validation.md b/work/projects/cpm-web-extension-breakage-validation.md index e03d5c45..5e0c0a76 100644 --- a/work/projects/cpm-web-extension-breakage-validation.md +++ b/work/projects/cpm-web-extension-breakage-validation.md @@ -2,6 +2,7 @@ > Актуально на 2026-09-23. Полная трассировка: [[cpm-web-extension-breakage-findings]]. > Experiment Definition: [[cpm-background-graveyard-experiment-definition]]. +> **2026-09-25 source audit:** [[cpm-service-worker-architecture-and-diagnostics]]. Earlier readiness/test results below are historical, not validation of the newly identified gaps. Additional gates: M2 stable-origin + M5/M6 reload/Fire interaction; worker exception contract; distinguish dashboard response from actual page initialization. No new device tests were run for this audit. ## Решение