300 lines
51 KiB
Markdown
300 lines
51 KiB
Markdown
---
|
||
title: CPM Web Extension — воспроизведение и фикс
|
||
date: '2026-10-08'
|
||
status: fix-uncommitted
|
||
tags:
|
||
- work
|
||
- cpm
|
||
- webkit
|
||
---
|
||
|
||
# CPM Web Extension — воспроизведение и фикс
|
||
|
||
Продолжение [[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>/`, где `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 приложения и фиксы. Актуальное — эта заметка.
|