android: less idle CPU work: TUN reader waits on poll+eventfd instead of a 1 s select; scenario app polling stops with the screen and slows down when idle; site DNS refresh waits for the screen; ServerActivity and rememberNow loops follow the lifecycle; energy plan
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e0113e3413
commit
38e5fd84bb
6 files changed
+316
-40
No files matched your search
+15
@@ -542,10 +542,22 @@ class RoutingManager private constructor(private val context: Context)
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
delay(SITE_REFRESH_MS)
|
delay(SITE_REFRESH_MS)
|
||||||
|
/* nobody looks at the screen: resolving only wakes the radio and
|
||||||
|
* CPU, so wait for it to come on (re-checked every few minutes),
|
||||||
|
* but never let the answers get older than the cap */
|
||||||
|
var waited = SITE_REFRESH_MS
|
||||||
|
while (waited < SITE_REFRESH_SCREEN_OFF_MAX_MS && !isScreenOn())
|
||||||
|
{
|
||||||
|
delay(SITE_SCREEN_RECHECK_MS)
|
||||||
|
waited += SITE_SCREEN_RECHECK_MS
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isScreenOn(): Boolean =
|
||||||
|
(context.getSystemService(Context.POWER_SERVICE) as? android.os.PowerManager)?.isInteractive ?: true
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Like [updateConfig], but force prefixes apply on the fly through the
|
* Like [updateConfig], but force prefixes apply on the fly through the
|
||||||
* atomic set swap of the userspace router - no reconnect needed.
|
* atomic set swap of the userspace router - no reconnect needed.
|
||||||
@@ -833,6 +845,9 @@ class RoutingManager private constructor(private val context: Context)
|
|||||||
private const val KEY_NOTICE_ONLY = "notice_only_profiles"
|
private const val KEY_NOTICE_ONLY = "notice_only_profiles"
|
||||||
private const val RECONNECT_DEBOUNCE_MS = 2000L
|
private const val RECONNECT_DEBOUNCE_MS = 2000L
|
||||||
const val SITE_REFRESH_MS = 30L * 60 * 1000
|
const val SITE_REFRESH_MS = 30L * 60 * 1000
|
||||||
|
/* with the screen off the refresh waits for it, up to this age */
|
||||||
|
private const val SITE_REFRESH_SCREEN_OFF_MAX_MS = 3L * 60 * 60 * 1000
|
||||||
|
private const val SITE_SCREEN_RECHECK_MS = 5L * 60 * 1000
|
||||||
|
|
||||||
/** Config schema: 1 = rule sets, 2 = direct apps + categories + patterns. */
|
/** Config schema: 1 = rule sets, 2 = direct apps + categories + patterns. */
|
||||||
const val CONFIG_VERSION = 2
|
const val CONFIG_VERSION = 2
|
||||||
|
|||||||
+66
-12
@@ -21,6 +21,7 @@ import android.app.usage.UsageStatsManager
|
|||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
import android.net.NetworkCapabilities
|
import android.net.NetworkCapabilities
|
||||||
import android.net.VpnService
|
import android.net.VpnService
|
||||||
@@ -161,6 +162,9 @@ object ScenarioEngine
|
|||||||
private const val TAG = "Scenarios"
|
private const val TAG = "Scenarios"
|
||||||
private const val FILE_NAME = "scenarios.json"
|
private const val FILE_NAME = "scenarios.json"
|
||||||
private const val POLL_MS = 4000L
|
private const val POLL_MS = 4000L
|
||||||
|
/* the foreground app did not change for a minute: poll half as often */
|
||||||
|
private const val SLOW_AFTER_POLLS = 15
|
||||||
|
private const val SLOW_POLL_MS = 8000L
|
||||||
private const val PREF_APPLIED = "pref_scenario_applied"
|
private const val PREF_APPLIED = "pref_scenario_applied"
|
||||||
private const val PREF_WAS_ON = "pref_scenario_was_on"
|
private const val PREF_WAS_ON = "pref_scenario_was_on"
|
||||||
private const val PREF_WAS_PROFILE = "pref_scenario_was_profile"
|
private const val PREF_WAS_PROFILE = "pref_scenario_was_profile"
|
||||||
@@ -174,6 +178,7 @@ object ScenarioEngine
|
|||||||
private val handler = Handler(Looper.getMainLooper())
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
private var appContext: Context? = null
|
private var appContext: Context? = null
|
||||||
private var polling = false
|
private var polling = false
|
||||||
|
private var unchangedPolls = 0
|
||||||
private var lastForeground: String? = null
|
private var lastForeground: String? = null
|
||||||
|
|
||||||
fun flow(context: Context): StateFlow<List<Scenario>>
|
fun flow(context: Context): StateFlow<List<Scenario>>
|
||||||
@@ -203,35 +208,84 @@ object ScenarioEngine
|
|||||||
if (needPoll && !polling)
|
if (needPoll && !polling)
|
||||||
{
|
{
|
||||||
polling = true
|
polling = true
|
||||||
handler.post(poll)
|
registerScreenReceiver(app)
|
||||||
|
if (isScreenOn(app))
|
||||||
|
{
|
||||||
|
handler.post(poll)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (!needPoll)
|
else if (!needPoll)
|
||||||
{
|
{
|
||||||
polling = false
|
polling = false
|
||||||
handler.removeCallbacks(poll)
|
handler.removeCallbacks(poll)
|
||||||
|
unregisterScreenReceiver(app)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isScreenOn(context: Context): Boolean =
|
||||||
|
(context.getSystemService(Context.POWER_SERVICE) as? PowerManager)?.isInteractive ?: true
|
||||||
|
|
||||||
|
/* the foreground app only changes while the screen is on: no polling (and no
|
||||||
|
* wake-ups for it) while it is off, one immediate poll when it comes back */
|
||||||
|
private val screenReceiver = object : BroadcastReceiver()
|
||||||
|
{
|
||||||
|
override fun onReceive(context: Context, intent: Intent)
|
||||||
|
{
|
||||||
|
handler.removeCallbacks(poll)
|
||||||
|
if (polling && intent.action == Intent.ACTION_SCREEN_ON)
|
||||||
|
{
|
||||||
|
unchangedPolls = 0
|
||||||
|
handler.post(poll)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private var screenReceiverRegistered = false
|
||||||
|
|
||||||
|
private fun registerScreenReceiver(app: Context)
|
||||||
|
{
|
||||||
|
if (screenReceiverRegistered)
|
||||||
|
{
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val filter = IntentFilter().apply {
|
||||||
|
addAction(Intent.ACTION_SCREEN_ON)
|
||||||
|
addAction(Intent.ACTION_SCREEN_OFF)
|
||||||
|
}
|
||||||
|
runCatching { app.registerReceiver(screenReceiver, filter, null, handler) }
|
||||||
|
.onSuccess { screenReceiverRegistered = true }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unregisterScreenReceiver(app: Context)
|
||||||
|
{
|
||||||
|
if (!screenReceiverRegistered)
|
||||||
|
{
|
||||||
|
return
|
||||||
|
}
|
||||||
|
runCatching { app.unregisterReceiver(screenReceiver) }
|
||||||
|
screenReceiverRegistered = false
|
||||||
|
}
|
||||||
|
|
||||||
private val poll = object : Runnable
|
private val poll = object : Runnable
|
||||||
{
|
{
|
||||||
override fun run()
|
override fun run()
|
||||||
{
|
{
|
||||||
val app = appContext ?: return
|
val app = appContext ?: return
|
||||||
if (!polling)
|
if (!polling || !isScreenOn(app))
|
||||||
{
|
{ /* resumed by the screen-on broadcast */
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val screenOn = (app.getSystemService(Context.POWER_SERVICE) as? PowerManager)?.isInteractive ?: true
|
val fg = foregroundApp(app)
|
||||||
if (screenOn)
|
if (fg != null && fg != lastForeground)
|
||||||
{
|
{
|
||||||
val fg = foregroundApp(app)
|
lastForeground = fg
|
||||||
if (fg != null && fg != lastForeground)
|
unchangedPolls = 0
|
||||||
{
|
evaluate(app)
|
||||||
lastForeground = fg
|
|
||||||
evaluate(app)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
handler.postDelayed(this, POLL_MS)
|
else
|
||||||
|
{
|
||||||
|
unchangedPolls++
|
||||||
|
}
|
||||||
|
handler.postDelayed(this, if (unchangedPolls >= SLOW_AFTER_POLLS) SLOW_POLL_MS else POLL_MS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+17
-11
@@ -71,6 +71,7 @@ import androidx.lifecycle.Lifecycle
|
|||||||
import androidx.lifecycle.LifecycleEventObserver
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||||
import com.journeyapps.barcodescanner.ScanContract
|
import com.journeyapps.barcodescanner.ScanContract
|
||||||
import com.journeyapps.barcodescanner.ScanOptions
|
import com.journeyapps.barcodescanner.ScanOptions
|
||||||
@@ -193,19 +194,24 @@ private fun ServerScreen(uuid: String, checkOnOpen: Boolean, onBack: () -> Unit)
|
|||||||
val extras by remember { ProfileExtras.flow(context) }.collectAsStateWithLifecycle()
|
val extras by remember { ProfileExtras.flow(context) }.collectAsStateWithLifecycle()
|
||||||
val extra = extras[uuid] ?: ProfileExtra(uuid)
|
val extra = extras[uuid] ?: ProfileExtra(uuid)
|
||||||
/* the live state of the tunnel and a direct ping of the server */
|
/* the live state of the tunnel and a direct ping of the server */
|
||||||
val vpn by produceState<Pair<VpnStateService.State, String?>?>(null) {
|
val vpn by produceState<Pair<VpnStateService.State, String?>?>(null, owner) {
|
||||||
while (true)
|
/* only while the page is on screen */
|
||||||
{
|
owner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
val service = VpnStateService.getLiveInstance()
|
while (true)
|
||||||
value = service?.let { it.state to it.profile?.getUUID()?.toString() }
|
{
|
||||||
delay(1500)
|
val service = VpnStateService.getLiveInstance()
|
||||||
|
value = service?.let { it.state to it.profile?.getUUID()?.toString() }
|
||||||
|
delay(1500)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val rtt by produceState<Long?>(null, profile.gateway) {
|
val rtt by produceState<Long?>(null, profile.gateway, owner) {
|
||||||
while (true)
|
owner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
{
|
while (true)
|
||||||
value = withContext(Dispatchers.IO) { ConnectivityProbe.pingServerDirect(context, profile.gateway, 1500)?.first }
|
{
|
||||||
delay(30_000)
|
value = withContext(Dispatchers.IO) { ConnectivityProbe.pingServerDirect(context, profile.gateway, 1500)?.first }
|
||||||
|
delay(30_000)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val active = vpn?.second == uuid
|
val active = vpn?.second == uuid
|
||||||
|
|||||||
+12
-5
@@ -40,6 +40,9 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import org.strongswan.android.R
|
import org.strongswan.android.R
|
||||||
import org.strongswan.android.logic.PauseManager
|
import org.strongswan.android.logic.PauseManager
|
||||||
@@ -103,12 +106,16 @@ internal fun HomeProblem.titleRes(): Int = when (this)
|
|||||||
internal fun rememberNow(): Long
|
internal fun rememberNow(): Long
|
||||||
{
|
{
|
||||||
var now by remember { mutableLongStateOf(System.currentTimeMillis()) }
|
var now by remember { mutableLongStateOf(System.currentTimeMillis()) }
|
||||||
LaunchedEffect(Unit)
|
val lifecycle = LocalLifecycleOwner.current.lifecycle
|
||||||
{
|
LaunchedEffect(lifecycle)
|
||||||
while (true)
|
{ /* no ticking while the app is in the background */
|
||||||
|
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED)
|
||||||
{
|
{
|
||||||
now = System.currentTimeMillis()
|
while (true)
|
||||||
delay(1000L - now % 1000L)
|
{
|
||||||
|
now = System.currentTimeMillis()
|
||||||
|
delay(1000L - now % 1000L)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return now
|
return now
|
||||||
|
|||||||
@@ -17,6 +17,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
|
#include <poll.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <sys/eventfd.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
#include "android_jni.h"
|
#include "android_jni.h"
|
||||||
@@ -70,6 +73,12 @@ struct private_android_service_t {
|
|||||||
*/
|
*/
|
||||||
int tunfd;
|
int tunfd;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* eventfd that wakes the TUN reader whenever tunfd is replaced or closed
|
||||||
|
* (-1 if it could not be created, then the reader falls back to a 1 s poll)
|
||||||
|
*/
|
||||||
|
int wakefd;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MTU of TUN device
|
* MTU of TUN device
|
||||||
*/
|
*/
|
||||||
@@ -136,6 +145,21 @@ static void engine_write_packet(void *ctx, ip_packet_t *packet)
|
|||||||
deliver_plain(ctx, packet);
|
deliver_plain(ctx, packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wake the TUN reader so it picks up a replaced or closed tunfd. Call it
|
||||||
|
* after this->tunfd was changed (the eventfd counter stays readable until the
|
||||||
|
* reader drains it, so a wake-up before the reader polls is not lost).
|
||||||
|
*/
|
||||||
|
static void wake_reader(private_android_service_t *this)
|
||||||
|
{
|
||||||
|
uint64_t one = 1;
|
||||||
|
|
||||||
|
if (this->wakefd >= 0 && write(this->wakefd, &one, sizeof(one)) < 0)
|
||||||
|
{
|
||||||
|
DBG1(DBG_DMN, "waking the TUN reader failed: %s", strerror(errno));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Job handling outbound plaintext packets
|
* Job handling outbound plaintext packets
|
||||||
*/
|
*/
|
||||||
@@ -143,16 +167,10 @@ static job_requeue_t handle_plain(private_android_service_t *this)
|
|||||||
{
|
{
|
||||||
ip_packet_t *packet;
|
ip_packet_t *packet;
|
||||||
chunk_t raw;
|
chunk_t raw;
|
||||||
fd_set set;
|
struct pollfd fds[2];
|
||||||
ssize_t len;
|
ssize_t len;
|
||||||
int tunfd;
|
int tunfd, wakefd, nfds = 1, timeout;
|
||||||
bool old, dns_proxy;
|
bool old, dns_proxy;
|
||||||
timeval_t tv = {
|
|
||||||
/* check every second if tunfd is still valid */
|
|
||||||
.tv_sec = 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
FD_ZERO(&set);
|
|
||||||
|
|
||||||
this->lock->read_lock(this->lock);
|
this->lock->read_lock(this->lock);
|
||||||
if (this->tunfd < 0)
|
if (this->tunfd < 0)
|
||||||
@@ -161,28 +179,60 @@ static job_requeue_t handle_plain(private_android_service_t *this)
|
|||||||
return JOB_REQUEUE_NONE;
|
return JOB_REQUEUE_NONE;
|
||||||
}
|
}
|
||||||
tunfd = this->tunfd;
|
tunfd = this->tunfd;
|
||||||
FD_SET(tunfd, &set);
|
wakefd = this->wakefd;
|
||||||
/* cache this while we have the lock */
|
/* cache this while we have the lock */
|
||||||
dns_proxy = this->use_dns_proxy;
|
dns_proxy = this->use_dns_proxy;
|
||||||
this->lock->unlock(this->lock);
|
this->lock->unlock(this->lock);
|
||||||
|
|
||||||
|
fds[0].fd = tunfd;
|
||||||
|
fds[0].events = POLLIN;
|
||||||
|
fds[0].revents = 0;
|
||||||
|
if (wakefd >= 0)
|
||||||
|
{ /* a replaced or closed tunfd wakes us, so the timeout is only a
|
||||||
|
* safety net; without the eventfd re-check every second as before */
|
||||||
|
fds[1].fd = wakefd;
|
||||||
|
fds[1].events = POLLIN;
|
||||||
|
fds[1].revents = 0;
|
||||||
|
nfds = 2;
|
||||||
|
timeout = 60000;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
timeout = 1000;
|
||||||
|
}
|
||||||
|
|
||||||
old = thread_cancelability(TRUE);
|
old = thread_cancelability(TRUE);
|
||||||
len = select(tunfd + 1, &set, NULL, NULL, &tv);
|
len = poll(fds, nfds, timeout);
|
||||||
thread_cancelability(old);
|
thread_cancelability(old);
|
||||||
|
|
||||||
if (len < 0)
|
if (len < 0)
|
||||||
{
|
{
|
||||||
if (errno == EBADF)
|
if (errno == EBADF)
|
||||||
{ /* the TUN device got closed just before calling select(), retry */
|
{ /* the TUN device got closed just before calling poll(), retry */
|
||||||
return JOB_REQUEUE_FAIR;
|
return JOB_REQUEUE_FAIR;
|
||||||
}
|
}
|
||||||
DBG1(DBG_DMN, "select on TUN device failed: %s", strerror(errno));
|
DBG1(DBG_DMN, "poll on TUN device failed: %s", strerror(errno));
|
||||||
return JOB_REQUEUE_NONE;
|
return JOB_REQUEUE_NONE;
|
||||||
}
|
}
|
||||||
else if (len == 0)
|
else if (len == 0)
|
||||||
{ /* timeout, check again right away */
|
{ /* timeout, check again right away */
|
||||||
return JOB_REQUEUE_DIRECT;
|
return JOB_REQUEUE_DIRECT;
|
||||||
}
|
}
|
||||||
|
if (nfds == 2 && (fds[1].revents & POLLIN))
|
||||||
|
{ /* tunfd was replaced or closed: drain the counter and start over with
|
||||||
|
* the current fd (pending TUN data stays, poll is level-triggered) */
|
||||||
|
uint64_t cnt;
|
||||||
|
|
||||||
|
if (read(wakefd, &cnt, sizeof(cnt)) < 0 && errno != EAGAIN)
|
||||||
|
{
|
||||||
|
DBG1(DBG_DMN, "reading the wake-up fd failed: %s", strerror(errno));
|
||||||
|
}
|
||||||
|
return JOB_REQUEUE_DIRECT;
|
||||||
|
}
|
||||||
|
if (fds[0].revents & POLLNVAL)
|
||||||
|
{ /* the fd was closed while polling, retry with the current one */
|
||||||
|
return JOB_REQUEUE_FAIR;
|
||||||
|
}
|
||||||
|
|
||||||
raw = chunk_alloc(this->mtu);
|
raw = chunk_alloc(this->mtu);
|
||||||
len = read(tunfd, raw.ptr, raw.len);
|
len = read(tunfd, raw.ptr, raw.len);
|
||||||
@@ -375,6 +425,8 @@ static bool setup_tun_device(private_android_service_t *this,
|
|||||||
}
|
}
|
||||||
this->tunfd = tunfd;
|
this->tunfd = tunfd;
|
||||||
this->lock->unlock(this->lock);
|
this->lock->unlock(this->lock);
|
||||||
|
/* a reader polling the previous fd must switch to the new one */
|
||||||
|
wake_reader(this);
|
||||||
|
|
||||||
DBG1(DBG_DMN, "successfully created TUN device");
|
DBG1(DBG_DMN, "successfully created TUN device");
|
||||||
|
|
||||||
@@ -422,6 +474,7 @@ static bool setup_tun_device_without_dns(private_android_service_t *this)
|
|||||||
}
|
}
|
||||||
this->tunfd = tunfd;
|
this->tunfd = tunfd;
|
||||||
this->lock->unlock(this->lock);
|
this->lock->unlock(this->lock);
|
||||||
|
wake_reader(this);
|
||||||
|
|
||||||
/* the fd changed, re-bind the userspace router to the new TUN */
|
/* the fd changed, re-bind the userspace router to the new TUN */
|
||||||
direct_engine_set_tun(this, engine_write_packet, this->mtu);
|
direct_engine_set_tun(this, engine_write_packet, this->mtu);
|
||||||
@@ -446,6 +499,8 @@ static void close_tun_device(private_android_service_t *this)
|
|||||||
tunfd = this->tunfd;
|
tunfd = this->tunfd;
|
||||||
this->tunfd = -1;
|
this->tunfd = -1;
|
||||||
this->lock->unlock(this->lock);
|
this->lock->unlock(this->lock);
|
||||||
|
/* let the reader notice tunfd < 0 and quit instead of polling a dead fd */
|
||||||
|
wake_reader(this);
|
||||||
|
|
||||||
/* drop the remembered binding too, so a lazily created engine does not
|
/* drop the remembered binding too, so a lazily created engine does not
|
||||||
* attach to the dead TUN, and flush the engine if it exists */
|
* attach to the dead TUN, and flush the engine if it exists */
|
||||||
@@ -977,6 +1032,10 @@ METHOD(android_service_t, destroy, void,
|
|||||||
close_tun_device(this);
|
close_tun_device(this);
|
||||||
this->dns_proxy->destroy(this->dns_proxy);
|
this->dns_proxy->destroy(this->dns_proxy);
|
||||||
this->lock->destroy(this->lock);
|
this->lock->destroy(this->lock);
|
||||||
|
if (this->wakefd >= 0)
|
||||||
|
{
|
||||||
|
close(this->wakefd);
|
||||||
|
}
|
||||||
this->settings->destroy(this->settings);
|
this->settings->destroy(this->settings);
|
||||||
free(this);
|
free(this);
|
||||||
}
|
}
|
||||||
@@ -1005,6 +1064,7 @@ android_service_t *android_service_create(android_creds_t *creds,
|
|||||||
.settings = settings,
|
.settings = settings,
|
||||||
.creds = creds,
|
.creds = creds,
|
||||||
.tunfd = -1,
|
.tunfd = -1,
|
||||||
|
.wakefd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC),
|
||||||
.mtu = settings->get_int(settings, "global.mtu", ANDROID_DEFAULT_MTU),
|
.mtu = settings->get_int(settings, "global.mtu", ANDROID_DEFAULT_MTU),
|
||||||
);
|
);
|
||||||
/* only allow queries for the VPN gateway */
|
/* only allow queries for the VPN gateway */
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
# План оптимизации энергопотребления 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 (ждать выхода/`ACTION_SCREEN_ON`).
|
||||||
|
|
||||||
|
### B. Keepalive и будильники strongSwan (высокий эффект, проверить)
|
||||||
|
`Scheduler.java` ставит `setExactAndAllowWhileIdle(RTC_WAKEUP)` на NAT-keepalive и ребилд IKE. Профильный `nat_keepalive`
|
||||||
|
ограничен 10–120 с (`Constants.NAT_KEEPALIVE_MIN/MAX`); при пустом значении берётся умолчание charon.
|
||||||
|
- проверить, что реально приходит из `.sswan` и какое значение у charon по умолчанию;
|
||||||
|
- рассмотреть умолчание 30–60 с для мобильной сети (после проверки, что NAT оператора не рвёт соединение) и большие значения на Wi-Fi;
|
||||||
|
- замерить число будильников в `dumpsys alarm` за ночь.
|
||||||
|
|
||||||
|
### C. Единый хаб сетевых колбэков (средний эффект, средняя сложность)
|
||||||
|
`registerNetworkCallback` вызывают минимум семь компонентов: `CharonVpnService`, `NetworkManager`, `NetworkMonitor`,
|
||||||
|
`WhiteListManager`, `HealthCheckManager`, `PauseManager`, `HomeViewModel`. Каждое изменение сети будит их все и дублирует
|
||||||
|
работу (запросы SSID, перепроверки). Сделать один подписчик в приложении, раздающий состояние через `StateFlow`;
|
||||||
|
остальные подписываются на него. Попутно уменьшает риск лимита колбэков (в `HomeViewModel` он уже обрабатывается `catch`).
|
||||||
|
|
||||||
|
### 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`.
|
||||||
Reference in new issue
Block a user