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

167 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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://<uniqueIdentifier>/ до 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 |