Files
obsidian-vault/work/projects/cpm-web-extension-breakage-session-summary-2026-10-07.md

33 KiB
Executable File
Raw Permalink Blame History

updated
updated
2026-10-20

CPM Web Extension breakage — полное summary сессии

Контекст актуализирован 20 октября 2026. Это summary исследований 7–9 октября с follow-up от 15 октября, а не текущий PR contract. Последние states/CI и implementations — cpm-status-2026-10-15, код — cpm-web-extension-code-guide-2026-10-09#0. Текущая карта worktree и точек входа.

#7376 реализован как один terminal episode report, daily+count; PR закрыт без merge. Текущие stuck, двухфазный bg_events, удалённые события/поля и ограничения — cpm-pr0-episode-telemetry-summary-2026-10-16. Старые daily event ratios не являются переходами одного пользователя; новый baseline нужен отдельно.

Source-linked P5/shared-window и условный P6 подтверждены; дальнейшие app/fix проверки датированы в основном статусе. Page ACK по-прежнему нет, а новые данные не объясняют весь Others. Production truncation evidence и оставшиеся измерения — §6.

Историческое продолжение — 15 октября 2026

Текущее продолжение — 15 октября 2026: cpm-status-2026-10-15; актуальная карта worktree — cpm-web-extension-code-guide-2026-10-09 (§0). Разделы 1–9 ниже относятся к 7 октября, §10 — к 9 октября; они не являются последним статусом.

Добавлены 14 P5/P6 tests, все текущие методы имеют green в раздельных запусках. Найдены и проверены пути browser window-close → WebKit forgetTab → P5/tab=false и условный main-feature-flag handler cycle → replacement controller → P6. Для P5/tab=true, естественной потери metadata, actual iOS suspend/resume и production main-flag transitions доказательств нет. Полный AppKit flow не запускался; browser source contract прочитан, production shared classes и system WebKit исполнены.

Пакет: 478 XCTest / 28 opt-in skipped / 0 failures; strict SwiftLint — 0. Ранние красные ожидания, проблемы постановки и отрицательные результаты не удалены: см. основной статус §10–12. Состояния PR/CI и нарезка worktree теперь проверены отдельно; прежнее «всё локально» относится к датированной части.

Дата: 7 октября 2026
Охват: public-attribution после релиза iOS 7.240 / macOS 1.210, исходный код Browser и WebKit, release bundle CPM, WebKit tracker и upstream regression tests.

Короткий вывод

Разделы 1–9 — состояние на 7 октября; раздел 10 — продолжение на 9 октября. Текущий статус — cpm-status-2026-10-15.

Главный production-сигнал — не доказанная потеря initResp, а отсутствие в течение 4 секунд native-вызова refreshCpmDashboardState после завершения навигации. Поэтому большая группа, где native-снимок выглядит здоровым, означает: WebKit controller/context/scripts могли существовать, но CPM не прислал или native не сопоставил ожидаемый dashboard refresh. Она не доказывает, что extension вообще не загрузился.

Наиболее близкие к этому симптому WebKit-кандидаты — гонки при cold start service worker:

  1. 317981: content script отправляет runtime.sendMessage, пока background ещё загружается; WebKit не должен терять сообщение.
  2. 324743: worker считается загруженным и получает сообщение до завершения activation/восстановления clients.

Два других WebKit-багa могут объяснять ухудшение после перезагрузки/recovery, но пока не доказано, что они происходят в CPM:

  1. 322881: после reload worker теряет существующие extension clients.
  2. 324754: при site isolation перезагруженный worker может оказаться в новом процессе без Web Extension bindings.

M4 recovery даёт сильный сигнал восстановления измеряемого background → native пути, но не подтверждает восстановление initResp или всей CPM-функции. Следующая работа: воспроизвести upstream-сценарии на system WebKit и одновременно добавить event-level instrumentation, которая разделит content init, worker receive, initResp и dashboard refresh.


1. Что измеряет current telemetry

Основной сигнал

После navigationDidFinish CPMMessagingHealthMonitor ждёт до 4 секунд native dashboardResponse. Он приходит только когда background вызывает refreshCpmDashboardState, а native handler принимает и сопоставляет его с текущим документом.

navigationDidFinish
  → ожидание dashboardResponse (4 s)
  → нет ответа
  → initialization.failed.after.*.daily
  → при следующем qualifying timeout в пределах episode
  → messaging.stuck.*.daily
  → M4 reload embedded extension

Следствия:

  • initialization.failed не равно «content script не отправил init».
  • initialization.failed не равно «background не получил init».
  • initialization.failed не равно «initResp не дошёл до content script».
  • context_loaded, живой background view, зарегистрированные scripts и native handler не подтверждают, что service worker уже активирован или обработал именно этот message.

context_loaded в WebKit означает, что controller уже назначен. Это происходит раньше асинхронной подготовки storage, создания background view, регистрации scripts и injected content. tab_has_ext_scripts отражает зарегистрированные WKUserScript, а не факт их исполнения в этом документе.

Почему initResp и dashboard refresh не одно и то же

В release bundle content script отправляет init в background. Успешная ветка background запускает updateCpmDashboardState(...).then(refreshDashboardState(...)) без await, а затем отдельно отправляет initResp во вкладку. В части веток (config_unavailable, settings_missing, setting_disabled, site_disabled) отправляется dashboard refresh, но initResp не отправляется.

Значит возможны оба расхождения:

  • initResp был, но refresh задержался, не пришёл или не сопоставился — monitor сообщает failure.
  • refresh пришёл, но initResp не было — monitor считает путь здоровым.

Дополнительные реальные задержки до refresh: remote config, native settings/site checks, последовательная async queue, session storage. JS native-request timeout в bundle — 20 секунд, тогда как monitor ждёт 4 секунды. URL сопоставляется по scheme, host, port, path и query; поздний refresh или другой query также дают failure при работающем JS.


2. Public attribution: охват и контрольные суммы

Окно: полный UTC-день 5 октября 2026, только события после release tag каждой платформы. .daily — максимум одно событие каждого имени в сутки на установку. Это не пользователи, не эпизоды и не transition rate.

После release tag iOS 7.240 macOS 1.210
Первое initialization.failed.after.*.daily 6 624 1 377
Последующее messaging.stuck.*.daily 1 440 487
Первый неудачный CPM probe после reload 670 232
recovered.after.extension.reload.daily 963 = 916 stuck + 47 init 362 = 354 stuck + 8 init
Все CPM daily по именам 11 252 2 804

Контроль: 8 001 init + 1 927 stuck = основные первичные/вторичные строки после tag. До macOS tag в выгрузку попали 28 init, 2 stuck и 39 CPM daily; они вынесены из public-среза. До iOS tag строк этой версии нет.

Группы снимков на первом failure

Каждая строка относится к одной группе по приоритету. Это описание состояния в момент timeout, не первопричина.

Группа iOS init macOS init iOS stuck macOS stuck
Native snapshot без видимой ошибки 3 717 911 789 335
Tab attachment / injection 931 116 53 38
Background state 768 158 457 35
Только история context errors 343 158 87 79
Недостаточно tab facts 865 34 54 0

Крупнейшая группа — 3 717 iOS / 911 macOS — не является подтверждением «нормальной загрузки». Она только не содержит видимого native-признака поломки на момент снимка.

Главные окружающие цепочки

  • Session restoration: 952 iOS / 68 macOS init failures. На iOS у 604 controller совпадает, scripts отсутствуют, background view не создавался; 754 лежат в первых 30 секундах запуска. Это согласуется с временным startup miss, но 260 снимков выглядят здоровыми и требуют timings сообщений.
  • Обычные навигации iOS: часты load → view → proxy_on и load → view → proxy_on → dealloc → view → proxy_on. dealloc означает освобождение background view, не process crash. Явный died_* есть лишь в 85 первичных iOS строках.
  • После extension reload: 670 iOS / 232 macOS первых timeout. Типичный хвост содержит load → base_url_changed → view → proxy_on; base_url_changed сопутствует reload и не доказывает URL defect.
  • macOS controller/scripts mismatch: после tag 77/1 377 init и 35/487 stuck имеют неверный controller и отсутствующие scripts; до tag таких строк нет. 45 из 77 совпали с app_version_change=updated, но update сам по себе не доказывает, что живая вкладка осталась на старом controller. Нужен отдельный event смены manager/controller при живой вкладке.
  • WebKit error 6: в 364 iOS stuck снимках есть WKWebExtensionContextErrorDomain:6; в 363 перед ним есть dealloc. Это BackgroundContentFailedToLoad: service worker/background не удалось загрузить. Пиксель не содержит первичную причину registration/load failure.

3. Связь init failure и stuck

В коде связь прямая: первый timeout создаёт Episode; stuck появляется только после следующего qualifying timeout, обычно в пределах пяти минут. Причина stuck наследуется от первого failure, а stuck snapshot снимается со второй вкладки. Recorder хранит общую history background context, поэтому stuck.bg_events может содержать события до первого init failure.

В public-срезе сравнивались длинные цепочки (>=6 событий) в окне пяти минут: порядок токенов и возраста событий должны были совпадать. Контроль — те же правила, но stuck timestamps сдвинуты на ±10 минут.

Platform Stuck Длинная цепочка Совпала с более ранним init Контроль +10 / −10 min Один кандидат
iOS 1 440 1 025 693 255 / 270 467
macOS 487 368 168 29 / 19 148

Это подтверждает, что stuck чаще продолжает предшествующий timeout-эпизод, чем случайную соседнюю цепочку. Это не соединение одного пользователя: нет reporter ID, .daily дедуплицирует события, короткие хвосты недостаточно уникальны.

Важно: один потерянный init сам по себе не доказывает global stuck. Для production stuck нужны два qualifying timeout в одном episode. Поэтому тест «один tab потерял init, второй сам сломался» не был честным доказательством и не получен. Проверять нужно: два timeout в одном стартовом окне → настоящий M4 recovery → третья вкладка без injection.


4. Проверка релизов и mitigation

Session-restoration gate

M7 gate уже ждёт initial loadAndSync перед первой main-frame HTTP(S) navigation restored tab с bounded timeout. По public data временные session-restoration failures не исчезли: 952 iOS / 68 macOS. Без связки событий нельзя посчитать, какая часть затем восстановилась.

Tab lifecycle fixes

Новые controller/scripts поля появились только в новом атрибуционном релизе, поэтому старые версии нельзя сравнивать по tab bugs напрямую. Доступен только proxy stuck.other / init.other на macOS:

Версия Proxy
1.208 39.52%
1.209, содержит M1 tab identity fix 38.81%
1.210 public day 39.81%

Разница 1.208 → 1.209 составляет 0.71 п.п. и не доказывает эффект или его отсутствие: различаются cohort и нагрузка.

M4 after-stuck recovery

M4 делает reload embedded extension после подтверждённого hang. После tag recovered.after.extension.reload.daily с recovery_from=messaging_stuck есть у 916 iOS и 354 macOS. Отношение всех recovered.after к stuck daily: 66.9% iOS и 74.3% macOS; на предыдущих версиях proxy был 3.23% / 2.70%.

Это сильный признак, что reload часто возвращает измеряемый background → native refresh. Это не success rate и не доказательство восстановленного CPM/initResp. После reload первый probe всё ещё timeout-ится 670 раз на iOS и 232 на macOS.


5. Другие CPM/Web Extension сигналы

После tag Web Extension продолжает работать на значимой части навигаций:

Signal iOS macOS Что доказывает
autoconsent_init.daily, fromExtension=1 56 906 2 825 Background получил какой-то init и отправил native pixel; не delivery initResp
web_extension_loaded.daily 61 902 2 870 Manager загрузил хотя бы одно extension
web_extension_state_checked.daily 62 004 2 805 Lifecycle проверил loaded contexts
web_extension_embedded_not_loaded.daily 3 389 1 Embedded extension отсутствовал в manager во время проверки
web_extension_load_error.daily 4 4 Ошибка общей загрузки extension

Также есть extension-sourced popup-found и done, значит CPM functional path работает у части навигаций. Общие имена пикселей разделены с legacy user script: выводы допустимы только с fromExtension=1.

На macOS лишь один embedded_not_loaded.daily при 77 init failures с controller mismatch/no scripts. Это согласуется с локальной проблемой вкладки при исправном глобальном manager.


6. WebKit hypotheses

317981 — сообщение в cold start worker

Upstream regression test SendMessageFromContentScriptWhileBackgroundIsLoading:

  1. Загружает MV3 extension с non-persistent background.
  2. Background синхронно регистрирует runtime.onMessage.
  3. Сразу открывает web tab без ожидания background readiness.
  4. Content script сразу вызывает runtime.sendMessage.
  5. Ожидает ответ background.

Правильное поведение — очередь до registration listener. Без фикса сообщение может быть потеряно. Это ближе всего к цепочке scripts/context есть → init отправлен → нет response/refresh и не требует искусственного JS timeout.

Статус: fix 317981 присутствует в public Safari/WebKit ветке safari-7625.1.29.*. macOS 27 VM из проверки имела WebKit bundle 22625.1.29.11.27; точное соответствие Apple OS build и WebKit commit нельзя доказать лишь CFBundleVersion. Нужен system-framework regression run на конкретной VM, а не предположение по номеру.

324743 — worker получает event до activation

Upstream test выгружает worker, будит его runtime.sendMessage из extension tab и прямо в message listener проверяет clients.matchAll() без polling. Offscreen document обязан быть уже controlled client. Без фикса worker обрабатывает message, пока activation/reconnect ещё не завершились.

Это также похоже на primary telemetry: controller/view выглядят живыми, но реальная обработка message идёт в неправильный момент.

322881 — reload потерял extension clients

Upstream test создаёт offscreen document, проверяет его presence в clients.matchAll(), reload-ит background и проверяет, что новый worker вновь видит тот же client. В production relevance пока не установлена: CPM обычно работает с web tab/content script, а не обязательно с extension-owned offscreen page.

324754 — worker в новом process без bindings

Upstream test включает site isolation, создаёт offscreen document, reload-ит worker, проверяет смену PID и затем наличие browser.runtime.getURL. Без фикса JavaScript worker стартует в процессе без Web Extension API bindings.

Это хороший кандидат для проблемы после reload/recovery, но не доказанный источник current CPM data. Сначала нужен upstream-shaped reproduction с offscreen/extension page, затем full CPM scenario.

Связь с error 6

Error 6 частично ложится на путь refreshCpmDashboardState: если background не загрузился, он не вызовет refresh. Но error 6 не говорит, где именно сломалось: service-worker registration, process, storage, bindings или другая внутренняя WebKit ошибка. Он не доказывает 317981, 324743, 322881 или 324754.


7. Fix в browser code

В рабочем дереве alex/cpm-web-extension-ready-handshake подготовлен некоммитнутый JS fix, ориентированный на потерянное/слишком раннее init сообщение: content side ждёт explicit readiness/ответ и повторяет handshake вместо того, чтобы считать один message окончательным. ZIP с extension — release artifact; в нормальной жизни он собирается из исходников расширения и попадает в bundle приложения на build/release pipeline, а не загружается пользователем.

Смысл фикса: не менять WebKit и не «лечить» искусственной задержкой, а сделать CPM handshake идемпотентным. Если первый init попал в cold-start race, content script повторно инициирует состояние и получает актуальный dashboard path. Это покрывает потерю первого сообщения/ранний readiness contract, но не восстановит worker без browser.runtime и не исправит WebKit process/client bug сам по себе.

Существующий fixture с setTimeout полезен как test retry protocol, но не доказывает настоящую WebKit race. Он показал важное ограничение: единичная потеря init не делает все tabs stuck автоматически.


8. План точного воспроизведения без изменения WebKit logic

Почему не C++ DYLD_INTERPOSE

Нужные C++ пути выполняются внутри отдельных WebContent/Network processes и используют internal hidden symbols. Вызовы внутри WebKit dylib обычно не проходят через interposable imported symbol stubs. Инъекция dylib в Apple WebContent subprocesses ненадёжна из-за hardened runtime, SIP и XPC environment. Это не практичный первый метод.

Рабочий harness

  1. Создать Swift integration harness с Objective-C runtime swizzle и RaceGate; тесты запускать serially, потому что swizzle process-global.
  2. Для 317981 не ставить gate: портировать exact upstream cold-start race и многократно запускать на system WebKit.
  3. Для 324743 swizzle -[WKWebExtensionContext loadBackgroundContentWithCompletionHandler:]: сообщить completion test coordinator раньше, а оригинальный вызов удержать до explicit release. Это контролирует scheduling на публичной ObjC boundary, не меняя extension logic или C++ WebKit code.
  4. Для real lifecycle использовать доступные private test SPI динамически: _terminateServiceWorkers, _backgroundWebView, _killWebContentProcessAndResetState, _webProcessIdentifier, _setSiteIsolationEnabled:.
  5. Логировать OS build, WebKit CFBundleVersion, timestamps init/response, background PID до/после, outcome каждой итерации.

Матрица

Сценарий Реальный триггер Успех/отказ
317981 Сразу открыть tab после extension load Каждый init получает ответ/refresh
324743 Gate вокруг background load completion, tab уже навигировался Message не должен исполняться до activation
322881 Создать extension client, _terminateServiceWorkers, разбудить message Client есть в clients.matchAll()
324754 Site isolation, убить/restart background, дождаться нового PID В новом worker есть browser.runtime.*
CPM amplification Два timeout в одном shared start window, настоящий M4 reload, затем third tab без injection Third tab работает; иначе доказан cascade/recovery defect

Нельзя подтверждать global stuck одним искусственно потерянным init на одной вкладке. Для честного test нужны два timeout, которые переводят health monitor в stuck, после чего injection выключается перед третьей вкладкой.


9. Какие данные нужны дальше

Приоритетная telemetry с общим, privacy-safe navigation/episode correlation ID:

  1. content_init_sent.
  2. background_init_received и окончание async prerequisites.
  3. initResp_sent / initResp_received.
  4. refreshCpmDashboardState_sent / received / URL match result.
  5. worker lifecycle: start, activation, PID/process generation, background load error code, recovery generation.

Эти точки разделят пять разных случаев, которые сегодня попадают в один init failure: content injection miss, runtime message miss, worker activation race, delayed/mismatched dashboard refresh и native routing failure.


10. Статус на 9 октября

Подробно: cpm-web-extension-repro-and-fix-2026-10-08. Сводки на английском для Asana: cpm-web-extension-data-analysis-summary-2026-10-08 и cpm-web-extension-repro-and-fixes-summary-2026-10-08. Карта кода по строкам: cpm-web-extension-code-guide-2026-10-09. Ничего не закоммичено.

Воспроизведено и закрыто фиксами (worktree alex/cpm-web-extension-ready-handshake):

  • Harness на реальном WebKit (раздел 8): production extension, WebExtensionManager, coordinator + navigation gate, монитор, M4, production-recorder диагностики.
  • Одна вкладка: потеря init при первой загрузке background нового идентификатора (установка или обновление), 317981. При запуске её закрывает gate coordinator'а; установку и обновление посреди сессии закрыл новый gate.
  • Полный отказ: падение Network-процесса оставляет background живым, а worker мёртвым; ломаются все вкладки и открытые документы; M4 чинит только новые навигации. Закрыто watchdog'ом, перезапускающим только background-процесс.
  • Смерть worker'а при живом Network-процессе из приложения не обнаружить; закрыто перезапуском background по первому сбою (стоит одну навигацию). Риск: при медленном пробуждении, когда процесс отвечает, перезапуск убьёт грузящийся worker.

Сверка с данными 5 октября:

  • Network-процесс объясняет не больше 20–45% «здоровых» stuck.
  • Группа 1 («здоровые» без смены Network-процесса, iOS 748 / macOS 228 daily): около половины — сразу после пробуждения background после idle-выгрузки; около четверти iOS и 15% macOS — после загрузки или reload контекста.
  • Поправка: recovered_without_extension_reload(initialization_failed) к stuck не относится — он закрывает только init-эпизоды. Init → recovery без reload: 23% iOS / 25% macOS устройство-дней; stuck → recovery после M4: 64% / 74%. Сбои после загрузки контекста чаще проходят сами; на пробуждении чаще доходят до stuck.

Новые находки 8–9 октября (worktree alex/cpm-web-extension-remaining-failures, без фиксов):

  • Медленный старт worker'а на пробуждении (> 4 с) воспроизводит сигнатуру группы 1 полностью: bg_process_responsive=true, context_errors=none, хвост dealloc → view → proxy_on@30s. 5 с → initialization_failed, затем поздний ответ; 12 с + следующая навигация → stuck → M4, который убивает живой медленный worker. Старт до 2,5 с — потерь нет. Почему на устройствах старт бывает долгим — неизвестно.
  • Error 6 (iOS, ~1/3 строк): причина в WebKit — 30-секундный таймер выгрузки взводится до окончания загрузки. Репро на macOS есть, на iPhone (iOS 26.7.1) не воспроизвёлся.
  • WebKit 320812 / 312231@main (26.6+): при смене baseURL загрузка контекста удаляет website data старого origin, включая весь дисковый кэш, и только потом инъецирует content scripts и грузит background. У DDG baseURL новый на каждом запуске и reload. На Mac с ~20–24 тыс. записей кэша задержка 0,75–1,35 с. Кандидат-митигация — стабильный baseURL (ветка alex/cpm-stable-base-url).
  • Другие баги WebKit: 324743 (worker «загружен» до завершения регистрации; исправлен 22 сен, нет в 26.7/27.0) и 314194 (Lockdown Mode блокирует все service worker'ы; открыт).
  • Сигналы готовности background: окончание загрузки страницы background view бесполезно (0,1 с); completion loadBackgroundContent и первое нативное сообщение worker'а точно отмечают его старт.
  • Отброшено: блокировка устройства / data protection, устаревший URL вкладки на iOS, мигание флага для привязки вкладок на macOS.

Телеметрия фазы 1 (worktree alex/cpm-messaging-readiness-telemetry, от свежего main):

  • На сбоях: bg_ready_after, bg_view_age, app_state, open_tabs, loading_tabs, bg_load_error_app_state, bg_load_error_after.
  • На восстановлениях: recovery_document, recovery_after.
  • Проверено на реальном WebKit; 677 тестов пакета, SwiftLint, validate-pixel-defs, сборка iOS и macOS — зелёные.

Дальше: фаза 2 детектора (отсчёт 4 с с момента готовности background, без эскалации до неё), раннее пробуждение background, стабильный baseURL, флаги и A/B для фиксов, репорты в WebKit. Не воспроизведены: привязка вкладок на macOS, «нет background view», код 7, первый сбой после M4.


Источники

  • Public attribution: cpm-web-extension-public-attribution-2026-10-05.md и папка cpm-public-attribution-2026-10-06/ в apple-browsers.git.
  • Historical validation/methodology: cpm-web-extension-breakage-validation.md.
  • WebKit 317981 regression: Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKWebExtensionAPIRuntime.mm, SendMessageFromContentScriptWhileBackgroundIsLoading.
  • WebKit 322881/324743/324754 regressions: Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKWebExtensionAPIOffscreen.mm.
  • Browser implementation: CPMMessagingHealthMonitor.swift, CPMMessagingDiagnosticsRecorder.swift, WebExtensionManager.swift, AutoconsentWebExtensionMessageHandler.swift.