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:

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.

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.

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 proporcionar 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.

Los vínculos de app son vínculos directos basados en la URL de tu sitio web que están verificados para pertenecer a tu sitio web. A continuación, se incluyen algunos motivos por los que pueden fallar las verificaciones de vínculos de apps.

  • Alcances del filtro de intents: Solo agrega autoVerify a los filtros de intents para las URLs a las que tu app puede responder.
  • Conmutadores de protocolo no verificados: los redireccionamientos del servidor y del subdominio no verificados se consideran riesgos de seguridad y falla la verificación. Estos provocan que todos los vínculos autoVerify fallen. Por ejemplo, redireccionar vínculos de HTTP a HTTPS, como example.com a www.example.com, sin verificar los vínculos HTTPS, puede generar errores en la verificación. Agrega filtros de intents para verificar los vínculos de apps.
  • Vínculos no verificables: agregar vínculos no verificables con fines de prueba puede hacer que el sistema no verifique los vínculos de apps para tu app.
  • Servidores poco confiables: Asegúrate de que tus servidores puedan conectarse a las apps cliente.

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 registro 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 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 justo a tiempo en segundo plano (JIT) puede generar una sobrecarga de rendimiento significativa, y se alcanza con frecuencia si reinstalas 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 framework 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.

Cómo depurar tu app

Los siguientes son métodos diferentes para depurar el rendimiento de tu app. Consulta el siguiente video para obtener una descripción general del registro del sistema y el uso del generador de perfiles de Android Studio.

Depura el inicio de una app con Systrace

Consulta Tiempo de inicio de la app para obtener una descripción general del proceso de inicio de la app y mira el siguiente video para obtener una descripción general del registro del sistema.

Puedes desambiguar los tipos de inicio en las siguientes etapas:

  • Inicio en frío: Comienza por crear un proceso nuevo sin estado guardado.
  • Inicio semicaliente: Recrea la actividad mientras reutilizas el proceso o el proceso con el estado guardado.
  • Inicio en caliente: Reinicia la actividad y comienza con el aumento.

Recomendamos capturar Systrace con la app de Registro del sistema en el dispositivo. En el caso de Android 10 y versiones posteriores, usa Perfetto. En Android 9 y versiones anteriores, usa Systrace. También te recomendamos ver los archivos de registro con el lector de registros de Perfetto basado en la Web. Para obtener más información, consulta Descripción general del registro del sistema.

A continuación, se indican algunos aspectos que debes tener en cuenta:

  • Contención de supervisión: la competencia por recursos protegidos por supervisión puede generar un retraso significativo en el inicio de la app.
  • Transacciones de Binder síncronas: busca transacciones innecesarias en la ruta de acceso crítica de tu app Si una transacción necesaria es costosa, considera trabajar con el equipo de plataforma asociado para realizar mejoras.

  • Recolección de elementos no utilizados simultánea: Esto es común y tiene un impacto relativamente bajo. Sin embargo, si la experimentas a menudo, considera analizarla con el Generador de perfiles de memoria de Android Studio.

  • E/S: Comprueba las operaciones de E/S realizadas durante el inicio y si hay interrupciones prolongadas.

  • Actividad significativa en otros subprocesos: pueden interferir con el de la IU, por lo que debes prestar atención a las tareas en segundo plano durante el inicio.

Te recomendamos que llames a reportFullyDrawn cuando se complete el inicio desde la perspectiva de la app para mejorar los informes de la métrica de inicio de la app. Consulta la sección Tiempo para la visualización completa para obtener más información sobre el uso de reportFullyDrawn. Puedes extraer las horas de inicio definidas por RFD a través del procesador de registros de Perfetto, y se emitirá un evento de registro visible para el usuario.

Cómo usar Registro del sistema en el dispositivo

Puedes usar la app a nivel del sistema llamada Registro del sistema para capturar un registro del sistema en un dispositivo. Esta app te permite capturar registros desde el dispositivo sin necesidad de conectarlo o conectarlo a adb.

Cómo usar el Generador de perfiles de memoria de Android Studio

Puedes usar el Generador de perfiles de memoria de Android Studio para inspeccionar la presión de memoria que podría deberse a fugas de memoria o patrones de uso incorrectos. Proporciona una vista en vivo de las asignaciones de objetos.

Puedes solucionar problemas de memoria en tu app si sigues la información del uso del Generador de perfiles de memoria para realizar un seguimiento de los motivos y la frecuencia de las recolecciones de elementos no utilizados.

Para generar perfiles de la memoria de la app, sigue estos pasos:

  1. Detecta problemas de memoria.

    Registra una sesión de generación de perfiles de memoria del recorrido del usuario en el que te quieres enfocar. Busca un aumento en la cantidad de objetos, como se muestra en la figura 7, que eventualmente genera recolecciones de elementos no utilizados, como se observa en la figura 8.

    alt_text Figura 7: Recuento de objetos en aumento.

    alt_text Figura 8: Recolección de elementos no utilizados

    Después de identificar el recorrido del usuario que agrega presión de memoria, analiza las causas raíz de esa presión.

  2. Diagnostica hotspots de presión de memoria.

    Selecciona un rango en el cronograma para visualizar las opciones Allocations y Shallow Size, como se muestra en la Figura 9.

    alt_text Figura 9: Valores para Allocations y Shallow Size.

    Existen varias maneras de ordenar estos datos. Los siguientes son algunos ejemplos de cómo cada vista puede ayudarte a analizar los problemas.

    • Arrange by class: Es útil cuando deseas encontrar clases que generen objetos que, de otro modo, se almacenan en caché o se reutilizan en un grupo de memoria.

      Por ejemplo, si ves una app que crea 2,000 objetos de clase llamados "Vertex" cada segundo, aumenta el recuento de Allocations por 2,000 por segundo y lo ves cuando lo ordenas por clase. Si deseas volver a usar estos objetos para evitar la generación de elementos no utilizados, implementa un grupo de memoria.

    • Arrange by callstack: Es útil cuando deseas encontrar dónde hay una ruta de acceso activa en la que se asigna la memoria; por ejemplo, dentro de un bucle o en una función específica que realiza una gran cantidad de trabajo de asignación.

    • Shallow Size: Solo hace un seguimiento de la memoria del objeto en sí. Resulta útil para el seguimiento de clases simples compuestas principalmente por valores primitivos.

    • Retained Size: Muestra la memoria total debido al objeto y las referencias a las que solo hace referencia el objeto. Es útil para hacer un seguimiento de la presión de la memoria debido a objetos complejos. Para obtener este valor, realiza un volcado de memoria completa, como se muestra en la Figura 10, y agrega Retained Size como columna, como se muestra en la Figura 11.

      alt_text Figura 10: Volcado de memoria completa.

      Columna Retained Size.
      Figura 11: Columna Retained Size.
  3. Mide el impacto de una optimización.

    Las recolecciones de elementos no utilizados son más evidentes y fáciles de medir el impacto de las optimizaciones de memoria. Cuando una optimización reduce la presión de memoria, verás menos GC.

    Para medir el impacto de la optimización, en el cronograma del generador de perfiles, mide el tiempo entre las recolecciones de elementos no utilizados. Es posible que el proceso tarde más tiempo entre GC.

    Los efectos finales de las mejoras en la memoria son los siguientes:

    • Es probable que los cierres por falta de memoria se reduzcan si la app no alcanza constantemente la presión de memoria.
    • Tener menos GC mejora las métricas de bloqueo, especialmente en P99. Esto se debe a que las GCs generan contención de CPU, lo que puede provocar el diferimiento de las tareas en procesamiento durante la recolección.