54 KiB
Executable File
title, date, status, tags, updated
| title | date | status | tags | updated | |||
|---|---|---|---|---|---|---|---|
| CPM Web Extension — воспроизведение и фикс | 2026-10-08 | historical-investigation |
|
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, двухфазный boundedbg_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)
- Gate для первой загрузки посреди сессии. После
installEmbeddedExtensionменеджер запоминает задачуloadBackgroundContent()нового контекста;coordinator.initialLoadWaiter(его уже вызывают обе платформы на каждой main-frame навигации) после открытия стартового gate отдаёт этот waiter, не дольше 5 с. EmbeddedBackgroundLivenessWatchdog. Запоминает PID Network-процесса background'а; при смене перезапускает background-процесс. Проверка на каждой навигации и раз в 1 с.- Перезапуск по первому сбою. Монитор вызывает
onEpisodeStartedна первом сбое обычной навигации; менеджер перезапускает background, если процесс отвечает (callAsyncJavaScript("return 1"), 1 с). Замороженный или занятый не трогаем: убийство остановленного процесса давало глухой новый worker. - Пункты 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 зелёные:
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и определения (iOSdebug_pixels.json5, macOSweb_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_documentvsnext_navigationи хвостbg_eventsна момент сбоя.
- У recovery-пикселей только
- Версии ОС: на 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 ждут окончания этой чистки. У DDGbaseURLновый при каждом запуске и каждом 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 при межоригинном переходе со страницы): вdidCommiturl = webView.urlвыставляется синхронно доinit→ для обычной навигации ответ приходит с верным URL.
Медленный старт worker'а на пробуждении — сигнатура группы 1 воспроизведена (8 октября, эксперимент, не закоммичено). Тест CPMReproWakeExperimentTests в обоих worktree: после idle-выгрузки в начало background-скрипта на диске добавляется busy-wait (только на этот wake), через 1 с скрипт восстанавливается.
- busy 0,3 / 1 / 2,5 с и задержка
activate1–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'а.loadBackgroundContentcompletion пришёл через 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; в backgroundgetURLтолько для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:
network_replaced_since_view(bool) — PID Network-процесса при последнем создании background view ≠ текущему. Прямая подпись мёртвого worker'а. Recorder уже вычисляет это (CPMMessagingDiagnosticsRecorder.swift:285-289), но только логирует. Плюс токенnet_replacedвbg_events, чтобы видеть порядок. Самое дешёвое и самое ценное.native_seen(none/site_check/refresh_other_url) — были ли нативные вызовы от background для этой навигации (background зовётisFeatureEnabledс URL вкладки при обработкеinit). Делит сбой на «не дошло до worker'а» / «worker обработал, refresh опоздал» / «рассогласование URL на native». Только app.bg_first_load(bool) + токеныinstall/upgrade/bg_loadedвbg_events— подпись бага первой загрузки.gate(none/launch/startup/timeout) — прошла ли навигация мимо gate или он открылся по таймауту 5 с.
Подтвердить, что фиксы работают:
- Пиксель
debug_web_extension_cpm_background_restarted(daily+count):trigger=network_process_replaced/initialization_failed;outcome=restarted/skipped_unresponsive/spi_unavailable. - Атрибуция восстановления:
recovery_action(none/background_restart) кrecovered_without_extension_reload, либо параrecovered_after_background_restart/background_restart_failed(следующая навигация после перезапуска тоже упала → M4). - Ожидаемый эффект: падают
messaging_stuckи M4 (recovered_after_extension_reload); почти исчезают вторые сбои эпизода сnative_seen=none+network_replaced_since_view=true; исчезают сбои сbg_first_load=trueвне запуска. - 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.
- Телеметрия для 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 приложения и фиксы. Актуальное — эта заметка.