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

75 lines
9.0 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.
---
title: 'CPM episode telemetry — фаза 2: паузы по фону/сну и смысл was_stuck (план анализа)'
date: '2026-10-20'
tags:
- 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`.