Files
obsidian-vault/work/projects/cpm-web-extension-breakage-validation.md
T

16 KiB
Raw Blame History

CPM Web Extension Breakage — статус и план

Короткая актуальная версия на 2026-09-14. Полная трассировка остаётся в cpm-web-extension-breakage-findings.

Итог

Есть две разные группы багов, их нельзя смешивать:

  1. All-tabs: смерть background WebContent process может сломать CPM во всех вкладках, включая новые. Это главный кандидат для telemetry → graveyard → A/B.
  2. Per-tab: приложение сообщает WebKit, что ещё живая вкладка закрыта. Ломается только конкретная вкладка или shared pinned tabs. Это отдельные фиксы и отдельные PR.

Статус проблем

Проблема Статус Что известно / что делать
Смерть background WebContent process → page-less service-worker process → BackgroundContentFailedToLoad loop Подтверждено воспроизведением 100% repro через kill -9 background PID. CPM не работает во всех вкладках, reload/new tab не помогают, Fire помогает. Естественная частота и реальные production reasons пока неизвестны. Это scope graveyard experiment.
Закрытие окна забывает shared pinned tabs в WebKit Подтверждено вручную didCloseWindow закрывает/forget-ит shared pinned Tab, хотя он остаётся в других окнах. Нужен отдельный per-tab lifecycle PR.
Close Other Tabs forget-ит оставленную вкладку Подтверждено по коду removeAll(andAppend:) посылает didCloseTab и для retained tab, затем возвращает тот же объект без didOpenTab. Нужен ручной regression repro и отдельный fix PR.
Duplicate Tab из popup переносит оригинальный Tab вместо копии Подтверждено по коду При закрытии popup WebKit забывает вкладку, оставшуюся в main window. Нужен ручной repro и отдельный fix PR.
Пропуск CPM content script при startup/session restore Подтверждено как transient Только текущий document; следующая навигация восстанавливает работу. Не объясняет устойчивый all-tabs outage.
Однократный failed background load остаётся в m_backgroundWebView Механизм подтверждён, исходный вывод отвергнут Самовосстановление есть: второй wake-up ставит 30 s eviction, затем возможна повторная загрузка. Устойчивый outage требует повторяемой причины или постоянного traffic.
Natural background-process death в production (crash / memory / CPU / jetsam) Не проверено в поле Forced crash доказывает цепочку, но не частоту и не распределение termination reasons. Нужна телеметрия до фикса.
Event starvation / JSC hang / stale listener bookkeeping Не проверено, низкий приоритет Состояния возможны, но стабильного repro и production transition нет. Оставить как residual diagnostic buckets.
Native messaging stall / malformed CPM config Не проверено, низкий приоритет Возможный all-tabs симптом, но подтверждённого production path нет.
Critical memory pressure как конкретный триггер Не воспроизведено Ручной memory_pressure CPM не сломал. Не использовать как доказанный root cause.
Kill/restart Network process как триггер Отвергнуто на проверенной сборке WebKit восстановился, CPM продолжил работать.
removeAllUserScripts() удаляет WebExtension scripts Отвергнуто WebKit использует RemoveWebExtensions::No.
Feature-flag transition, scriptlet reload, обычный long-running JS страницы Отвергнуто как объяснение all-tabs outage Другой extension context или tab-local execution.
Safari SW registration DB fix Нерелевантно Extension SW registration не хранится как обычная persistent registration.

Текущий worktree

Сейчас в одной незавершённой пачке смешаны:

  • diagnostic model/recorder, pixel parameters и hooks в health monitor/manager;
  • background WKNavigationDelegate proxy, process-death/hang timeline;
  • graveyard treatment и два feature flags/cohorts;
  • debug notifications/menu, crash simulations, большие UI tests и локальная repro HTML;
  • посторонние/временные изменения (NSObject deallocation assertion выключен, Package.resolved bump, правило UI tests, accessibility refactor).

Состояние: код не считать готовым к PR; в этой сессии он не собирался и тесты не запускались. Отдельный blocker: proxy уже сам менял lifetime/timing и в наблюдении предотвращал repro. В текущем коде install сохраняет associated object на original delegate, а isInstalled/uninstall ищут его на webView — это надо исправить и покрыть тестом до PR. Поэтому текущий control не является доказанно нейтральным baseline.

Milestones

Два трека:

  • Correctness: M1 tabs и M2 stable base URL независимы друг от друга и от graveyard.
  • All-tabs: M3 telemetry → M4 production validation → M5 graveyard → M6 A/B выполняются последовательно; M5/M6 лучше делать stacked на M3.

M0 — Разобрать worktree

Цель: получить чистые группы изменений без изменения поведения.

  • Убрать из feature stack выключенный NSObject assertion, случайный Package.resolved bump и изменение repo rule.
  • Debug menu/notifications/repro HTML оставить локально или вынести в отдельный debug-tooling PR.
  • Большие AutoconsentUITests, accessibility cleanup и restart(beforeLaunch:) вынести в test-infrastructure PR; оставить только детерминированные regression tests.

Done: каждый оставшийся diff однозначно относится к одному milestone; нет смешанных production/debug/test-infrastructure изменений.

M1 — Исправить tab lifecycle

Один общий PR допустим только при введении общего lifecycle API; иначе лучше три небольших feature PR.

  1. Close Other Tabs: заменить перегруженный removeAll(andAppend:) на removeAll(except:); retained Tab не получает didCloseTab.
  2. Duplicate Tab from popup: создавать новый Tab/tabCopy и переносить копию; один Tab/WKWebView не должен принадлежать двум коллекциям.
  3. Shared pinned tabs on window close / Merge All Windows: didCloseWindow не должен закрывать shared pinned Tab, который остаётся видимым в другом окне.

Validation:

  • Unit tests проверяют identity и точную последовательность didOpenTab/didCloseTab/didCloseWindow, а не только массив и selection.
  • Close Other Tabs: retained loaded tab остаётся известен WebKit; CMP работает после последующей navigation/reload.
  • Popup Duplicate: source и duplicate — разные объекты; закрытие popup не ломает CPM в main window.
  • Shared pinned: закрытие второго окна и Merge All Windows не дают tab not found; pinned tab работает без открытия нового окна.
  • Negative coverage: реально закрытые tabs по-прежнему получают ровно один didCloseTab.
  • Diagnostic signature после фикса: tab_known_to_webkit=true, tab_controller_matches_context=true, нет content_script_send_error=tab_not_found.

Done: три repro проходят; обычные open/close/move/Fire сценарии не меняют баланс lifecycle events.

M2 — Stable WebExtension base URL

Цель: задать context.baseURL = webkit-extension:/// до WKWebExtensionController.load(context), чтобы после первой миграции WebKit больше не запускал rename origin на каждом load.

Validation matrix:

  • Fresh install: сразу используется stable base URL; CPM/background/content scripts загружаются.
  • Existing install с предыдущим random base URL: ровно одна migration/rename, затем stable URL сохраняется между relaunch/reload.
  • Повторные cold launches: rename origin больше не вызывается; context достигает safe-to-inject раньше.
  • storage.local и extension preferences переживают миграцию; отдельно проверить, что web-origin storage либо переносится, либо осознанно не используется.
  • Extension update/reinstall, explicit reload и Fire: identifier/base URL соответствуют ожидаемой semantics и не сталкиваются между extension contexts.
  • Session restoration: детерминированный lifecycle test подтверждает, что load завершён до addInjectedContent; небольшой manual stress run подтверждает отсутствие пропуска CPM на restored document. Не делать flaky тест с десятками случайных relaunch обязательным CI gate.

Telemetry/validation: временно логировать или считать reason/duration base-URL migration только в internal build; сравнить cold-load timing до/после. Не отправлять URL или identifier в pixel.

Done: one-time migration подтверждена, повторных rename нет, storage не потерян, restored CMP page работает без ручного reload.

M3 — CPM failure-attribution telemetry

Самостоятельный production PR без graveyard и без recovery behavior.

  • CPMMessagingDiagnostics с короткими PII-free/bucketed параметрами.
  • Diagnostics на cpmInitializationFailed и cpmMessagingStuck.
  • Passive facts: context errors, background view count/alive, WebProcess/NetworkProcess state, tab known to WebKit, native handler и controller/context match.
  • Ordered background_events только если observation не меняет lifetime.
  • Manager hooks, macOS wiring, pixel definitions/privacy review и unit tests на bucketing/sanitization.

Критический design constraint: delegate proxy не должен сам удерживать WKWebView/WebPageProxy. В текущем WIP proxy уже менял timing и скрывал repro; кроме того, associated object сохраняется на original delegate, а ищется на webView.

Done: forced crash классифицируется как died_* → error_background_failed_to_load при network_process_restarted=false; tab repro классифицируется отдельно как tab_not_found.

M4 — Production validation milestone

Без feature fix. Дождаться достаточного объёма одного релиза telemetry.

Ответить данными:

  • встречаются ли реальные died_memory_limit / died_cpu_limit / died_crash;
  • следует ли за ними BackgroundContentFailedToLoad/stuck signature;
  • какова доля per-tab tab_not_found относительно all-tabs failures;
  • есть ли другие dominant buckets, которые graveyard не исправит.

Go/No-Go: переходить к graveyard rollout только если в production видна связка process death → all-tabs failure. Forced kill подтверждает механизм, но сам по себе не доказывает ценность rollout.

M5 — Background WebView graveyard mitigation

Stacked на M3; implementation/failsafe PR без включённого массового эксперимента.

  • Forwarding delegate proxy для termination callback.
  • Bounded hold dead WKWebView, затем гарантированный release.
  • Независимый runtime kill switch, сначала выключен.
  • Не более одного retained view на context; cleanup при unload/reload; защита от повторных deaths и retain cycles.
  • Tests: public/private termination callback forwarding, reason mapping, immediate control release, treatment timeout, unload cleanup, repeated termination.

Обязательная валидация: proxy-only control должен стабильно воспроизводить исходный kill -9 failure. Если proxy сам лечит race, control не нейтрален и реализацию нельзя использовать для A/B.

Done: с одинаковым observation path control ломается, treatment восстанавливается; разница создаётся только hold duration.

M6 — Graveyard A/B experiment

Отдельный stacked PR: cohorts, outcome measurement и rollout.

  • control: тот же proxy/observation path, немедленный release.
  • treatment: bounded graveyard hold.
  • Stable cohort assignment; kill switch не зависит от cohort.
  • Denominator pixel на каждую background-process termination: cohort + bucketed reason.
  • Outcome после следующей CPM navigation или ограниченного окна: healthy, background_failed_to_load, stuck, recovered.
  • Primary metric: доля background_failed_to_load|stuck после termination по cohort.
  • Guardrails: app/WebKit crashes, retained-view count, recovery latency, repeated termination loops.

background_events на stuck pixel полезен для диагностики, но не является A/B denominator: успешный treatment иначе не попадёт в выборку.

Rollout: internal/review → небольшой 50/50 production sample → проверить denominator и cohort balance → расширять только после достаточного числа termination events.

Done: статистически и технически подтверждено снижение post-termination failures без ухудшения guardrails; после решения о rollout эксперимент удаляется или treatment превращается в обычный remotely controlled fix.

Предлагаемая PR-карта

PR Milestone Зависимость
Tab lifecycle API + Close Other Tabs M1 independent
Popup Duplicate Tab identity fix M1 возможно stacked на lifecycle API
Shared pinned window-close fix M1 возможно stacked на lifecycle API
Stable WebExtension base URL M2 independent
CPM attribution telemetry M3 independent
Graveyard mitigation/failsafe M5 stacked на telemetry
Graveyard A/B wiring M6 stacked на mitigation
Debug/repro tooling M0 отдельный optional PR или local-only