Investigate Wear OS battery drain — continuous 30s polling and never-stopping sync service #16
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Symptom
A field test of the watch app surfaced noticeably high battery drain. No power profiling (Battery Historian / on-device battery stats) has been captured yet, so the magnitude and the dominant cause are not measured. This issue records the candidate power sinks found by a static code scan so a measurement pass has concrete starting points. App sources live under
clients/wearos/app/src/main/java/com/doctate/watch.Candidate sinks (code verified; contribution to drain not yet measured)
Sync foreground service never stops, polls forever.
SyncServiceis adataSyncforeground service (Doze-exempt), started at app launch and returningSTART_STICKY—clients/wearos/app/src/main/java/com/doctate/watch/sync/SyncService.kt:77. A grep acrosssync/finds nostopSelf/stopForeground/stopServiceanywhere, so it runs for the whole process lifetime. The worker loop is infinite (while (currentCoroutineContext().isActive),sync/SyncWorkerLoop.kt:115) and at idle waits on a 30 s timeout (DEFAULT_POLL_INTERVAL_MS = 30_000,sync/SyncWorkerLoop.kt:245;waitForKickOrTimeoutat:124). Net effect: a server poll roughly every 30 s, indefinitely, even with an empty upload queue and no recent recording. On a watch this round-the-clock radio wake is the strongest candidate.Post-upload burst polling. After each successful upload the loop self-kicks a 2 s poll for a 60 s window —
sync/SyncWorkerLoop.kt:172(BurstPoll(... nowMs() + BURST_POLL_BUDGET_MS)), constants at:246(BURST_POLL_INTERVAL_MS = 2_000) and:247(BURST_POLL_BUDGET_MS = 60_000). That is up to ~30 radio wakeups per case, an intentional trade for sub-five-second oneliner feedback (comment atsync/SyncWorkerLoop.kt:70).No push path — drain is structural, not incidental. Oneliner readiness is discovered purely by polling; there is no server-push (e.g. FCM) to replace the steady poll. So the 30 s cadence is the design, not a stuck retry.
Transient-error backoff keeps the radio busy. On transient upload errors the loop backs off 2 s→60 s doubling (
sync/SyncWorkerLoop.kt:175, constants:243/:244), with OkHttpretryOnConnectionFailure(true)(net/HttpClientProvider.kt:15). Under poor connectivity this keeps retrying rather than yielding.Ruled out by the same scan (verified clean)
PowerManager/WakeLockusage anywhere.finally(audio/AudioRecorder.kt:48,:59); 16 kHz mono AAC, no on-device transcoding.keepScreenOnis scoped to the recording screen only and cleared inonDispose(presentation/RecordingScreen.kt:75).Open — needs measurement before any fix
dumpsys batterystats) to confirm which candidate dominates, rather than optimizing blind.Possible directions (after measurement)
SyncService(or drop out of the foreground/loop) when the pending queue is empty and no burst is active, restarting on a new recording or kick.DEFAULT_POLL_INTERVAL_MS; soften the burst (longer interval or shorter budget).Note: battery behaviour is not unit-testable in the usual sense; acceptance will rest on a before/after power measurement plus a test pinning any new service-stop/poll-interval logic.