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):
- Start streaming ASR with
speakerId enabled.
- Talk continuously for 60 seconds (~30–60 VAD segments).
- Each segment triggers
processSamples + computeEmbedding on the SpeakerID handler.
- 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:
- Remove the
methodQueue override (RN's default queue is fine).
- 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.
- 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.
- 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.
Summary
On iOS,
packages/sherpa-onnx.rn/ios/bridge/SherpaOnnxRnModule.mmoverridesmethodQueueto returndispatch_get_main_queue(), forcing everyRCT_EXPORT_METHODonto 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
AppHangFullyBlockedevents / 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):speakerIdenabled.processSamples+computeEmbeddingon the SpeakerID handler.AppHangFullyBlockedwith main-thread stacks ending inMlasGemm…/Ort::Runinside[SherpaOnnxSpeakerIdHandler computeEmbedding].In our production app this produced 11 distinct
AppHangFullyBlockedSentry issues over a few days of beta usage, all main-thread, all rooted inSherpaOnnxRnModulecalls. 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
…combined with
RCT_EXPORT_METHODbodies 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:
methodQueueoverride (RN's default queue is fine).com.sherpaonnx.{asr,tts,audiotagging,speakerid}, using the samestatic dispatch_queue_t+dispatch_once_tpattern as the existing 7 handlers.dispatch_async(<queue>(), ^{ @try { ... } @catch { ... } }), exactly matching the existing LangID/KWS/VAD style.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 patchagainst1.3.0. Happy to iterate on naming, queue granularity, or test approach.Notes