14 KiB
title, date, tags
| title | date | tags | ||||
|---|---|---|---|---|---|---|
| CPM episode telemetry — фаза 2: паузы по фону/сну и смысл was_stuck (план анализа) | 2026-10-20 |
|
CPM episode telemetry — фаза 2
Статус: план, не реализовано. Фаза 1 — PR #7376 (web_extension_cpm_episode, was_stuck = любой поздний не-restoration таймаут). Состав отчёта фазы 1: cpm-pr0-episode-telemetry-summary-2026-10-16. Общий контекст: cpm-status-2026-10-15.
Работа ведётся отдельной веткой поверх #7376 (alex/cpm-episode-background-pause, стекнутый PR). Obsidian MCP не подключался, файл записан напрямую.
Зачем
Проверка кода 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) тестами не покрыта.
Ограничения и риски
- Если iOS убьёт приложение в фоне, незавершённый эпизод теряется (принято в фазе 1).
- Пропущенное событие паузы ломает учёт; начальное состояние (запуск в фоне) брать из системы, не считать «активным».
was_stuckменяет смысл относительно описания в PR #7376 и в любых данных до выкатки; данные до и после изменения не склеивать по этому полю.- Сохранённые, но не используемые в этой фазе правки (
stuck= true/false × one/many, продолжениеbg_eventsчерез;) лежат на локальной веткеalex/cpm-7376-extras-backup.