23 KiB
title, date, tags
| title | date | tags | ||||
|---|---|---|---|---|---|---|
| CPM episode telemetry — фаза 2: паузы по фону/сну и смысл was_stuck (план анализа) | 2026-10-20 |
|
CPM episode telemetry — фаза 2
Статус (21 октября 2026): реализовано и смержено в оба релиза (см. «Что ушло в релиз»). Фаза 1 — PR #7376 (web_extension_cpm_episode). Состав отчёта фазы 1: cpm-pr0-episode-telemetry-summary-2026-10-16. Общий контекст: cpm-status-2026-10-15.
Паузы по фону/сну и новый смысл was_stuck вошли в сам #7376 (коммит 88154a4c4a), а не в отдельную ветку, как планировалось 20 октября. Obsidian MCP не подключался, файл записан напрямую.
Что ушло в релиз
Проверено 21 октября 2026 по tip релизных веток: все 8 PR смержены (squash), результат совпадает с запушенными ветками по CPM-путям, CPM-файлы пакета идентичны в двух релизах (отличаются только файлы с контентом релиза: remote-disable-enforcer и т. п.), тесты пакета зелёные (iOS 934, macOS 965), validate-pixel-defs для iOS и macOS = 0.
| Что | iOS release/ios/7.243.0 |
macOS release/macos/1.213.0 |
|---|---|---|
Эпизодный пиксель, паузы, was_stuck (2+ вкладки) |
#7376 5dca645528 |
#7484 bd68e42a15 |
tabs_affected, repeat_failures, interrupted, токен interruption в bg_events |
#7520 507567edaf |
#7521 8e6b98ae3b |
Network process recovery (флаг cpmNetworkProcessRecovery) |
#7459 77d7589b79 |
#7531 5512905ae2 |
| P1/P2/P4: старт и пробуждение фона (7379) | #7379 543b073685 |
#7536 12ebc16fcd |
Флаги, по умолчанию: ON — cpmNetworkProcessRecovery, cpmBackgroundLoadRecovery, cpmMessagingHangRecovery, cpmBackgroundDelegateProxy, cpmDiagnosticsRecorder; OFF — cpmLateBackgroundDocumentRecovery, cpmBackgroundGraveyardTreatment. При отсутствии записи в remote config считаются значения по умолчанию, то есть ON-флаги работают сразу, выключаются только записью disabled.
Отличия от плана ниже:
- Post-reload grace 8 с (пункт в «Таймауты») не реализован: после перезагрузки расширения первый замер по-прежнему 4 с. 8 с применяется только к ожиданиям, прерванным паузой.
- «Новых параметров нет» относится к фазе A. В релиз ушли ещё три параметра (
tabs_affected: 1/2/3+,repeat_failures: 0/1/2+,interrupted) через #7520/#7521 с отдельным privacy-триажем. Читать данные надо уже с ними (см. «Как читать данные»). interrupted= первое сработавшее ожидание (замер, открывший эпизод) было перезапущено паузой, включая ожидание загрузки расширения (30 с). Приостановка позже внутри эпизода флаг не ставит.
Зачем
Проверка кода 20 октября 2026 показала два смещения:
- Таймеры считают wall clock.
nowпо умолчаниюDate.init;discardExpiredState()вызывается в началеhandle(),reportFailureиreportSuccess. Если процесс спал или был в фоне внутри 5-минутного окна, первое событие после пробуждения закрывает эпизод какno_response_before_deadline, а пришедший сразу ответ теряется. ДляTask.sleep(nanoseconds:)считает ли он сон — не проверено. - 4-секундный замер тоже. Фон/сон внутри 4 с даёт ложный первый таймаут, потому что веб-процесс ещё просыпается.
Следствие: завышена доля no_response_before_deadline, занижены доли исходов с ответом. Число эпизодов, _daily/_count и диагностический снимок не страдают.
Решения (согласованы 20 октября 2026)
Таймауты
- Первый замер: 4 с без изменений. Ожидание загрузки расширения: 30 с без изменений.
- События паузы: iOS
didEnterBackground/willEnterForeground; macOSNSWorkspace.willSleep/didWake.resignActiveне паузит: процесс жив. - На паузе замеры, ждавшие ответа, замораживаются. На пробуждении они перезапускаются полным окном 8 с (отдельная константа, стартовое значение; общий 4 с не трогаем, чтобы не сдвигать базу сравнения).
- Дедлайн эпизода (5 мин) на паузе замораживается и продолжается с остатком, не начинается заново, но не меньше 30 с после пробуждения (если остаток меньше, ждём 30 с).
response_afterпосле этого — время наблюдения, не wall clock. - Первый замер после перезагрузки расширения (post-reload): та же длинная grace 8 с (общая константа с пробуждением); остальные замеры остаются на 4 с. Поздний ответ на том же замере всё равно закрывает эпизод как
failed_measurement_responseсreload_state=response_timed_out, так что «медленная перезагрузка» и «не помогла» различимы поresponse_after. - Замеры, уже получившие ответ, не трогаем.
was_stuck
- Переопределяем смысл (тип boolean не меняется):
was_stuck=true— в рамках эпизода таймаут минимум в двух разных вкладках (вторая не restoration). Повторы в одной вкладке остаютсяwas_stuck=false. - Триггер автоперезагрузки
onConfirmedHangне меняем: любой поздний не-restoration таймаут, как сейчас. Отчётность и логика развязаны. Иначе у пользователей с одной активной вкладкой (iOS) перезагрузка не наступила бы никогда. - Новых параметров нет (счётчик повторов и число вкладок потребовали бы нового privacy-триажа). Цена: счётчик повторов в одной вкладке недоступен.
- Частичная замена счётчику:
was_stuck=false+reload_stateнеnone≈ «в одной вкладке сбой повторился, перезагрузили». Не точно:reload_stateменяется от любых событий перезагрузки расширения, не только от нашего подтверждения. - Требуется обновить описание в json5 (iOS и macOS) и сообщить владельцу privacy-ревью; нужен ли повторный триаж — решает процесс.
Как читать данные (после изменения)
Поля: reason, was_stuck, outcome, reload_state, response_after, плюс снимок первого сбоя (bg_*, 8 булевых, bg_events).
| Конфигурация | was_stuck |
outcome |
reload_state |
Смысл |
|---|---|---|---|---|
| One tab, timeout, then response on the same measurement | false | failed_measurement_response | none | Slow, not broken (warm-up / slow response). response_after gives the delay bucket. |
| One tab, timeout, later navigation in that tab answers | false | failed_tab_response | none | Transient; recovered without reload. |
| One tab, timeout repeated on 2+ navigations (reload fired) | false | depends (see below) | pending / loaded / failed / response_timed_out | Single-tab confirmed hang; automatic reload applied. |
| ... then a post-reload measurement answers | false | failed_tab_response (or other_tab_response) | loaded | Reload fixed it. |
| ... then post-reload measurement times out again | false | any | response_timed_out | Reload did not help in this tab. |
| ... then nothing observed | false | no_response_before_deadline | loaded / pending / failed | Reload done, no evidence either way. |
| Timeouts in 2+ distinct tabs (2nd is not restoration) | true | no_response_before_deadline | loaded or response_timed_out | Likely persistent, not proven (deadline is not proof). |
| Timeouts in 2+ distinct tabs, then any tab answers | true | other_tab_response / failed_tab_response | any | Extension-wide issue that recovered; other_tab_response does not prove the failed tabs recovered. |
| Restoration-batch timeouts only | false | usually other_tab_response / failed_tab_response | none | Startup race; restoration failures cannot confirm each other. reason=session_restoration. |
| First post-reload measurement fails | any | any | response_timed_out | reason=extension_reload: reload itself did not restore messaging. |
| Idle after first timeout, no further navigation | false | no_response_before_deadline | none | Undetermined. Before the pause fix this also includes sleep/background (biased). |
| Response matched several documents | any | unattributed_response | none or loaded | Extension answers, tab not attributable. After a reload this outcome cannot close the episode. |
Outcome no_response_before_deadline is never proof of permanent failure. reload_state is none unless an extension reload lifecycle event occurred during the episode.
Фаза 2 анализа (после выкатки)
- Распределение
outcome×was_stuck×reload_state, по платформам. - Эффективность автоперезагрузки: среди
reload_state=loadedдоляfailed_tab_responseпротивresponse_timed_out. - Оценка смещения фона/сна: сравнить долю
no_response_before_deadlineв сборках до и после паузы (фаза 1 vs фаза 2). Разница — размер смещения. Нужны обе когорты, поэтому фазу 1 нужно выкатить первой. - Прогрев против поломки по снимку: сильные признаки —
bg_process_alive=false,bg_process_responsive=false, error 6 (bg_load_error_*),bg_ready_after=failed; прогрев —bg_ready_after=pendingпри маломbg_view_age. was_stuck=trueдолю смотреть отдельно по iOS и macOS: на iOS мало одновременных вкладок, значение будет другим.- Оценка fix-пикселей #7379 (P1/P2/P4) — см. следующий раздел.
Как читать fix-пиксели #7379 вместе с эпизодом (ревью 20 октября 2026)
Дизайн P1/P2: либо fix срабатывает и шлёт success-пиксель, либо мы продолжаем получать обычную failure-телеметрию (эпизод). Отдельных пикселей «fix не помог» нет, и они не нужны: сбой, видимый пользователю, всегда проходит через 4-секундный таймаут навигации и начинает эпизод.
- Success-пиксели:
web_extension_cpm_{install_readiness, upgrade_readiness, background_wake}иweb_extension_cpm_background_load_retry,outcome=background_ready. Означает только успешный callbackloadBackgroundContent(P1: после ≥1 с и если навигация ждала; P2: успешный retry). Не доказывает, что messaging заработал: после него эпизод всё ещё может прийти. - P1: gate 5 с держит навигацию. Ready ≤5 с и ответ вовремя → есть success-пиксель, эпизода нет. Gate истёк на 5 с и старт всё ещё идёт → эпизод есть (4-секундный таймер начинается после снятия gate), success-пиксель придёт позже, когда callback завершится. Разделить «ready ≤5 с» и «ready поздно» по самому пикселю нельзя.
- P2: retry успешен до таймаута навигации → success-пиксель, эпизода нет. Retry пропущен (cooldown 60 с, приложение в фоне, kill switch) или снова error 6 → success-пикселя нет; если навигация упала, эпизод несёт
bg_load_error_*иbg_ready_after=failed. Если навигаций не было, не видно ничего, но и пользовательского сбоя нет. - P4:
web_extension_cpm_startup_response_missing— это failure-сигнал, не success. Шлётся в том же месте, где начинается эпизод, независимо от флага late reload. Рост означает, что gate не удержал (первая загрузка >5 с).
Что сравнивать после выкатки:
- Долю эпизодов в когортах P1/P2/P4 (по снимку:
bg_ready_after,bg_load_error_*, recent wake/load) до и после включения fix. Success-пиксели считать не «исправленными», а «fix сработал технически». - Отношение success-пикселей к эпизодам той же когорты по installation-day; общего идентификатора у них нет, склейка только по дню.
- P4: динамику
startup_response_missingотдельно от эпизодов; она показывает остаточные первые загрузки сверх бюджета gate.
Ограничения:
- Обе стороны зависят от выкатки нового эпизодного пикселя (#7376, стек); иначе остаются старые раздельные daily-пиксели без парных переходов. Новый baseline нужен.
- В тестах #7379 нет проверки, что менеджер реально шлёт success-пиксели P1/P2 (только маппинг метаданных). Тихо пропавший success-пиксель выглядел бы как «fix не работает».
- P2 retry идёт под общим флагом
cpmMessagingHangRecovery(по умолчанию ON), своего kill switch нет. Повторный error 6 через ~30 с попадает в cooldown 60 с. - Доказательства fix: P1/P4 — red/green на harness; P2 — частично (последний полный ON-прогон 8 PASS / 1 P2 FAIL), iOS-часть (основная доля error 6 в stuck) тестами не покрыта.
#7379 × #7459: смысловые пересечения (в релиз ушли как есть) и что смотреть в данных
Обе фичи в релизе, оба включены по умолчанию, текстово слиты (аудит 20 октября, слияния 21 октября), но смысловые пересечения не устранены и на реальном WebKit вместе не проверялись (тесты с заглушками). Это сознательное решение: правка ниже узкая, меняет поведение в уже одобренных PR и без данных не оправдана.
Пересечения:
- Рестарт #7459 (
_killWebContentProcessAndResetState, без события жизненного цикла) невидим для записи о старте #7379. Если у менеджера в этот момент висит старт, он остаётся привязан к убитому view; следующая навигация получает старый старт и новыйloadBackgroundContentне вызывается, пока не сработает колбэк убитого view. - Ошибка 6 от самого убийства может запустить повтор загрузки #7379 и потратить его бюджет (не чаще раза в 60 с); от этого повтора может прийти «ложный»
background_load_retry. - Флаг «идёт восстановление» у #7379 защищает от гонки только с перезагрузкой по зависанию, не с рестартом #7459; проверка «приложение в фоне» есть у #7379, нет у #7459 (его 5-секундный опрос может сработать в фоне).
- Окна #7379 (30 с) на реальных часах и о паузах монитора не знают (после сна истекают сами).
- Три независимых вызова
loadBackgroundContent: наблюдатель готовности (#7376), стартовый путь и навигационный гейт (#7379).
Что смотреть в данных (общего идентификатора у пикселей нет, только по дню и платформе):
web_extension_cpm_network_process_recovery_applied:_countсильно больше_dailyзначит цикл рестартов.web_extension_cpm_startup_response_missingв дни с большимrecovery_applied; рост указывает на пересечение 1 или 3.- Эпизоды: доля
no_response_before_deadlineиbg_eventsсdied_clientпосле рестарта (неудачное восстановление);reload_stateкак запасная перезагрузка по зависанию. - Success-пиксели #7379 (
outcome=background_ready) вместе сrecovery_applied: резкий рост повторов после рестартов указывает на пункт 2.
Отложенная правка (комментарий 21 октября 2026): подписать рестарт #7459 на сброс состояния #7379 только для перезапущенного контекста. Worktree удалены 21 октября 2026; полный патч (с тестами) сохранён в ~/DuckDuckGo/cpm-worktree-backups-20261021/alex_cpm-fix-init-loss-production.diff (iOS) и alex_cpm-fix-init-loss-macos.diff (macOS); применяется поверх release/ios/7.243.0 и release/macos/1.213.0 через git apply --3way. Ветки alex/cpm-fix-init-loss и alex/cpm-fix-init-loss-macos-release смержены в релизы. Оценка: польза узкая (работает, только если на момент рестарта старт ещё не завершён; обычно он уже завершён), побочные эффекты (ждущие навигации отпускаются раньше 5 с, теряется замер первой загрузки и install/upgrade для перезапущенной попытки). Ревью дважды (независимо): безопасно, крашей и рекурсии нет; нужно добавить тест менеджера (открыть cpmNetworkProcessRecovery internal) и записать ограничения в описание PR. Отдельно отвергнута первая версия, которая сбрасывала состояние на каждый возврат приложения: на iOS это каждое переключение приложений и сброс легитимного старта.
Ключевая часть патча (плюс два теста: «хук вызывается один раз с идентификатором» и «не вызывается при неудачном рестарте»):
--- CPMNetworkProcessRecovery.swift
+ /// Called with the context identifier after a restart, so other recoveries can drop state tied to the replaced background.
+ var onBackgroundRestarted: (@MainActor (String) -> Void)?
...
removeWatch(identifier: identifier, matching: watch)
recovered = true
+ onBackgroundRestarted?(identifier)
pixelFiring.fire(.cpmNetworkProcessRecoveryApplied)
--- WebExtensionManager.swift (после настройки healthMonitor)
+ cpmNetworkProcessRecovery.onBackgroundRestarted = { [weak self] identifier in
+ guard let self, self.embeddedBackgroundStartupContext?.uniqueIdentifier == identifier else { return }
+ self.backgroundRecovery?.invalidate()
+ self.clearEmbeddedBackgroundStartup(identifier: identifier)
+ }
Не покрыто и не сделано: подавление ошибки 6 от самого убийства (пересечение 2), учёт события рестарта во флаге «идёт восстановление», тест связки менеджера. Если данные покажут пересечения 1–3, начинать с этого патча.
Ограничения и риски
- Если iOS убьёт приложение в фоне, незавершённый эпизод теряется (принято в фазе 1).
- Пропущенное событие паузы ломает учёт; начальное состояние (запуск в фоне) брать из системы, не считать «активным».
was_stuckменяет смысл относительно описания в PR #7376 и в любых данных до выкатки; данные до и после изменения не склеивать по этому полю.- Сохранённые, но не используемые в этой фазе правки (
stuck= true/false × one/many, продолжениеbg_eventsчерез;) лежат на локальной веткеalex/cpm-7376-extras-backup.