Skip to content

ANR: ACRA silent-exception reporting blocks the main thread (FOSS wake path) #542

Description

@Monkopedia

Symptom

Recurring background ANR on the FOSS build (observed on 1.0.4 AND 1.0.7). exit-info: reason=6 (ANR), subject Broadcast of Intent ... UnifiedPushReceiver (MESSAGE), ~60s timeout, not user-perceptible. The wake broadcast is the victim, not the cause — the main thread is already stuck.

Root cause (deobfuscated 1.0.7 trace)

FutureTask.get()                                              ← blocks the MAIN thread
org.acra.data.CrashReportDataFactory.collect(:81)            ← ACRA gathers report data (blocking)
org.acra.builder.ReportExecutor.execute
org.acra.reporter.ErrorReporterImpl.handleSilentException
com.rousecontext.app.support.AcraCrashReporter.logCaughtException(AcraCrashReporter.kt:26)
com.rousecontext.work.TunnelForegroundService$collectIncomingSessions...(:257)
kotlinx.coroutines.DispatchedTask.run                        ← Dispatchers.Main

TunnelForegroundService.collectIncomingSessions does lifecycleScope.launch { try { sessionHandler.handleStream(stream) } catch (e) { crashReporter.logCaughtException(e) } }. lifecycleScope = Dispatchers.Main.immediate, so the catch runs on Main. AcraCrashReporter.logCaughtExceptionACRA.errorReporter.handleSilentException, and ACRA's CrashReportDataFactory.collect fans collectors into a thread pool and blocks the calling thread on FutureTask.get until collection finishes/times out. On Main that's a ~60s freeze → ANR. Any incoming-session error (which can repeat) re-triggers it — the exit-info showed two stacked 60s timeouts.

(The google/Firebase reporter is immune — recordException is async.)

Fix

Make AcraCrashReporter.logCaughtException (and any other blocking ACRA call) never run on the caller's thread: dispatch handleSilentException onto a background dispatcher, fire-and-forget (inject the app scope; appScope.launch(Dispatchers.IO) { ACRA.errorReporter.handleSilentException(throwable) }). Silent (non-fatal) reports don't need to block, so async collection is correct. This protects ALL call sites, not just collectIncomingSessions.

Secondary hardening: collectIncomingSessions runs handleStream on lifecycleScope (Main) too — consider a background dispatcher there (session/tool work shouldn't sit on Main). Not required once the reporter is off-main, but cleaner.

Test

  • logCaughtException returns immediately and does not block the calling thread even if ACRA collection is slow (fake/inject a slow handleSilentException; assert the call site returns before it completes / runs off the caller thread).

Notes

Deobfuscated via a reproducible rebuild of the v1.0.7 tag (same R8 map-id 13eced08…). This is the ANR Jason has hit repeatedly on-device; supersedes the earlier vague #506-era suspicion — the goAsync fix was fine; the real blocker is ACRA-on-Main.

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent-workableClear, scoped, no user-judgment needed; triage dispatches work_on_issuebugSomething isn't workingcrashApp crash / fatal exception

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions