Exemple pratique de débogage des performances : ANR

Cette section explique comment déboguer une erreur Application ne répond pas (ANR) à l'aide de ProfilingManager avec un exemple de trace.

Configurer l'application pour collecter les erreurs ANR

Commencez par configurer un déclencheur d'erreur ANR dans votre application :

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

Après avoir capturé et importé une trace d'erreur ANR, ouvrez-la dans l'interface utilisateur de Perfetto.

Analyser la trace

Étant donné que l'erreur ANR a déclenché la trace, vous savez que la trace s'est terminée lorsque le système a détecté un manque de réactivité dans le thread principal de votre application. La figure 1 montre comment accéder au thread principal de votre application, qui est balisé en conséquence dans l'interface utilisateur.

Navigation dans l&#39;interface utilisateur Perfetto vers le thread principal de l&#39;application.
Figure 1. Navigation vers le thread principal de l'application.

La fin de la trace correspond à l'horodatage de l'erreur ANR, comme illustré à la figure 2.

Interface utilisateur de Perfetto montrant la fin d&#39;une trace, mettant en évidence l&#39;emplacement du déclencheur ANR.
Figure 2. Emplacement du déclencheur d'erreur ANR.

La trace indique également les opérations que l'application exécutait lorsque l'erreur ANR s'est produite. Plus précisément, l'application a exécuté du code dans la tranche de trace handleNetworkResponse. Cette tranche se trouvait dans la tranche MyApp:SubmitButton. Elle a utilisé 1,48 seconde de temps CPU (figure 3).

Interface utilisateur Perfetto affichant le temps processeur consommé par l&#39;exécution de handleNetworkResponse au moment de l&#39;ANR.
Figure 3. Exécution au moment de l'erreur ANR.

Si vous vous fiez uniquement aux traces de la pile au moment de l'erreur ANR pour le débogage, vous risquez d'attribuer à tort l'erreur ANR entièrement au code exécuté dans la tranche de trace handleNetworkResponse, qui n'était pas terminée lorsque l'enregistrement du profil s'est arrêté. Toutefois, 1,48 seconde ne suffit pas à déclencher une erreur ANR à elle seule, même s'il s'agit d'une opération coûteuse. Vous devez remonter plus loin dans le temps pour comprendre ce qui a bloqué le thread principal avant cette méthode.

Pour trouver un point de départ pour rechercher la cause de l'ANR, nous commençons à chercher après la dernière frame générée par le thread UI, qui correspond à la tranche Choreographer#doFrame 551275. Il n'y a pas de sources de délai importantes avant le début de la tranche MyApp:SubmitButton qui s'est terminée par l'ANR (figure 4).

Interface utilisateur Perfetto affichant le dernier frame rendu par le thread UI avant l&#39;ANR.
Figure 4. Dernière frame d'application générée avant l'erreur ANR.

Pour comprendre le blocage, effectuez un zoom arrière pour inspecter la tranche MyApp:SubmitButton complète. Vous remarquerez un détail essentiel dans les états du thread, comme illustré à la figure 4 : le thread a passé 75 % du temps (6,7 secondes) à l'état Sleeping et seulement 24 % du temps à l'état Running.

L&#39;interface utilisateur Perfetto affiche les états des threads pendant une opération, en mettant en évidence 75 % de temps de veille et 24 % de temps d&#39;exécution.
Figure 5. États du thread lors de l'opération `MyApp:SubmitButton`.

Cela indique que la cause principale de l'erreur ANR était l'attente, et non le calcul. Examinez les occurrences de veille individuelles pour trouver un modèle.

Interface utilisateur Perfetto affichant le premier intervalle de veille dans le segment de trace MyAppSubmitButton.
Figure 6. Première période de veille dans `MyAppSubmitButton`.
Interface utilisateur Perfetto affichant le deuxième intervalle de sommeil dans le segment de trace MyAppSubmitButton.
Figure 7. Deuxième période de veille dans `MyAppSubmitButton`.
Interface utilisateur Perfetto affichant le troisième intervalle de veille dans la tranche de trace MyAppSubmitButton.
Figure 8. Troisième période de veille dans `MyAppSubmitButton`.
Interface utilisateur Perfetto montrant le quatrième intervalle de veille dans le segment de trace MyAppSubmitButton.
Figure 9. Quatrième période de veille dans `MyAppSubmitButton`.

Les trois premières périodes de veille (figures 6 à 8) sont presque identiques, d'environ deux secondes chacune. La quatrième période de veille (figure 9) est une valeur aberrante de 0,7 seconde. Une durée de deux secondes exactement est rarement une coïncidence dans un environnement informatique. Cela suggère fortement un délai avant expiration programmé plutôt qu'une contention de ressources aléatoire. La dernière période de veille peut être due au fait que le thread a terminé son attente, car l'opération sur laquelle il attendait a réussi.

L'hypothèse est que l'application a atteint plusieurs fois un délai avant expiration défini par l'utilisateur de deux secondes et a finalement réussi, ce qui a entraîné un délai suffisant pour déclencher une erreur ANR.

Interface utilisateur Perfetto affichant un récapitulatif des délais pendant la tranche de trace MyApp:SubmitButton, indiquant plusieurs intervalles de veille de deux secondes.
Figure 10. Résumé des délais pendant la tranche `MyApp:SubmitButton`.

Pour vérifier cela, inspectez le code associé à la section de trace 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();
}

Le code confirme cette hypothèse. La méthode onClickSubmit exécute une requête réseau sur le thread UI avec un NETWORK_TIMEOUT_MILLISECS codé en dur de 2 000 ms. Surtout, elle s'exécute dans une boucle while qui effectue jusqu'à 10 tentatives.

Dans cette trace spécifique, l'utilisateur avait probablement une mauvaise connectivité réseau. Les trois premières tentatives ont échoué, ce qui a entraîné trois délais avant expiration de deux secondes (soit six secondes au total). La quatrième tentative a réussi après 0,7 seconde, ce qui a permis au code de passer à handleNetworkResponse. Toutefois, le temps d'attente cumulé a déjà déclenché l'erreur ANR.

Évitez ce type d'erreur ANR en plaçant les opérations liées au réseau qui ont des latences variables dans un thread en arrière-plan au lieu de les exécuter dans le thread principal. Cela permet à l'interface utilisateur de rester réactive même en cas de mauvaise connectivité, ce qui élimine complètement cette classe d'erreurs ANR.