Dev.to WebDev 🛠 Dev 👁 0 📖 4 min read

Android ANR Deep Dive

--- title: "Android ANR Deep Dive: Tracing Main Thread Stalls with Perfetto" published: true description: "Master ANR diagnosis using Perfetto traces. Identify binder contention, input dispatch watchdog failures, and Cho

---
title: "Android ANR Deep Dive: Tracing Main Thread Stalls with Perfetto"
published: true
description: "Master ANR diagnosis using Perfetto traces. Identify binder contention, input dispatch watchdog failures, and Choreographer vsync overruns before they kill your Play Store rating."
tags: android, kotlin, mobile, architecture
canonical_url: https://mvpfactory.co/blog/android-anr-deep-dive-perfetto
---

## What you will learn

By the end of this workshop you will be able to open a Perfetto trace, identify the three ANR patterns responsible for the majority of Play Console reports, and apply targeted fixes for each one — in under 30 minutes per investigation.

The three patterns are: binder transaction spill from background services, input dispatch watchdog expiry from synchronous I/O on the main thread, and Choreographer vsync budget exhaustion.

## Prerequisites

- Android Studio with a physical device or emulator running API 29+
- Access to Play Console ANR reports for your app
- Perfetto installed (`https://ui.perfetto.dev` works in browser)
- Basic familiarity with Kotlin coroutines

---

## Why ANRs are worse than crashes

Most teams treat ANRs as edge cases. Crash rates are visible and dramatic. ANRs are quiet — the user just stares at a frozen screen, then leaves a one-star review.

The system issues an ANR when:

| Trigger | Timeout |
|---|---|
| Input dispatch unresponsive | 5 seconds |
| Broadcast receiver execution | 10 seconds (foreground) |
| Service start/bind | 20 seconds |
| Content provider | 10 seconds |

Five seconds is nothing. A single blocking binder call to a system service under memory pressure will eat that budget whole.

---

## Pattern 1: Binder transaction spill

Binder is Android's IPC mechanism. When background services saturate the binder thread pool — all 16 threads occupied — the next synchronous binder call from *any* thread, including the main thread, blocks.

In Perfetto, filter for `binder_transaction` slices on the main thread. If you see the main thread parked in `binder_suspend` while a WorkManager job hammers `PackageManager` or `AccountManager`, that is your culprit.

Let me show you a pattern I use in every project — offload every IPC call that is not latency-critical:


kotlin
// Before: blocking main thread
val packages = packageManager.getInstalledPackages(0)

// After: offload to IO dispatcher
val packages = withContext(Dispatchers.IO) {
packageManager.getInstalledPackages(0)
}


---

## Pattern 2: Input dispatch watchdog internals

The `InputDispatcher` runs in `system_server` and expects your main thread to dequeue touch and key events within 5 seconds. If the main thread is stuck on a lock, a database query, or a synchronous `SharedPreferences.commit()`, the watchdog fires.

In Perfetto, match the ANR timestamp against main thread slices in your app process. The blocking slice will be visible — often a `monitor` lock acquisition or a `Cursor.moveToFirst()` call.

Here is the gotcha that will save you hours: the most common offender in production Kotlin codebases is `runBlocking` on the main thread combined with a downstream suspension point that hits the network or disk.


kotlin
// This is an ANR waiting to happen
class MyActivity : AppCompatActivity() {
override fun onCreate(...) {
val result = runBlocking { repository.fetchConfig() } // NEVER
}
}


Replace `runBlocking` on the main thread unconditionally. Use `lifecycleScope.launch` and model loading state explicitly.

---

## Pattern 3: Choreographer vsync budget exhaustion

The Choreographer receives a vsync signal every ~16.6ms at 60 Hz (or ~8.3ms at 120 Hz). Every frame must complete `INPUT → ANIMATION → TRAVERSAL` within budget. Sustained overruns in `onDraw()`, `onMeasure()`, or any `doFrame` callback cause the system to conclude the app is unresponsive.

In Perfetto, the `Choreographer#doFrame` slice tells you exactly where the budget is going. Here is the minimal setup to instrument this in production without a trace:


kotlin
window.addOnFrameMetricsAvailableListener({ _, frameMetrics, _ ->
val totalDuration = frameMetrics.getMetric(FrameMetrics.TOTAL_DURATION)
if (totalDuration > 16_000_000L) {
// log or report to your observability stack
}
}, Handler(Looper.getMainLooper()))


---

## The diagnosis workflow

1. Pull the ANR trace from Play Console (`tombstone` or `anr` file)
2. Record a Perfetto system trace reproducing the condition locally
3. Cross-reference the main thread stack in the tombstone with Choreographer and binder slices in Perfetto
4. Confirm with `adb shell am bug-report` if device access is available

---

## Gotchas

- **The tombstone tells you where, Perfetto tells you why.** Do not fix code based on the tombstone stack alone — you will fix the symptom, not the cause.
- **`runBlocking` in production is almost always wrong** on the main thread. Lint rules will catch what code review misses — write one that flags it explicitly.
- **Choreographer overruns are silent until they are not.** By the time they appear in your ANR rate, they have already damaged your store ranking. Surface `FrameMetrics` through your observability pipeline now.
- **Tags: max 4** — the `cleanarchitecture` tag in the original post is not a recognised dev.to tag; `architecture` covers it.

---

## Conclusion

Pull a Perfetto trace for every ANR category in Play Console before you touch a line of code. Ban synchronous operations on the main thread at the architecture level and enforce it with a custom lint rule. Instrument `FrameMetrics` in production. These three habits eliminate the majority of ANR reports I see in Play Console across production apps.

The docs do not mention this, but the binder thread pool exhaustion pattern is frequently misattributed to the UI work itself — always check background service IPC volume first.
📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.