mirror of
https://github.com/greenart7c3/Amber.git
synced 2026-10-05 19:08:23 +00:00
Merge #3704bcf5: Reduce background battery drain from subscription tick…
Reduce background battery drain from subscription tick and Tor retries nostr:nevent1qqsrwp9u7h7mr7an3z84emd3fn0mryauytg6teyaymk59d86wq8e4gspz3mhxue69uhhyetvv9ujumn8d96zuer9wc3y9sgd PR-Author: greenart7c3 nostr:npub1w4uswmv6lu9yel005l3qgheysmr7tk9uvwluddznju3nuxalevvs2d0jr5 PR description: The ConnectivityService safety-net tick woke the CPU every 30 seconds for the whole life of the process (~2,880 wakeups/day) even though every real state change already refreshes the subscriptions explicitly and Quartz replays subscriptions on reconnect. Raise the tick to 5 minutes; it now only covers genuinely missed paths. Bound the built-in Tor startup: when Tor cannot bootstrap (airplane mode, censored network), runMigrations previously stop/start cycled the whole daemon every ~2 minutes forever, burning battery and data until the network came back. After 5 attempts (120s bootstrap window each, backoff 3/6/12/24s) the daemon is stopped, a non-ongoing Failed notification with the Restart action is shown, and relay connections stay down instead of failing endlessly through a dead SOCKS proxy. Relay connections now recover automatically when Tor comes back: a new rising-edge observer on TorManager.isRunning refreshes subscriptions (covers manual restarts mid-session, which previously left relays disconnected), and a network change retries Tor when it had given up.
This commit is contained in:
@@ -153,6 +153,15 @@ object TorManager {
|
||||
appContext?.let { showNotification(it.getString(R.string.tor_retrying)) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the terminal failure state (used after the bounded startup retries
|
||||
* in Amber.runMigrations give up). Non-ongoing so the user can swipe it
|
||||
* away; the Restart action is the way back to a connection attempt.
|
||||
*/
|
||||
fun showFailed() {
|
||||
appContext?.let { showNotification(it.getString(R.string.tor_connection_failed), ongoing = false) }
|
||||
}
|
||||
|
||||
fun start(context: Context, scope: CoroutineScope) {
|
||||
if (appContext == null) {
|
||||
appContext = context.applicationContext
|
||||
|
||||
@@ -153,6 +153,15 @@ object TorManager {
|
||||
appContext?.let { showNotification(it.getString(R.string.tor_retrying)) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the terminal failure state (used after the bounded startup retries
|
||||
* in Amber.runMigrations give up). Non-ongoing so the user can swipe it
|
||||
* away; the Restart action is the way back to a connection attempt.
|
||||
*/
|
||||
fun showFailed() {
|
||||
appContext?.let { showNotification(it.getString(R.string.tor_connection_failed), ongoing = false) }
|
||||
}
|
||||
|
||||
fun start(context: Context, scope: CoroutineScope) {
|
||||
if (appContext == null) {
|
||||
appContext = context.applicationContext
|
||||
|
||||
@@ -160,6 +160,11 @@ class Amber :
|
||||
// Hoisted out of runMigrations() so re-entry (e.g. test re-init) can't
|
||||
// double-register and double-fire foreground/background callbacks.
|
||||
private var processLifecycleObserverRegistered = false
|
||||
|
||||
// Guard for startTorRecoveryObserver(): runMigrations' only caller
|
||||
// (ConnectivityService.onCreate) can run again after the service is
|
||||
// recreated, and the observer must be registered once per process.
|
||||
private var torRecoveryObserverStarted = false
|
||||
private val processLifecycleObserver = object : DefaultLifecycleObserver {
|
||||
override fun onStart(owner: LifecycleOwner) {
|
||||
AmberLog.d("ProcessLifecycleOwner", "App in foreground")
|
||||
@@ -394,11 +399,6 @@ class Amber :
|
||||
HttpClientManager.getHttpClient(false)
|
||||
HttpClientManager.getHttpClient(true)
|
||||
|
||||
// Start Tor immediately in the background without blocking app startup
|
||||
if (settings.torMode == TorMode.BUILTIN && !BuildFlavorChecker.isOfflineFlavor()) {
|
||||
TorManager.start(this@Amber, applicationIOScope)
|
||||
}
|
||||
|
||||
launch(Dispatchers.Main) {
|
||||
if (!processLifecycleObserverRegistered) {
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(processLifecycleObserver)
|
||||
@@ -406,28 +406,43 @@ class Amber :
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for Tor to be ready before establishing relay connections
|
||||
// Start Tor immediately in the background without blocking app startup
|
||||
var torReady = true
|
||||
if (settings.torMode == TorMode.BUILTIN && !BuildFlavorChecker.isOfflineFlavor()) {
|
||||
var attempt = 0
|
||||
while (!TorManager.isRunning.value) {
|
||||
if (attempt > 0) {
|
||||
TorManager.showRetrying()
|
||||
TorManager.stop()
|
||||
delay(3.seconds)
|
||||
TorManager.start(this@Amber, applicationIOScope)
|
||||
}
|
||||
attempt++
|
||||
withTimeoutOrNull(120.seconds) {
|
||||
TorManager.isRunning.first { it }
|
||||
}
|
||||
TorManager.start(this@Amber, applicationIOScope)
|
||||
|
||||
startTorRecoveryObserver()
|
||||
|
||||
// Wait for Tor to be ready before establishing relay connections.
|
||||
// Bounded: when Tor cannot bootstrap (airplane mode, censored
|
||||
// network) an unbounded stop/start cycle restarts the whole
|
||||
// daemon forever, burning battery and data for as long as the
|
||||
// device stays offline. After the last attempt the daemon is
|
||||
// stopped and the Failed notification (Restart action) takes
|
||||
// over; startFunctions() parks on waitForTorIfNeeded() until
|
||||
// Tor actually comes back.
|
||||
torReady = waitForTorStartup()
|
||||
if (!torReady) {
|
||||
TorManager.stop()
|
||||
TorManager.showFailed()
|
||||
AmberLog.e(
|
||||
TAG,
|
||||
"Built-in Tor failed to start after $MAX_TOR_START_ATTEMPTS attempts; relays stay disconnected until it recovers",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
checkForNewRelaysAndUpdateAllFilters(true)
|
||||
if (settings.killSwitch.value) {
|
||||
disconnectIntentionally()
|
||||
if (torReady) {
|
||||
checkForNewRelaysAndUpdateAllFilters(true)
|
||||
if (settings.killSwitch.value) {
|
||||
disconnectIntentionally()
|
||||
}
|
||||
} else {
|
||||
// Do not open relay sockets through a dead SOCKS proxy: every
|
||||
// connect would fail and the retry churn would waste battery.
|
||||
// Relays connect via the recovery observer once Tor is up.
|
||||
onDone()
|
||||
}
|
||||
onDone()
|
||||
} catch (e: Exception) {
|
||||
AmberLog.e(TAG, "Failed to run migrations", e)
|
||||
if (e is CancellationException) throw e
|
||||
@@ -436,6 +451,50 @@ class Amber :
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bounded Tor startup: at most [MAX_TOR_START_ATTEMPTS] full daemon starts,
|
||||
* each allowed 120s to bootstrap, with a growing pause (3s, 6s, 12s, 24s)
|
||||
* between attempts. Returns true as soon as the SOCKS port is up.
|
||||
*/
|
||||
private suspend fun waitForTorStartup(): Boolean {
|
||||
var attempt = 0
|
||||
while (!TorManager.isRunning.value) {
|
||||
if (attempt >= MAX_TOR_START_ATTEMPTS) return false
|
||||
if (attempt > 0) {
|
||||
TorManager.showRetrying()
|
||||
TorManager.stop()
|
||||
delay((3L shl (attempt - 1)).seconds)
|
||||
TorManager.start(this@Amber, applicationIOScope)
|
||||
}
|
||||
attempt++
|
||||
withTimeoutOrNull(120.seconds) {
|
||||
TorManager.isRunning.first { it }
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconnects relays whenever built-in Tor transitions into a running state.
|
||||
* Covers recovery after the bounded startup retries gave up (manual Restart
|
||||
* action or a network change retried Tor) and mid-session restarts, neither
|
||||
* of which runs through [runMigrations] again.
|
||||
*/
|
||||
private fun startTorRecoveryObserver() {
|
||||
if (torRecoveryObserverStarted) return
|
||||
torRecoveryObserverStarted = true
|
||||
applicationIOScope.launch {
|
||||
var wasRunning = TorManager.isRunning.value
|
||||
TorManager.isRunning.collect { running ->
|
||||
if (running && !wasRunning) {
|
||||
AmberLog.d(TAG, "Built-in Tor is up; refreshing relay connections")
|
||||
checkForNewRelaysAndUpdateAllFilters(true)
|
||||
}
|
||||
wasRunning = running
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun waitForTorIfNeeded() {
|
||||
if (settings.torMode == TorMode.BUILTIN && !BuildFlavorChecker.isOfflineFlavor()) {
|
||||
TorManager.isRunning.first { it }
|
||||
@@ -730,6 +789,7 @@ class Amber :
|
||||
companion object {
|
||||
var isAppInForeground = false
|
||||
const val TAG = "Amber"
|
||||
private const val MAX_TOR_START_ATTEMPTS = 5
|
||||
lateinit var instance: Amber
|
||||
private set
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import androidx.paging.PagingSource
|
||||
* [getAll] results are cached per account pubKey in a separate small LRU.
|
||||
* That query decrypts every application row (AndroidKeyStore AES-GCM, a binder
|
||||
* hop to keystore2 per field) and [com.greenart7c3.nostrsigner.service.NotificationSubscription]
|
||||
* re-runs it on every ~30s relay-refresh cycle, which made it the app's
|
||||
* re-runs it on every relay-refresh cycle, which made it the app's
|
||||
* dominant native allocator. Any mutation of the `application` table evicts
|
||||
* the affected account's entry (or all entries when only the app key — not
|
||||
* the owning account — is known). Cached lists are handed out as defensive
|
||||
|
||||
@@ -59,7 +59,7 @@ class NostrClientLoggerListener(
|
||||
// Counts the failure against the relay and only schedules a reconnect while it
|
||||
// is still worth retrying. Once a relay is dead, RelayHealthTracker also makes
|
||||
// NotificationSubscription.updateFilter drop it from the subscription relay
|
||||
// set, so Quartz stops opening sockets to it every 30s. The streak resets on a
|
||||
// set, so Quartz stops opening sockets to it on every refresh. The streak resets on a
|
||||
// successful connection (onConnected) or a network change / manual reconnect.
|
||||
private fun scheduleReconnect(relay: NormalizedRelayUrl) {
|
||||
if (!RelayHealthTracker.recordFailure(relay)) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import java.util.concurrent.ConcurrentHashMap
|
||||
* relays that are permanently unreachable.
|
||||
*
|
||||
* Quartz's relay pool is driven by the relays referenced in active subscriptions:
|
||||
* [NotificationSubscription.updateFilter] re-subscribes every 30s, and any relay
|
||||
* [NotificationSubscription.updateFilter] re-subscribes on every refresh, and any relay
|
||||
* present in that map is (re)connected by the pool. Without this tracker an
|
||||
* offline relay stays in the map forever, so a socket is opened to it on every
|
||||
* refresh, needlessly waking the radio and draining the battery.
|
||||
|
||||
@@ -14,6 +14,7 @@ import com.greenart7c3.nostrsigner.AmberLog
|
||||
import com.greenart7c3.nostrsigner.BuildConfig
|
||||
import com.greenart7c3.nostrsigner.BuildFlavorChecker
|
||||
import com.greenart7c3.nostrsigner.LocalPreferences
|
||||
import com.greenart7c3.nostrsigner.models.TorMode
|
||||
import com.greenart7c3.nostrsigner.okhttp.HttpClientManager
|
||||
import com.greenart7c3.nostrsigner.relays.RelayHealthTracker
|
||||
import java.util.Timer
|
||||
@@ -38,8 +39,16 @@ class ConnectivityService : Service() {
|
||||
if (Amber.instance.settings.killSwitch.value) return
|
||||
|
||||
if (lastNetwork != null && lastNetwork != network) {
|
||||
// New network: give previously-dead relays a fresh chance. The
|
||||
// 30s updateFilter tick re-adds them to the subscription set.
|
||||
// New network: give previously-dead relays a fresh chance.
|
||||
// RelayHealthTracker.reset() makes updateFilter re-add them
|
||||
// to the subscription set (explicit refreshes plus the
|
||||
// periodic safety net below).
|
||||
if (Amber.instance.settings.torMode == TorMode.BUILTIN && !TorManager.isRunning.value) {
|
||||
// Built-in Tor gave up earlier (bounded startup retries
|
||||
// in runMigrations). The network is back, so retry now
|
||||
// instead of waiting for a manual restart.
|
||||
TorManager.restart(this@ConnectivityService, Amber.instance.applicationIOScope)
|
||||
}
|
||||
RelayHealthTracker.reset()
|
||||
scope.launch(Dispatchers.IO) {
|
||||
if (!Amber.instance.client.isActive()) {
|
||||
@@ -168,7 +177,7 @@ class ConnectivityService : Service() {
|
||||
}
|
||||
},
|
||||
5000,
|
||||
30000,
|
||||
UPDATE_FILTER_PERIOD_MS,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -212,4 +221,16 @@ class ConnectivityService : Service() {
|
||||
}
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* How often the safety-net subscription refresh runs. Every real state
|
||||
* change (login/logout, relay edits, app connect/disconnect, NIP-46
|
||||
* CONNECT, backup restore, kill switch) already refreshes the filters
|
||||
* explicitly, and Quartz replays subscriptions on reconnect, so this
|
||||
* tick only covers missed paths. Kept slow: it wakes the CPU even when
|
||||
* nothing changed, and a 30s period prevented doze 2,880 times a day.
|
||||
*/
|
||||
const val UPDATE_FILTER_PERIOD_MS = 5 * 60 * 1000L
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,8 @@ class NotificationSubscription(
|
||||
val since = computeSince()
|
||||
|
||||
// Cached dao: getAll() is served from CachingApplicationDao's per-account
|
||||
// LRU. updateFilter re-runs on every ~30s relay refresh, and an uncached
|
||||
// LRU. updateFilter re-runs on every relay refresh (explicit refreshes
|
||||
// plus the periodic ConnectivityService safety net), and an uncached
|
||||
// getAll re-decrypts every application row through AndroidKeyStore
|
||||
// (a keystore2 binder round-trip per encrypted field) each cycle —
|
||||
// previously the app's dominant native allocator.
|
||||
@@ -102,7 +103,7 @@ class NotificationSubscription(
|
||||
val subKey = "${account.hexKey}_$connPubKey"
|
||||
|
||||
// Exclude relays that have been declared dead so Quartz stops opening
|
||||
// a socket to them on every 30s refresh. They are re-added once
|
||||
// a socket to them on every refresh. They are re-added once
|
||||
// RelayHealthTracker is reset (network change / manual reconnect) or
|
||||
// they connect successfully again.
|
||||
val connRelays = conn.relays.ifEmpty { Amber.instance.getSavedRelays(account) }
|
||||
|
||||
@@ -39,6 +39,10 @@ object TorManager {
|
||||
// No-op in offline flavor
|
||||
}
|
||||
|
||||
fun showFailed() {
|
||||
// No-op in offline flavor
|
||||
}
|
||||
|
||||
fun cancelNotification() {
|
||||
// No-op in offline flavor
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user