Skip to content

fix(android): avoid holding initLock while calling into TextToSpeech to prevent ABBA deadlock#12

Open
RemiHin wants to merge 1 commit into
mhpdev-com:mainfrom
RemiHin:main
Open

fix(android): avoid holding initLock while calling into TextToSpeech to prevent ABBA deadlock#12
RemiHin wants to merge 1 commit into
mhpdev-com:mainfrom
RemiHin:main

Conversation

@RemiHin

@RemiHin RemiHin commented Jun 8, 2026

Copy link
Copy Markdown

Fixes #11

ninjz added a commit to ninjz/react-native-speech that referenced this pull request Jun 30, 2026
… OOM)

Two crashes on low-end Android share one root cause: the onInit SUCCESS
branch enumerates voices/engines synchronously on the main thread.

Android delivers the TextToSpeech OnInitListener on the main thread
(SetupConnectionAsyncTask.onPostExecute). The SUCCESS branch then ran
synthesizer.engines, applyGlobalOptions() (-> synthesizer.voices) and
processPendingOperations() (which drains queued getAvailableVoices /
getEngines / configure) on that thread -- all Binder IPC round-trips to
the engine process.

ANR: on a cold engine under memory pressure these take seconds;
back-to-back on the UI thread they exceed the 5s ANR threshold (observed
on a 4GB Android 15 device). This is distinct from the mhpdev-com#11 deadlock --
the main thread is executing synthesizer.voices, not waiting on initLock.

OOM crash: setLanguage()/voices hit TextToSpeech.getVoices(), and some
cheap engines (seen on MediaTek/UMIDIGI Android 12) return a pathological
serialized Voice[] that OutOfMemoryErrors while deserializing -- a runaway
allocation, not memory pressure (seen with ~2.9GB free). The existing
catch (e: Exception) does not catch it (OOM is an Error, not an Exception),
so it escaped uncaught and crashed the app.

Fixes:
- Run the engine/voice enumeration, global config and queue drain off the
  main thread. Apply config before flipping isInitialized so callers can't
  speak before config is applied. initLock guards only the flag flip;
  applyGlobalOptions() and the drain run lock-free, since holding initLock
  across a TextToSpeech call is an ABBA deadlock (see mhpdev-com#11). cachedEngines
  becomes @volatile.
- Widen catch (e: Exception) -> catch (e: Throwable) in applyGlobalOptions()
  and applyOptions() so a getVoices() OOM degrades instead of crashing, and
  backstop the background thread with try/catch (Throwable).
- Resolve synthesizer.defaultEngine once in getEngines() instead of once
  per engine inside the loop.

Complements mhpdev-com#11 / mhpdev-com#12: keeps initLock off the TextToSpeech call path, so it
does not reintroduce that deadlock. Rebase onto mhpdev-com#12 if it lands first.
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.

Android ANR/deadlock: ensureInitialized holds initLock while calling TextToSpeech APIs

1 participant