Skip to content

Address various ANR and crashes in 1.0.12#951

Open
davecraig wants to merge 6 commits into
Scottish-Tech-Army:v1.0from
davecraig:v1.0
Open

Address various ANR and crashes in 1.0.12#951
davecraig wants to merge 6 commits into
Scottish-Tech-Army:v1.0from
davecraig:v1.0

Conversation

@davecraig

Copy link
Copy Markdown
Contributor

No description provided.

davecraig added 6 commits July 1, 2026 10:21
There was a race here between LazeColumn item instantation and LaunchedEffect.
Moving the title text out of the LazyColumn avoids this. Crashlytics saw a
crash at this point.
We still see timeouts when starting the foreground service. This change
makes sure that startAsForegroundService is called immediately in onCreate
rather than waiting until onStartCommand. This should improve behaviour on
slower phones.
There was the possibility of an ANR due to some locking. Each WAV decode was
doing resampling so it's a bit more work than just opening a file. Claude took
a look and said:

  The individual decode is not the 5-second cost. I checked the actual earcon files: they range from ~2.6KB up to ~1MB (the largest, 2.4_roadFinder_loop_rev2_wFades.wav, is ~1MB). .wav is in Android's default aapt no-compress
  list, so these are stored uncompressed in the APK — AAssetManager_open doesn't need to inflate anything, it just locates the entry and returns a pointer. Parsing + float conversion + resampling even the biggest file is
  low-single-digit milliseconds. So no single WavDecoder call should ever take seconds.

  What actually stretches this to 5+ seconds is cumulative lock starvation, not one long hold:

  1. engineMutex in NativeAudioEngine.kt is a plain Kotlin synchronized(Object) monitor — Java/ART intrinsic locks give no fairness guarantee. A thread that keeps re-acquiring the same lock in a tight loop can keep winning the
  race against a thread that's been waiting the whole time.
  2. speakCallout() doesn't take the lock once for its whole body — it calls createEarcon()/createTextToSpeech() per item in callout.positionedStrings, and each one re-enters its own separate synchronized(engineMutex) block.
  For a callout describing several POIs, that's many short lock acquisitions back-to-back on thread 35.
  3. GeoEngine.startMonitoringLocation's flow re-invokes speakCallout on every location emission. If updates arrive faster than callouts finish (or several nearby POIs trigger a burst of callouts), thread 35 can be in a
  near-continuous stream of createEarcon/createTextToSpeech calls.
  4. Each of those also funnels through AAssetManager_open's own internal native mutex — also unfair — so the same "whoever's already spinning wins" dynamic happens at that layer too.

  So the main thread isn't blocked on one slow disk read — it's losing dozens-to-hundreds of small, fast races in a row while the worker thread churns through a burst of earcon/TTS creations, and repeated small losses compound
  past the 5-second ANR threshold. That also matches the trace: thread 35 was caught mid-AAssetManager_open for one specific earcon, but the real damage was already done by however many iterations came before it in that burst.

  Where this leaves the fix: caching decoded WAVs collapses every repeat play (which is most plays — EARCON_MODE_ENTER/EARCON_MODE_EXIT and the common callout earcons repeat constantly) down to a cache lookup instead of an
  asset open + parse + resample. That shrinks each synchronized block enough that the main thread should win the lock race quickly even under a burst, rather than losing it repeatedly. It doesn't add fairness to the locks
  themselves — a sufficiently large burst of first-time (cold-cache) earcon loads could in theory still starve the main thread — but in practice the earcon set is small and fixed, so after the first few seconds of any session
  everything is warm.
startBeacon was unintentionally happening all on the main thread. Thie change fixes
that along with some other beacon control functions which can run asynchronously.
Maplibre can read assets directly and so doesn't need the file copying done
by processMaps. processMaps could be time consuming and so this improves
start up.
On each new map that is displayed maplibre is opening the font assets and
decompressing them. By storing the resources uncompressed it makes this much
more efficient.
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