25 KiB
План оптимизации энергопотребления 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 — замер базовой линии (до любых правок)
Без цифр правки ниже — гипотезы. Сценарии (эмулятор не годится, нужен реальный телефон):
- Ночь: VPN соединён, экран выключен, Wi-Fi, 8 ч. То же на мобильной сети.
- Те же условия с включёнными: проверкой здоровья, сценарием «приложение открыто», сайтами в правилах, «сайты РФ напрямую».
- День: экран включён, вкладки «Главная» / «Маршруты» / «Диагностика».
Инструменты: 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. Порядок работ
- Фаза 0: базовая линия на реальном телефоне (раздел 2); для CPU добавить
adb shell top -H -p <pid>/ Perfetto по потокам (счёт пробуждений потока чтения TUN). - Быстрые правки с низким риском: секундный
selectTUN (раздел 1a), D, E, F,DiagLog— один релиз, сравнить с базой. - A (пассивная проверка здоровья) и B (keepalive) — отдельные релизы, каждый со своим замером; B только после проверки на сетях операторов.
- C (хаб колбэков) — рефакторинг, после A/B; регрессия по сценариям смены сети (Wi-Fi ↔ мобильная, роуминг, белые списки, пауза).
- G — по результатам Perfetto.
- После каждой правки: тот же прогон из фазы 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 на AVDshx36, выгрузка в 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-таймауты и энергопотребление.