Descripción general de la medición del rendimiento de apps

Este documento te permite identificar y corregir problemas clave de rendimiento en tu app.

Problemas clave de rendimiento

Existen muchos problemas que pueden ser causas del rendimiento deficiente de una app; pero, a continuación, se explican algunos problemas habituales que debes tener en cuenta en tu app:

Bloqueo de desplazamiento

Bloqueo es el término que describe el problema visual que se produce cuando el sistema no puede compilar ni proporcionar fotogramas a tiempo para dibujarlos en la pantalla a la cadencia solicitada de 60 Hz o más. El bloqueo es más evidente durante el desplazamiento, ya que, en lugar de ver un flujo animado y fluido, se producen errores. El bloqueo aparece cuando se detiene el movimiento durante el proceso de uno o más fotogramas, ya que la app tarda más en renderizar contenido que la duración de un fotograma en el sistema.

Las apps deben fijar como objetivo frecuencias de actualización de 90 Hz. Las tasas de renderización convencionales son de 60 Hz, pero muchos dispositivos más nuevos operan en el modo de 90 Hz durante las interacciones del usuario, como el desplazamiento. Algunos dispositivos admiten frecuencias aún más altas, de hasta 120 Hz.

Para verificar qué frecuencia de actualización usa un dispositivo en un momento determinado, habilita una superposición en Opciones para desarrolladores > Frecuencia de actualización en la sección Depuración.

Latencia de inicio

La latencia de inicio es la cantidad de tiempo que se tarda entre el momento en que se presiona el ícono de la app, la notificación o algún otro punto de entrada, y el momento en que se muestran los datos del usuario en la pantalla.

Apunta a los siguientes objetivos de inicio en tus apps:

  • Inicio en frío en menos de 500 ms. Un inicio en frío ocurre cuando la app que se inicia no está presente en la memoria del sistema. Ocurre cuando se inicia la app por primera vez desde el reinicio o desde el momento en que el usuario o el sistema detienen el proceso de la app.

    Por el contrario, un inicio semicaliente se produce cuando la app ya se está ejecutando en segundo plano. El inicio en frío requiere la mayor cantidad de trabajo del sistema, ya que tiene que cargar todo desde el almacenamiento e inicializar la app. Intenta que los inicios en frío tarden 500 ms o menos.

  • Latencias de p95 y p99 muy cercanas a la latencia mediana. Cuando la app tarda mucho tiempo en iniciarse, la experiencia del usuario es deficiente. Las comunicaciones entre procesos (IPC) y las E/S innecesarias durante la ruta de acceso crítica del inicio de la app pueden experimentar contención de bloqueo e introducir incoherencias.

Transiciones no fluidas

Esto se hace evidente durante interacciones, como cambiar de pestaña o cargar una actividad nueva. Estos tipos de transiciones deben tener animaciones fluidas y no deben incluir retrasos ni parpadeos visuales.

Ineficiencias energéticas

Llevar a cabo una tarea reduce la carga de la batería, y realizar una tarea innecesaria reduce la duración de la batería.

Las asignaciones de memoria, que provienen de la creación de objetos nuevos en el código, pueden ser la causa de una tarea significativa en el sistema. Esto se debe a que las asignaciones en sí no solo requieren esfuerzo de Android Runtime (ART), sino que liberar esos objetos más tarde (recolección de elementos no utilizados) también requiere tiempo y esfuerzo. La asignación y la recolección son mucho más rápidas y eficientes, en especial, para los objetos temporales. Aunque solía ser una práctica recomendada evitar la asignación de objetos siempre que fuera posible, sugerimos hacer lo que tenga más sentido para tu app y arquitectura. Ahorrar en asignaciones ante el riesgo de que se imposibilite el mantenimiento del código no es la práctica recomendada, debido a la capacidad de ART.

Sin embargo, requiere esfuerzo, así que ten en cuenta que puede ser uno de los factores de problemas de rendimiento si asignas muchos objetos en tu bucle interno.

Identifica los problemas

Recomendamos el siguiente flujo de trabajo para identificar y solucionar problemas de rendimiento:

  1. Identifica e inspecciona los siguientes recorridos críticos del usuario:
    • Flujos de inicio frecuentes, lo que incluye el selector y la notificación
    • Cualquier pantalla en la que el usuario se desplace por los datos
    • Transiciones entre pantallas
    • Flujos de larga duración, como la navegación o la reproducción de música
  2. Inspecciona lo que sucede durante los flujos anteriores con las siguientes herramientas de depuración:
    • Perfetto: Te permite ver lo que sucede en todo el dispositivo con datos precisos de latencia.
    • Generador de perfiles de memoria: Te permite ver qué asignaciones de memoria se producen en el montón.
    • Simpleperf: Muestra un gráfico de qué llamadas a función usan más CPU durante un período determinado. Cuando identificas algo que lleva mucho tiempo en Systrace, pero no sabes por qué, Simpleperf puede brindar información adicional.

Para comprender y depurar estos problemas de rendimiento, es fundamental depurar de forma manual ejecuciones individuales de pruebas. No puedes reemplazar los pasos anteriores con el análisis de datos agregados. Sin embargo, para comprender lo que los usuarios ven realmente e identificar cuándo se pueden producir regresiones, es importante configurar la recopilación de métricas en pruebas automatizadas y en el campo:

  • Flujos de inicio
  • Bloqueo
    • Métricas de campo
      • Métricas de fotogramas de Play Console: En Play Console, no puedes limitar las métricas a un recorrido específico del usuario. Solo informa los bloqueos generales en toda la app.
      • Medición personalizada con FrameMetricsAggregator: Puedes usar FrameMetricsAggregator para registrar las métricas de bloqueo durante un flujo de trabajo particular.
    • Pruebas de lab
      • Desplazamiento con macrocomparativas.
      • La macrocomparativa recopila la latencia de fotogramas con comandos dumpsys gfxinfo que encierran un solo recorrido del usuario. Esta es una manera razonable de comprender la variación en el bloqueo de un recorrido específico del usuario. Las métricas RenderTime, que destacan el tiempo que tarda en dibujarse un fotograma, son más importantes que el recuento de fotogramas con bloqueos para identificar regresiones o mejoras.

Cómo configurar tu app para el análisis del rendimiento

Es esencial realizar la configuración adecuada para obtener comparativas precisas, repetibles y prácticas desde una app. Realiza pruebas en un sistema que esté lo más cerca posible de la producción y, al mismo tiempo, suprime las fuentes de ruido. En las siguientes secciones, se explica una serie de pasos específicos del APK y del sistema que puedes seguir para preparar una configuración de prueba, algunos de los cuales son específicos de casos de uso.

Puntos de seguimiento

Las apps pueden instrumentar su código con eventos de seguimiento personalizados.

Mientras se capturan los seguimientos, estos generan una pequeña sobrecarga de aproximadamente 5 μs por sección, así que no los encierres en cada método. Realizar un seguimiento de grandes fragmentos de trabajo de más de 0.1 ms puede proporcionar estadísticas significativas sobre los cuellos de botella.

Consideraciones del APK

Las variantes de depuración pueden ser útiles para solucionar problemas y simbolizar muestras de pilas, pero tienen un impacto no lineal grave en el rendimiento. Los dispositivos que ejecutan Android 10 (nivel de API 29) y versiones posteriores pueden usar profileable android:shell="true" en su manifiesto para habilitar la generación de perfiles en las compilaciones de lanzamiento.

Usa la configuración de reducción de código de nivel de producción. Según los recursos que use tu app, puede tener un impacto significativo en el rendimiento. Algunas configuraciones de ProGuard quitan los puntos de seguimiento; por lo tanto, considera quitar esas reglas para la configuración en la que ejecutas las pruebas.

Compilación

Compila tu app en el dispositivo a un estado conocido (generalmente, speed o speed-profile). La actividad Just-in-time (JIT) en segundo plano puede generar una sobrecarga significativa de rendimiento, que se alcanza con frecuencia si vuelves a instalar el APK entre las ejecuciones de prueba. El siguiente es un comando para hacerlo:

adb shell cmd package compile -m speed -f com.google.packagename

El modo de compilación speed compila la app por completo. El modo speed-profile compila la app según un perfil de las rutas de código utilizadas que se recopilan durante el uso de la app. Puede ser difícil recopilar perfiles de manera coherente y correcta, por lo que, si decides usarlos, confirma que recopilen lo que esperas. Los perfiles se encuentran en la siguiente ubicación:

/data/misc/profiles/ref/[package-name]/primary.prof

La macrocomparativa te permite directamente especificar el modo de compilación.

Consideraciones del sistema

Para las mediciones de bajo nivel y alta fidelidad, calibra los dispositivos. Ejecuta las comparaciones A/B en el mismo dispositivo y la misma versión de SO. Se pueden producir variaciones significativas en el rendimiento, incluso en el mismo tipo de dispositivo.

En dispositivos con derechos de administrador, considera usar una secuencia de comandos lockClocks para obtener microcomparativas. Entre otras cosas, estas secuencias de comandos hacen lo siguiente:

  • Colocar las CPU a una frecuencia fija
  • Inhabilitar los núcleos pequeños y configurar la GPU
  • Inhabilitar la limitación térmica

No recomendamos usar una secuencia de comandos lockClocks para las pruebas enfocadas en la experiencia del usuario, como el inicio de la app, las pruebas de DoU y las pruebas de bloqueos, pero puede ser esencial para reducir el ruido en las pruebas de microcomparativas.

Cuando sea posible, considera usar un marco de trabajo de prueba, como macrocomparativas, que pueda reducir el ruido en las mediciones y evitar una medición incorrecta.

Inicio lento de la app: actividad innecesaria de trampolín

Una actividad de trampolín puede extender el tiempo de inicio de la app de manera innecesaria, y es importante que sepas si tu app lo está haciendo. Como se muestra en el siguiente seguimiento de ejemplo, un activityStart es seguido inmediatamente por otro activityStart sin que la primera actividad dibuje ningún fotograma.

alt_text

Figura 1: Seguimiento que muestra la actividad de trampolín

Esto puede suceder en un punto de entrada de una notificación y en un punto de entrada de inicio normal de la app, y, a menudo, puedes abordarlo con una refactorización. Por ejemplo, si usas esa actividad para realizar la configuración antes de que se ejecute otra actividad, factoriza ese código en un componente o una biblioteca reutilizables.

Asignaciones innecesarias que activan GC frecuentes

Es posible que observes que se producen recolecciones de elementos no utilizados (GC) con mayor frecuencia de la esperada en Systrace.

En el siguiente ejemplo, cada 10 segundos durante una operación de larga duración es un indicador de que la app podría asignar de forma innecesaria pero consistente a lo largo del tiempo:

alt_text

Figura 2: Seguimiento que muestra el espacio entre eventos de GC

También puedes notar que una pila de llamadas específica realiza la gran mayoría de las asignaciones cuando se usa el Generador de perfiles de memoria. No es necesario que elimines todas las asignaciones de forma activa, ya que, de esta manera, se puede dificultar el mantenimiento del código. En cambio, comienza por trabajar en los hotspots de las asignaciones.

Fotogramas con bloqueos

La canalización de gráficos es relativamente complicada, y puede haber algún matiz para determinar si un usuario finalmente podría ver un fotograma descartado. En algunos casos, la plataforma puede "rescatar" un fotograma desde el almacenamiento en búfer. Sin embargo, puedes ignorar la mayor parte de esa complejidad para identificar los fotogramas problemáticos desde la perspectiva de tu app.

Cuando se dibujan fotogramas con poco trabajo requerido de la app, los puntos de seguimiento Choreographer.doFrame() se producen en una cadencia de 16.7 ms (si se supone que es dispositivo con 60 FPS):

alt_text

Figura 3: Seguimiento que muestra fotogramas rápidos frecuentes

Si te alejas y navegas por el seguimiento, con frecuencia, observarás que los fotogramas tardan un poco más en completarse, pero está bien porque no tardan más de su tiempo asignado de 16.7 ms:

alt_text

Figura 4: Seguimiento que muestra fotogramas rápidos frecuentes con aumentos periódicos de actividad de trabajo

Cuando observes una interrupción en esta cadencia regular, se tratará de un fotograma con bloqueos, como se muestra en la figura 5:

alt_text

Figura 5: Seguimiento que muestra un fotograma con bloqueos

Puedes practicar identificarlos.

alt_text

Figura 6: Seguimiento que muestra más fotogramas con bloqueos

En algunos casos, debes hacer zoom en un punto de seguimiento para obtener más información sobre qué vistas se aumentan o qué hace RecyclerView. En otros casos, es posible que debas realizar una inspección más detallada.

Para obtener más información sobre la identificación de fotogramas con bloqueos y la depuración de sus causas, consulta Renderización lenta.

Errores comunes de RecyclerView

La invalidación innecesaria de todos los datos de copia de seguridad de RecyclerView puede generar bloqueos y tiempos de renderización prolongados de fotogramas. En cambio, para minimizar la cantidad de vistas que deben actualizarse, invalida solo los datos que cambian.

Consulta Cómo presentar datos dinámicos para conocer las formas de evitar llamadas costosas a notifyDatasetChanged(), que causan que el contenido se actualice en lugar de reemplazarlo por completo.

Si no admites todos los RecyclerView anidados correctamente, es posible que el RecyclerView interno se vuelva a crear por completo cada vez. Cada RecyclerView interno anidado debe tener un RecycledViewPool configurado para garantizar que las vistas se puedan reciclar entre cada RecyclerView interno.

Si no se realiza la carga previa de los datos suficientes o no se realiza la carga previa de manera oportuna, es posible que llegar al final de una lista de desplazamiento resulte un inconveniente cuando un usuario necesita esperar más datos del servidor. Aunque, técnicamente, esto no es un bloqueo, ya que no se pierde ningún plazo de fotogramas, puedes mejorar significativamente la UX modificando el tiempo y la cantidad de carga previa de modo que el usuario no tenga que esperar datos.