이 페이지에서는 예시 시스템 트레이스를 사용하여 ProfilingManager로 애플리케이션 응답 없음(ANR) 오류를 디버그하는 방법을 설명합니다.
ANR을 수집하도록 앱 설정
먼저 앱에서 ANR 트리거를 설정합니다.
Kotlin
fun addANRTrigger() { val profilingManager = applicationContext.getSystemService(ProfilingManager::class.java) ?: return val triggers = ArrayList<ProfilingTrigger>() val triggerBuilder = ProfilingTrigger.Builder(ProfilingTrigger.TRIGGER_TYPE_ANR) triggers.add(triggerBuilder.build()) val callbackExecutor: Executor = Executors.newSingleThreadExecutor() val resultCallback = Consumer<ProfilingResult> { profilingResult -> // Handle uploading trace to your back-end } profilingManager.registerForAllProfilingResults(callbackExecutor, resultCallback) profilingManager.addProfilingTriggers(triggers) }
자바
public void addANRTrigger() { ProfilingManager profilingManager = getApplicationContext().getSystemService( ProfilingManager.class); List<ProfilingTrigger> triggers = new ArrayList<>(); ProfilingTrigger.Builder triggerBuilder = new ProfilingTrigger.Builder( ProfilingTrigger.TRIGGER_TYPE_ANR); triggers.add(triggerBuilder.build()); Executor callbackExecutor = Executors.newSingleThreadExecutor(); Consumer<ProfilingResult> resultCallback = profilingResult -> { // Handle uploading trace to your back-end }; profilingManager.registerForAllProfilingResults(callbackExecutor, resultCallback); profilingManager.addProfilingTriggers(triggers); }
ANR 트레이스를 캡처하고 업로드한 후 다음 단계에 따라 Perfetto UI에서 수동으로 검사하거나 AI 에이전트를 사용하여 ANR 조사를 자동화할 수 있습니다.
trace 분석
ANR이 트레이스를 트리거했으므로 시스템이 앱의 기본 스레드에서 응답 없음을 감지했을 때(또는 다른 제한 시간이 발생했을 때) 트레이스가 종료되었음을 알 수 있습니다. 그림 1은 UI 내에서 적절하게 태그가 지정된 앱의 기본 스레드로 이동하는 방법을 보여줍니다.

그림 2와 같이 트레이스의 끝은 ANR의 타임스탬프와 일치합니다.

ANR이 발생했을 때 기본 스레드는 MyApp:SubmitButton 내에서 handleNetworkResponse 슬라이스를 실행하고 있었으며, 이로 인해 1.48초의 CPU 시간이 소모되었습니다(그림 3).

ANR이 발생한 순간에 생성된 스택 덤프에만 의존하면 정지 현상이 전적으로 handleNetworkResponse에 잘못 기여할 수 있습니다. 하지만 1.48초는 ANR을 트리거하기에 충분히 길지 않으므로 이전에 기본 스레드를 차단한 항목을 검사해야 합니다.
먼저 정지 전에 렌더링된 마지막 UI 프레임(Choreographer#doFrame 551275)을 찾습니다. 이 프레임은 MyApp:SubmitButton 슬라이스 바로 앞에 있습니다(그림 4).

차단을 이해하려면 축소하여 전체 MyApp:SubmitButton 슬라이스를 검사합니다. 그림 5에서 수동으로 선택한 범위의 스레드 상태에서 중요한 세부정보를 확인할 수 있습니다. 스레드가 시간의 약 75%(6.7초)를 Sleeping 상태에서 보냈고 시간의 24% 만 Running 상태에서 보냈습니다.
MyApp:SubmitButton 작업 중 스레드 상태이는 ANR의 주요 원인이 연산이 아닌 대기임을 나타냅니다. 개별 수면 발생을 검토하여 패턴을 찾습니다.
MyApp:SubmitButton 이내의 첫 번째 수면 시간
MyApp:SubmitButton 이내의 두 번째 수면 시간
MyApp:SubmitButton 이내의 세 번째 수면 시간
MyApp:SubmitButton 내 네 번째 수면 시간처음 세 개의 절전 간격(그림 6~8)은 각각 약 2초이고 네 번째 간격(그림 9)은 0.7초입니다. 2초 일시중지가 반복되면 네 번째 시도에서 성공하기 전에 세 번 재시도하는 프로그래밍된 제한 시간이 있음을 강력하게 시사합니다. 이로 인해 ANR을 트리거하기에 충분한 지연이 누적됩니다(그림 10).

MyApp:SubmitButton 슬라이스 중 지연 요약코드에서 근본 원인 식별
이 가설을 확인하려면 MyApp:SubmitButton trace 섹션과 연결된 코드를 검사합니다.
Kotlin
fun setupButtonCallback() { findViewById<View>(R.id.submit).setOnClickListener { trace("MyApp:SubmitButton") { onClickSubmit() } } } fun onClickSubmit() { prepareNetworkRequest() var networkRequestSuccess = false var maxAttempts = 10 while (!networkRequestSuccess && maxAttempts > 0) { networkRequestSuccess = performNetworkRequest(NETWORK_TIMEOUT_MILLISECS) maxAttempts-- } if (networkRequestSuccess) { handleNetworkResponse() } } fun performNetworkRequest(timeoutMilliseconds: Int): Boolean { // ... } fun prepareNetworkRequest() { // ... } fun handleNetworkResponse() { trace("handleNetworkResponse") { // ... } } companion object { private const val NETWORK_TIMEOUT_MILLISECS = 2000 }
자바
private static final int NETWORK_TIMEOUT_MILLISECS = 2000; public void setupButtonCallback() { findViewById(R.id.submit).setOnClickListener(submitButtonView -> { Trace.beginSection("MyApp:SubmitButton"); onClickSubmit(); Trace.endSection(); }); } public void onClickSubmit() { prepareNetworkRequest(); boolean networkRequestSuccess = false; int maxAttempts = 10; while (!networkRequestSuccess && maxAttempts > 0) { networkRequestSuccess = performNetworkRequest(NETWORK_TIMEOUT_MILLISECS); maxAttempts--; } if (networkRequestSuccess) { handleNetworkResponse(); } } boolean performNetworkRequest(int timeoutMiliseconds) { // ... } void prepareNetworkRequest() { // ... } public void handleNetworkResponse() { Trace.beginSection("handleNetworkResponse"); // ... Trace.endSection(); }
코드는 이 가설을 확인합니다. onClickSubmit 메서드는 하드코딩된 NETWORK_TIMEOUT_MILLISECS 2,000ms를 사용하여 UI 스레드에서 네트워크 요청을 실행합니다.
중요한 점은 최대 10회까지 재시도하는 while 루프 내에서 실행된다는 것입니다.
이 특정 trace에서는 사용자의 네트워크 연결 상태가 좋지 않았을 가능성이 높습니다. 처음 세 번의 시도가 실패하여 2초 제한 시간이 세 번 발생했습니다(총 6초).
네 번째 시도는 0.7초 후에 성공하여 코드가 handleNetworkResponse로 진행될 수 있습니다. 하지만 누적된 대기 시간으로 인해 이미 ANR이 트리거되었습니다.
ANR 수정
지연 시간이 다양한 네트워크 관련 작업을 기본 스레드에서 실행하는 대신 백그라운드 스레드로 이동하여 이러한 유형의 ANR을 방지하세요. 이렇게 하면 연결 상태가 좋지 않은 경우에도 UI가 반응성을 유지하므로 이러한 유형의 ANR이 제거됩니다.
AI 에이전트로 ANR 조사 자동화
Perfetto UI에서 타임라인과 절전 모드 간격을 수동으로 검사하는 대신 Perfetto AI 기술로 구성된 AI 코딩 에이전트를 사용하여 ANR 트레이스를 쿼리하고 이를 코드베이스와 연결할 수 있습니다.
Analyze the Perfetto system trace at </path/to/trace.perfetto-trace>, which
corresponds to an ANR trace, and find the root cause of the ANR using the
Perfetto AI skill. The codebase is located at </path/to/codebase>.
에이전트는 trace_processor로 트레이스를 쿼리하고 수동 조사와 동일한 결론에 도달합니다. 트레이스 끝에 있는 완료되지 않은 handleNetworkResponse 슬라이스로 인해 잘못된 결론에 도달하지 않습니다. MyApp:SubmitButton가 9.2초 넘게 기본 스레드를 차단했음을 식별하고, performNetworkRequest에서 2초 네트워크 제한 시간 3회와 0.7초의 네 번째 시도를 재구성하고, prepareNetworkRequest 및 handleNetworkResponse에서 CPU 사용 중 대기 루프를 재구성하고, 각 단계를 ProfilingManagerJavaSnippets.java에 매핑하고, onClickSubmit를 UI 스레드에서 이동하라는 권장사항을 제공합니다.
# Perfetto ANR Root-Cause Analysis Report
* **Source Code:** `ProfilingManagerJavaSnippets.java`
* **Target Process:** `com.example.sampleapp` (`pid = 32579`, `upid = 27`)
* **Blocked Thread:** Main UI thread `ample.sampleapp` (`tid = 32579`, `utid = 760`)
* **Collection Mechanism:** Android `ProfilingManager` (`ProfilingTrigger.TRIGGER_TYPE_ANR`)
---
## 1. Executive Summary
> **Important:** The ANR in `com.example.sampleapp` is caused by executing **synchronous, blocking network retry logic (`Thread.sleep`) and heavy CPU busy-wait loops (`cpuIntensiveComputation`) directly on the main UI thread** inside the submit button click listener (`MyApp:SubmitButton` -> `onClickSubmit()`).
When the user taps the submit button at `7,967.78 ms` into the trace (`ts = 80459171444519`), the main UI thread is blocked continuously for **more than `9,281.30 ms`** (still unfinished when the trace ends at `17,249.08 ms`):
1. **`prepareNetworkRequest()`** runs a CPU busy-wait loop on the main thread for **`~1,019 ms`**.
2. **`performNetworkRequest(2000)`** executes in a synchronous `while` retry loop on the main thread. Each attempt begins with **`20 ms`** of CPU busy-waiting followed by a `Thread.sleep()`. The request times out (`2,000 ms` sleep) **3 times in a row** and succeeds on the **4th attempt** (`~701 ms` sleep), blocking the main thread for **`~6,764 ms`** across the 4 attempts (`6,703.18 ms` sleeping + `~60 ms` CPU).
3. **`handleNetworkResponse()`** begins at **`+7,783.44 ms`** after the button click and runs another `2,000 ms` CPU busy-wait loop on the main thread (`1,497.86 ms` captured before the trace ends).
[Touch UP & Click (t = +0 ms)]
-> prepareNetworkRequest() (~1,000 ms CPU Busy-Wait)
-> Attempt #1 (Timeout: 20 ms CPU + 2,001 ms Sleep)
-> Attempt #2 (Timeout: 20 ms CPU + 2,001 ms Sleep)
-> Attempt #3 (Timeout: 20 ms CPU + 2,001 ms Sleep, crosses 5s ANR threshold)
-> Attempt #4 (Success: 20 ms CPU + 701 ms Sleep)
-> handleNetworkResponse() (1,498 ms CPU, unfinished at trace end)
## 2. PerfettoSQL Trace Analysis
### 2.1 Open Main-Thread Slices at Trace End
Querying `thread_or_process_slice` on the main thread (`utid = 760`) around and after the touch event shows that `MyApp:SubmitButton` and its nested slice `handleNetworkResponse` both have `dur = -1` (still open when the trace completed):
```sql
INCLUDE PERFETTO MODULE slices.with_context;
SELECT
id,
ts,
(ts - trace_start()) / 1e6 AS rel_ts_ms,
dur,
IIF(dur = -1, trace_end() - ts, dur) / 1e6 AS effective_dur_ms,
depth,
name
FROM thread_or_process_slice
WHERE utid = 760 AND ts >= 80459169800000
ORDER BY ts;
```
| Slice ID | Slice Name | Start (`ts` ns) | Offset from Trace Start | Raw `dur` | Effective Duration (to trace end) | Depth |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| `40070` | `dispatchInputEvent MotionEvent UP` | `80459169801575` | `7,966.14 ms` | `190,633 ns` | `0.19 ms` | `0` |
| `40156` | **`MyApp:SubmitButton`** | `80459171444519` | `7,967.78 ms` | `-1` | **`9,281.30 ms`** | `0` |
| `97500` | **`handleNetworkResponse`** | `80466954886580` | `15,751.23 ms` | `-1` | **`1,497.86 ms`** | `1` |
---
### 2.2 Aggregate Main-Thread State (`thread_state`) During `MyApp:SubmitButton`
Aggregating `thread_state` for `utid = 760` from the start of `MyApp:SubmitButton` (`ts >= 80459171444519`) to the end of the trace reveals that the main thread spent **72.3%** of the time sleeping (`S`) and **27.6%** actively spinning on CPU (`Running`):
```sql
SELECT
state,
COUNT(*) AS cnt,
SUM(dur) / 1e6 AS total_dur_ms,
MIN(dur) / 1e6 AS min_dur_ms,
MAX(dur) / 1e6 AS max_dur_ms
FROM thread_state
WHERE utid = 760 AND ts >= 80459171444519
GROUP BY state
ORDER BY total_dur_ms DESC;
```
| Thread State | Interval Count | Total Duration | Min Duration | Max Duration | Cause in Code |
| :--- | :--- | :--- | :--- | :--- | :--- |
| **`S` (Sleeping)** | `4` | **`6,703.18 ms`** | `700.78 ms` | `2,000.82 ms` | 4 `Thread.sleep()` calls in `performNetworkRequest()` |
| **`Running` (On CPU)** | `20` | **`2,559.81 ms`** | `0.00 ms` | `578.77 ms` | Busy-wait `while` loops in `cpuIntensiveComputation()` |
| **`R` (Runnable)** | `20` | **`2.11 ms`** | `0.01 ms` | `1.21 ms` | Scheduler wakeups and brief preemptions |
---
### 2.3 Phase-by-Phase Reconstruction (`thread_state` Intervals)
Querying individual `thread_state` intervals (`dur > 2 ms`) shows the exact sequence of CPU bursts and sleeps on the main thread:
```sql
SELECT
ts,
(ts - 80459171444519) / 1e6 AS ms_since_submit,
dur / 1e6 AS dur_ms,
state
FROM thread_state
WHERE utid = 760 AND ts >= 80459171444519 AND dur > 2e6
ORDER BY ts;
```
| Phase | Offset from Submit (`ms_since_submit`) | State | Duration (`dur_ms`) | Corresponding Code Execution |
| :--- | :--- | :--- | :--- | :--- |
| **1. Request Prep + Attempt #1 CPU** | `+0.63 ms` -> `+1,019.36 ms` | `Running` | **`1,018.68 ms`** *(across 5 slices)* | `prepareNetworkRequest()` (`1,000 ms` CPU) + Attempt #1 `cpuIntensiveComputation(20)` |
| **2. Network Attempt #1 (Timeout)** | `+1,019.36 ms` -> `+3,020.18 ms` | `S` (Sleep) | **`2,000.82 ms`** | `performNetworkRequest(2000)` -> `Thread.sleep(2000)` (`return false`) |
| **3. Network Attempt #2 (CPU)** | `+3,020.23 ms` -> `+3,040.24 ms` | `Running` | **`20.00 ms`** | Attempt #2 `cpuIntensiveComputation(20)` |
| **4. Network Attempt #2 (Timeout)** | `+3,040.24 ms` -> `+5,041.01 ms` | `S` (Sleep) | **`2,000.78 ms`** | `performNetworkRequest(2000)` -> `Thread.sleep(2000)` (`return false`) -- **5s input ANR threshold crossed** |
| **5. Network Attempt #3 (CPU)** | `+5,041.06 ms` -> `+5,061.30 ms` | `Running` | **`20.11 ms`** *(8.05 + 7.93 + 4.14 ms)* | Attempt #3 `cpuIntensiveComputation(20)` |
| **6. Network Attempt #3 (Timeout)** | `+5,061.30 ms` -> `+7,062.09 ms` | `S` (Sleep) | **`2,000.80 ms`** | `performNetworkRequest(2000)` -> `Thread.sleep(2000)` (`return false`) |
| **7. Network Attempt #4 (CPU)** | `+7,062.15 ms` -> `+7,082.38 ms` | `Running` | **`20.22 ms`** | Attempt #4 `cpuIntensiveComputation(20)` |
| **8. Network Attempt #4 (Success)** | `+7,082.38 ms` -> `+7,783.16 ms` | `S` (Sleep) | **`700.78 ms`** | `performNetworkRequest(2000)` hits `Math.random() < 0.2`, sleeps `~700 ms`, `return true` |
| **9. Handle Response (CPU)** | `+7,783.21 ms` -> `+9,281.30 ms` | `Running` | **`1,479.68 ms`** *(across 6 slices)* | `handleNetworkResponse()` (`Trace.beginSection("handleNetworkResponse")` at `+7,783.44 ms`) runs `cpuIntensiveComputation(2000)` until trace ends |
---
## 3. Code Mapping (`ProfilingManagerJavaSnippets.java`)
In `ProfilingManagerJavaSnippets.java` (lines `38` and `158-221`), the `OnClickListener` for `R.id.submit` runs `onClickSubmit()` directly on the main UI thread:
```java
private static final int NETWORK_TIMEOUT_MILLISECS = 2000;
public void setupButtonCallback() {
findViewById(R.id.submit).setOnClickListener(submitButtonView -> {
Trace.beginSection("MyApp:SubmitButton");
onClickSubmit();
Trace.endSection();
});
}
public void onClickSubmit() {
// 1. Blocks main thread on CPU for 1,000 ms
prepareNetworkRequest();
// 2. Synchronous retry loop on main thread (up to 10 attempts * 2,020 ms = 20.2 seconds!)
boolean networkRequestSuccess = false;
int maxAttempts = 10;
while (!networkRequestSuccess && maxAttempts > 0) {
networkRequestSuccess = performNetworkRequest(NETWORK_TIMEOUT_MILLISECS);
maxAttempts--;
}
// 3. Blocks main thread on CPU for another 2,000 ms
if (networkRequestSuccess) {
handleNetworkResponse();
}
}
boolean performNetworkRequest(int timeoutMilliseconds) {
cpuIntensiveComputation(20);
try {
if (Math.random() < 0.2) {
// Simulate performing a network request by waiting a random period of time
int networkRequestTimeMs = (int) (Math.random() * timeoutMilliseconds);
Thread.sleep(networkRequestTimeMs);
return true;
} else {
// Simulate a timeout
Thread.sleep(timeoutMilliseconds);
}
} catch (InterruptedException e) {
}
return false;
}
void cpuIntensiveComputation(int durationMs) {
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < durationMs) {
}
}
void prepareNetworkRequest() {
cpuIntensiveComputation(1000);
}
public void handleNetworkResponse() {
Trace.beginSection("handleNetworkResponse");
cpuIntensiveComputation(2000);
Trace.endSection();
}
```
---
## 4. Recommended Remediation
> **Tip:** Never perform network I/O, `Thread.sleep()`, or multi-hundred-millisecond CPU computations on the Android main UI thread.
1. **Offload `onClickSubmit()` to a Background Thread / Coroutine:**
* In Java, dispatch `prepareNetworkRequest()`, the `performNetworkRequest()` retry loop, and heavy response processing in `handleNetworkResponse()` to a background `ExecutorService` (e.g., `Executors.newSingleThreadExecutor()`), then marshal only lightweight UI updates back to the main thread via `runOnUiThread(...)` or `View.post(...)`.
* In Kotlin, use `lifecycleScope.launch(Dispatchers.IO)` (for network waiting) and `withContext(Dispatchers.Default)` (for CPU-intensive preparation/parsing).
2. **Use Non-Blocking / Asynchronous Retries with Backoff:**
* Avoid holding threads with `Thread.sleep()` inside tight retry loops; use asynchronous HTTP client timeouts/retries or `WorkManager` for deferrable background network tasks.