Files
strongswan-ext/src/frontends/android/docs/energy-plan.md
T

25 KiB
Raw Blame History

План оптимизации энергопотребления SHX VPN (Android)

Основа — разбор кода версии 2.18.0 (165). Пункты, помеченные «проверить», в коде не подтверждены и требуют замера или чтения кода перед правкой. Цель: меньше пробуждений CPU и радио при подключённом туннеле с погашенным экраном.

1. Что уже сделано (не трогать без замеров)

  • HealthCheckManager: адаптивный режим (экран выключен → интервал не меньше 60 с, экономия заряда или низкий заряд → 300 с, без быстрых опросов), экспоненциальный бэкофф при сбое, без будильников и wakelock'ов.
  • WatchdogReceiver: будильники растягиваются — «припаркован» 5 мин / 30 мин (wakeup), «соединён» 5 мин / 60 мин.
  • HomeViewModel: опрос трафика (1 с) и проб (10 с) только пока видна вкладка «Главная» и VPN соединён (repeatOnLifecycle + whileHomeConnected).
  • CharonVpnService: диагностика маршрутов раз в 5 мин только в расширенном режиме; уведомление не чаще раза в 5 с (NOTIFICATION_MIN_INTERVAL_MS).
  • Фоновая работа: WorkManager (обновления 12 ч — с условиями сети и зарядки, профили 24 ч, итоги недели 7 дн.), wakelock'ов в коде нет.
  • DiagLog: писатель открыт, ротация по счётчику байт, а не по stat на каждую строку.

1a. Главный упор: лишняя работа CPU

Приоритет всего плана — убрать работу, которую телефон делает без пользы (пробуждения, холостые циклы, повторные вычисления). Критерий: меньше CPU-времени и пробуждений в час на подключённом туннеле, при погашенном экране — близко к нулю сверх IKE/ESP.

Подтверждено по коду:

  • Холостое пробуждение раз в секунду в нативе. backend/android_service.c: цикл чтения TUN делает select() с таймаутом 1 с («check every second if tunfd is still valid») и по таймауту сразу возвращается в очередь заданий — поток просыпается каждую секунду даже без трафика. Заменить на ожидание без таймера: poll() по TUN и eventfd/pipe, который будит закрытие TUN (или таймаут 30–60 с как страховка). Это самый дешёвый и самый бесспорный выигрыш: один поток, ~3600 пробуждений в час → единицы.
  • Движок прямых потоков (routing/direct_engine.c) уже адаптивен: простой растягивает poll до 4 с (1 с, пока есть TCP-потоки). Остаётся: не вызывать tcp->check_timers / sys_check_timeouts и не пересобирать pollfd в холостых раундах, довести холостой интервал до 10–30 с при погашенном экране (будить через wake-fd при новом пакете — он уже есть).
  • Java: ScenarioEngine.poll (4 с, IPC в UsageStatsManager), rememberNow() (1 с), опрос трафика 1 с — только при видимой «Главной», это нормально.
  • RoutingManager.refreshSites каждые 30 мин резолвит все домены — разовая нагрузка на радио и CPU, но без пользы при погашенном экране.
  • DiagLog: каждая строка — format.format(Date()) + toByteArray + flush() (системный вызов). При частых событиях сети это лишний CPU и запись во флеш.

Проверить замером (гипотезы):

  • сколько CPU съедает разбор/поиск по базе адресов РФ (RuIpListManager, IPRangeSet) при загрузке и обновлении, и не пересобирается ли набор без изменений (сравнивать хэш до применения, не вызывать pushUserspaceRouting без реального диффа);
  • Compose: лишние рекомпозиции «Главной» (счётчик трафика раз в секунду трогает большое дерево?) — Layout Inspector / recomposition counts;
  • число одновременных NetworkCallback (см. C) — каждое событие сети запускает дублирующую работу в семи местах.

2. Фаза 0 — замер базовой линии (до любых правок)

Без цифр правки ниже — гипотезы. Сценарии (эмулятор не годится, нужен реальный телефон):

  1. Ночь: VPN соединён, экран выключен, Wi-Fi, 8 ч. То же на мобильной сети.
  2. Те же условия с включёнными: проверкой здоровья, сценарием «приложение открыто», сайтами в правилах, «сайты РФ напрямую».
  3. День: экран включён, вкладки «Главная» / «Маршруты» / «Диагностика».

Инструменты: adb shell dumpsys batterystats --reset → прогон → dumpsys batterystats one.shx.strongswan.ext, dumpsys alarm (число и тип будильников пакета), dumpsys jobscheduler, Perfetto (пробуждения CPU, радио), Battery Historian. Фиксировать: mAh за час, число wakeup-будильников в час, время активного радио, CPU нативного роутера.

3. Кандидаты на оптимизацию (по убыванию ожидаемого эффекта)

A. Проверка здоровья: пассивные сигналы вместо проб (сделано в этой ветке)

HealthCheckManager каждые 30 с (по умолчанию в HealthConfig; проверить фактический дефолт в UI настроек) шлёт HTTP/ICMP-пробы через туннель — каждая держит радио. Идеи:

  • пропускать цикл, если по TrafficStats за интервал был входящий трафик через туннель (туннель очевидно жив);
  • при выключенном экране: один таргет вместо трёх, интервал 5 мин;
  • не стартовать цикл вообще, пока устройство в Doze — не делалось: корутинные таймеры и так не будят спящий CPU.

Реализовано: в адаптивном режиме цикл пропускает пробы, если за интервал через туннель пришло ≥ 4 КБ входящих данных (CharonVpnService.queryTraffic()), но реальная проба идёт не реже раза в 5 минут; при наблюдаемом сбое, ожидающем решении, агрессивном режиме или неизвестном счётчике пропуска нет. При выключенном экране проверяется один адрес вместо трёх. Сама проверка здоровья включается пользователем (HealthConfig.enabled = false по умолчанию), поэтому выигрыш — для тех, кто её включил.

B. Keepalive и будильники strongSwan — проверено, код не менялся

Умолчание уже экономное: charonservice.c ставит charon.keep_alive = 45 с (ANDROID_KEEPALIVE_INTERVAL, у upstream-strongSwan 20 с) и DPD-запас 20 с; профильный nat_keepalive (10–120 с) его переопределяет. Keepalive уходит только когда исходящего трафика не было. Поднимать 45 с выше без замеров нельзя: UDP-таймауты NAT операторов бывают 30–60 с, туннель «жив», но молчит — это прямое нарушение условия раздела 5. Что остаётся: замерить число будильников (dumpsys alarm) за ночь и, если вклад велик, добавить в профиль необязательную настройку, не меняя умолчание; рекомендовать значения по результатам теста на конкретной сети пользователя.

C. Сетевые колбэки — единый хаб отклонён, исправлен один шумный

Разбор показал, что хаб не нужен: NetworkMonitor и WhiteListManager уже отсекают тики уровня сигнала в onCapabilitiesChanged, PauseManager регистрируется только на время паузы, проверка здоровья — только при включённой функции и соединённом VPN, а NetworkManager и UnderlyingNetworkTracker в CharonVpnService — часть самого туннеля (трогать нельзя по разделу 5). Выигрыш от объединения — единицы вызовов Binder на смену сети при заметном риске для автоподключения и белых списков. Исправлено единственное реальное место: HomeViewModel.onCapabilitiesChanged на каждый тик сигнала Wi-Fi заново читал SSID (системный вызов); теперь состояние пересчитывается только при смене транспорта.

D. Опрос переднего приложения в сценариях (средний эффект, низкая сложность)

ScenarioEngine.poll — каждые 4 с UsageStatsManager.queryEvents (IPC), пока есть сценарий с триггером «приложение». При выключенном экране выборка пропускается, но postDelayed продолжает тикать.

  • остановить цикл по ACTION_SCREEN_OFF, запускать по ACTION_SCREEN_ON;
  • увеличить интервал до 8–10 с, если foreground-приложение не менялось несколько циклов.

E. Обновление адресов сайтов из правил (средний эффект, низкая сложность)

RoutingManager.refreshSites каждые 30 мин резолвит все домены правил, пока соединён VPN, независимо от экрана.

  • пока экран выключен — не резолвить; обновить один раз при ACTION_SCREEN_ON, если прошло больше SITE_REFRESH_MS;
  • резолвить только домены, чьи TTL истекли.

F. UI-циклы после ухода с экрана (низкий эффект, низкая сложность)

Проверить, что все циклы отвязаны от жизненного цикла (через repeatOnLifecycle(STARTED)): RoutingScreen (5 с, уже на repeatOnLifecycle), DiagnosticsScreen (5 с), ProfilesScreen и ServerActivity (30 с, 1,5 с), RecentAppsScreen (10 с), LogActivity (800 мс), rememberNow() в HomeStatus.kt (тик каждую секунду, пока composable жив). Отдельно: rememberNow() должен тикать только если на экране есть обратный отсчёт паузы или время подключения.

G. Нативный роутер «сайты РФ напрямую» (неизвестный эффект, замер)

Пользовательский роутер (getRoutingStats, relay UDP/TCP) обрабатывает пакеты в процессе. Замерить CPU-время потоков в Perfetto в режимах «Стабильно» и «Гибко» на типовой нагрузке (видео, мессенджер). Если вклад заметный — смотреть размеры очередей, размер буферов, пакетную обработку и спин-ожидание в нативной части (app/src/main/jni).

H. Мелочи

  • DiagLog: flush на каждую строку нужен для живучести; при выключенном экране можно понизить подробность (watchdog, network-change) или не логировать повторы.
  • Проверить WhiteListManager (перепроверки recheckJob, refreshIfStale) на периодические пробы при выключенном экране.
  • Подсказка пользователю на экране «Батарея» (BatteryActivity): показывать, какие функции (проверка здоровья, сценарии по приложению) заметно расходуют заряд и как их смягчить.

4. Порядок работ

  1. Фаза 0: базовая линия на реальном телефоне (раздел 2); для CPU добавить adb shell top -H -p <pid> / Perfetto по потокам (счёт пробуждений потока чтения TUN).
  2. Быстрые правки с низким риском: секундный select TUN (раздел 1a), D, E, F, DiagLog — один релиз, сравнить с базой.
  3. A (пассивная проверка здоровья) и B (keepalive) — отдельные релизы, каждый со своим замером; B только после проверки на сетях операторов.
  4. C (хаб колбэков) — рефакторинг, после A/B; регрессия по сценариям смены сети (Wi-Fi ↔ мобильная, роуминг, белые списки, пауза).
  5. G — по результатам Perfetto.
  6. После каждой правки: тот же прогон из фазы 0; принимается, если mAh/ч и число будильников не растут, а надёжность не падает.

5. Жёсткое условие: туннель не должен падать в фоне

Экономия не оправдывает потери туннеля при свёрнутом приложении, погашенном экране или Doze. Правила для каждой правки:

  • Не трогать то, что держит процесс и туннель живыми: foreground-сервис CharonVpnService (startForegroundSafe), будильники Scheduler (NAT-keepalive, rekey), FgsStartRetry, страховочные будильники WatchdogReceiver, DPD charon. Их интервалы не растягивать «заодно».
  • Растягивать можно только вторичные проверки поверх IKE DPD (пробы здоровья, опросы UI/сценариев, диагностика, DNS сайтов). Любое растяжение обязано иметь быстрый выход по событию: смена сети, ACTION_SCREEN_ON, потеря валидации VPN-сети (vpnCallback).
  • TUN select (1а): секундный таймаут сейчас ещё и ловит закрытие TUN (EBADF). Замена обязана будить поток при закрытии (eventfd пишется в close/реконнекте/реавторизации) — иначе поток зависнет на закрытом дескрипторе, и туннель не переподнимется. Страховочный таймаут оставить (30–60 с), не убирать совсем.
  • Keepalive (B): менять умолчание только после проверки на реальных операторах; слишком большой интервал — NAT закрывает соединение, туннель «жив», но молчит.
  • Пробы здоровья (A): пассивный сигнал «трафик идёт» не должен подавлять проверку, если исходящий трафик есть, а входящего нет.
  • Приёмка каждого релиза (кроме замеров): ночь с погашенным экраном — туннель не разорван, входящий пуш/пинг проходит утром без ручных действий; dumpsys deviceidle force-idle → выход из Doze → трафик идёт; смена Wi-Fi ↔ мобильная сеть при погашенном экране; убийство процесса (adb shell am kill) → watchdog поднимает туннель; adb logcat -b crash чист. Не проходит хотя бы один пункт — правка откатывается.

6. Риски и ограничения

  • Надёжность туннеля важнее экономии: правки A, B, E не должны ухудшать восстановление после смены сети и Doze (смоделировать adb shell dumpsys deviceidle force-idle, отключение и возврат сети, подмену SSID).
  • minSdk 24: setExactAndAllowWhileIdle и ограничения точных будильников (Android 12+/13+) уже учтены в Scheduler и WatchdogReceiver; не ломать.
  • Релиз — по процедуре из docs/README.md (smoke на AVD shx36, выгрузка в S3 только с подтверждения).
  • Строки UI, если появятся (подсказки в «Батарее»), добавлять сразу во все семь strings.xml.

7. Версия 2.19.0: надёжность и лишняя работа (по результатам разбора багов)

Стабильность соединения

  • Scheduler (будильники keepalive и rekey): ELAPSED_REALTIME_WAKEUP вместо RTC_WAKEUP — сдвиг часов (NTP, ручная смена, пояс) больше не уводит срабатывание на часы вперёд. Очередь вынесена в ScheduledJobQueue (тесты): задания в окне 2 с делят один будильник (раньше срабатывает не более чем на 2 с, поздно — никогда; charon ставит задания дольше 3 с). Каждый сэкономленный будильник — слот в квоте allow-while-idle в Doze. Интервалы и keepalive 45 с не менялись. В diag.log один раз пишется, точные ли будильники.
  • BootReceiver (BOOT_COMPLETED, TIME_SET, TIMEZONE_CHANGED): после перезагрузки и смены времени заново взводит будильник пауз (раньше пауза после перезагрузки не заканчивалась никогда) и будильник окон сценариев, правила решают заново. Туннель сам не стартует; профиль без сохранённого пароля по-прежнему не запускается.
  • ScenarioReceiver ждёт привязки VpnStateService (до 8 с) через goAsync; раньше срабатывание свежего процесса терялось. restoreIfApplied стирает запись о применённом сценарии только когда сервис есть.
  • Будильник окон сценариев: точный, если разрешено (как у паузы).
  • Сетевые сценарии (Wi-Fi, мобильная сеть, роуминг) слушают сеть и без включённых правил сетей (AutoConnectManager.scenariosChanged).
  • Watchdog, поднявший туннель авто-сессии в свежем процессе, помечает сессию авто (markAutoSessionRestored): туннель паркуется на доверенной Wi-Fi, а не работает всю ночь. Постоянный флаг не вводился сознательно: устаревший флаг запарковал бы туннель, запущенный вручную.
  • NetworkManager: набор сетей вместо счётчика (повтор onAvailable или пропуск onLost не сдвигают состояние); подряд идущие одинаковые события схлопываются в одно.

Расход ресурсов

  • direct_engine.c: tcp_active сбрасывается после холостого раунда без TCP-потоков (flow_count в direct_tcp.c объявлен, но не был реализован — теперь реализован), холостой интервал снова растягивается до 4 с; ветка «TUN не привязан» наращивает интервал, а не опрашивает 4 раза в секунду; reprotect очищает отрицательный кэш и при спящем lwIP.
  • TrafficHistory: история в памяти, на диск раз в 5 мин, при остановке и смене дня; с погашенным экраном опрос раз в минуту; виджет обновляется только при включённом экране. Теряется не более 5 мин статистики при убийстве процесса.

Сознательно не делали (нет выигрыша или он не стоит риска)

  • Проверка здоровья уже растягивает паузы до штатного интервала и выключена по умолчанию, отдельная «остановка на мёртвом туннеле» не нужна.
  • Проверка белых списков: цепочка идёт только пока белый список обнаружен, шаг не больше 5 мин; удлинение при погашенном экране отложило бы возврат туннеля на минуты ради долей процента.
  • Сеть без процесса (registerNetworkCallback на PendingIntent) — гипотеза не проверена на устройстве.
  • UnderlyingNetworkTracker и SSID для сценариев из кэша NetworkMonitor — нужен лог с реальных сетей.

Известное ограничение: без «Будильников и напоминаний» keepalive в глубоком Doze остаётся под квотой; склейка заданий лишь освобождает слоты.

Найдено при smoke-тесте 2.19.0: CharonVpnService при старте по Intent без пароля (watchdog, конец паузы, FgsStartRetry) затирал сохранённый в профиле пароль значением null; туннель после смерти процесса не возвращался («Password unavailable»). Теперь пароль из Intent подставляется, только если он там есть. Проверено на эмуляторе shx36 (профиль на 192.0.2.1, туннель не поднимается, проверялась логика): am crash → watchdog поднимает сервис (~80 с, окно неточного будильника); сценарий «Wi-Fi → включить» срабатывает по событию сети при выключенных правилах сетей (~10 с); будильник сценария по времени будит убитый процесс, ScenarioReceiver ждёт сервис и возвращает прежнее состояние VPN. Без реального сервера не проверены keepalive/rekey, NAT-таймауты и энергопотребление.