Skip to content

iOS: ASR/TTS/AudioTagging/SpeakerID block the main thread, causing AppHangFullyBlocked #405

Description

@brainlybai

Summary

On iOS, packages/sherpa-onnx.rn/ios/bridge/SherpaOnnxRnModule.mm overrides methodQueue to return dispatch_get_main_queue(), forcing every RCT_EXPORT_METHOD onto the main thread.

For ASR, TTS, Audio Tagging, and Speaker ID, the method bodies call into handlers that synchronously run ONNX inference. The result is AppHangFullyBlocked events / iOS App Hangs whenever a streaming pipeline calls these methods. The same file already migrated KWS, VAD, Language ID, Punctuation, Speaker Diarization, Denoising, and Inference to per-handler serial queues — ASR/TTS/AudioTagging/SpeakerID were never migrated.

Reproduction

In a LiveAsrSession-style pipeline (VAD-gated streaming ASR + per-segment speaker embedding):

  1. Start streaming ASR with speakerId enabled.
  2. Talk continuously for 60 seconds (~30–60 VAD segments).
  3. Each segment triggers processSamples + computeEmbedding on the SpeakerID handler.
  4. UI freezes are visible; Sentry / Xcode Organizer reports AppHangFullyBlocked with main-thread stacks ending in MlasGemm… / Ort::Run inside [SherpaOnnxSpeakerIdHandler computeEmbedding].

In our production app this produced 11 distinct AppHangFullyBlocked Sentry issues over a few days of beta usage, all main-thread, all rooted in SherpaOnnxRnModule calls. We have not seen any reports against the already-migrated subsystems (KWS, VAD, LangID, etc.) — only the four that still run on main.

Root Cause

- (dispatch_queue_t)methodQueue {
    return dispatch_get_main_queue();
}

…combined with RCT_EXPORT_METHOD bodies that synchronously invoke ONNX inference (50–500 ms per call depending on model and audio length).

Proposed Fix

Finish the per-handler-serial-queue migration that's already partially done in this file:

  1. Remove the methodQueue override (RN's default queue is fine).
  2. Add 4 new serial queues: com.sherpaonnx.{asr,tts,audiotagging,speakerid}, using the same static dispatch_queue_t + dispatch_once_t pattern as the existing 7 handlers.
  3. Wrap the 30 affected method bodies (10 ASR + 4 TTS + 4 Audio Tagging + 12 Speaker ID) in dispatch_async(<queue>(), ^{ @try { ... } @catch { ... } }), exactly matching the existing LangID/KWS/VAD style.
  4. Leave 5 trivial diagnostic/setup methods (validateLibraryLoaded, testOnnxIntegration, getArchitectureInfo, getSystemInfo, extractTarBz2) on main — they don't run inference.

Per-handler queues (vs one shared queue) match existing convention, preserve call ordering within a subsystem (important for acceptSamples → decode), and avoid head-of-line blocking between subsystems.

PR with the full change: #404

We're running this in production via pnpm patch against 1.3.0. Happy to iterate on naming, queue granularity, or test approach.

Notes

  • API-compatible: no JS surface change, no signature change, no new dependencies.
  • Android side is unaffected — the JNI bridge already dispatches off main.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions