Skip to content

perf: reduce battery drain by 88% fewer timer wake-ups#504

Merged
torlando-tech merged 3 commits intomainfrom
perf/reduce-battery-drain
Feb 21, 2026
Merged

perf: reduce battery drain by 88% fewer timer wake-ups#504
torlando-tech merged 3 commits intomainfrom
perf/reduce-battery-drain

Conversation

@torlando-tech
Copy link
Copy Markdown
Owner

Summary

  • Remove WiFi lock (WIFI_MODE_FULL_HIGH_PERF) — the battery optimization whitelist already ensures network access during Doze, and 802.11 power-save mode only adds 100-300ms latency without dropping the TCP connection. Briar (closest comparable mesh messenger) operates successfully without one. Reduces WiFi radio power ~10x during idle.
  • Increase health check interval from 5s → 30s (stale threshold 10s → 60s) — detecting a dead Python process in ~90s vs ~15s makes no practical difference to the user.
  • Increase lock refresh interval from 5min → 2h — wake lock timeout is 10h, so 2h refresh gives 5 refreshes per timeout (was 120, wildly over-provisioned).
  • Slow Python maintenance loop from 1s → 30s — only does work when failed_interfaces is non-empty; sleeping 1s was pure overhead.
  • Slow Python heartbeat idle from 1s → 5s — Kotlin's 60s stale threshold means 5s updates have a 12x safety margin.

Wake-up budget

Source Before (/hr) After (/hr)
Python heartbeat loop 3,600 720
Python maintenance loop 3,600 120
Kotlin HealthCheckManager 720 120
Kotlin MaintenanceManager 12 0.5
WiFi radio Constant active PSP (10-20x less power)
Total timer wake-ups ~7,932 ~960

What we keep (and why)

Foreground service, PARTIAL_WAKE_LOCK, battery whitelist, MulticastLock (needed for LAN discovery), START_STICKY + explicit restart, ConnectivityManager.NetworkCallback, CompanionDeviceService, auto-announce, identity resolution, and ConversationLinkManager — all either zero-cost event-driven or functionally required.

Test plan

  • All unit tests pass (LockManager, HealthCheckManager, MaintenanceManager, DebugViewModel, IdentityScreen, ErrorHandling — 7 test suites)
  • Build succeeds (assembleNoSentryDebug)
  • Emulator functional testing:
    • Foreground service running
    • Wake lock held
    • WiFi lock absent (verified via dumpsys wifi)
    • Health check fires at 30s intervals (verified 3 consecutive cycles in logcat)
    • Heartbeat age consistently <10ms (no false stale detection)
    • Doze mode survival
    • Process kill recovery via START_STICKY
  • Overnight real-device soak test (deploy, let run 8+ hours, check dumpsys batterystats)
  • Verify messages arrive within ~5s while in Doze on real device

🤖 Generated with Claude Code

Remove WiFi lock (WIFI_MODE_FULL_HIGH_PERF) — the battery whitelist already
ensures network access during Doze, and 802.11 power-save mode only adds
100-300ms latency without dropping the TCP connection. Briar operates
successfully without a WiFi lock. This alone reduces WiFi radio power by ~10x
during idle periods.

Increase health check interval from 5s to 30s (stale threshold 10s→60s),
lock refresh interval from 5min to 2h, Python maintenance loop from 1s to
30s, and Python heartbeat idle interval from 1s to 5s. These were all
over-provisioned relative to their actual detection/timeout requirements.

Net effect: timer-driven wake-ups drop from ~7,932/hour to ~960/hour.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Feb 19, 2026

Greptile Summary

This PR reduces battery drain by 88% through strategic timer interval adjustments and WiFi lock removal. The changes are well-coordinated across Python and Kotlin layers with proper safety margins maintained.

Key changes:

  • Removed WIFI_MODE_FULL_HIGH_PERF WiFi lock — battery whitelist already ensures network access during Doze, and 802.11 PSP mode reduces WiFi power ~10x for imperceptible latency increase
  • Increased health check interval (5s→30s) and stale threshold (10s→60s) — Python's 5s heartbeat provides 12x safety margin
  • Increased wake lock refresh interval (5min→2h) — 10h timeout allows 5 refreshes per cycle instead of 120
  • Slowed Python maintenance loop (1s→30s) and idle heartbeat (1s→5s) — both properly coordinated with Kotlin monitoring

Technical correctness:

  • All timing values properly synchronized between Python and Kotlin layers
  • Tests comprehensively updated to reflect new intervals
  • WiFi lock removal cleanly propagated through entire codebase (UI, ViewModel, Protocol, Binder, tests)
  • Safety margins maintained: 60s stale threshold vs 5s heartbeat = 12x margin, 10h wake lock vs 2h refresh = 5x margin

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • Changes are well-architected with proper safety margins, comprehensive test coverage, and clean removal of WiFi lock throughout the codebase. The timing coordination between Python (5s heartbeat) and Kotlin (60s stale threshold) provides 12x safety margin. Wake lock refresh (2h) provides 5x margin against 10h timeout. All tests updated correctly.
  • No files require special attention

Important Files Changed

Filename Overview
app/src/main/java/com/lxmf/messenger/service/manager/LockManager.kt Removed WiFi high-performance lock to allow 802.11 power-save mode, reducing WiFi power consumption ~10x; multicast and wake locks properly maintained
app/src/main/java/com/lxmf/messenger/service/manager/HealthCheckManager.kt Increased health check interval from 5s→30s and stale threshold from 10s→60s; properly coordinated with Python's 5s heartbeat updates
app/src/main/java/com/lxmf/messenger/service/manager/MaintenanceManager.kt Reduced lock refresh interval from 5min→2h; reasonable given 10h wake lock timeout provides 5x refresh margin
python/reticulum_wrapper.py Slowed idle heartbeat from 1s→5s and maintenance loop from 1s→30s; properly coordinated with Kotlin's 60s stale threshold (12x safety margin)
app/src/main/java/com/lxmf/messenger/viewmodel/DebugViewModel.kt Removed wifiLockHeld field from DebugInfo data class and parsing logic
app/src/main/java/com/lxmf/messenger/ui/screens/IdentityScreen.kt Removed WiFi Lock status row from UI; minor code formatting changes (chained modifiers)

Sequence Diagram

sequenceDiagram
    participant Python as Python Process<br/>(heartbeat & maintenance)
    participant Kotlin as Kotlin HealthCheck<br/>& Maintenance
    participant System as Android System<br/>(Wake Lock)
    
    Note over Python: Idle heartbeat: 5s<br/>(was 1s)
    Note over Kotlin: Health check: 30s<br/>(was 5s)
    Note over Kotlin: Stale threshold: 60s<br/>(was 10s)
    
    loop Every 5 seconds
        Python->>Python: Update heartbeat timestamp
        Note over Python: Active sync: 100ms<br/>Idle: 5s
    end
    
    loop Every 30 seconds
        Kotlin->>Python: getHeartbeat()
        Python-->>Kotlin: timestamp
        Kotlin->>Kotlin: Check if age > 60s
        alt Heartbeat fresh (age < 60s)
            Note over Kotlin: Reset stale counter
        else Heartbeat stale (age > 60s)
            Note over Kotlin: Increment stale counter
            alt 2 consecutive stale checks
                Kotlin->>System: Trigger service restart
            end
        end
    end
    
    loop Every 2 hours (was 5 min)
        Kotlin->>System: Refresh wake lock
        Note over System: Wake lock timeout: 10h<br/>5 refreshes per timeout
    end
    
    loop Every 30 seconds (was 1s)
        Python->>Python: Check failed_interfaces
        alt interfaces need retry
            Python->>Python: Retry failed interfaces
        end
    end
    
    Note over Python,System: Wake-ups reduced from ~7,932/hr to ~960/hr (88% reduction)
Loading

Last reviewed commit: 6b3e697

torlando-tech and others added 2 commits February 20, 2026 10:19
Update 4 Python docstrings and 1 log message that still referenced
the old 1s heartbeat interval after it was changed to 5s idle.
Remove dead WifiLock entries from detekt NoRelaxedMocksRule allowlist.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The idle heartbeat interval was changed from 1s to 5s, but the test's
join(timeout=3) was not updated. The thread may be sleeping when
initialized is set to False and needs up to 5s to wake and check the
loop condition.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@sentry
Copy link
Copy Markdown
Contributor

sentry bot commented Feb 20, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@torlando-tech torlando-tech merged commit 7748d06 into main Feb 21, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant