Ejemplo práctico de depuración del rendimiento: ANR

En esta sección, se muestra cómo depurar un error de Aplicación que no responde (ANR) con ProfilingManager y un ejemplo de registro.

Configura la app para recopilar errores de ANR

Para comenzar, configura un activador de ANR en tu 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);
}

Después de capturar y subir un registro de ANR, ábrelo en la IU de Perfetto.

Analiza el registro

Dado que el error de ANR activó el registro, sabes que este finalizó cuando el sistema detectó que el subproceso principal de tu app no respondía. En la figura 1, se muestra cómo navegar al subproceso principal de tu app, que está etiquetado de forma adecuada en la IU.

Navegación de la IU de Perfetto al subproceso principal de la app.
Figura 1: Navegación al subproceso principal de la app.

El final del registro coincide con la marca de tiempo del ANR, como se muestra en la Figura 2.

IU de Perfetto que muestra el final de un registro, destacando la ubicación del activador de ANR.
Figura 2: Ubicación del activador de ANR.

El registro también muestra las operaciones que la app estaba ejecutando cuando ocurrió el error de ANR. Específicamente, la app ejecutó código en el segmento de registro handleNetworkResponse. Esta división estaba dentro de la división MyApp:SubmitButton. Usó 1.48 segundos de tiempo de CPU (figura 3).

La IU de Perfetto muestra el tiempo de CPU consumido por la ejecución de handleNetworkResponse en el momento del ANR.
Figura 3: Ejecución en el momento del error de ANR.

Si solo confías en los seguimientos de pila en el momento del ANR para depurar, es posible que atribuyas erróneamente el ANR por completo al código que se ejecuta dentro del segmento de seguimiento de handleNetworkResponse que no había finalizado cuando el perfil terminó de registrarse. Sin embargo, 1.48 segundos no son suficientes para activar un error de ANR por sí solos, aunque se trate de una operación costosa. Debes mirar más atrás en el tiempo para comprender qué bloqueó el subproceso principal antes de este método.

Para obtener un punto de partida para buscar la causa del error de ANR, comenzamos a buscar después del último fotograma generado por el subproceso de IU, que corresponde al segmento Choreographer#doFrame 551275, y no hay grandes fuentes de demora antes de iniciar el segmento MyApp:SubmitButton que finalizó en el error de ANR (figura 4).

La IU de Perfetto muestra el último fotograma renderizado por el subproceso de IU antes del error de ANR.
Figura 4: Es el último fotograma de la app que se generó antes del error de ANR.

Para comprender el bloqueo, aleja el zoom y examina el segmento MyApp:SubmitButton completo. Observarás un detalle fundamental en los estados del subproceso, como se muestra en la figura 4: el subproceso pasó el 75% del tiempo (6.7 segundos) en el estado Sleeping y solo el 24% del tiempo en el estado Running.

La IU de Perfetto muestra los estados del subproceso durante una operación, destacando un 75% de tiempo de suspensión y un 24% de tiempo de ejecución.
Figura 5. Estados del subproceso durante la operación `MyApp:SubmitButton`.

Esto indica que la causa principal del ANR fue la espera, no el procesamiento. Examina los episodios de sueño individuales para encontrar un patrón.

IU de Perfetto que muestra el primer intervalo de suspensión dentro del segmento de registro de MyAppSubmitButton.
Figura 6: Primer tiempo de espera dentro de `MyAppSubmitButton`.
La IU de Perfetto muestra el segundo intervalo de sueño dentro del segmento de registro de MyAppSubmitButton.
Figura 7. Segundo tiempo de sueño dentro de `MyAppSubmitButton`.
La IU de Perfetto muestra el tercer intervalo de suspensión dentro del segmento de registro de MyAppSubmitButton.
Figura 8. Tercer tiempo de sueño dentro de `MyAppSubmitButton`.
La IU de Perfetto muestra el cuarto intervalo de suspensión dentro del segmento de registro de MyAppSubmitButton.
Figura 9. Cuarto tiempo de sueño dentro de `MyAppSubmitButton`.

Los primeros tres intervalos de sueño (figuras 6 a 8) son casi idénticos, de aproximadamente 2 segundos cada uno. Un cuarto sueño atípico (figura 9) dura 0.7 segundos. En un entorno de procesamiento, una duración de exactamente 2 segundos rara vez es una coincidencia. Esto sugiere un tiempo de espera programado en lugar de una contención de recursos aleatoria. El último período de inactividad puede deberse a que el subproceso finalizó su espera porque la operación que esperaba se completó correctamente.

La hipótesis es que la app alcanzó un tiempo de espera definido por el usuario de 2 segundos varias veces y, finalmente, tuvo éxito, lo que provocó una demora suficiente para activar un ANR.

La IU de Perfetto muestra un resumen de las demoras durante el segmento de registro MyApp:SubmitButton, lo que indica varios intervalos de suspensión de 2 segundos.
Figura 10: Resumen de las demoras durante el segmento de `MyApp:SubmitButton`.

Para verificar esto, inspecciona el código asociado con la sección de seguimiento 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) {
  // ...
}

  // ...
}

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

El código confirma esta hipótesis. El método onClickSubmit ejecuta una solicitud de red en el subproceso de IU con un NETWORK_TIMEOUT_MILLISECS codificado de 2,000 ms. Es fundamental que se ejecute dentro de un bucle while que vuelve a intentarlo hasta 10 veces.

En este registro específico, es probable que el usuario haya tenido una conectividad de red deficiente. Los primeros tres intentos fallaron, lo que provocó tres tiempos de espera de 2 segundos (un total de 6 segundos). El cuarto intento se realizó correctamente después de 0.7 s, lo que permitió que el código continuara con handleNetworkResponse. Sin embargo, el tiempo de espera acumulado ya activó el ANR.

Para evitar este tipo de error de ANR, coloca las operaciones relacionadas con la red que tienen latencias variables en un subproceso en segundo plano en lugar de ejecutarlas en el subproceso principal. Esto permite que la IU siga respondiendo incluso con una conectividad deficiente, lo que elimina por completo esta clase de ANR.