--- title: CPM Web Extension — воспроизведение и фикс date: '2026-10-08' status: historical-investigation tags: - work - cpm - webkit updated: '2026-10-20' --- # CPM Web Extension — воспроизведение и фикс > **Актуализировано 20 октября 2026.** Этот документ сохраняет последовательность исследования и старый A/B/phase-1 design 8–9 октября. «Ничего не закоммичено», общая ready-handshake branch и line/test counts ниже относятся к своим датам. > > Текущие implementations: #7379 P1/P2/P4; #7459 Network recovery с applied daily+count; #7493 ограниченный stable-origin P3 policy; #7473 shared-pinned window close **merged 20 октября в `release/macos/1.213.0` (`9bcce0264e`)**; rollout не подтверждён. Heads, bases и CI — [[cpm-status-2026-10-15#2. PR и quality gates]], worktrees — [[cpm-web-extension-code-guide-2026-10-09#0. Текущая карта worktree и точек входа]]. > > #7376 уже переработан в один terminal episode report: four-state `stuck`, двухфазный bounded `bg_events`, daily+count. Он закрыт без merge; macOS counterpart отстаёт. Полный контракт — [[cpm-pr0-episode-telemetry-summary-2026-10-16]]. Final pending-load outcome, page ACK и network attribution episode пока отсутствуют; весь production Others эти данные не объясняют. ## Исторический контекст — 15 октября 2026 > **Текущий статус — 15 октября 2026:** [[cpm-status-2026-10-15]]. Актуальная карта файлов и worktree — [[cpm-web-extension-code-guide-2026-10-09]] (§0). «Ничего не закоммичено», общий ready-handshake worktree и counts ниже относятся к состоянию 8–9 октября; A/B/telemetry уже разделены на PR, их актуальные heads/CI проверены в основном статусе. > > Новое продолжение в `alex/cpm-web-extension-remaining-failures`: 14 P5/P6 методов с green по отдельным логам, включая shared-window callback → system WebKit → P5/tab=false и условный main-flag handler cycle → P6. Факт flag-transition у пользователей и естественный вход P5/tab=true/listener metadata неизвестны. Контроли не дали P5 при длинных документах, одном missing-listener wake или host main-actor stall. Embedded-only first-init loss относится к A; после explicit ready wait controller остаётся correct. > > Последний обычный пакет этого worktree: 478 XCTest, 28 skipped, 0 failures; 14 slow repro выполнялись отдельно. Цифры 490/677/761 в других branches описывают иные тестовые базы, не этот прогон. Все исходные красные эксперименты и ограничения сохранены в основном статусе §10–12; этот отчёт ниже сохранён как историческая цепочка расследования. Продолжение [[cpm-web-extension-breakage-session-summary-2026-10-07]] (§8 «План точного воспроизведения»). Среда: macOS 26.7.1 (25G241), system WebKit, production embedded extension 2026.9.29. Код: worktree `alex/cpm-web-extension-ready-handshake`, пакет `SharedPackages/WebExtensions`. **Ничего не закоммичено.** ## Статус (9 октября) Карта кода по строкам: [[cpm-web-extension-code-guide-2026-10-09]]. Сводки на английском: [[cpm-web-extension-data-analysis-summary-2026-10-08]], [[cpm-web-extension-repro-and-fixes-summary-2026-10-08]]. **Ничего не закоммичено.** - **Фиксы** — worktree `alex/cpm-web-extension-ready-handshake`: gate первой загрузки посреди сессии, watchdog Network-процесса, перезапуск по первому сбою. Тесты зелёные (490 в пакете на момент фикса). - **Расследование без фиксов** — worktree `alex/cpm-web-extension-remaining-failures`: error 6 (причина в WebKit, репро на macOS), задержка очистки при смене `baseURL` (репро на macOS), медленный старт worker'а на пробуждении — сигнатура группы 1 воспроизведена. - **Телеметрия фазы 1** — worktree `alex/cpm-messaging-readiness-telemetry` от свежего `main`: готовность background, возраст view, состояние приложения, вкладки, error 6, детали восстановления. 677 тестов, SwiftLint, валидатор пикселей, сборка iOS и macOS — зелёные. - **Эксперимент** — ветка `alex/cpm-stable-base-url`: стабильный `baseURL` (на старой базе, не прогнан через harness). - Не воспроизведены: привязка вкладок на macOS, «нет background view», код 7, первый сбой после M4; error 6 на iPhone. ## Что нашли ### Отказ одной вкладки — потеря `init` при первой загрузке background - WebKit при *первой* загрузке extension с новым идентификатором (первая установка или обновление — апгрейд ставит новый UUID) грузит background сразу. `runtime.sendMessage`, пришедший до выполнения background-скрипта, резолвится `undefined`, listener не вызывается. - Без gate: 14/20 при первой установке, 18/20 при обновлении; с busy-wait 1 с в скрипте — 3/3 детерминированно. - При обычном перезапуске (extension уже установлен), после reload (M4) и при пробуждении после idle WebKit ставит сообщения в очередь — потерь нет. - **Поправка к первому отчёту:** при запуске это уже закрывает gate в `WebExtensionLifecycleCoordinator` (флаг `webExtensionStateRestorationGate`). Дыры были: установка или обновление посреди сессии (`sync()`, отложенная установка на iOS до protected data), таймаут gate 5 с. ### Полный отказ — мёртвый worker при живом background - **Краш Network-процесса** (`SIGKILL`, аналог jetsam на iOS): background view и его PID прежние, WebKit считает background загруженным, worker мёртв. Теряются сообщения из всех вкладок и уже открытых документов. Монитор: init failed → stuck → M4. После M4 новые навигации работают, **документы, открытые до сбоя, сломаны навсегда**. Без M4 держится, пока пользователь активен; снимается после ~30 с idle-выгрузки. 20/20. - **Смерть worker'а при живом Network-процессе:** `_terminateServiceWorkers` (SPI) или удаление service worker registrations из data store (то, что делает очистка данных). Состояние то же. - Fire в обоих приложениях выгружает extensions на время очистки и загружает потом, поэтому сам Fire сюда не приводит. Других путей в проде, которые убивают только worker, не нашёл. - **Не триггеры:** `org.WebKit.lowMemory`, заморозка background на 10–75 с, kill background-процесса (WebKit перезапускает сам). - Если background занят или заморожен дольше ~30 с, WebKit выгружает его, и сообщение «в полёте» теряется навсегда (одна вкладка, не каскад). ### Восстановление - Убить **только background WebContent-процесс** (`_killWebContentProcessAndResetState`) — WebKit поднимет новый worker на следующем сообщении; новые навигации **и уже открытые документы** работают. - Reload extension (M4) чинит только новые навигации. - `loadBackgroundContent()` ничего не даёт: WebKit считает background загруженным. ### Почему мёртвый worker не обнаружить из приложения - Регистрация SW остаётся `activated`. - `runtime.sendMessage` из page world background web view до worker'а не доходит (WebKit считает страницу самим background-контекстом) — и у живого, и у мёртвого worker'а сразу `undefined`. - Background CPM слушает только `onMessage`, `onConnect` (порт privacy-dashboard) и `onAlarm` — приложение не может их вызвать. ## Фикс (app, пакет WebExtensions) 1. **Gate для первой загрузки посреди сессии.** После `installEmbeddedExtension` менеджер запоминает задачу `loadBackgroundContent()` нового контекста; `coordinator.initialLoadWaiter` (его уже вызывают обе платформы на каждой main-frame навигации) после открытия стартового gate отдаёт этот waiter, не дольше 5 с. 2. **`EmbeddedBackgroundLivenessWatchdog`.** Запоминает PID Network-процесса background'а; при смене перезапускает background-процесс. Проверка на каждой навигации и раз в 1 с. 3. **Перезапуск по первому сбою.** Монитор вызывает `onEpisodeStarted` на первом сбое обычной навигации; менеджер перезапускает background, **если процесс отвечает** (`callAsyncJavaScript("return 1")`, 1 с). Замороженный или занятый не трогаем: убийство остановленного процесса давало глухой новый worker. 4. Пункты 2 и 3 включаются одним переключателем `isEmbeddedBackgroundRecoveryEnabled` (по умолчанию `true`). | Сценарий | Без фикса | С фиксом | |---|---|---| | Установка посреди сессии | 3/3 потерь | 5/5 доставлено | | Обновление посреди сессии | 5/5 потерь | 5/5 доставлено | | Краш Network-процесса, 4 вкладки + открытые документы | все сломаны | всё работает | | Краш Network-процесса без M4 | не восстанавливается | работает | | Смерть worker'а (SPI / удаление регистраций) | 2 таймаута → M4, старые документы сломаны | 1 потерянная навигация, затем всё работает без reload | ## Фиксы ↔ гипотезы | Фикс | Гипотеза | Подтверждена данными? | |---|---|---| | Gate на первую загрузку посреди сессии | Потеря `init` при eager-загрузке background нового идентификатора (317981), не про Network-процесс | Нет. Косвенно — `app_version_change=updated`, но запуск и так под gate; частота установок посреди сессии неизвестна | | Watchdog по PID Network-процесса | Краш Network-процесса → мёртвый worker при «живом» background | Частично (см. ниже) | | Перезапуск по первому сбою | Мёртвый worker по **любой** причине при отзывчивом процессе background'а | Не проверена: из app причину не видно | ## Сверка с собранными данными (5 октября) Источник: `apple-browsers.git/cpm-public-attribution-2026-10-06/events.tsv` (строки `stuck` .count) и `events_daily.tsv` (в скобках). Группы по приоритету, как в атрибуции. | Группа по снимку | iOS: всего / `network_restarted=true` | macOS: всего / `network_restarted=true` | |---|---|---| | Вкладка: controller/scripts/tab | 81 / 10% (13%) | 102 / 1% (2%) | | Нет background view | 158 / 11% (14%) | 35 / 3% (3%) | | Error 6 (background не загрузился) | **579** / 10% (14%) | 3 / – | | `died_*` (процесс background умер) | 14 / 86% (85%) | 0 | | Снимок без видимой ошибки | **1 410** / **17% (21%)** | **705** / **30% (45%)** | **За гипотезу о Network-процессе:** смена Network-процесса заметно чаще в группе «без видимой ошибки» — там, где её предсказывает механизм. В остальных группах ~10% на iOS и 1–3% на macOS. Смерть процесса background'а почти всегда вместе со сменой Network-процесса — похоже на jetsam на iOS, убивающий оба. **Почему это не доказательство:** `network_restarted` сравнивает PID с загрузкой *контекста*, а не с последним созданием background view. В почти всех таких строках последние события — `view, proxy_on` за 30 с – 5 мин до снимка. Если view пересоздан *после* смены процесса, worker свежий и механизм не применим. Порядок из текущих данных не восстановить. **Итог:** механизм объясняет в лучшем случае 20–45% «здоровых» `stuck`. Остальное — другие дефекты, которые текущими фиксами не покрыты: - **Error 6 — ~1/3 всех строк iOS.** WebKit сам сообщает, что background не загрузился (`dealloc → error:6 → view…`). Worker не поднимается, а не тихо умирает. Перезапуск по сбою почти не поможет: после error 6 view обычно нет. **Отдельный приоритет для iOS.** - **Вкладка не привязана** (macOS: неверный controller и нет scripts; iOS: нет scripts) — привязка вкладки к контексту, фиксы не трогают. - **Нет background view** — WebKit не пересоздал background; обоим механизмам нужен живой view. - **«Здоровые» без смены Network-процесса — крупнейший необъяснённый остаток** (1 170 iOS / 497 macOS). Кандидаты: другая смерть worker'а (поймает перезапуск по сбою), поздний или несопоставленный `refreshCpmDashboardState` при живом worker'е (перезапуск — ложное срабатывание), первая загрузка вне gate. Без `native_seen` не разделить. ## Непокрытые группы: расследование (8 октября) Worktree `alex/cpm-web-extension-remaining-failures`, ветка от того же коммита `a0717cf098`, **без фиксов** — чтобы воспроизводить на коде как в проде. Фиксы остаются незакоммиченными в `alex/cpm-web-extension-ready-handshake`. - В harness перенесена инфраструктура (без watchdog/recovery) и подключён production `CPMMessagingDiagnosticsRecorder`: воспроизведённые сбои дают те же параметры пикселей, что и прод (`bg_events`, `context_errors`, `network_restarted`, …). Watcher логирует `context.errors`. - Новый набор `CPMReproBackgroundLoadFailureTests`: idle-выгрузка (~32 с), затем пробуждение навигацией при заданном условии. ### Error 6 (`BackgroundContentFailedToLoad`, ~1/3 строк `stuck` на iOS) WebKit (`WebExtensionContextCocoa.mm`): error 6 ставится, когда `_loadServiceWorker` вернул `success=false` (`:2796`) или упала навигация background-страницы (`:3052`). **Код 7 — внутренний `ScriptExecutionError`** (`didEncounterScriptError`, `:284`): JS-ошибка в скрипте extension во время работы. В проде: `7,6` — 283, только `6` — 259, `6,7` — 103 из 645 строк iOS с error 6. | Условие при пробуждении (macOS) | Результат | |---|---| | Без помех | доставлено | | Network-процесс заморожен 5/10/60 с | пробуждение потеряно, error 6 нет, потом всё работает | | Network-процесс убит перед пробуждением | доставлено | | Background убит во время загрузки | пробуждение потеряно (`died_client`), error 6 нет | | Файлы extension отсутствуют | error 6 + `WKWebExtensionErrorDomain:2` — **в проде кода 2 нет, не причина** | | Background-скрипт бросает на верхнем уровне / нет `browser`/`chrome` | error 6 **без** кода 7 — точная продовая сигнатура «только 6», но продовой причины падения скрипта не нашлось | | **Новый процесс background заморожен ≥30 с во время загрузки** | **таймаут загрузки SW → `background none` + error 6**, цепочка `view, proxy_on, unresponsive, error:6, dealloc`; 10 с — error 6 нет; после разморозки работает | **Причина в коде WebKit (подтверждено):** `WebExtensionContext::wakeUpBackgroundContentIfNecessary` (`WebExtensionContext.cpp`) первым делом вызывает `scheduleBackgroundContentToUnload()` — взводит 30-секундный idle-таймер выгрузки **до** того, как service worker загрузился. Если загрузка не закончилась за 30 с, таймер вызывает `unloadBackgroundContentIfPossible` → `unloadBackgroundWebView()` → view закрывается посреди загрузки → колбэк `_loadServiceWorker(success=false)` → error 6. Это объясняет продовый порядок `dealloc → error:6 → view` и репро с заморозкой: 45 с → error 6, 10 с → нет. **Тест-репро на macOS** (`Tests/WebExtensionsTests/CPMRepro/CPMReproBackgroundLoadFailureTests.swift`, worktree `alex/cpm-web-extension-remaining-failures`), с проверками, все 9 зелёные: ```bash cd SharedPackages/WebExtensions && CPM_REPRO=1 swift test --filter CPMReproBackgroundLoadFailureTests ``` - `testBackgroundStalledPastUnloadTimerFailsToLoad` — новый процесс background заморожен на 45 с при пробуждении → `context_errors` = только `:6`, `bg_load_error_after=25s_to_35s`, `bg_load_error_app_state=active`, следующее пробуждение работает. - `testBackgroundStalledLessThanUnloadTimerLoads` — 10 с → error 6 нет. - `testMissingExtensionFilesFailWithAFileError`, `testThrowingWorkerScriptFailsToLoad`, `testWorkerWithoutExtensionBindingsFailsToLoad` — error 6 через `under_5s` (отличаются от таймера по новому параметру). - `testWakeWithoutInterferenceLoads`, `testFrozenNetworkProcessDoesNotFailTheLoad`, `testNetworkProcessCrashBeforeWakeDoesNotFailTheLoad`, `testBackgroundKilledDuringLoadDoesNotFailTheLoad` — error 6 нет. **iOS Simulator:** тесты пакета непоказательны — процесс `xctest` не получает RunningBoard assertions, background не работает с начала. Приложение DDG в симуляторе работает, но фоновое приложение не приостанавливается. Видно, что CPM сам будит background будильником `cpm-summary` (через 2 мин после активности), в том числе когда приложение в фоне. **iPhone 12 mini, iOS 26.7.1, debug-сборка DDG (установлена поверх 7.221.0)** — error 6 **не воспроизвёлся**: | Сценарий | Результат | |---|---| | Свернуть на 2,5 мин при стоящем будильнике | приложение приостановлено целиком, будильник срабатывает при возврате, загрузка ~0,1 с | | Разбудить и сразу свернуть (4 задержки) | загрузка успевает за ~0,1 с | | Звук играет в фоне, будильник будит background в фоне | background создаётся и грузится, через 30 с штатно выгружается | | Фоновый запуск системой (`--no-activate`) | не проверено: консоль не подключается, `log collect` требует `sudo` | Вывод: на ненагруженном устройстве загрузка всегда укладывается в 30 с. Оставшиеся кандидаты: нехватка памяти / jetsam на нагруженных устройствах; фоновый запуск системой (`fetch`/`processing`), когда приложение приостанавливают посреди загрузки, а при возврате просроченный таймер срабатывает раньше её завершения. Решено не подбирать условия вслепую, а добавить телеметрию (ниже). **Телеметрия для error 6 — реализована (не закоммичено, worktree `alex/cpm-web-extension-remaining-failures`):** - Новые параметры у всех 8 CPM-пикселей с диагностикой на iOS и macOS (`debug_web_extension_cpm_initialization_failed_after_*`, `debug_web_extension_cpm_messaging_stuck_*`): - `bg_load_error_app_state` — состояние приложения, когда контекст последний раз записал error 6: `none` / `active` / `inactive` / `background`; - `bg_load_error_after` — время от создания view, чья загрузка упала, до error 6: `none` / `under_5s` / `5s_to_25s` / `25s_to_35s` / `over_35s`. `25s_to_35s` — idle-таймер WebKit. - Код: `CPMAppSessionDiagnostics` получил провайдер `applicationState` (iOS — `UIApplication.applicationState`, macOS — `NSApp.isActive`); `CPMMessagingDiagnosticsRecorder` фиксирует состояние и задержку в момент записи error 6, сбрасывает при новом контексте; `CPMMessagingDiagnostics` отдаёт параметры (`none`, если ошибки не было; опускает, если неизвестно). - JSON5: `params_dictionary.json5` и определения (iOS `debug_pixels.json5`, macOS `web_extensions.json5`); `validate-pixel-defs` проходит на обеих платформах. - Тесты: 2 unit-теста в `CPMMessagingDiagnosticsTests.swift` (захват состояния и задержки, бакеты) + сквозная проверка в тесте-репро. Весь пакет: 459 тестов, 0 падений. SwiftLint чистый. - Ожидаемое чтение данных: `25s_to_35s` + `background` → загрузка, застрявшая из-за фона; `25s_to_35s` + `active` → медленная загрузка на переднем плане (память, нагрузка); `under_5s` → сломанный скрипт или файлы. Возможный фикс после данных: при возврате приложения на передний план, если `context.errors` содержит error 6, — перезапустить background; либо не будить background, пока приложение в фоне (изменение extension или WebKit). ### Группа 1 («здоровый» снимок без смены Network-процесса) — разбор 8 октября **Данные (`events_daily.tsv`, `stuck`):** iOS 748, macOS 228. - Самый частый хвост `bg_events` — `dealloc → view → proxy_on`, последний `view` моложе 30 с до снимка: iOS 219 (+154 при 1m), macOS 120 из 228. Сбой совпадает с **пробуждением background после idle-выгрузки**. - Около четверти iOS (и ~15% macOS) — после загрузки контекста: хвосты `load → view` / `base_url_changed → view`, `reason=extension.reload` (iOS 71, macOS 39). - macOS: `app_version_change=updated` у 144 из 228 (63%; в среднем по stuck — 52%). - `context_errors=none` у 675/748 iOS; память (`critical_memory_age`) почти везде `none`. - ~~Ответ часто опаздывает, а не теряется~~ — **поправка 8 октября:** `recovered_without_extension_reload(initialization_failed)` к stuck не относится. Он закрывает только эпизод, который ещё **не** stuck: пришёл опоздавший ответ на упавшее измерение или следующая навигация в той же вкладке ответила. Stuck-эпизод сразу уходит в M4, и его восстановление считается как `recovered_after_extension_reload(messaging_stuck)`; без reload из stuck — 0 строк. - У recovery-пикселей только `appVersion` и `cpm_recovery_from`: хвостов, задержки и признака «тот же документ / следующая навигация» нет. - `initialization_failed` — только `.daily` (на причину), recovery — daily+count. Сравнимо только daily/daily за 5 октября: iOS 1 555 / 6 624 = 23% (33% от `after_other`), macOS 353 / 1 405 = 25% (33% от `after_other`). count/daily recovery ≈ 1,9 на устройство-день на iOS — у одних и тех же устройств повторяется. - Stuck → recovered after M4 (daily): iOS 916 / 1 440 = 64%, macOS 362 / 489 = 74%. - Хвосты «здоровых» init-сбоев vs stuck (iOS): `load/base_url_changed → view` — ~32% init против ~18% stuck; `dealloc → view` (пробуждение) — ~44% init против ~61% stuck (macOS: ~36% → 52% только `@30s`). То есть сбои после загрузки контекста чаще проходят сами (согласуется с задержкой чистки при смене `baseURL`), а пробуждение чаще доходит до stuck. - Нужно в телеметрию: на recovery-пиксель — задержку ответа от начала измерения (бакеты 4–8 с / 8–30 с / >30 с), `same_document` vs `next_navigation` и хвост `bg_events` на момент сбоя. - Версии ОС: на macOS «здоровые» сбои есть и на 26.7, и на 27.0 (92 и 178 init в срезе сравнения ОС). Версии iOS в выгрузке нет — нужен запрос в ClickHouse. **WebKit-баги (bugs.webkit.org, компонент WebKit Extensions, изменения с мая 2026):** - **320812 / 312231@main — медленная чистка при загрузке контекста (главная находка).** Начиная с 26.6, `WebExtensionContext::load()` при смене `baseURL` переименовывает localStorage/IndexedDB и удаляет **все** website data предыдущего origin, включая DiskCache (обход всего сетевого кэша на главном потоке Network-процесса). **`m_safeToInjectContent`, `addInjectedContent()` и загрузка background ждут окончания этой чистки.** У DDG `baseURL` новый при каждом запуске и каждом M4 reload. Код есть и в 7624.5.1.11.3 (macOS 26.7.1), и в 7625.1.29 (27.0). Фикс 318756@main (7 авг) попал в ветку safari-7625 (`316606.407@safari-7625-branch`); в какую сборку ОС — не проверено. - **324743** (fixed 22 сен, нет в 26.7/27.0) — worker считается загруженным до завершения регистрации. Зона «пробуждение после idle»; наши тесты пробуждения на Mac проходят, детерминированного репро нет. - **322881** (fixed 31 авг) — worker теряет clients после выгрузки и перезагрузки; CPM clients не использует. - **314194** (открыт) — в **Lockdown Mode** блокируются все service worker'ы, включая background extension → CPM никогда не работает. Доля пользователей DDG с Lockdown Mode неизвестна; сигнатура в наших данных не определена. - **319976** (fixed) — `loadBackgroundContentWithCompletionHandler` вызывал completion дважды при ошибке; мы используем этот API в gate. **Репро медленной чистки на Mac** (`CPMReproExtensionLoadCleanupTests.swift`, нужен локальный сервер `CPM_REPRO_CACHE_URL` с `/fill.html?n=N`): кэш тестового процесса наполнен ~24 тыс. записями (у реального DDG на этом Mac — 18,7 тыс., 877 МБ). | | Пустой кэш | ~24 тыс. записей | |---|---:|---:| | Перезапуск: content script выполнился на мс документа | 23 | 770–1 350 | | Перезапуск: background создан через | 0,2 с | 0,9–1,5 с | | Reload (как M4): content script на мс документа | — | 780–790 | На быстром Mac задержка меньше 4 с монитора, поэтому сбоя нет. На iPhone с большим кэшем и медленным CPU нужно мерить (на тестовом iPhone кэш маленький: «Context will load» → «Background web view created» за 30–70 мс). **Проверено и отброшено:** - Блокировка устройства: у iOS-приложения нет entitlement'а `default-data-protection` → класс `CompleteUntilFirstUserAuthentication`; файлы доступны при блокировке после первой разблокировки. Исключение — запуск до первой разблокировки, для него есть `protectedDataLoadGate`. - Устаревший URL вкладки на iOS (`addressBarURLFilter` не обновляет URL при межоригинном переходе со страницы): в `didCommit` `url = webView.url` выставляется синхронно до `init` → для обычной навигации ответ приходит с верным URL. **Медленный старт worker'а на пробуждении — сигнатура группы 1 воспроизведена (8 октября, эксперимент, не закоммичено).** Тест `CPMReproWakeExperimentTests` в обоих worktree: после idle-выгрузки в начало background-скрипта на диске добавляется busy-wait (только на этот wake), через 1 с скрипт восстанавливается. - busy 0,3 / 1 / 2,5 с и задержка `activate` 1–3 с: сообщения ставятся в очередь и доставляются, задержка ответа = время старта. Потерь нет. - busy 5 с: `initialization_failed_after_other` с `bg_process_responsive=true`, `context_errors=none`, хвост `dealloc/view/proxy_on@30s` — как в проде. Ответ приходит через ~1 с после сбоя → `recovered_without_extension_reload(initialization_failed)`. - busy 12 с + следующая навигация после сбоя: второй сбой → `messaging_stuck_other` с той же сигнатурой → M4. M4 убивает медленный, но живой worker: `init` уже открытых документов теряется, новые навигации работают (`recovered_after_extension_reload`). - С фиксами (worktree A) то же самое. Рестарт на первом сбое (`onEpisodeStarted`) не сработал: проба отзывчивости по 1 с истекла на занятом процессе. Если же медленный старт не загружает CPU (например, ждёт I/O), проба ответит, и рестарт убьёт worker, который ещё грузится. Это нужно закрыть. - Чем реально вызван старт дольше 4 с на устройствах, неизвестно. Кандидаты: конкуренция за CPU с загружаемой вкладкой, запуск процесса без prewarm на iOS, низкий приоритет процесса extension (RunningBoard), занятый Network-процесс. Мерить на iPhone: от wake до первого `recv init`. - Speculative fix: (1) будить background заранее — при старте навигации или при выходе приложения на передний план, без блокировки; (2) не эскалировать в M4 и не рестартовать, пока background view моложе ~15 с; (3) телеметрия: возраст background view в секундах на момент сбоя и задержка позднего ответа на recovery-пикселе; (4) опционально держать background живым, пока приложение на переднем плане (цена — память одного WebContent-процесса). **Сигналы готовности background (тест `testReadinessSignalsDuringSlowWake`, worker стартует 5 с):** - `isLoading=false` у background view через 0,11 с — бесполезен: страница загружается задолго до worker'а. - `loadBackgroundContent` completion пришёл через 5,096 с, сразу после выполнения кода worker'а верхнего уровня («prelude evaluated» на 5,08 с). Это точный сигнал. Вызов сам будит выгруженный background. - Первое нативное сообщение worker'а (`getResourceIfNew`) — на 5,088 с. Тоже точный сигнал, менять расширение не нужно. - SPI для готовности нет: у `WKWebExtensionContext` из относящегося к background есть только `_backgroundWebView`, `_backgroundContentURL`, `isLoaded` (это контекст, не background) и `loadBackgroundContentWithCompletionHandler:`. - В диагностике нет числа вкладок и числа загружающихся вкладок; гипотезу «много вкладок → медленный старт» проверить нечем. **Стабильный `baseURL` (ветка `alex/cpm-stable-base-url`, не закоммичено): оценка.** - Объясняет только подгруппу «после load/reload» (~¼ iOS, ~15% macOS группы 1) и, вероятно, первый сбой после M4. Основная часть (пробуждение после idle) не затронута: чистка идёт только в `load()`. - Изменение: `context.baseURL = webkit-extension:///`, где `uniqueIdentifier` = `UUID()` на установку/обновление (`WebExtensionManager+EmbeddedExtensions.swift:157`). - Приватность: во всех 4 встроенных манифестах нет `web_accessible_resources`; `cpm.js` не вызывает `runtime.getURL`; в background `getURL` только для `tabs.create` (страница расширения). Страница не может прочитать или проверить URL. Данные origin расширения и сейчас переживают перезапуск: WebKit их переносит (`_renameOrigin`). Риск появится, если расширение с WAR получит стабильный URL. Поэтому стабильный URL нужен только для встроенных расширений без WAR. - Обновление: новый UUID → новый URL; старый контекст выгружается до загрузки нового — проверить. M4 reload с тем же URL: проверить, что нет `BaseURLAlreadyInUse` и что битая SW-регистрация не переживает reload (сценарии network-crash + watchdog). - До принятия: прогнать harness (cleanup, M4, network crash) на ветке; телеметрия длительности load→view. ### Привязка вкладок на macOS (`tab_controller_match=false` + нет scripts, ~100 строк) - **Гипотеза атрибуции не подтвердилась.** Путь в коде есть: выключение флага `webExtensions` вызывает `uninstallAllExtensions()` и обнуляет менеджер, включение создаёт новый с новым controller, а `WKWebViewConfiguration` старых вкладок не меняется. Но флаг `.webExtensions` = `remoteReleasable(WebExtensionsSubfeature.featureEnabled)` с `defaultValue: .enabled`, а подфичи `featureEnabled` нет ни во встроенном, ни в текущем удалённом `macos-config.json` → флаг всегда `enabled` (отсутствующая подфича возвращает `defaultValue`). Мигать нечему. - **Порядок запуска не объясняет:** `setupWebExtensions()` (`AppDelegate.swift:1509`) вызывается до восстановления сессии (`:1526`), раньше вкладки не создаются. - `tab_controller_match` считается как `tabController != nil && tabController === context.webExtensionController` (`CPMMessagingDiagnosticsRecorder.swift:378`) — `false` и при **отсутствующем** controller у вкладки, и при устаревшей ссылке на контекст. - Следующие кандидаты: вкладки из `createWebViewWith` (popup / `target=_blank`) и кэш закреплённых вкладок; пересборка user scripts в `UserContentController` приложения при обновлении правил content blocking — может стирать скрипты extension из вкладки (на iOS есть группа «скриптов нет при верном controller»). ### Ещё не начато - **Нет background view** (158 iOS / 35 macOS). - Причина долгого старта worker'а на реальных устройствах (группа 1): мерить на iPhone от пробуждения до первого `recv init` при загружающейся вкладке; дождаться `bg_ready_after` / `bg_view_age` / `open_tabs` из прода. ## Телеметрия для валидации Выпустить раньше фиксов или вместе с ними в A/B — иначе эффект фиксов не отделить. Все значения — bool/enum/bucket, без URL и идентификаторов; оформить через `ddg-add-pixel`, privacy triage (прецедент — [[cpm-m3-telemetry-privacy-triage]]). **Подтвердить сбои — параметры к существующим `initialization_failed` / `stuck`:** 1. `network_replaced_since_view` (bool) — PID Network-процесса при последнем создании background view ≠ текущему. Прямая подпись мёртвого worker'а. Recorder уже вычисляет это (`CPMMessagingDiagnosticsRecorder.swift:285-289`), но только логирует. Плюс токен `net_replaced` в `bg_events`, чтобы видеть порядок. **Самое дешёвое и самое ценное.** 2. `native_seen` (`none` / `site_check` / `refresh_other_url`) — были ли нативные вызовы от background для этой навигации (background зовёт `isFeatureEnabled` с URL вкладки при обработке `init`). Делит сбой на «не дошло до worker'а» / «worker обработал, refresh опоздал» / «рассогласование URL на native». Только app. 3. `bg_first_load` (bool) + токены `install` / `upgrade` / `bg_loaded` в `bg_events` — подпись бага первой загрузки. 4. `gate` (`none` / `launch` / `startup` / `timeout`) — прошла ли навигация мимо gate или он открылся по таймауту 5 с. **Подтвердить, что фиксы работают:** 1. Пиксель `debug_web_extension_cpm_background_restarted` (daily+count): `trigger` = `network_process_replaced` / `initialization_failed`; `outcome` = `restarted` / `skipped_unresponsive` / `spi_unavailable`. 2. Атрибуция восстановления: `recovery_action` (`none` / `background_restart`) к `recovered_without_extension_reload`, либо пара `recovered_after_background_restart` / `background_restart_failed` (следующая навигация после перезапуска тоже упала → M4). 3. Ожидаемый эффект: падают `messaging_stuck` и M4 (`recovered_after_extension_reload`); почти исчезают вторые сбои эпизода с `native_seen=none` + `network_replaced_since_view=true`; исчезают сбои с `bg_first_load=true` вне запуска. 4. **A/B, а не просто флаг:** recovery и новый gate — за экспериментальным флагом (`ddg-add-feature-flag`), когорта в CPM-пикселях. Сравнение версий (1.208 → 1.209) уже оказалось неинформативным. Не измерить без extension: восстановление уже открытых документов (монитор меряет только навигации) и причину смерти worker'а при живом Network-процессе. ### Фаза 1 реализована — worktree `alex/cpm-messaging-readiness-telemetry` (от `main` 92e09cdbbc, 8 октября, не закоммичено) Только телеметрия, поведение детектора не меняется. - На `initialization_failed_*` и `messaging_stuck_*` (iOS + macOS): - `bg_ready_after` — `none` / `pending` / `failed` / `under_1s` … `over_30s`. Источник — completion у `loadBackgroundContent`. Он вызывается один раз на следующем шаге run loop после `didCreateBackgroundWebView`: view уже есть, поэтому вызов лишь присоединяется к идущей загрузке и ничего не будит. Отключается тем же флагом, что и delegate proxy (`cpmBackgroundDelegateProxy`). Код — `CPMBackgroundReadinessObserver.swift`. - `bg_view_age` — `under_4s` / `4s_to_8s` / `8s_to_30s` / `30s_to_2m` / `over_2m`. - `app_state` — состояние приложения в момент сбоя. - `open_tabs` — размер `context.openTabs`, бакетами; `loading_tabs` — сколько из них в `isLoading`. - Перенесены из worktree B: `bg_load_error_app_state` и `bg_load_error_after` (error 6). - На `recovered_without/after_extension_reload`: - `recovery_document` — `failed_navigation` (опоздавший ответ), `later_navigation`, `unattributed`; - `recovery_after` — время от первого сбоя эпизода. - Проверено на реальном WebKit (harness, медленный старт worker'а 5 с на пробуждении): сбой ушёл с `bg_ready_after=pending`, `bg_view_age=4s_to_8s`, а восстановление — с `recovery_document=failed_navigation`, `recovery_after=under_4s`. - Пакет: 677 тестов зелёные. SwiftLint чистый. `validate-pixel-defs` проходит на iOS и macOS. - Замечание по harness: `network_restarted` там иногда `true` без падения Network-процесса; вероятная причина — тестовый процесс перезапускает Network-процесс между загрузкой контекста и сбоем (не проверено). К продовой телеметрии это не относится, но в тестах этот флаг для классификации не годится. ## Тесты Папка `Tests/WebExtensionsTests/CPMRepro/`, запуск из `SharedPackages/WebExtensions`: - `CPM_REPRO=1 swift test --filter CPMRepro` — путь приложения: `CPMReproSingleTabTests` (10), `CPMReproFullFailureTests` (8). Должны быть зелёными. - `CPM_REPRO=1 CPM_REPRO_WEBKIT=1` — ещё `CPMReproWebKitTests` (7): защиты выключены, тесты утверждают, что баги WebKit всё ещё есть. Покраснеют, когда Apple исправит — сигнал убрать обходы. - Ручки: `CPM_REPRO_ITERATIONS`, `CPM_REPRO_FREEZE`, `CPM_REPRO_INSTALL_DIR` (настоящий перезапуск процесса: запустить дважды). - Unit-тесты без env: `EmbeddedBackgroundLivenessWatchdogTests` (11), 4 новых в `WebExtensionLifecycleCoordinatorTests`, 3 новых в `CPMMessagingHealthMonitorTests`. ## Открытые вопросы — решить до коммита - [ ] **Feature flag.** Watchdog и перезапуск по первому сбою сейчас включены по умолчанию (`isEmbeddedBackgroundRecoveryEnabled = { true }`), в приложениях не подключены к удалённому флагу. Завести флаг (`ddg-add-feature-flag`), желательно раздельно для watchdog и для перезапуска по сбою. - [ ] **Риск перезапуска по первому сбою.** Первых сбоев много (~6.6k установок/день на iOS); часть — не мёртвый worker (нет инъекции, медленный native). Перезапуск дешевле M4, но рвёт незавершённую работу background для других вкладок. Решить: включать сразу или через эксперимент. - [ ] **Телеметрия.** Восстановление после перезапуска сейчас попадает в `recovered_without_extension_reload` и неотличимо. Реализовать план из раздела «Телеметрия для валидации»; минимум — `network_replaced_since_view`, `native_seen` и пиксель `background_restarted`. - [ ] **Порядок выкатки.** Телеметрия раньше фиксов или вместе с ними в A/B-эксперименте с когортой в CPM-пикселях. - [ ] **Error 6 на iOS** (~1/3 строк `stuck`) не покрыт фиксами. Причина найдена в WebKit (idle-таймер 30 с взводится до окончания загрузки), репро на macOS есть. На iPhone (iOS 26.7.1) в четырёх сценариях не воспроизвёлся. Выпустить телеметрию `bg_load_error_app_state` / `bg_load_error_after`, по данным выбрать фикс. Опционально: фоновый запуск системой с `sudo log collect` на Mac пользователя. - [ ] На тестовом iPhone стоит debug-сборка DDG вместо 7.221.0 — вернуть из App Store, когда тесты закончатся. - [ ] **Привязка вкладок на macOS:** гипотеза мигания флага отброшена; проверить popup-вкладки, кэш закреплённых вкладок и пересборку user scripts. - [x] Телеметрия для error 6: состояние приложения и задержка до ошибки — реализовано в `alex/cpm-web-extension-remaining-failures` (не закоммичено). Нужны privacy triage и решение, в каком PR выпускать. - [ ] **Остаток «здоровых» сбоев без смены Network-процесса** (группа 1): около половины — пробуждение, сигнатура воспроизведена медленным стартом worker'а; около четверти iOS — после загрузки контекста (задержка очистки при смене `baseURL`). Подтвердить телеметрией фазы 1. - [ ] **SPI.** Перезапуск через `_killWebContentProcessAndResetState`, детекция через `_networkProcessIdentifier`; есть `responds(to:)`. Согласовать с командой (приложение уже использует SPI WebKit в этом месте). - [ ] **Сборка приложений с фиксами.** Worktree с фиксами (`alex/cpm-web-extension-ready-handshake`) iOS и macOS не собирал; изменения обратно совместимы. Worktree телеметрии собран на обеих платформах. - [ ] **Перезапуск по первому сбою vs медленное пробуждение.** Если медленный старт не занимает CPU, проба ответит и перезапуск убьёт грузящийся worker. Не перезапускать и не эскалировать в M4, пока background не готов (`bg_ready_after=pending`) или view моложе ~15 с — это фаза 2 детектора. - [ ] **Фаза 2 детектора:** отсчёт 4 с от готовности background; отдельный пиксель «background не стартовал за 30 с»; stuck / M4 / перезапуск только при готовом background. - [ ] **Раннее пробуждение background** при начале навигации и выходе приложения на передний план (`loadBackgroundContent`, без блокировки). - [ ] **Стабильный `baseURL`:** ограничить встроенными расширениями без `web_accessible_resources` (сейчас диф меняет URL для всех в `makeContext`), прогнать harness (M4, краш Network-процесса, обновление), перенести на свежий `main`. - [ ] **Выпуск телеметрии фазы 1:** privacy triage, решить, отдельным PR или вместе с фиксами в A/B. - [ ] **Номер сборки ОС с фиксом 318756@main** (очистка при смене `baseURL`) — не проверен. - [ ] **Zero-loss для смерти worker'а.** Нужен ping-канал в extension (native port с heartbeat или обработчик ping) — задача в репозитории extension. - [ ] **Не воспроизведено:** продовое «первый probe после M4 падает» (670 iOS / 232 macOS). В тестах после M4 навигации проходят. - [ ] **WebKit-баги для репорта:** (1) потеря сообщения при eager-загрузке background первой загрузки; (2) после краша Network-процесса или удаления регистраций `WebExtensionContext` не замечает мёртвый worker. - [ ] **Прогнать на macOS 27 VM** — фикс 317981 может уже стоять; тогда `testWebKitDropsInitDuringFirstBackgroundLoad` покраснеет. - [ ] Отчёт `cpm-web-extension-repro-results-2026-10-07.md` в `apple-browsers.git` устарел: он не учитывает gate приложения и фиксы. Актуальное — эта заметка.