Esempio pratico di debug delle prestazioni: ANR

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

Configurare l'app per raccogliere gli errori ANR

Per iniziare, configura un attivatore 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 in Perfetto UI.

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 dell'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 fino 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. Località trigger ANR.

La traccia mostra anche le operazioni che l'app stava eseguendo quando si è verificato l'errore ANR. Nello specifico, l'app ha eseguito il codice nella sezione di traccia handleNetworkResponse. Questa fetta si trovava all'interno della fetta 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 per il debug ti affidi esclusivamente alle analisi dello stack al momento dell'errore ANR, potresti attribuire erroneamente l'errore ANR interamente al codice in esecuzione all'interno della sezione di 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 solo, anche se si tratta di un'operazione costosa. Devi esaminare un periodo di tempo più lungo per capire cosa ha bloccato il thread principale prima di questo metodo.

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

UI di Perfetto che mostra l&#39;ultimo frame sottoposto a rendering dal thread UI prima dell&#39;errore ANR.
Figura 4. Ultimo frame dell'app generato prima dell'ANR.

Per capire il blocco, diminuisci lo zoom per esaminare l'intera sezione MyApp:SubmitButton. Come mostrato nella Figura 4, noterai un dettaglio fondamentale negli stati dei thread: 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 dei thread durante l'operazione `MyApp:SubmitButton`.

Ciò indica che la causa principale dell'ANR era l'attesa, non il calcolo. Esamina i singoli episodi di sonno per trovare uno schema.

La UI di Perfetto che mostra il primo intervallo di sospensione all&#39;interno della
 sezione della traccia MyAppSubmitButton.
Figura 6. Primo periodo di inattività 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. Seconda ora di sonno all'interno di `MyAppSubmitButton`.
La UI di Perfetto che mostra il terzo intervallo di sospensione all&#39;interno della sezione
MyAppSubmitButton.
Figura 8. Terzo tempo 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 orario di sonno all'interno di `MyAppSubmitButton`.

I primi tre intervalli di sonno (Figure 6-8) sono quasi identici, circa 2 secondi ciascuno. Un quarto sonno anomalo (Figura 9) è di 0,7 secondi. Una durata di esattamente 2 secondi è raramente una coincidenza in un ambiente informatico. Ciò suggerisce fortemente un timeout programmato anziché una contesa di risorse casuale. L'ultimo stato di sospensione potrebbe essere causato dal completamento dell'attesa del thread perché l'operazione che 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 riuscita, causando un ritardo sufficiente ad 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 lo slice `MyApp:SubmitButton`.

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

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) {
  // ...
}

  // ...
}

public void handleNetworkResponse() {
  Trace.beginSection("handleNetworkResponse");
  // ...
  Trace.endSection();
}

Il codice conferma questa ipotesi. Il metodo onClickSubmit esegue una richiesta di rete sul thread UI con un NETWORK_TIMEOUT_MILLISECS hardcoded di 2000 ms. Fondamentalmente, viene eseguito all'interno di un ciclo while che esegue fino a 10 tentativi.

In questa traccia specifica, l'utente probabilmente aveva una connettività di rete scarsa. I primi tre tentativi non sono riusciti, causando tre timeout di 2 secondi (per un totale di 6 secondi). Il quarto tentativo è riuscito 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, la UI rimane reattiva anche in caso di scarsa connettività, eliminando completamente questo tipo di ANR.