Files
obsidian-vault/work/projects/cpm-episode-telemetry-phase2-plan-2026-10-20.md
T

9.0 KiB
Raw Blame History

title, date, tags
title date tags
CPM episode telemetry — фаза 2: паузы по фону/сну и смысл was_stuck (план анализа) 2026-10-20
work
cpm
telemetry
plan

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 показала два смещения:

  1. Таймеры считают wall clock. now по умолчанию Date.init; discardExpiredState() вызывается в начале handle(), reportFailure и reportSuccess. Если процесс спал или был в фоне внутри 5-минутного окна, первое событие после пробуждения закрывает эпизод как no_response_before_deadline, а пришедший сразу ответ теряется. Для Task.sleep(nanoseconds:) считает ли он сон — не проверено.
  2. 4-секундный замер тоже. Фон/сон внутри 4 с даёт ложный первый таймаут, потому что веб-процесс ещё просыпается.

Следствие: завышена доля no_response_before_deadline, занижены доли исходов с ответом. Число эпизодов, _daily/_count и диагностический снимок не страдают.

Решения (согласованы 20 октября 2026)

Таймауты

  • Первый замер: 4 с без изменений. Ожидание загрузки расширения: 30 с без изменений.
  • События паузы: iOS didEnterBackground / willEnterForeground; macOS NSWorkspace.willSleep / didWake. resignActive не паузит: процесс жив.
  • На паузе замеры, ждавшие ответа, замораживаются. На пробуждении они перезапускаются полным окном 8 с (отдельная константа, стартовое значение; общий 4 с не трогаем, чтобы не сдвигать базу сравнения).
  • Дедлайн эпизода (5 мин) на паузе замораживается и продолжается с остатком, не начинается заново. response_after после этого — время наблюдения, не wall clock.
  • Замеры, уже получившие ответ, не трогаем.

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 анализа (после выкатки)

  1. Распределение outcome × was_stuck × reload_state, по платформам.
  2. Эффективность автоперезагрузки: среди reload_state=loaded доля failed_tab_response против response_timed_out.
  3. Оценка смещения фона/сна: сравнить долю no_response_before_deadline в сборках до и после паузы (фаза 1 vs фаза 2). Разница — размер смещения. Нужны обе когорты, поэтому фазу 1 нужно выкатить первой.
  4. Прогрев против поломки по снимку: сильные признаки — bg_process_alive=false, bg_process_responsive=false, error 6 (bg_load_error_*), bg_ready_after=failed; прогрев — bg_ready_after=pending при малом bg_view_age.
  5. was_stuck=true долю смотреть отдельно по iOS и macOS: на iOS мало одновременных вкладок, значение будет другим.

Ограничения и риски

  • Если iOS убьёт приложение в фоне, незавершённый эпизод теряется (принято в фазе 1).
  • Пропущенное событие паузы ломает учёт; начальное состояние (запуск в фоне) брать из системы, не считать «активным».
  • was_stuck меняет смысл относительно описания в PR #7376 и в любых данных до выкатки; данные до и после изменения не склеивать по этому полю.
  • Сохранённые, но не используемые в этой фазе правки (stuck = true/false × one/many, продолжение bg_events через ;) лежат на локальной ветке alex/cpm-7376-extras-backup.