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 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 Macrobenchmark.
      • 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 la aplicación son vínculos directos basados en la URL de tu sitio web que se verificaron que pertenecen a tu sitio web. Los siguientes son motivos que pueden causar fallas en las verificaciones de App Link.

  • Alcances de filtros de intents: Solo agrega autoVerify a los filtros de intents para las URLs a las que puede responder tu app.
  • Conmutadores de protocolo no verificados: Los redireccionamientos de subdominio y del servidor no verificados se consideran riesgos de seguridad y no pasan la verificación. Hacen que falle todo vínculo autoVerify. Por ejemplo, redireccionar vínculos de HTTP a HTTPS, como example.com a www.example.com, sin verificar los vínculos HTTPS puede provocar que la verificación falle. Asegúrate de verificar los vínculos de apps agregando filtros de intents.
  • Vínculos no verificables: Si agregas vínculos no verificables con fines de prueba, es posible 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 tus apps de 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 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 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 para la simplicidad o speed-profile para que coincida más con el rendimiento de producción, aunque esto requiere calentar la aplicación y volcar perfiles, o compilar los perfiles de Baseline de la app).

Tanto speed como speed-profile reducen la cantidad de código que se ejecuta y se interpreta desde dex y, en consecuencia, la cantidad de compilación en tiempo real (JIT) en segundo plano, lo que puede causar interferencias significativas. Solo speed-profile reduce el impacto de la carga de clases del entorno de ejecución desde dex.

El siguiente comando compila la aplicación con el modo speed:

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

El modo de compilación speed compila los métodos de la app por completo. El modo speed-profile compila los métodos y las clases de la app según un perfil de las instrucciones 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

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 Macrobenchmark, que puede 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

A continuación, se muestran diferentes métodos para depurar el rendimiento de tu app. Mira el siguiente video para obtener una descripción general del registro del sistema y el uso del generador de perfiles de Android Studio.

Cómo depurar el inicio de apps 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 seguimiento 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 se reutiliza el proceso o recrea el proceso con el estado guardado.
  • Inicio en caliente: Reinicia la actividad y comienza en la inflación.

Te recomendamos que captures registros del sistema con la app de registros del sistema en el dispositivo. En Android 10 y versiones posteriores, usa Perfetto. En Android 9 y versiones anteriores, usa Systrace. También te recomendamos que veas 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.

Estos son algunos aspectos que debes tener en cuenta:

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

  • GC simultánea: es común y tiene un impacto relativamente bajo, pero, si la experimentas con frecuencia, te recomendamos que la analices con el generador de perfiles de memoria de Android Studio.

  • E/S: Verifica si se realizó E/S durante el inicio y busca bloqueos largos.

  • Actividad significativa en otros subprocesos: Estos pueden interferir con el subproceso de 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 métricas de inicio de la app. Consulta la sección Tiempo para la pantalla completa para obtener más información sobre el uso de reportFullyDrawn. Puedes extraer las horas de inicio definidas por la RFD a través del procesador de seguimiento de Perfetto, y se emite un evento de seguimiento visible para el usuario.

Cómo usar el 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 registrar seguimientos 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 se puede producir debido a fugas de memoria o patrones de uso incorrectos. Proporciona una visualización en vivo de las asignaciones de objetos.

Para solucionar problemas de memoria en tu app, sigue la información del Generador de perfiles de memoria para realizar un seguimiento de las causas y la frecuencia de las GC.

Para generar un perfil de la memoria de la app, sigue estos pasos:

  1. Detecta problemas de memoria.

    Graba una sesión de perfilamiento de memoria del recorrido del usuario en el que deseas enfocarte. Busca un recuento de objetos creciente, como se muestra en la figura 7, que, en última instancia, genera GC, como se muestra en la figura 8.

    alt_text Figura 7: Aumento del recuento de objetos

    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 la presión de memoria.

  2. Diagnostica hotspots de presión de memoria.

    Selecciona un rango en el cronograma para visualizar las asignaciones y el tamaño aplanado, 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 la forma en que cada vista puede ayudarte a analizar los problemas.

    • Organizar por clase: Es útil cuando quieres encontrar clases que generan objetos que, de otro modo, se almacenan en caché o se vuelven a usar desde un grupo de memoria.

      Por ejemplo, si ves una app que crea 2,000 objetos de clase por segundo llamados "Vertex", aumenta el recuento de Asignaciones en 2,000 por segundo y lo ves cuando ordenas los datos por clase. Si quieres volver a usar estos objetos para evitar generar elementos no utilizados, implementa un grupo de memoria.

    • Organizar por pila de llamadas: Es útil cuando quieres encontrar una ruta de acceso frecuente en la que se asigna memoria, como dentro de un bucle o dentro de una función específica que realiza muchas tareas de asignación.

    • Shallow Size: Solo realiza un seguimiento de la memoria del objeto en sí. Es útil para hacer un seguimiento de clases simples compuestas principalmente de valores primitivos.

    • Retained Size: Muestra la memoria total debido al objeto y las referencias a las que solo hace referencia el objeto. Resulta útil para hacer un seguimiento de la presión de memoria causada por objetos complejos. Para obtener este valor, toma un volcado de memoria completo, como se muestra en la Figura 10, y Retained Size se agrega como una 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 GC 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, ves menos GC.

    Para medir el impacto de la optimización, mide el tiempo entre las GC en el cronograma del Generador de perfiles. Luego, puedes ver que tarda más entre las GC.

    Los impactos más importantes de las mejoras de memoria son los siguientes:

    • Es probable que se reduzcan las interrupciones por falta de memoria si la app no ejerce presión de memoria constantemente.
    • Tener menos GC mejora las métricas de bloqueo, especialmente en el 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.