Files
obsidian-vault/work/projects/cpm-status-2026-10-15.md
T

45 KiB
Executable File
Raw Blame History

title, date, tags
title date tags
CPM Web Extension — единый статус (PR, данные, гипотезы, телеметрия) 2026-10-15
work
cpm
status

CPM Web Extension: единый статус на 15 октября 2026

Внутренний документ. Цифры из ClickHouse, в публичные PR, комментарии и issues не переносить (репозитории duckduckgo/apple-browsers и duckduckgo/duckduckgo-privacy-extension публичные). Заменяет заметку cpm-telemetry-data-requirements-2026-10-15 (теперь ссылка сюда). Старые документы с устаревшими числами: cpm-web-extension-public-attribution-2026-10-05, cpm-web-extension-data-analysis-summary-2026-10-08, cpm-web-extension-repro-and-fix-2026-10-08, cpm-web-extension-code-guide-2026-10-09 (карта «большого» worktree, больше не актуальна: всё нарезано на PR ниже).

1. Коротко

  • Фикс B (падение Network-процесса) проверен локально и красным прогоном на CI. Основание в данных есть, но неполное: обогащение в stuck есть, а доказать, что строки это именно мёртвый worker, нельзя (не хватает network_replaced_since_view).
  • Фикс A (первая загрузка посреди сессии) реален, но мелкий: около 1% stuck на iOS, на macOS почти не виден, чаще самоизлечивается.
  • Три самые крупные группы сбоев фиксами A и B не закрыты: пробуждение после idle, error 6 (iOS), «нет background view» на запуске (iOS). Механизмы воспроизведены локально на macOS, но проверенного рычага в приложении нет.
  • Телеметрия PR0 пока в виде, вызвавшем вопросы о склейке пикселей. Решение по правкам не принято (раздел 8).

2. Цепочки PR (все в duckduckgo/apple-browsers, задачи Asana в телах PR; не читать)

Телеметрия:

PR Ветка База Что Статус
#7376 (PR0) alex/cpm-messaging-readiness-telemetry main параметры готовности и тайминга на failure/recovery пикселях (без новых пикселей) CI зелёный, нужен ревью. Ревьюер по пикселям задал вопросы JA1–JA3 о склейке, ответ не отправлен
#7384 (0b) alex/cpm-readiness-baseline #7376 ежедневные базлайн-пиксели cpm_background_ready_{context_load,wake}, нагрузка устройства, возраст возобновления CI в основном зелёный
#7385 (0c) alex/cpm-init-progress-native #7384 init_stage на failure-пикселях (нужен JS из privacy-extension#3727) CI в основном зелёный
privacy-extension #3727 alex/cpm-init-progress уведомления cpmInitProgress из встроенного расширения открыт, нужен ревью

Harness и фиксы (красный коммит, затем зелёный; не ребейзить и не форс-пушить, иначе пропадёт доказательство красного прогона):

PR Ветка База Что Статус
#7378 alex/cpm-repro-harness main harness (только тесты); 4 быстрых теста в CI около 3 с, остальное локально через CPM_REPRO=1 CI зелёный, нужен ревью. Danger: 1570 добавленных строк (предупреждение). В ветке есть коммит другой сессии «Require initResp in the page…»
#7379 alex/cpm-fix-init-loss harness фикс A: ожидание первой загрузки background после установки/апгрейда посреди сессии красный прогон и зелёный: CI зелёный
#7381 alex/cpm-fix-network-crash фикс A фикс B: перезапуск background при замене Network-процесса и мёртвом worker'е красный прогон подтверждён (падает testNetworkProcessCrashKeepsEveryTabWorking, 3 ретрая, Non-Sandbox). Фикс-коммит f1085846e7 локально, не запушен
(нет PR) alex/cpm-fix-pixels фикс B + PR0 пиксели background_restart и first_load_wait для измерения эффекта держим до слияния PR0, A, B

Локальные проверки фикса A: на коде без фикса оба repro-теста красные (5 из 5 итераций), с фиксом 0 из 10 потерь, 761 тест пакета зелёный. Фикс B: на main красные тесты падения Network-процесса и смерти worker'а, 5 из 5 итераций.

Недоделано в harness и CI: CI гоняет тесты с -test-iterations 3 -retry-tests-on-failure, красный тест должен падать стабильно; тесты WebExtensions идут внутри схем macOS Browser и iOS Browser на osx-tahoe-27. Медленные эксперименты (заморозка 45 с, заполнение кэша около 10 минут, медленный старт worker'а) локальные и не для коммита.

3. Данные (ClickHouse, 12–14 октября, версии iOS 7.240+ и macOS 1.210+)

Единица: daily-пиксель = устройство-день на причину, не пользователь и не эпизод. initialization_failed только .daily; stuck, recovered_*, reload_failed: daily+count. Параметры снимка есть только на init_failed и stuck.

iOS macOS
initialization_failed.daily 3 273 025 119 950
stuck.daily 511 660 40 842
init восстановились без reload 1 135 477 (34.7%) 30 738 (25.6%)
stuck → восстановились после M4 reload 293 557 (57.4%) 21 865 (53.5%)
reload_failed.daily 140 921 (27.5% от stuck) 17 796 (43.6%)

Когорты (по приоритету: error 6 → нет view → вкладка не подключена → Network заменён → пробуждение (view <30 с после dealloc) → недавняя загрузка (load/base_url_changed ≤2 мин) → other). Доля init → stuck:

Когорта iOS macOS Смысл роста или падения
error 6 6.1% → 21.5% 1.0% → 1.9% остаётся сломанной
нет background view 48.1% → 10.9% 3.3% → 1.8% самоизлечивается
вкладка не подключена 0.03% → 0.05% 6.2% → 8.9% остаётся сломанной
Network заменён 16.8% → 39.7% 13.6% → 26.6% остаётся сломанной
пробуждение 7.9% → 9.5% 32.5% → 32.2% без изменений
недавняя загрузка контекста 15.7% → 6.0% 20.0% → 6.4% самоизлечивается
other 5.3% → 12.5% 23.4% → 22.2%

Объёмы stuck.count в разных версиях на порядки выше daily: дашборды сравнивать только по версии и частоте.

4. Основание фиксов

  • B: доля «Network заменён» растёт в 2–2.4× при переходе init → stuck. reload_failed высок: reload расширения мёртвый worker не лечит. Оговорка: network_restarted сравнивает PID с загрузкой контекста, а не с последним view. Если view пересоздан после смены процесса, worker свежий и механизм неприменим. По заметке 8 октября механизм объясняет в лучшем случае 20–45% «здоровых» stuck. Причинность подтвердят network_replaced_since_view (нет ни в одной ветке) и init_stage=none (#7385).
  • A: сигнатура «загрузка контекста ≤2 мин, не при запуске, первый view»: iOS 129k init (3.9%) → 4.6k stuck (0.9%); macOS 505 → 118. Разовая потеря одной навигации (0.23× при переходе в stuck). Сигнатура неточна (захватывает M4-reload). Название связанной Asana-задачи («init loss on background unload») относится скорее к группе пробуждения, чем к фиксу A; несоответствие надо снять с автором задачи.

5. Картины событий (stuck.daily, 14 октября, хвост bg_events без возрастов) и воспроизведение

Картина Хвост iOS / macOS Воспроизведено? Рычаг в приложении
P1. Цикл пробуждения view>proxy_on>dealloc>view>proxy_on 16.0k / 4.3k (wake) + 16.3k / 2.4k (старше 30 с) только с искусственно занятым worker'ом (5 с: init_failed и поздний ответ; 12 с: stuck → M4); органическое пробуждение в тестах проходит. WebKit-баг 324743 (исправлен 22 сентября, нет в 26.7/27.0) гипотезы: отсчёт 4 с от готовности background («фаза 2 детектора»), ожидание готовности при пробуждении, предпрогрев
P2. «Пробуждение → error 6», циклы proxy_on>dealloc>view>proxy_on>error_E:6 (23.6k), error>dealloc>view>proxy_on>error (5.7k), после died_crash (1.5k) iOS; macOS 118 macOS: заморозка нового процесса на ≥30 с даёт error 6, корзина 25–35 с. iPhone: 4 сценария безуспешно. Главный iOS-хвост без unresponsive (в воспроизведении он есть) корень в WebKit (wakeUpBackgroundContentIfNecessary взводит 30-секундный таймер до загрузки worker'а). Гипотеза: перезагрузка расширения при возврате на передний план
P3. Контекст загружен, view нет load / load>base_url_changed 17.1k + 0.9k / 162 + 81 задержка очистки при смене baseURL воспроизведена на macOS при большом кэше (около 10 минут заполнения); iOS-гонка запуска (bg_view_creations=0, восстановление сессии, первые 30 с) не воспроизведена: холодный старт в harness'е отвечает за миллисекунды кандидат: стабильный baseURL (ветка cpm-stable-base-url: не доведён, меняет URL всем расширениям, не прогнан через harness, старая база). Фикс WebKit 318756@main, номер сборки ОС не проверен
P4. Первая загрузка load>view>proxy_on 6.8k + 2.3k / 570 да (фикс A) фикс A
P5. Фон выгружен в момент сбоя proxy_on>dealloc>view>proxy_on>dealloc 1.9k / 494 да для test-only потери регистрации живой вкладки; другие гипотезы проверяются (§10) кандидат для tab_in_context=false: восстановить корректный tab lifecycle; естественный вход не доказан
P6. macOS: вкладка без расширения load>view>proxy_on>dealloc / load>view>proxy_on; варианты с extension_updated отдельно iOS ≈0 / 627 + 235 до idle воспроизведено отсутствием controller; полный прогон см. §10 гипотеза: view создана без текущего controller или сохраняет controller прежнего менеджера; естественный вход не найден
Network заменён см. раздел 4 да (краш, worker мёртв при том же view) фикс B
«Здоровый» остаток, other iOS 12.5%, macOS 22% нет нет гипотезы

Размер группы не равен возможности её починить: P1 и P3 в основном опаздывающие ответы, а часть «сбоев» это срабатывание детектора («нет ответа за 4 с», опоздавший ответ тоже считается). iOS-когорта «other» на самом деле тот же рисунок P1, но со view старше 30 секунд.

6. Телеметрия

Что делает PR0 (#7376): на 8 failure-пикселей и 2 recovery-пикселя добавлены параметры bg_ready_after, bg_view_age, bg_load_error_after, bg_load_error_app_state, cpm_app_state, cpm_open_tabs, cpm_loading_tabs (failure) и cpm_recovery_document, cpm_recovery_after (recovery). Новых пикселей нет. Recovery-пиксели получают только свои два параметра, extension_reload_failed без параметров.

Замечания ревьюера (JA1–JA3): корреляция между пикселями из-за множества перечислений; сколько пикселей на ошибку; константность cpm_open_tabs/cpm_app_state; что такое background view.

Что верно по пикселям (исправление моей прежней ошибки): initialization_failed только daily (не больше одного на устройство в день на причину); stuck daily+count (daily и count одного события идут с одинаковыми параметрами); snapshot пересобирается при каждой отправке, поэтому быстро меняющиеся поля (bg_view_age, bg_ready_after) между init_failed и stuck различаются, медленные (app_state, open_tabs) повторяются.

Уникальность строк stuck.daily внутри дня (доля уникальных по всем параметрам):

Вариант iOS macOS
сейчас, bg_events из 12 событий 8.3% 30%
информативные токены, последние 4 2.1% 5.2%
скаляры вместо bg_events 0.96% 4.4%
без bg_events 0.25% 2.0%

Верхняя граница прироста от новых полей (синтетическое независимое равновероятное поле): со старым bg_events при десяти значениях iOS 21%, macOS 65%; со скалярами 3.6% / 15.8%. Вывод: «хуже не будет» можно утверждать только со скалярами. Главный идентификатор уже выкачен (bg_events).

bg_events: единственный источник давности загрузки (load, base_url_changed) и причины смерти (died_*). view, dealloc, proxy_on, error_* дублируют bg_view_creations и context_errors; hold/release/proxy_off равны нулю. Скаляры воспроизводят когорты загрузки и пробуждения на 100% строк. Теряется: поиск неожиданных последовательностей в остатке (смягчить удалённым флагом).

Минимум данных по гипотезам:

Поле / пиксель Вопрос Где
bg_ready_after, bg_view_age идёт ли загрузка, пробуждение или давно PR0
bg_load_error_after, bg_load_error_app_state (только при error 6) таймер 30 с, состояние приложения PR0
recovery_document, recovery_after опоздавший ответ или потерянный PR0
app_state, open_tabs (грубо, 1 / 2–5 / >5), только на initialization_failed когорта «нет view» на запуске PR0
новое: context_load_cause, context_load_age (вместо load@ и extension_updated) точная сигнатура A PR0
новое: bg_died_reason, base_url_changed смерть процесса, очистка при смене baseURL PR0
новое: мелкие корзины app_launch_age для первых 30 с (<4 / 4–10 / 10–30) гонка запуска PR0
новое на stuck: stuck_after сколько от первого сбоя до stuck PR0
новое, срочно: network_replaced_since_view доля реальных мёртвых worker'ов в когорте B (рекордер вычисляет, но только логирует; предложено 8 октября, нигде не реализовано) PR0
init_stage дошёл ли init до background #7385 (срочно)
cpm_background_ready_{context_load,wake} базлайн здоровых загрузок и пробуждений #7384
background_restart (причина, исход; убрать из него bg_ready_after, он дублирует соседний failure-пиксель), first_load_wait эффект B и A удержанная ветка
убрать loading_tabs; из таймлайна view, dealloc, proxy_on, error_*, hold/release/proxy_off

Связность стека: #7384 добавляет ещё 5 измерений на те же 8 failure-пикселей (context_load_to_background_view, cpm_low_power_mode, cpm_thermal_state, cpm_lockdown_mode, cpm_resume_age); оценку склейки надо повторять и по нему. background_restart срабатывает вместе с первым initialization_failed: общий ключ bg_ready_after.

7. Критерии успеха и опровержения

  • B подтверждается, если после релиза падают доля «Network заменён» среди stuck и reload_failed, background_restart показывает восстановление, а у таких stuck init_stage=none.
  • A подтверждается, если сокращается когорта «мид-сессионная первая загрузка» (context_load_cause). Если нет, основание слабое.
  • Остаток P1/P3: гипотеза «медленный старт, а не потеря» подтверждается, если bg_ready_after у упавших заметно хуже базлайна здоровых загрузок и recovery_document=failed_navigation.

8. Открытые решения и следующие шаги

  1. Правки PR0 (ждут вашего «да»): скаляры вместо bg_events, context_load_cause, stuck_after, network_replaced_since_view, мелкие корзины app_launch_age, убрать loading_tabs, константы только на initialization_failed, open_tabs грубо, bg_load_error_* только при error 6; после этого ребейз #7384 и #7385. Альтернатива «оставить bg_events» требует ответа ревьюеру без утверждения «хуже не будет» и обязательства перемерить после релиза.
  2. Ответ ревьюеру (публично, без цифр): JA1 (init_failed daily-only; stuck daily+count с одинаковыми параметрами; recovered несут только recovery_*), JA2 (медленные поля повторяются, быстрые различаются; планируется сократить набор), JA3 (background view = скрытый WKWebView, в котором WebKit запускает фоновый скрипт расширения; выгружается через около 30 с простоя и пересоздаётся при пробуждении). Обязательство: перемерить различимость строк после первого релиза.
  3. Запушить фикс B (f1085846e7): красный прогон подтверждён, ждёт зелёного.
  4. Судьба фикса A: слабое основание в данных, не совпадает с названием задачи; решить, сливать ли без зелёного измерения эффекта через first_load_wait.
  5. P3: довести стабильный baseURL (ограничить встроенными расширениями без web_accessible_resources, прогнать harness, перенести на свежий main), проверить номер сборки ОС с фиксом 318756@main.
  6. Локальные repro для P1–P3 отдельным PR без фиксов (по образцу harness'а, requireLocalRun), чтобы гонять после релизов WebKit.
  7. iOS: воспроизведение error 6 на реальном устройстве; сценарий «вкладка без расширения» на macOS.
  8. Блокеры по ревью: #7376 и #7378 ждут ревью; #7384/#7385 стоят на #7376.

9. Методика и инструменты

  • Доступ: ~/Developer/grafana-data-tools (Grafana → ClickHouse metrics.pixels, read-only; авторизация через grafana-auth). Ad-hoc SQL запускался отдельным раннером через ~/DuckDuckGo/grafana-dashboards/tools/grafana-query/read-dashboard.py. Сканы по pixel LIKE без фильтра по дате, агенту и pixel_id='debug' превышают лимит строк. Временные запросы лежали в папке сессии и могут пропасть; ключевая классификация ниже.
  • Версии с диагностикой: iOS 7.240+, macOS 1.210+. Окно: 12–14 октября (3 полных дня). Ограничения: сигнатуры эвристические; network_restarted только прокси; устройства нельзя проследить от init до stuck, сравниваются только доли; доля не равна пользовательскому эффекту.
-- классификация когорт для init_failed.daily / stuck.daily
multiIf(
  errs LIKE '%ContextErrorDomain:6%', '1_error6',
  creations = '0', '2_no_background_view',
  scripts = 'false', '3_tab_not_connected',
  net = 'true', '4_network_replaced',
  has(ev,'view@30s') AND arrayExists(x -> startsWith(x,'dealloc@'), ev), '5_wake_view_lt30s',
  arrayExists(x -> x IN ('load@30s','load@1m','load@2m','base_url_changed@30s','base_url_changed@1m','base_url_changed@2m'), ev), '6_context_load_recent',
  '7_other')
-- параметры: network_restarted, context_errors, bg_view_creations, tab_has_ext_scripts, bg_events (decodeURLComponent)

10. Журнал воспроизведения P5/P6 — 15 октября 2026

Работа начата по запросу: добавить тесты новых гипотез и попытаться воспроизвести наблюдаемые цепочки. Продакшен-фиксы и публичные PR этим запросом не меняются.

Выбор worktree: /Users/admin/DuckDuckGo/apple-browsers.git/alex/cpm-web-extension-remaining-failures, ветка alex/cpm-web-extension-remaining-failures, HEAD a0717cf098cd1cbd32d5bc9d19aa7db0fab2399f. Здесь общий harness, production recorder, локальные эксперименты пробуждения, error 6 и очистки baseURL (P1–P3), без watchdog/фиксов A/B. Полного единого набора всех сценариев сейчас нет: базовые A/B находятся в cpm-repro-harness/фикс-ветках, init-progress E2E — в cpm-e2e-init. Существующие dirty-изменения сохранены; добавления этой работы ограничиваются тестовым harness.

Среда: macOS 26.7.1 (25G241), Xcode 27.1 (27A9275), системный WebKit. Прогон на macOS проверяет механизм; перенос на iOS требует отдельной проверки.

Уточнение данных перед тестами: read-only перепроверка ClickHouse за 14 октября, stuck.daily, iOS 7.240+/macOS 1.210+:

  • P5: в выборке хвоста proxy_on>dealloc>view>proxy_on>dealloc без error 6/Network replacement/scripts=false — 1953 iOS и 499 macOS, bg_view_alive=false во всех снимках; последняя view везде старше 30 с. Это определение чуть шире строки P5 выше, поэтому 499 не подменяет прежние 494 без сверки дополнительных фильтров. На macOS 68 снимков с tab_in_context=false; на iOS у 1299 не разрешилась вкладка для табличных диагностических полей.
  • P6: load>view>proxy_on>dealloc — 627 macOS; load>view>proxy_on — 235. В обеих подгруппах context_loaded=true, tab_in_context=true, tab_controller_match=false, tab_has_ext_scripts=false. В первых двух минутах запуска — 468/627 и 234/235. 235 — это хвост без dealloc, а не маркер extension_updated; варианты с extension_updated существуют отдельно.

Проверяемые гипотезы:

  1. P5: потеря регистрации вкладки или runtime listener приводит к отсутствию пробуждения после штатного idle; сравнить content-script send/error, background receive, текущую view и параметры failure-пикселя.
  2. P5: долгий документ/пауза приложения позволяют background выгрузиться до диагностического снимка; отличить запоздалый снимок от неработающего worker.
  3. P6: вкладка создана без CPM controller, затем попала в openTabs при загрузке контекста.
  4. P6: существующая вкладка сохранила controller предыдущего менеджера; новый контекст видит вкладку через openTabs, но не внедряет в неё скрипты.

Текущий статус: читаю точки расширения harness; тесты и результаты ещё не получены. Контролируемая инъекция состояния будет явно отделена от естественного воспроизведения.

P5/P6 — тесты добавлены, начат первый прогон

В CPMReproLifecycleHypothesisTests.swift добавлены 7 локальных XCTest-проверок, отдельный opt-in CPM_REPRO_P5_P6=1 поверх CPM_REPRO=1:

  • P6: отсутствующий controller до idle; тот же случай после штатной выгрузки и после reload контекста; перенос существующего WKWebView в replacement manager с сохранением прежнего controller.
  • P5: забытая в WebKit, но оставшаяся открытой вкладка после двух idle-циклов; отсутствие runtime listener после следующего пробуждения; два здоровых документа с реальной задержкой image-subresource на 35 с; те же документы с искусственно потерянными dashboard-уведомлениями.

Harness позволяет отдельно выбирать controller вкладки, переносить существующую view и подавлять конкретный native method только в тестах. CPMReproDelayedResource.swift удерживает реальный subresource через WKURLSchemeHandler; навигационные finish-события не подделываются. Проверки требуют actual stuck от production monitor, полный набор диагностических признаков, content-script initResp для здоровых контролей и восстановление после снятия инъекции.

Первым запущен testTabWithoutControllerMatchesP6BeforeIdleUnload; пока идёт сборка, результата нет. Логи сессии: /private/tmp/cpm-p5-p6-20261015/. Команда сборки: CPM_REPRO=1 CPM_REPRO_P5_P6=1 swift test --disable-automatic-resolution --filter CPMReproLifecycleHypothesisTests/testTabWithoutControllerMatchesP6BeforeIdleUnload.

Первый результат P6 до idle

Сборка прошла. Первый тест получил реальный initialization_failed и затем stuck с load>view>proxy_on, context_loaded=true, tab_in_context=true, tab_controller_match=false, tab_has_ext_scripts=false, bg_view_alive=true, bg_view_creations=1. Content marks пустые: CPM не внедрён. Новая вкладка с current controller получила initResp и вызвала recovery.

Прогон пока красный (1 assertion): дополнительное ожидание network_restarted=false не выполнено — поле true. Падения/замены Network-процесса тест не инъецирует. Проверяю холодный старт Network до контекста: recorder сравнивает PID с context-load, что может смешивать первичный запуск и замену. Результат нельзя объявлять полным совпадением всех признаков до этой проверки. Лог: build-and-p6-before-idle.log. SwiftLint пяти изменённых файлов прошёл с --no-cache, 0 нарушений; обычный режим упёрся в недоступный для записи кэш.

Уточнение harness и первый зелёный P6

Проверенный прогрев default WKWebsiteDataStore перед context-load даёт ненулевой Network PID; в последующем failure network_restarted=false. Это сняло лишнее отличие первого холодного прогона. testTabWithoutControllerMatchesP6BeforeIdleUnload теперь зелёный: совпадают хвост и все проверяемые признаки, новая правильно подключённая вкладка работает. Механизм получен контролируемым отсутствием controller; естественный путь создания такой вкладки в приложении ещё не найден.

Проверка replacement manager сначала не дошла до failure-пикселя: teardown первого harness снимает navigationDelegate, и при переносе view его надо восстановить. Исправлен только тестовый adoptTab; это ошибка постановки эксперимента, не результат о CPM. Начат полный прогон семи тестов с исправлением: p5-p6-full.log.

Дополнительный контроль P6

Добавлен восьмой тест testCurrentControllerBeforeContextLoadRemainsHealthy: создать вкладку до завершения context-load, но сразу с правильным controller, и потребовать initResp, скрипты и отсутствие initialization_failed. Это отделяет обычный ранний startup от отсутствующей привязки controller. Он будет выполнен после текущего запущенного бинарника; результат пока не получен.

Подтверждены два контролируемых механизма

P6, replacement manager — зелёный, 9.186 с. Сначала существующая вкладка получила initResp со старым менеджером. После teardown старого менеджера её тот же WKWebView и прежний controller зарегистрированы в replacement manager. Получены два failure и actual stuck с load>view>proxy_on, context/tab=true, controller/scripts=false, view=true, creations=1, errors=none, network_restarted=false. Новая вкладка replacement manager получила initResp. Это воспроизводит сохранённую привязку; естественный триггер замены менеджера не доказан.

P5, забытая живая вкладка — зелёный, 72.744 с. Два нормальных initResp и две реальные idle-выгрузки, затем test-only didCloseTab без удаления WKWebView. Получен actual stuck с load>view>proxy_on>dealloc>view>proxy_on>dealloc, creations=2, view=false, context=true, tab=false, controller/scripts=true, errors=none, network_restarted=false. Content script отправляет init и получает init-error. После didOpenTab эта же вкладка снова получила initResp. Механизм подходит под P5-подгруппу с tab_in_context=false; его доля в проде и естественное действие приложения, создающее нарушение lifecycle, этим тестом не установлены.

Оба результата находятся в p5-p6-full.log; другие сценарии продолжают выполняться.

Полный первый прогон: положительные и отрицательные результаты

Прогон p5-p6-full.log завершён: 7 тестов, 4 зелёных и 3 красных (11 assertions), 273.8 с. Не все красные результаты означают дефект harness:

  • P6 после idle — зелёный, 38.658 с. Получена цепочка load>view>proxy_on>dealloc с исходной сигнатурой controller/scripts=false, tab/context=true. Reload контекста не исправил существующую вкладку; новая правильно подключённая вкладка получила initResp.
  • Потеря runtime listener только на одном wake не воспроизвела P5. Инъекция действительно потеряла первый init (native dashboard не пришёл), но после восстановления файла и следующего idle новый worker принял следующий init и прислал initResp, stuck=0. Ожидание устойчивого P5 в тесте было неверным; превращаю его в отрицательную characterization-проверку этого результата.
  • Долгий документ: первый image-subresource действительно удерживался 35 с. Background выгрузился примерно на 32 с, но на finish навигации в 35 с WebKit снова создал background; на failure в 39 с view уже жива. При искусственной потере dashboard получен stuck с живой view, а не P5. Здоровый документ получил initResp и не вызвал failure.
  • Ошибка постановки второго долгого документа: одинаковый URL image взялся из кэша, поэтому второй load не был долгим. Исправляю уникальный URL на каждую навигацию и повторю оба контроля.
  • Наблюдение о trace: watcher держал strong reference на view через 50-мс await. При переходе сразу после context._backgroundWebView=nil dealloc старой view иногда попадал после создания новой. Убираю удержание через await и буду ждать одновременно отсутствия WebKit background и dealloc в production recorder, чтобы не менять исследуемую последовательность самим наблюдением.

Все неподтверждённые результаты выше остаются отрицательными/ограниченными; совпадение P5 пока подтверждено только для test-only потери регистрации живой вкладки.

Уточнённые проверки P5 — начат повтор

Теперь 9 тестов. В новом testNoRuntimeListenerMetadataFromFirstStartupMatchesP5 runtime listener не регистрируется с самого первого старта; затем background явно загружается второй раз и оба раза штатно выгружается. Это отдельно проверяет пустые listener metadata, которые нельзя было получить простым пропуском регистрации на одном wake после здорового старта. Условия искусственные; естественная утрата metadata не доказана. После проверки инъекция снимается, выполняются explicit background load и проверка initResp.

Первоначальный listener-тест переименован в testMissingRuntimeListenerOnOneWakeRecoversAfterNextIdle и проверяет реально наблюдённое восстановление, отсутствие stuck и третью view.

Оба 35-секундных документа теперь используют разные URL image. Ожидания изменены по наблюдению первого прогона: здоровый длинный документ — no failure; потеря dashboard — stuck с живой view, не P5. Расширен test-only background trace на все типы autoconsent-сообщений, чтобы установить, какое сообщение повторно будит worker на finish.

Watcher больше не удерживает WKWebView через await, а idle-helper требует и освобождения background в WebKit, и dealloc по recorder. Повтор пяти новых/уточнённых проверок: p5-refined-and-controls.log.

Здоровый startup-контроль подтверждён

testCurrentControllerBeforeContextLoadRemainsHealthy — зелёный, 1.190 с: вкладка с правильным controller проходит стартовый gate, получает initResp и скрипты, initialization_failed=0. Одной ранней загрузки контекста для P6 в этом сценарии недостаточно.

Повтор one-wake listener подтвердил восстановление и отсутствие stuck. Остался только ошибочный assertion формата счётчика: pixel bg_view_creations для трёх view равен 3_to_5, а не строке 3. Проверка заменена на raw backgroundWebViewCreateCount == 3; будет повторена. Расширенный trace действительно показывает дополнительные report/eval после init — проверяю их связь с повторным пробуждением длинного документа.

P5 с исправными tab/controller/scripts — воспроизведён

testNoRuntimeListenerMetadataFromFirstStartupMatchesP5 зелёный, 70.149 с. Пропуск runtime listener с первого старта, две реальные idle-выгрузки (между ними explicit background load), затем две навигации дали actual stuck: load>view>proxy_on>dealloc>view>proxy_on>dealloc, view=false, creations=2, context/tab/controller/scripts=true, errors=none, network_restarted=false. Content script отправляет init; новые view не создаются. После разрешения регистрации listener в установленной тестовой копии скрипта, explicit background load и навигации получен initResp.

Это соответствует P5 с внешне исправной вкладкой. Доказан механизм пустых listener metadata при test-only инъекции; естественная потеря metadata в приложении/WebKit не найдена. В отличие от него, одноразовый пропуск listener после здорового старта восстанавливается на следующем wake.

Два долгих документа с потерянными dashboard после устранения image-cache — зелёная отрицательная проверка, 80.181 с: оба load действительно по 35 с, в обоих документах есть initResp, production monitor получает stuck, но в его snapshot view=true и цепочка заканчивается view>proxy_on. Это воспроизводит сбой диагностического канала, но не P5. Здоровый контроль ещё выполняется. Лог: p5-refined-and-controls.log.

Долгие документы: повтор завершён

Здоровый контроль с двумя разными 35-секундными image — зелёный, 70.683 с: оба документа получили initResp, initialization_failed=0 и stuck=0. На finish background снова просыпается: trace показывает recv report и recv eval примерно на 35.39 с после background none на 32.35 с. Таким образом, в проверенном сценарии длинная загрузка сама по себе не даёт P5.

Повтор пяти тестов завершён: четыре зелёных, один красный исключительно из-за ранее описанного сравнения bucket со строкой «3» (исправлено). Начат финальный прогон оставшихся сценариев и исправленного assertion: p5-p6-final-regressions-and-stall.log.

Добавлен десятый эксперимент testMainActorStallDelaysActualFailureSnapshots: после реального initResp искусственно теряется dashboard, затем test host main actor дважды блокируется на 35 с при ожидающем production timeout. Тест требует actual failures/stuck и восстановления; совпадение P5 отдельно записывается как matchesP5, а не предполагается. Это исследование задержанного выполнения host callbacks, не реальная iOS suspend/resume.