From 9f3e3c50015909195dc6c76fc31147b74ff0baa2 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Fri, 30 Oct 2026 17:24:49 +0600 Subject: [PATCH] [2026-10-30] eagle: work/projects/cpm-ios-idle-wake-investigation-2026-10-30.md work/projects/cpm-ios-uncovered-groups-2026-10-30.md --- ...-ios-idle-wake-investigation-2026-10-30.md | 220 ++++++++++++++++++ .../cpm-ios-uncovered-groups-2026-10-30.md | 2 + 2 files changed, 222 insertions(+) create mode 100644 work/projects/cpm-ios-idle-wake-investigation-2026-10-30.md diff --git a/work/projects/cpm-ios-idle-wake-investigation-2026-10-30.md b/work/projects/cpm-ios-idle-wake-investigation-2026-10-30.md new file mode 100644 index 00000000..aa4e749a --- /dev/null +++ b/work/projects/cpm-ios-idle-wake-investigation-2026-10-30.md @@ -0,0 +1,220 @@ +--- +tags: + - cpm + - ios + - webkit + - investigation +updated: '2026-10-30' +status: investigation-and-proposal +--- +# CPM iOS idle wake: действия, возможные механизмы и диагностический план +Дата: 30 октября 2026. Продолжение [[cpm-ios-uncovered-groups-2026-10-30]]. + +**Основной вывод:** idle wake остаётся приоритетным сценарием, но текущий пиксель объединяет разные механизмы. Нужно разделить выполнение content script, доставку init в worker, ожидания внутри worker, dashboard → native → monitor и реальное получение initResp страницей. Проверка shipped bundle выявила две неограниченные storage-зависимости; контролируемые проверки показали, что общая очередь cpmState может остановить dashboard-ответы нескольких вкладок при продолжении отправок initResp. Естественный production-trigger такого storage stall пока не найден. + +## Что проверено и чего эти результаты не доказывают + +Проверены исходники **реально измеряемого iOS 7.243.0**, tag `7.243.0+ios`, commit `9b67ed7c720d274a554a86052881d2d690cccb3e`; ZIP extension извлечён из этого tag. Его manifest version — **2026.10.14**. Это важнее версии локального extension checkout: там уже есть ещё не выпущенные init progress marks, в shipped ZIP их нет. + +Дополнительно: текущий main `3934949758`; WebKit source snapshot `99051d5d08cd9f19ec76ce599f7539d070b1ae09` от 29 сентября; проверены upstream patches четырёх связанных WebKit bugs. **Наличие исправления в upstream main не устанавливает, в какой iOS версии оно включено.** Этот проход не создавал production PR и не менял приложение/флаги. + +Перечитаны записи 27–30 октября. В частности, гипотеза неограниченного `storage.session.get("config")` уже была записана 27 октября; это не впервые обнаруженная идея. Новое здесь — разбор общей state queue и контролируемое разделение dashboard и initResp на shipped методах. + +В этом проходе выполнены три изолированные проверки JS-логики с подменёнными browser/native I/O. **Новый естественный iOS/WebKit outage не воспроизведён**, физический iPhone в этом проходе не использовался. Предыдущие H1/H2, idle и real-page результаты учитываются со своими ограничениями, а не переобъявляются новыми. + +## 1. Какие действия пользователя дают эту картину + +### A. Достаточно читать страницу в открытом браузере, затем перейти по ссылке + +1. Открыть обычную HTTPS-страницу, получить работающий CPM. +2. Читать/прокручивать её, не создавая новых extension-событий. Примерно через 30 секунд последней активности WebKit может выгрузить непостоянный background. Это нормальный lifecycle, не crash. +3. Нажать ссылку, обновить страницу или открыть новую вкладку с URL. +4. На main-frame HTTP(S) navigation app запрашивает готовность extension **на policy stage**, до загрузки документа. WebKit создаёт новый background view, recorder устанавливает proxy. +5. Получаем `dealloc → view → proxy_on`; фон может успешно загрузиться за <1 секунды. +6. Документ выполняет CPM content script, отправляет init. Если требуемый dashboard-сигнал не дошёл до monitor за 4 секунды после finish, появляется episode и первый snapshot. + +Поэтому `view_age=4s_to_8s` естественен: время на загрузку страницы плюс четырёхсекундное ожидание. Он не доказывает, что view появился в момент сбоя. Несколько `dealloc>view>proxy_on` подряд могут быть обычными переходами с интервалами >30 секунд. + +Если страница регулярно шлёт extension-сообщения или открыт активный port, выгрузка может откладываться; ровно 30 секунд от открытия вкладки не является гарантией. + +### B. Home / блокировка телефона / возвращение в браузер + +1. Открыть 2–3 здоровые вкладки, перейти в другое приложение или заблокировать экран. +2. Во время простоя background может выгрузиться; native callbacks/timers могут отработать после resume. +3. Вернуться, затем **сделать новую navigation**: ссылка, reload, новый URL в другой вкладке. Получается та же wake-история, иногда с `interruption`. +4. Для подтверждённого cross-tab случая после первого timeout сделать свежую обычную navigation во второй вкладке; один только возврат к старому документу этого не доказывает. + +В shipped monitor resume **перезапускает уже ожидающие замеры**, обычно с 8-секундным grace; он не создаёт новый init/замер на любой показ старой страницы. Чистый back/forward restore исключён из health measurement. Поэтому «разблокировать и посмотреть старую страницу» и «разблокировать и перейти на новую страницу» — разные опыты. + +`interruption` в bg_events говорит о событии в доступной истории; `interrupted=true` означает, что именно первый измеряемый запрос пересёк suspension. Их нельзя смешивать. + +### C. Граница unload/wake и две почти одновременные navigation + +После 25–35 секунд idle начать переход в A и сразу в B; отдельно — заблокировать телефон во время перехода и завершить после resume. Навигации разделяют startup gate, но документы и сообщения имеют разные поколения. Это сильный сценарий для routing/registration и attribution races. + +Чтобы доказать browser-wide failure, после первоначального сбоя нужна ещё одна navigation, начатая уже после него; один пакет одновременно восстановленных вкладок для этого недостаточен. + +### D. M4 после второго timeout, затем тишина + +Второй квалифицирующий timeout запускает context reload. Пользователь может после этого просто остаться на странице и перестать навигировать. В конце активного окна получается no-response, хотя **проверки нового контекста свежим документом могло не быть**. + +Это существенная граница интерпретации прежних чисел. У wake cross-tab no-response 29 октября: +- `reload_state=loaded`: **2 140 из 2 920** count-эпизодов. +- `response_timed_out`: **776**. +- failed/pending: 4. + +`loaded` доказывает успешный context reload, но не проведённый и проваленный post-reload messaging test. `response_timed_out` сильнее: monitor зарезервировал post-reload regular measurement, и он истёк. Даже он не доказывает вечную поломку. + +Следовательно, фразу «все 2 920 пережили M4 без восстановления» применять нельзя. Для опытов после M4 обязательно отдельно открыть новый документ и проверить actual initResp. + +### E. Похожий footprint может создать не поломка worker + +Валидная страница после idle может: +- не выполнить ожидаемый content script; +- выполнить init, но застрять на config/storage; +- получить initResp, пока dashboard ждёт сохранения state; +- прислать dashboard, который не прошёл parsing/маршрутизацию/атрибуцию. + +Все эти пути способны дать готовый background и одинаковые native flags. Эти флаги проверяют конфигурацию и WebKit verdict, а не прохождение конкретного JS-запроса. + +## 2. Реальная цепочка в shipped коде + +`navigation policy → native background gate → WebKit background load → content script → runtime.sendMessage(init) → MessageRouter → handleAutoConsentMessage → config/settings/site checks → [dashboard branch || initResp branch]`. + +**Ветки после init различаются:** +- Dashboard: `updateCpmDashboardState → modifyCpmState → storage.session → refreshCpmDashboardState native message → validation → health monitor correlation`. +- Ответ странице: `tabs.sendMessage(initResp) → content-script listener → AutoConsent.initialize`. + +Dashboard запускается через promise chain и **не является ACK доставки initResp**. Возможны оба расхождения: dashboard есть, actual initResp потерян; actual initResp отправляется/доставляется, dashboard отсутствует. + +В 7.243 MessageRouter завершает autoconsent-handler через `.then(() => {})`: успешный `runtime.sendMessage(init)` сам по себе может вернуть **undefined**. У нас undefined нельзя считать признаком потерянного worker, даже если в чужом ping/pong reproduction это аномалия. Нужны явные stages/ACK. + +Полезные source anchors: +- [App navigation gate](/Users/admin/DuckDuckGo/apple-browsers.git/cpm-ios-idle-wake-investigation-2026-10-30/shipped-app/TabViewController.swift:3662). +- [Shipped config/state/init](/Users/admin/DuckDuckGo/apple-browsers.git/cpm-ios-idle-wake-investigation-2026-10-30/shipped-extension/public/js/background-embedded.js:2570). +- [Независимая ветка dashboard](/Users/admin/DuckDuckGo/apple-browsers.git/cpm-ios-idle-wake-investigation-2026-10-30/shipped-extension/public/js/background-embedded.js:2678). +- [Content script ждёт отдельный initResp](/Users/admin/DuckDuckGo/apple-browsers.git/cpm-ios-idle-wake-investigation-2026-10-30/shipped-extension/public/js/content-scripts/cpm.js:2846). + +## 3. Конкретные механизмы и места поиска + +### H1. Unbounded config/storage и общий state queue — проверяемая app/extension-гипотеза + +В `refreshRemoteConfig` первый `getFromSessionStorage("config")` выполняется **перед** native request и его timeout. После успешного обновления config запись `storage.session.set` также ожидается без своего timeout. Все init ждут общий `remoteConfigJson`. + +Отдельно `modifyCpmState` сериализует обновления разных вкладок через `_stateQueue`. Операция ожидает `getCpmState`, затем `updateCpmState`, включая storage write. Rejection ловится и позволяет очереди продолжить; never-settled promise удерживает всё последующее. Dashboard каждого init зависит от этой очереди. Это глобальная точка блокировки, даже если JS event loop и browser.runtime работают. + +Проверки на извлечённых shipped классах: +1. При удерживаемом config read native request вообще не начался; его 20-секундный timeout ещё не мог помочь. После отпускания storage handler завершился. +2. Healthy control: два init дали две попытки initResp и два dashboard-вызова. +3. Pending cpmState write: два init дали **две попытки initResp**, **ноль dashboard-вызовов**, выполнена только первая заблокированная запись. После отпускания записи дошли оба dashboard-вызова. + +Это **логическая демонстрация механизма**, не доказательство естественного зависания browser.storage, доставки initResp в настоящий документ или доли production false positives. Browser/native I/O подменены управляемыми promises. + +Свежий JS realm после M4 должен очистить чисто in-memory очередь. Поэтому для повторения после M4 нужны повторный storage stall/общая нижележащая проблема либо другой механизм. Не всякий пяти минутный episode доказывает, что очередь оставалась зависшей после reload. + +Куда смотреть: config storage get/set; cpmState get/set; возраст головы очереди; глубина очереди; отдельный health/ACK путь. При будущем исправлении persistence не должна безгранично удерживать ответ/наблюдение. Одного `Promise.race(timeout)` недостаточно для безопасных записей: старая незавершённая запись может позднее завершиться и затереть более новую; нужны revision/order/coalescing semantics. + +### H2. Worker готов по load callback, но init не маршрутизируется в listener + +WebKit `runtimeSendMessage` после wake получает набор процессов для `RuntimeOnMessage`; если он пуст, завершает вызов пустым результатом. Выборка идёт по listener frames/content world, а не просто по живому PID background view. + +`bg_process_responsive` проверяет WebKit verdict о процессе WKWebView. Он не проверяет, что нужный SW listener зарегистрирован и выполняется. Carrier page, SW thread, API bindings и listener routing — разные уровни. + +Нужно поймать: content script точно начал init; worker ingress отсутствует при известной диагностической поддержке; альтернативный worker/native путь проверен. Одно отсутствие native progress mark недостаточно — сам мост может не работать. + +Предыдущие проверки H1 Network crash и H2 skip-listener из заметок **не подтвердили естественный persistent trigger** на production 30-секундном unload. Быстрый TestWebKitAPI режим давал артефакты; его красный результат не переносится на production без повторения. + +### H3. Init дошёл, но native bridge / handler / dashboard correlation потеряли результат + +Handler registered проверяет наличие native handler в словаре. Это не факт получения текущего сообщения. После message delivery есть отдельные parsing, URL/consentStatus validation и correlation по tab, document URL и extension generation. + +Monitor сравнивает scheme/host/port/path/query, игнорируя fragment. Bundle берёт tab URL из `sender.tab.url/pendingUrl`; при redirects, URL rewrite и пересечении context reload возможны несовпадения. При известных mismatch до failure monitor часто отменяет недоказуемый замер, поэтому нельзя любой mismatch объявить false-positive. Но после первого failure отсутствие подходящего current-generation response может оставить episode открытым. + +Нужны отдельные счётчики handler-entry, parse rejection, stale context, URL mismatch, zero candidates, ambiguous candidates и accepted attribution. Без них полученный, но отвергнутый ответ внешне равен отсутствующему. + +### H4. Script не выполнялся / ранний JS exception / неправильный sender + +`tab_has_ext_scripts=true` означает зарегистрированный user script нужного content world, а не исполнение в конкретном документе. Перед обработкой init bundle проверяет sender.tab, frameId и tabId; часть неверных метаданных ведёт к раннему return. + +Искать после independent cs-start evidence. Исследовать обычную navigation, восстановление/переиспользование web view, process swap и previews отдельно. Большая часть P6/controller mismatch к iOS wake-группе не относится. + +### H5. Idle wake может быть обычным состоянием большинства navigation + +Большая доля wake среди failures не определяет риск wake. После 30-секундной выгрузки многие здоровые navigation тоже создают новый background. + +Нужен знаменатель: `failed wake / все eligible wake navigation` против `failed warm / все eligible warm navigation`, одинаковая выборка и правила. Без него keep-warm может лечить действительно важный lifecycle race или просто сдвигать классификацию. До такого сравнения blanket keep-warm не является первым выбором. + +## 4. Что найдено в WebKit и публичных обсуждениях + +- [317981](https://bugs.webkit.org/show_bug.cgi?id=317981): потеря sendMessage на первоначальном старте при ещё неизвестном listener set; исправление добавляет очередь для initial load. **Первый load после очистки listener state — его scope**, обычный повторный wake он полностью не объясняет. +- [324743](https://bugs.webkit.org/show_bug.cgi?id=324743): readiness объявлялась до завершения registration; исправлено ожидание нужного состояния. Это показывает ограниченность прежнего loader-ready как messaging-ready, но не устанавливает наличие бага на нашей OS. +- [324754](https://bugs.webkit.org/show_bug.cgi?id=324754), [patch](https://github.com/WebKit/WebKit/commit/001f7dbb8bbf): после unload/reload worker мог оказаться в процессе, где отсутствует carrier page и не инжектируются extension bindings. Хорошее структурное сходство, однако требуется совпадение trigger/OS; прежняя наша Network-проверка это не подтвердила. +- [322881 / patch](https://github.com/WebKit/WebKit/commit/4ce58a7ff78ce27cf6646691c6ddfb966b22b51c): восстановление SW clients при повторном background load. Upstream regression проверяет offscreen client; обычные HTTPS-вкладки с content scripts не следует автоматически считать теми же SW clients. +- [Публичный iPhone reproduction](https://github.com/Happygallo/SafariExtensionBackgroundUndefinedBug): автор сообщает о сбое после lock/inactivity на iOS 18/26 и отсутствии проявления в Simulator/с Web Inspector. Исходники проверены: worker возвращает явный pong; manifest использует module worker, content script document_end и ping раз в секунду. У нас classic worker/document_start, явный native pre-wake и отдельный initResp. Это мотивирует device/без-inspector опыт, а не доказывает одинаковую причину. +- [Apple Forums 758346](https://developer.apple.com/forums/thread/758346): исторические device-only wake failures; автор описывает улучшение при непостоянной background page вместо service worker. Apple сообщала о фиксе в 17.6.1, позже есть сообщения пользователей о сохранении проблемы. Это не единый доказанный баг всех новых OS. +- [WWDC26](https://developer.apple.com/videos/play/wwdc2026/216/) описывает поддержку background page и service worker. Локальный WebKit manifest parser тоже поддерживает непостоянный document background при MV3. Поэтому **SW ↔ nonpersistent background page — допустимый диагностический A/B-кандидат**, а не keep-alive workaround. + +Наличие этих upstream fixes не означает, что надо ждать очередного Safari release. Сначала нужны наши stages и совпадающее воспроизведение. Safari-hosted extension и WKWebExtension внутри DDG отличаются управлением и lifecycle. + +## 5. Телеметрия, которая действительно различит эти ветки + +Это предложение контракта, не добавленные пиксели. Значения — ограниченные enum/bool/buckets; URL, tab IDs, document IDs, worker IDs и nonce в аналитику не отправляются. + +### Минимальный приоритетный набор + +**A. Per-document trace с локальной корреляцией.** Временный navigation/document token + extension generation только в памяти app/JS. Token проходит вместе с init/ответом, не сохраняется между сессиями и не уходит в ClickHouse. Он должен различать повторную загрузку того же URL и поколения после M4. Capability handshake: `unsupported/unknown/supported`; отсутствие marks у старого bundle/старого документа нельзя писать как `init_not_received`. + +**B. Раздельные положительные свидетельства на границах:** +- content script started; +- init send attempted / rejected / settled; +- worker init ingress; +- dashboard handler entry / validation / attribution; +- initResp send attempt / transport completion; +- content script actually received initResp и завершил предусмотренную initialize-стадию. + +Нельзя заменить последнюю точку успешным `tabs.sendMessage`: это транспортный результат, не прикладной ACK. И нельзя считать успешный runtime promise с undefined ACK принятия init. + +**C. Где ждёт worker.** Отдельные bounded markers: +`config_session_get`, `native_config_request`, `config_session_set`, `user_settings`, `site_policy`, `state_queue_wait`, `state_session_get`, `state_session_set`, `dashboard_notify`; возраст текущего ожидания и глубина очереди в buckets. Маркеры пишутся **до await**, collector не зависит от той же state queue/storage. Для параллельных dashboard/initResp нужны отдельные bits/statuses, а не один максимальный stage, который теряет информацию о второй ветке. + +**D. Native rejection reason.** `no_handler / malformed / stale_context / url_mismatch / no_candidate / ambiguous / attributed`, с отдельным `unknown` если входящий вызов не наблюдался. В аналитику идёт классификация расхождения, не адрес страницы. + +**E. First и end evidence + recovery opportunity.** Сохранить первый snapshot; дополнить bounded end-state и событиями, произошедшими после него: +- view/context replaced, background ready/failure; +- retry/M4 attempted либо suppression reason; +- количество eligible regular measurements после recovery: 0 / 1 / 2+; +- post-recovery timed out / dashboard attributed / page ACK / no opportunity; +- initial-document recovery и ответ другой вкладки отдельно. + +Пятиминутное отсутствие ответа при нуле post-reload opportunities не должно означать доказанный провал recovery. + +### Два обязательных условия дизайна + +**Независимость диагностического канала.** Если все worker marks идут только через потенциально сломанный native messaging, отсутствие marks не различает «init не пришёл» и «worker не может отчитаться». Нужен дополнительный app↔content-script diagnostic bridge с проверенной привязкой content world и/или post-failure диагностический runtime reply, который content script передаст native независимо. App-owned probe в другом world доказывает работу того probe, а не исполнение CPM script; эти результаты нельзя объединять. + +Positive marker надёжно показывает пройденную границу. Negative marker требует поддержки контракта и проверки канала; иначе результат остаётся unknown. Стопроцентно назвать внутренний баг WebKit только этими полями нельзя, но можно выбрать узкий участок для engine trace. + +**Healthy baseline и отсутствие изменения поведения до failure.** Те же bounded признаки для небольшой случайной выборки здоровых eligible navigation: wake/warm, gate outcome, time since resume, readiness, page ACK. Измерение выбирается до результата, с известным sampling. Readiness-пробы не должны специально будить фон до контрольного snapshot; ping после обнаруженного failure отмечается как вмешательство. Никаких постоянных heartbeat/ports в основном диагностическом варианте. + +Вспомогательные native поля: wake trigger `navigation / retry / network recovery / context reload / WebKit-or-unknown`, resume-age bucket, current measured navigation kind, diagnostic schema support; пересечения вызовов помечать как multiple/unknown, а не выдумывать один trigger. + +## 6. Как превратить расследование в решение + +1. **Первый небольшой диагностический change:** per-document correlation, worker ingress, bounded pending-await/state queue, native attribution reason и настоящий page ACK. Добавить end-of-episode recovery opportunity. Адаптировать идею #7385 к нынешнему episode contract, а не публиковать старый stacked PR без переноса. +2. **Контролируемые fault cases:** dropped init; config get never settles; state set never settles; native dashboard rejected; response URL/generation mismatch; initResp lost. Для каждого диагностический результат должен различаться. Уже выполненный storage control покрывает первые две storage-развилки на уровне JS. +3. **Физический iPhone, штатные 30 секунд, без debugger/Web Inspector:** foreground idle control; lock/resume 1–5–30 минут; lock during startup; fresh navigation в A и B. Протокол требует и dashboard, и page ACK; после M4 нужна новая regular navigation. Нельзя ограничиваться старой страницей или одним успешным callback. +4. **Сильный причинный A/B:** тот же CPM JS и непостоянный background, но service worker против document background. Сохранить idle unload, одинаковые профили/флаги и реально проверить unload/reload в обеих ветках. Если сбой воспроизводится в SW и исчезает в document при равной нагрузке, это сужает поиск до SW-specific registration/routing/bindings. Изменение сохраняет различия окружения и само по себе не идентифицирует одну строку WebKit. +5. **Второй A/B по нашему коду:** убрать блокировку delivery/health-evidence на сохранении CPM state, сохранив корректность serial writes и настройки consent. Если page ACK здоровый, а старая dashboard-ветка висит, это отдельное исправление наблюдения/state pipeline; оно не должно маскировать реальные потери initResp. +6. Только после совпадающего естественного fault решать о re-init, bounded restart или изменении background environment. Keep-warm оставлять отдельным causal experiment с расходом ресурсов и знаменателем wake, не первым безусловным лечением. + +**Практический следующий шаг:** двигаться с двумя узкими гипотезами — init не дошёл до реально работающего worker; init дошёл, но общий config/state/native/attribution путь удержал наблюдаемый ответ. Новая телеметрия должна первым делом развести их и отдельно показать page ACK. Увеличение одного bg_events хвоста этого не сделает. + +## Артефакты + +`/Users/admin/DuckDuckGo/apple-browsers.git/cpm-ios-idle-wake-investigation-2026-10-30/`: +- `shipped-app/`, `shipped-extension/`, `revisions.json` — проверенные версии и извлечённый исходник. +- `webkit-commit-*.json` — реальные upstream patches; source snapshot отдельно в `/Users/admin/Downloads/WebKit_sparse`. +- `storage-probes.cjs`, `storage-probe-results.json` — три проверки shipped методов, включая healthy control и освобождение заблокированной операции. +- `public-repro/` — прочитанный публичный минимальный пример для сравнения scope. + +Основание количественных выводов и SQL: [[cpm-ios-uncovered-groups-2026-10-30]]. История прошлых воспроизведений: [[cpm-post-release-analysis-2026-10-27]]. diff --git a/work/projects/cpm-ios-uncovered-groups-2026-10-30.md b/work/projects/cpm-ios-uncovered-groups-2026-10-30.md index 9a42bee1..984c2977 100644 --- a/work/projects/cpm-ios-uncovered-groups-2026-10-30.md +++ b/work/projects/cpm-ios-uncovered-groups-2026-10-30.md @@ -8,6 +8,8 @@ status: analysis --- # CPM iOS: оставшиеся группы и следующий фокус — 30 октября 2026 +Продолжение: [[cpm-ios-idle-wake-investigation-2026-10-30]] — browser actions, shipped app/extension/WebKit paths, проверка storage/state queue, публичные аналоги и конкретный telemetry proposal. Уточнение интерпретации M4: `reload_state=loaded` без квалифицирующего нового замера не доказывает провал messaging recovery; отдельно выделены `response_timed_out` и предложен post-recovery opportunity counter. + Обновлён ClickHouse-срез и проверены актуальные PR. **Следующий основной фокус — потеря ответа после idle wake при уже готовом фоне; второй — старт с `pending` без текущей error 6. Отдельно остаётся непроверенный хвост error-6 history → replacement view → pending: его нельзя объявлять закрытым #7730.** Это выбор следующего расследования и исправления по наблюдаемым симптомам, а не доказанная причинная классификация. macOS в этом пересчёте не анализировалась.