Files
obsidian-vault/work/projects/cpm-web-extension-repro-and-fix-2026-10-08.md
T

54 KiB
Executable File
Raw Blame History

title, date, status, tags, updated
title date status tags updated
CPM Web Extension — воспроизведение и фикс 2026-10-08 historical-investigation
work
cpm
webkit
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 зелёные:

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.
  • Телеметрия для 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 приложения и фиксы. Актуальное — эта заметка.