[2026-09-14] eagle: work/projects/cpm-web-extension-breakage-validation.md
This commit is contained in:
@@ -1,108 +1,166 @@
|
||||
---
|
||||
title: CPM Web Extension Breakage — Hypothesis Validation
|
||||
date: '2026-09-08'
|
||||
status: validated
|
||||
tags:
|
||||
- work
|
||||
- cpm
|
||||
- webkit
|
||||
- web-extension
|
||||
related: '[[cpm-web-extension-breakage-findings]]'
|
||||
---
|
||||
# CPM Web Extension Breakage — Hypothesis Validation
|
||||
# CPM Web Extension Breakage — статус и план
|
||||
|
||||
Validation of [[cpm-web-extension-breakage-findings]] against source. Line numbers refer to:
|
||||
> Короткая актуальная версия на 2026-09-14. Полная трассировка остаётся в [[cpm-web-extension-breakage-findings]].
|
||||
|
||||
- WebKit trunk `~/Downloads/WebKit_sparse` @ `0c7e7ad97b` (2026-08-23). Shipping macOS WebKit is older; every path cited below is long-standing, but re-check line numbers against the shipping tag before quoting them in a bug report.
|
||||
- `apple-browsers` branch `cpm-breakage-pixel`; bundled extension `duckduckgo-embedded-web-extension.zip` v2026.9.2.
|
||||
## Итог
|
||||
|
||||
## Verdict table
|
||||
Есть две разные группы багов, их нельзя смешивать:
|
||||
|
||||
| Claim in findings doc | Verdict | Where |
|
||||
1. **All-tabs:** смерть background WebContent process может сломать CPM во всех вкладках, включая новые. Это главный кандидат для telemetry → graveyard → A/B.
|
||||
2. **Per-tab:** приложение сообщает WebKit, что ещё живая вкладка закрыта. Ломается только конкретная вкладка или shared pinned tabs. Это отдельные фиксы и отдельные PR.
|
||||
|
||||
## Статус проблем
|
||||
|
||||
| Проблема | Статус | Что известно / что делать |
|
||||
|---|---|---|
|
||||
| Failed `_loadServiceWorker` retains `m_backgroundWebView`, does not run pending actions, does not call `unloadBackgroundWebView` | **Confirmed** | `WebExtensionContextCocoa.mm:2666-2671` |
|
||||
| Later messages append to the queue and `loadBackgroundWebViewIfNeeded` returns because the view exists | **Confirmed** | `WebExtensionContext.cpp:1856-1865`, `WebExtensionContextCocoa.mm:2608-2616` |
|
||||
| "The 30-second eviction does not recover the failed-first-load sequence" | **Refuted** | `WebExtensionContext.cpp:1854` — every `wakeUpBackgroundContentIfNecessary` calls `scheduleBackgroundContentToUnload()` *before* the loaded check. Only the very first call (view still nil) skips the timer. The **second** CPM message from any tab arms a 30 s one-shot; `unloadBackgroundContentIfPossible` (`Cocoa.mm:2721-2758`) unloads the failed view (no ports, no inspector, no permission prompts for CPM); the next message creates a fresh view and retries. Queued actions survive the unload (`unloadBackgroundWebView` does not clear them) and run on the eventual success. |
|
||||
| Debug simulation models a persistent state | **Corrected** | Injection is one-shot (`WebExtensionManager.swift:648-652`). Per the above, the simulated freeze should end ≈30 s after the first post-reload CPM message, at the next message. If it is observed to persist longer than that, shipping WebKit differs from trunk — verify empirically. |
|
||||
| Lost-completion variants (completion never called) | **Mostly refuted** | `WebPageProxy::callLoadCompletionHandlersIfNecessary(false)` fires on `didFailProvisionalLoad` (8592), `didFailLoad` (9382) and `resetState` — which `close()` calls (2002 → 14084). So closing/evicting the view always settles a pending completion. A completion can hang only while the generated page is *loaded successfully* and the `register()` job never settles; eviction still resolves it. Client-side job abandonment always notifies the server (`ServiceWorkerContainer::stop` 711-725, `SWClientConnection::startScriptFetchForServer` 123-130), so the `SWServerJobQueue` cannot be wedged by a dead client. |
|
||||
| Network-process disconnect during registration → `completion(false)` | **Confirmed** chain as written | |
|
||||
| `m_safeToLoadBackgroundContent` gate blocked by `_renameOrigin` | **Confirmed shape, and it is on the critical path on every load** | See "Base URL rename" below |
|
||||
| Listener-gating freeze (`RuntimeOnMessage` absent after first load) | **Confirmed shape, no trigger found** | `WebExtensionContext.cpp:1876-1894`. Listener counts are only decremented by JS `removeListener` (`APIEventCocoa.mm:88-113`); nothing in the bundle removes `onMessage`. Low probability. |
|
||||
| `tab not found` before wake-up is tab-local | **Confirmed** | `APIRuntimeCocoa.mm:141-147`; lookup iterates `openWindows()`→`tabs(for:)`→`webView` (`Cocoa.mm:1105-1114`, `MainWindowController+WKWebExtensionWindow.swift:29-35`) |
|
||||
| `tabs.sendMessage(initResp)` second leg can silently drop | **Confirmed** | `APITabsCocoa.mm:474-511` returns `{}` when tab has no web view or no ContentScript `RuntimeOnMessage` frame |
|
||||
| "Memory pressure by itself" and "routine process crash" not supported | **Refuted for the extension SW** — see sequences A and B | |
|
||||
| Opening a new window does not reload the context | **Confirmed** | `didOpenWindow` only registers window/tabs |
|
||||
| Safari 26.6 SW-registration DB fix is relevant | **Irrelevant** | Extension SW registrations are never persisted (`SWServerWorker.cpp:561-565`); registration is cleared when the SW page client dies (`SWServer.cpp:1527-1539`) |
|
||||
| Смерть background WebContent process → page-less service-worker process → BackgroundContentFailedToLoad loop | **Подтверждено воспроизведением** | 100% repro через kill -9 background PID. CPM не работает во всех вкладках, reload/new tab не помогают, Fire помогает. Естественная частота и реальные production reasons пока неизвестны. Это scope graveyard experiment. |
|
||||
| Закрытие окна забывает shared pinned tabs в WebKit | **Подтверждено вручную** | didCloseWindow закрывает/forget-ит shared pinned Tab, хотя он остаётся в других окнах. Нужен отдельный per-tab lifecycle PR. |
|
||||
| Close Other Tabs forget-ит оставленную вкладку | **Подтверждено по коду** | removeAll(andAppend:) посылает didCloseTab и для retained tab, затем возвращает тот же объект без didOpenTab. Нужен ручной regression repro и отдельный fix PR. |
|
||||
| Duplicate Tab из popup переносит оригинальный Tab вместо копии | **Подтверждено по коду** | При закрытии popup WebKit забывает вкладку, оставшуюся в main window. Нужен ручной repro и отдельный fix PR. |
|
||||
| Пропуск CPM content script при startup/session restore | **Подтверждено как transient** | Только текущий document; следующая навигация восстанавливает работу. Не объясняет устойчивый all-tabs outage. |
|
||||
| Однократный failed background load остаётся в m_backgroundWebView | **Механизм подтверждён, исходный вывод отвергнут** | Самовосстановление есть: второй wake-up ставит 30 s eviction, затем возможна повторная загрузка. Устойчивый outage требует повторяемой причины или постоянного traffic. |
|
||||
| Natural background-process death в production (crash / memory / CPU / jetsam) | **Не проверено в поле** | Forced crash доказывает цепочку, но не частоту и не распределение termination reasons. Нужна телеметрия до фикса. |
|
||||
| Event starvation / JSC hang / stale listener bookkeeping | **Не проверено, низкий приоритет** | Состояния возможны, но стабильного repro и production transition нет. Оставить как residual diagnostic buckets. |
|
||||
| Native messaging stall / malformed CPM config | **Не проверено, низкий приоритет** | Возможный all-tabs симптом, но подтверждённого production path нет. |
|
||||
| Critical memory pressure как конкретный триггер | **Не воспроизведено** | Ручной memory_pressure CPM не сломал. Не использовать как доказанный root cause. |
|
||||
| Kill/restart Network process как триггер | **Отвергнуто на проверенной сборке** | WebKit восстановился, CPM продолжил работать. |
|
||||
| removeAllUserScripts() удаляет WebExtension scripts | **Отвергнуто** | WebKit использует RemoveWebExtensions::No. |
|
||||
| Feature-flag transition, scriptlet reload, обычный long-running JS страницы | **Отвергнуто как объяснение all-tabs outage** | Другой extension context или tab-local execution. |
|
||||
| Safari SW registration DB fix | **Нерелевантно** | Extension SW registration не хранится как обычная persistent registration. |
|
||||
|
||||
## The gap the findings doc misses
|
||||
## Текущий worktree
|
||||
|
||||
The findings doc assumes WebKit's view of "background loaded" and the service worker actually running are the same thing. They are not coupled:
|
||||
Сейчас в одной незавершённой пачке смешаны:
|
||||
|
||||
- WebKit UIProcess tracks only `m_backgroundWebView`, `m_backgroundContentIsLoaded` and the listener-frame set (`m_eventListenerFrames`), all keyed on the **SW page frame**, which outlives the worker.
|
||||
- Message dispatch reaches the worker only through `Page::m_serviceWorkerGlobalScope`, a `WeakPtr` (`Page.cpp:5294-5307`) read via `WebFrame::jsContextForServiceWorkerWorld` (`WebFrame.cpp:1057-1063`) inside `enumerateFramesAndNamespaceObjects` (`WebExtensionContextProxy.cpp:220-250`).
|
||||
- If the worker thread has been stopped, that context is null, `JSContextGetGlobalObject(nullptr)` returns null in release builds (`JSContextRef.cpp:188-193`), the frame is skipped, no listener replies, and `runtimeSendMessage` resolves the content script with `undefined` (`APIRuntimeCocoa.mm:156-175`, `internalDispatchRuntimeMessageEvent` 639-694).
|
||||
- There is **no hook** from SW termination back to `WebExtensionContext` (grep: nothing SW-related in `UIProcess/Extensions` besides `_loadServiceWorker`). No error is recorded in `WKWebExtensionContext.errors`.
|
||||
- diagnostic model/recorder, pixel parameters и hooks в health monitor/manager;
|
||||
- background WKNavigationDelegate proxy, process-death/hang timeline;
|
||||
- graveyard treatment и два feature flags/cohorts;
|
||||
- debug notifications/menu, crash simulations, большие UI tests и локальная repro HTML;
|
||||
- посторонние/временные изменения (NSObject deallocation assertion выключен, Package.resolved bump, правило UI tests, accessibility refactor).
|
||||
|
||||
Because every wake-up restarts the 30 s idle timer (`startOneShot` at `Cocoa.mm:2718`), and CPM sends `init` from **every frame of every navigation** (`all_frames: true`), the dead view is never evicted while the user browses. Recovery only happens after ≥30 s with no extension events, or an extension reload. This produces exactly "stuck across all tabs, page reload does not help, later recovered without extension reload".
|
||||
**Состояние:** код не считать готовым к PR; в этой сессии он не собирался и тесты не запускались. Отдельный blocker: proxy уже сам менял lifetime/timing и в наблюдении предотвращал repro. В текущем коде install сохраняет associated object на original delegate, а isInstalled/uninstall ищут его на webView — это надо исправить и покрыть тестом до PR. Поэтому текущий control не является доказанно нейтральным baseline.
|
||||
|
||||
## Prod-plausible exact sequences
|
||||
## Milestones
|
||||
|
||||
### A. Critical memory pressure kills the "idle" extension worker (macOS only)
|
||||
Два трека:
|
||||
|
||||
Preconditions: CPM context loaded, background SW running, no `waitUntil` pending. WebKit's SW server never learns about extension messages (they bypass it), so `SWServerWorker::isIdle()` (`SWServerWorker.cpp:341-344`) is true ~10 s after activation; `needsRunning()` is only called from `SWServer::runServiceWorkerIfNecessary` (`SWServer.cpp:1226`) and on entering `Running` (`SWServerWorker.cpp:398`).
|
||||
- **Correctness:** M1 tabs и M2 stable base URL независимы друг от друга и от graveyard.
|
||||
- **All-tabs:** M3 telemetry → M4 production validation → M5 graveyard → M6 A/B выполняются последовательно; M5/M6 лучше делать stacked на M3.
|
||||
|
||||
1. System memory pressure becomes Critical. Each WebProcess's `MemoryPressureHandler` sends `WebProcessProxy::MemoryPressureStatusChanged` (`WebProcess.cpp:554-557`).
|
||||
2. For the process hosting the CPM SW page, `WebProcessProxy::memoryPressureStatusChanged` → `WebProcessPool::memoryPressureStatusChangedForProcess` (`WebProcessProxy.cpp:2415-2423`, guarded by `ENABLE(WEB_PROCESS_SUSPENSION_DELAY)` = macOS, `PlatformEnableCocoa.h:1144-1145`).
|
||||
3. `status == Critical && process.isRunningServiceWorkers()` → `NetworkProcessProxy::terminateIdleServiceWorkers(pid)` (`WebProcessPool.cpp:2937-2942`).
|
||||
4. Network process: `SWServer::terminateIdleServiceWorkers(connection)` terminates every idle worker on that context connection, including the CPM worker (`SWServer.cpp:1807-1823`).
|
||||
5. WebProcess: `SWContextManager::terminateWorker` → thread stops → `ServiceWorkerGlobalScope` destroyed → `Page::m_serviceWorkerGlobalScope` becomes null. SW page, its frame, and its `RuntimeOnMessage/Main` listener registration remain.
|
||||
6. UIProcess `WebExtensionContext`: unchanged. `m_backgroundWebView != nil`, `m_backgroundContentIsLoaded == true`.
|
||||
7. Any tab: `cpm.js` sends `{messageType:"autoconsent", init}` (`cpm.js:2896-2901, 3393-3398`) → `runtimeSendMessage` → tab found → `RuntimeOnMessage` in listener set → `wakeUpBackgroundContentIfNecessary` → timer restarted → `backgroundContentIsLoaded()` true → dispatch to SW page process.
|
||||
8. WebProcess: SW context null → no listeners → null reply → UIProcess aggregator default `{}` → content script `await sendMessage` resolves `undefined`; no error thrown. Background never receives `init`, never sends `initResp`. Content script stays in `waitingForInitResponse`.
|
||||
9. Repeats for every tab, every frame, every navigation; step 7 keeps the eviction timer armed.
|
||||
10. Ends only when: no extension event for 30 s → `unloadBackgroundContentIfPossible` → `unloadBackgroundWebView` → next message reloads a fresh SW (health monitor should then fire `cpmMessagingRecoveredWithoutExtensionReload`); or Fire/reload; or app restart.
|
||||
### M0 — Разобрать worktree
|
||||
|
||||
### B. Network process crash or unresponsiveness
|
||||
**Цель:** получить чистые группы изменений без изменения поведения.
|
||||
|
||||
1. Network process dies or is killed as unresponsive (`NetworkProcessProxy::didClose` 522-532, `didBecomeUnresponsive` 181-187, `ExceededMemoryLimit` 1850).
|
||||
2. UIProcess: `WebProcessPool::networkProcessDidTerminate` → `terminateServiceWorkers()` → `disableRemoteWorkers(ServiceWorker)` on every SW-running process → sends `WebSWContextManagerConnection::Close` (`WebProcessPool.cpp:497-509, 1850-1860`; `WebProcessProxy.cpp:3094-3126`). Process is not shut down because it still owns the SW page.
|
||||
3. WebProcess independently: `networkProcessConnectionClosed` → `SWContextManager::stopAllServiceWorkers()` (`WebProcess.cpp:1467-1468`).
|
||||
4. From here identical to A steps 5-10. Extension SW registration is gone (in-memory only), nothing re-registers it until the view is recreated.
|
||||
- Убрать из feature stack выключенный NSObject assertion, случайный Package.resolved bump и изменение repo rule.
|
||||
- Debug menu/notifications/repro HTML оставить локально или вынести в отдельный debug-tooling PR.
|
||||
- Большие AutoconsentUITests, accessibility cleanup и restart(beforeLaunch:) вынести в test-infrastructure PR; оставить только детерминированные regression tests.
|
||||
|
||||
### C. Repeatable registration failure (only if a persistent cause exists)
|
||||
**Done:** каждый оставшийся diff однозначно относится к одному milestone; нет смешанных production/debug/test-infrastructure изменений.
|
||||
|
||||
The retained-failed-view state from the findings doc becomes persistent only when every retry fails. The one repeatable cause found: the SW script is read from disk on every load, uncached (`WebExtensionURLSchemeHandlerCocoa.mm:120-124` → `WebExtension::resourceDataForPath`, default `CacheResult::No`, `WebExtensionCocoa.mm:252-265`, `WebExtension.h:250`). If `<Application Support>/…/<uuid>/public/js/background-embedded.js` is missing or unreadable while the context stays loaded, each retry gets `NSURLErrorFileDoesNotExist` → script fetch rejected → `completion(false)` → retained view → 30 s later eviction → retry → fail again. Content scripts keep working (they were read at context load), so the symptom is exactly "content script talks, background never answers".
|
||||
### M1 — Исправить tab lifecycle
|
||||
|
||||
Repeat cycle per retry: message → `loadBackgroundWebView` → fail → second message arms timer → 30 s → unload → third message → fail again. Every retry records `BackgroundContentFailedToLoad`, so unlike A/B this variant **is** visible in `WKWebExtensionContext.errors`.
|
||||
Один общий PR допустим только при введении общего lifecycle API; иначе лучше три небольших feature PR.
|
||||
|
||||
App-side removal paths checked: `cleanupOrphanedExtensions` (`WebExtensionStorageProviding.swift:180-199`) only deletes identifiers not in the installation store; upgrade installs the new UUID before uninstalling the old (`WebExtensionManager+EmbeddedExtensions.swift:90-108`); scriptlet installer only creates directories and overwrites individual files (`ScriptletInstaller.swift:29-59`), never removes the bundle. No in-app path found that removes a loaded extension's files; external causes (disk cleaners, sync tools, disk full) remain possible.
|
||||
1. **Close Other Tabs:** заменить перегруженный removeAll(andAppend:) на removeAll(except:); retained Tab не получает didCloseTab.
|
||||
2. **Duplicate Tab from popup:** создавать новый Tab/tabCopy и переносить копию; один Tab/WKWebView не должен принадлежать двум коллекциям.
|
||||
3. **Shared pinned tabs on window close / Merge All Windows:** didCloseWindow не должен закрывать shared pinned Tab, который остаётся видимым в другом окне.
|
||||
|
||||
### D. Tab-local: sender page not resolvable
|
||||
**Validation:**
|
||||
|
||||
`getCurrentTab` iterates `windowControllersManager.mainWindowControllers` → `loadedPinnedTabs + loadedTabs` → `Tab.webView` (`WebExtensionWindowTabProvider+macOS.swift:33-40`, `MainWindowController+WKWebExtensionWindow.swift:29-35`). Any WKWebView whose Tab is not in a registered window's loaded tab list fails with `runtime.sendMessage(): tab not found` (logged as "Tab not found for message for content script message"). This is the only path that explains "one window broken, new window fine": a `MainWindowController` missing from `mainWindowControllers` while its window is still visible (`WindowControllersManager.swift:158-177`). Not proven from source; needs the log line correlated with window state.
|
||||
- Unit tests проверяют identity и точную последовательность didOpenTab/didCloseTab/didCloseWindow, а не только массив и selection.
|
||||
- Close Other Tabs: retained loaded tab остаётся известен WebKit; CMP работает после последующей navigation/reload.
|
||||
- Popup Duplicate: source и duplicate — разные объекты; закрытие popup не ломает CPM в main window.
|
||||
- Shared pinned: закрытие второго окна и Merge All Windows не дают tab not found; pinned tab работает без открытия нового окна.
|
||||
- Negative coverage: реально закрытые tabs по-прежнему получают ровно один didCloseTab.
|
||||
- Diagnostic signature после фикса: tab_known_to_webkit=true, tab_controller_matches_context=true, нет content_script_send_error=tab_not_found.
|
||||
|
||||
## Base URL rename on every load (startup window, not a freeze)
|
||||
**Done:** три repro проходят; обычные open/close/move/Fire сценарии не меняют баланс lifecycle events.
|
||||
|
||||
`WebExtensionContext` builds `m_baseURL` from the *random default* identifier at construction (`WebExtensionContextCocoa.mm:222`); `setUniqueIdentifier` does not update it (`WebExtensionContext.cpp:154-166`) and the app never calls `setBaseURL` (`WebExtensionLoader.swift:153-170`). The persisted `lastSeenBaseURL` therefore never matches, so **every launch and every reload** runs `_renameOrigin` (`Cocoa.mm:297-298, 311, 516-525`) and content injection + background load wait on a Network-process round trip. This is the mechanism behind the session-restoration miss. Fix: set `context.baseURL = webkit-extension://<uniqueIdentifier>/` in `makeContext`; the rename then short-circuits synchronously (`Cocoa.mm:518-520`).
|
||||
### M2 — Stable WebExtension base URL
|
||||
|
||||
## Instrumentation corrections
|
||||
**Цель:** задать context.baseURL = webkit-extension://<uniqueIdentifier>/ до WKWebExtensionController.load(context), чтобы после первой миграции WebKit больше не запускал rename origin на каждом load.
|
||||
|
||||
The "Evidence needed next" plan would misclassify A/B: `errors` is empty and `loadBackgroundContent(completionHandler:)` returns success immediately because WebKit believes the background is loaded. Add a third class:
|
||||
**Validation matrix:**
|
||||
|
||||
- After a health failure, send a probe `runtime.sendMessage` from a tab (or via the native-messaging `healthCheckRequest` the background already handles, `background-embedded.js:3185`) and treat `undefined` with `errors == []` as "background loaded, worker dead".
|
||||
- Log `WKWebExtensionContext.errors` **and** whether the probe got a real reply.
|
||||
- Correlate with `SWServer::terminateIdleServiceWorkers` / `memoryPressureStatusChangedForProcess` / `networkProcessDidTerminate` in `log show --predicate 'subsystem == "com.apple.WebKit"'`.
|
||||
- The 30 s idle eviction means the health monitor's 5-minute episode should often close with `recoveredWithoutExtensionReload`; check pixel data for that pattern.
|
||||
- Fresh install: сразу используется stable base URL; CPM/background/content scripts загружаются.
|
||||
- Existing install с предыдущим random base URL: ровно одна migration/rename, затем stable URL сохраняется между relaunch/reload.
|
||||
- Повторные cold launches: rename origin больше не вызывается; context достигает safe-to-inject раньше.
|
||||
- storage.local и extension preferences переживают миграцию; отдельно проверить, что web-origin storage либо переносится, либо осознанно не используется.
|
||||
- Extension update/reinstall, explicit reload и Fire: identifier/base URL соответствуют ожидаемой semantics и не сталкиваются между extension contexts.
|
||||
- Session restoration: детерминированный lifecycle test подтверждает, что load завершён до addInjectedContent; небольшой manual stress run подтверждает отсутствие пропуска CPM на restored document. Не делать flaky тест с десятками случайных relaunch обязательным CI gate.
|
||||
|
||||
## Fix candidates (app side, no WebKit change needed)
|
||||
**Telemetry/validation:** временно логировать или считать reason/duration base-URL migration только в internal build; сравнить cold-load timing до/после. Не отправлять URL или identifier в pixel.
|
||||
|
||||
1. On `cpmMessagingStuck`, unload + load the context through `WKWebExtensionController` (the existing `reloadExtension(identifier:)` path). `loadBackgroundContent(completionHandler:)` alone does not help in A/B because WebKit already considers the background loaded; there is no public API to drop only the background view.
|
||||
2. Set a stable `baseURL` (above).
|
||||
3. Use a persistent background page? Not available on iOS and not needed; alternatively keep the worker "non-idle" — not controllable from the extension.
|
||||
4. Have the content script retry `init` with backoff and treat `undefined` as failure; today `cpm.js` waits forever.
|
||||
**Done:** one-time migration подтверждена, повторных rename нет, storage не потерян, restored CMP page работает без ручного reload.
|
||||
|
||||
## Repro proposals
|
||||
### M3 — CPM failure-attribution telemetry
|
||||
|
||||
- **A**: run with `memory_pressure -l critical` (macOS) for a few seconds after the extension is idle >10 s, then load a CMP page. Expect no `initResp`, `errors == []`.
|
||||
- **B**: `kill -9` the `com.apple.WebKit.Networking` process, then load a CMP page.
|
||||
- **Debug sim persistence check**: enable the sim, load one page (fails), wait 35 s with no navigation, load again — expected recovery per trunk source.
|
||||
Самостоятельный production PR без graveyard и без recovery behavior.
|
||||
|
||||
- CPMMessagingDiagnostics с короткими PII-free/bucketed параметрами.
|
||||
- Diagnostics на cpmInitializationFailed и cpmMessagingStuck.
|
||||
- Passive facts: context errors, background view count/alive, WebProcess/NetworkProcess state, tab known to WebKit, native handler и controller/context match.
|
||||
- Ordered background_events только если observation не меняет lifetime.
|
||||
- Manager hooks, macOS wiring, pixel definitions/privacy review и unit tests на bucketing/sanitization.
|
||||
|
||||
**Критический design constraint:** delegate proxy не должен сам удерживать WKWebView/WebPageProxy. В текущем WIP proxy уже менял timing и скрывал repro; кроме того, associated object сохраняется на original delegate, а ищется на webView.
|
||||
|
||||
**Done:** forced crash классифицируется как died_* → error_background_failed_to_load при network_process_restarted=false; tab repro классифицируется отдельно как tab_not_found.
|
||||
|
||||
### M4 — Production validation milestone
|
||||
|
||||
Без feature fix. Дождаться достаточного объёма одного релиза telemetry.
|
||||
|
||||
Ответить данными:
|
||||
|
||||
- встречаются ли реальные died_memory_limit / died_cpu_limit / died_crash;
|
||||
- следует ли за ними BackgroundContentFailedToLoad/stuck signature;
|
||||
- какова доля per-tab tab_not_found относительно all-tabs failures;
|
||||
- есть ли другие dominant buckets, которые graveyard не исправит.
|
||||
|
||||
**Go/No-Go:** переходить к graveyard rollout только если в production видна связка process death → all-tabs failure. Forced kill подтверждает механизм, но сам по себе не доказывает ценность rollout.
|
||||
|
||||
### M5 — Background WebView graveyard mitigation
|
||||
|
||||
Stacked на M3; implementation/failsafe PR без включённого массового эксперимента.
|
||||
|
||||
- Forwarding delegate proxy для termination callback.
|
||||
- Bounded hold dead WKWebView, затем гарантированный release.
|
||||
- Независимый runtime kill switch, сначала выключен.
|
||||
- Не более одного retained view на context; cleanup при unload/reload; защита от повторных deaths и retain cycles.
|
||||
- Tests: public/private termination callback forwarding, reason mapping, immediate control release, treatment timeout, unload cleanup, repeated termination.
|
||||
|
||||
**Обязательная валидация:** proxy-only control должен стабильно воспроизводить исходный kill -9 failure. Если proxy сам лечит race, control не нейтрален и реализацию нельзя использовать для A/B.
|
||||
|
||||
**Done:** с одинаковым observation path control ломается, treatment восстанавливается; разница создаётся только hold duration.
|
||||
|
||||
### M6 — Graveyard A/B experiment
|
||||
|
||||
Отдельный stacked PR: cohorts, outcome measurement и rollout.
|
||||
|
||||
- control: тот же proxy/observation path, немедленный release.
|
||||
- treatment: bounded graveyard hold.
|
||||
- Stable cohort assignment; kill switch не зависит от cohort.
|
||||
- Denominator pixel на каждую background-process termination: cohort + bucketed reason.
|
||||
- Outcome после следующей CPM navigation или ограниченного окна: healthy, background_failed_to_load, stuck, recovered.
|
||||
- Primary metric: доля background_failed_to_load|stuck после termination по cohort.
|
||||
- Guardrails: app/WebKit crashes, retained-view count, recovery latency, repeated termination loops.
|
||||
|
||||
background_events на stuck pixel полезен для диагностики, но не является A/B denominator: успешный treatment иначе не попадёт в выборку.
|
||||
|
||||
**Rollout:** internal/review → небольшой 50/50 production sample → проверить denominator и cohort balance → расширять только после достаточного числа termination events.
|
||||
|
||||
**Done:** статистически и технически подтверждено снижение post-termination failures без ухудшения guardrails; после решения о rollout эксперимент удаляется или treatment превращается в обычный remotely controlled fix.
|
||||
|
||||
## Предлагаемая PR-карта
|
||||
|
||||
| PR | Milestone | Зависимость |
|
||||
|---|---|---|
|
||||
| Tab lifecycle API + Close Other Tabs | M1 | independent |
|
||||
| Popup Duplicate Tab identity fix | M1 | возможно stacked на lifecycle API |
|
||||
| Shared pinned window-close fix | M1 | возможно stacked на lifecycle API |
|
||||
| Stable WebExtension base URL | M2 | independent |
|
||||
| CPM attribution telemetry | M3 | independent |
|
||||
| Graveyard mitigation/failsafe | M5 | stacked на telemetry |
|
||||
| Graveyard A/B wiring | M6 | stacked на mitigation |
|
||||
| Debug/repro tooling | M0 | отдельный optional PR или local-only |
|
||||
|
||||
Reference in New Issue
Block a user