Esempio pratico di debug delle prestazioni: ANR

Questa sezione mostra come eseguire il debug di un errore ANR (L'applicazione non risponde) utilizzando ProfilingManager con una traccia di esempio.

Configurare l'app per raccogliere gli errori ANR

Inizia configurando un trigger ANR nella tua app:

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 mainExecutor = Executors.newSingleThreadExecutor();
  Consumer<ProfilingResult> resultCallback =
      profilingResult -> {
        // Handle uploading trace to your back-end
      };
  profilingManager.registerForAllProfilingResults(mainExecutor, resultCallback);
  profilingManager.addProfilingTriggers(triggers);
}

Dopo aver acquisito e caricato una traccia ANR, aprila nell'UI Perfetto.

Analizzare la traccia

Poiché l'errore ANR ha attivato la traccia, sai che la traccia è terminata quando il sistema ha rilevato la mancata risposta nel thread principale della tua app. La Figura 1 mostra come navigare al thread principale dell'app, che è contrassegnato di conseguenza all'interno dell'UI.

Navigazione nell&#39;interfaccia utente di Perfetto al thread principale dell&#39;app.
Figura 1. Navigazione al thread principale dell'app.

La fine della traccia corrisponde al timestamp dell'errore ANR, come mostrato nella Figura 2.

UI di Perfetto che mostra la fine di una traccia, evidenziando la posizione del trigger ANR.
Figura 2. Posizione del trigger ANR.

La traccia mostra anche le operazioni che l'app stava eseguendo quando si è verificato l'errore ANR. In particolare, l'app ha eseguito il codice nella sezione della traccia handleNetworkResponse. Questa sezione si trovava all'interno della sezione MyApp:SubmitButton. Ha utilizzato 1,48 secondi di tempo della CPU (Figura 3).

L&#39;interfaccia utente di Perfetto mostra il tempo di CPU consumato dall&#39;esecuzione di handleNetworkResponse
al momento dell&#39;errore ANR.
Figura 3. Esecuzione al momento dell'errore ANR.

Se al momento dell'errore ANR ti affidi esclusivamente alle analisi dello stack per il debug, potresti attribuire erroneamente l'errore ANR interamente al codice in esecuzione nella sezione della traccia handleNetworkResponse, che non era terminata al termine della registrazione del profilo. Tuttavia, 1,48 secondi non sono sufficienti per attivare un errore ANR da soli, anche se si tratta di un'operazione costosa. Devi tornare indietro nel tempo per capire cosa ha bloccato il thread principale prima di questo metodo.

Per trovare un punto di partenza per cercare la causa dell'errore ANR, iniziamo a cercare dopo l'ultimo frame generato dal thread dell'interfaccia utente, che corrisponde alla sezione Choreographer#doFrame 551275. Non ci sono grandi fonti di ritardo prima di iniziare la sezione MyApp:SubmitButton che ha portato all'errore ANR (Figura 4).

UI di Perfetto che mostra l&#39;ultimo frame di cui è stato eseguito il rendering dal thread dell&#39;interfaccia utente prima dell&#39;ANR.
Figura 4. Ultimo frame dell'app generato prima dell'errore ANR.

Per comprendere il blocco, riduci lo zoom per esaminare l'intera sezione MyApp:SubmitButton. Noterai un dettaglio fondamentale negli stati del thread, come mostrato nella Figura 4: il thread ha trascorso il 75% del tempo (6,7 secondi) nello stato Sleeping e solo il 24% del tempo nello stato Running .

UI di Perfetto che mostra gli stati dei thread durante un&#39;operazione, evidenziando il 75%
 di tempo di sospensione e il 24% di tempo di esecuzione.
Figura 5. Stati del thread durante l'operazione `MyApp:SubmitButton`.

Ciò indica che la causa principale dell'errore ANR era l'attesa, non il calcolo. Esamina le singole occorrenze di sospensione per trovare un pattern.

La UI di Perfetto che mostra il primo intervallo di sospensione all&#39;interno della
 sezione della traccia MyAppSubmitButton.
Figura 6. Primo periodo di sospensione all'interno di `MyAppSubmitButton`.
La UI di Perfetto che mostra il secondo intervallo di sonno all&#39;interno della
sezione della traccia MyAppSubmitButton.
Figura 7.
Secondo periodo di sospensione all'interno di `MyAppSubmitButton`.
La UI di Perfetto che mostra il terzo intervallo di sospensione all&#39;interno della sezione
MyAppSubmitButton.
Figura 8. Terzo periodo di sospensione all'interno di `MyAppSubmitButton`.
La UI di Perfetto che mostra il quarto intervallo di sospensione all&#39;interno della
sezione della traccia MyAppSubmitButton.
Figura 9. Quarto periodo di sospensione all'interno di `MyAppSubmitButton`.

I primi tre intervalli di sospensione (Figure 6-8) sono quasi identici, circa 2 secondi ciascuno. Un quarto periodo di sospensione anomalo (Figura 9) è di 0,7 secondi. Una durata di esattamente 2 secondi è raramente una coincidenza in un ambiente di computing. Ciò suggerisce fortemente un timeout programmato anziché una contesa di risorse casuale. L'ultima sospensione potrebbe essere causata dal fatto che il thread ha terminato l'attesa perché l'operazione su cui stava aspettando è andata a buon fine.

L'ipotesi è che l'app abbia raggiunto più volte un timeout definito dall'utente di 2 secondi e alla fine sia andata a buon fine, causando un ritardo sufficiente per attivare un errore ANR.

Perfetto UI che mostra un riepilogo dei ritardi durante la sezione di traccia MyApp:SubmitButton, che indica più intervalli di sospensione di 2 secondi.
Figura 10. Riepilogo dei ritardi durante la sezione `MyApp:SubmitButton`.

Per verificarlo, esamina il codice associato alla sezione della traccia MyApp:SubmitButton:

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();
}

Il codice conferma questa ipotesi. Il metodo onClickSubmit esegue una richiesta di rete sul thread dell'interfaccia utente con un valore NETWORK_TIMEOUT_MILLISECS hardcoded di 2000 ms. Fondamentalmente, viene eseguito all'interno di un loop while che riprova fino a 10 volte.

In questa traccia specifica, l'utente probabilmente aveva una scarsa connettività di rete. I primi tre tentativi non sono andati a buon fine, causando tre timeout di 2 secondi (per un totale di 6 secondi). Il quarto tentativo è andato a buon fine dopo 0,7 secondi, consentendo al codice di procedere a handleNetworkResponse. Tuttavia, il tempo di attesa accumulato ha già attivato l'errore ANR.

Evita questo tipo di errore ANR inserendo le operazioni correlate alla rete con latenze variabili in un thread in background anziché eseguirle nel thread principale. In questo modo, l'UI rimane reattiva anche in caso di scarsa connettività, eliminando completamente questa classe di errori ANR.