Repository navigation
core: stop the AACP reader spinning when the peer tears the channel down - #875
jevonlipsey wants to merge 1 commit into
Conversation
The reader coroutine started by AACPManager.connect() loops on
`while (socket.isConnected)`. Two of its three exit paths did not exit:
- `bytesRead == -1` (EOF) logged and fell back into the loop.
- a read exception logged, printed a stack trace, and fell back into
the loop.
`BluetoothSocket.isConnected` only goes false once *we* close the socket,
and a remote teardown never calls disconnect(). So when the peer drops the
channel - which on AirPods happens every time another device takes them -
read() returns -1 immediately and forever, and the loop becomes an
unbounded busy loop emitting one log line per iteration.
Measured on a Retroid Pocket Nova (SM8550, Android 13) with v2.0.0-alpha.4:
~112% of one core sustained across a 1h38m foreground session, 486M log
entries (~9.7k lines/s, ~77 bytes each), logd pinned at 146%, and the SoC
held at 93.5 C on an idle device until the app was force-stopped.
The reader Job was also never retained, so each reconnect started another
reader and the already-spinning ones were never reaped. CPU therefore grew
with the number of handoffs; samples during one session read 226% and ~370%.
Fix:
- break out of the loop on EOF and on a failed read. Both are terminal
for a Bluetooth socket.
- keep retaining the per-packet catch, so one malformed packet still does
not take the connection down.
- hold the reader and init Jobs and cancel them in connect() and
disconnect(), so readers cannot stack up.
Deliberately left out: notifying the device of a peer-initiated teardown
from the reader. That is a behaviour change, and recovery already works -
the next ACL_CONNECTED calls connect(), which now reaps the dead reader.
|
Device-verified now, on the hardware the measurements in the description came from (Retroid Pocket Nova, SM8550, Android 13, Provoked real handoffs by connecting the AirPods to the handheld and then taking them back with another Apple device. The reader now terminates on the peer teardown instead of spinning: The EOF line appears once and the loop exits in the same millisecond. Before the change that same line was the flood — ~9,760/s until the process was killed. Over a 21-minute window containing two connects and one peer teardown:
Also confirmed no reader stacking: two Build note, in case it helps anyone reproducing: the fix is entirely in |
Problem
AACPManager.connect()starts a reader coroutine that loops onwhile (socket.isConnected). Two of its three exit paths did not exit:BluetoothSocket.isConnectedonly becomes false once we close the socket.AACPManager.disconnect()does, but a peer-initiated teardown never calls it. So when another device takes the AirPods,read()returns-1immediately and forever, the-1branch logs and falls straight back into the loop, and the loop becomes an unbounded busy loop emitting one log line per iteration. It only ends when the process dies.The reader
Jobwas also never retained, so every reconnect started another reader and the already-spinning ones were never reaped.Measured impact
Retroid Pocket Nova (SM8550, Android 13), v2.0.0-alpha.4, device idle on a desk with AirPods shared between a Mac, an iPhone and the handheld:
logdlogd146% → 12%dumpsys batterystatsshows the trigger: repeatedACL_CONNECTED/ACL_DISCONNECTEDbroadcasts throughout the session, including a disconnect 1.4 s after the preceding connect. Each handoff away from the device tears the AACP channel down and starts another spinning reader, which is why CPU climbed with the number of handoffs.This is not device- or vendor-specific — it will spin on any Android device whose AirPods are disconnected remotely. It is just much easier to hit when several Apple devices are competing for the same buds.
Fix
breakonbytesRead == -1and on a failed read. Both are terminal for a Bluetooth socket.catchnon-fatal, so one malformed packet still does not drop the connection.Jobs and cancel them inconnect()anddisconnect(), so readers cannot stack.Deliberately left out: notifying the device of a peer-initiated teardown from inside the reader. That is a behaviour change rather than a bug fix, and recovery already works — the next
ACL_CONNECTEDcallsconnect(), which now cancels the dead reader first.Testing
Built as
fossReleaseagainst compileSdk 37 (AGP 9.4.1 / Gradle 9.8.1) and installed on the affected device (Retroid Pocket Nova, SM8550, Android 13, arm64).Verified against real handoffs: the EOF line now appears once and the loop exits in the same millisecond, where before it was the flood. Process CPU over a 21-minute window containing two connects and one peer teardown was 12 cpu-seconds / 1250 s alive = 1.00% of one core, against ~112% sustained before. No reader stacking, and nothing burning in per-thread sampling. Full timeline in the comment below.