Realiza análisis con Profile GPU Rendering

La herramienta Profile GPU Rendering indica el tiempo relativo que cada etapa de la canalización de renderización toma para renderizar el fotograma anterior. Este conocimiento puede ayudarte a identificar cuellos de botella en la canalización para que puedas optimizar el rendimiento de la renderización de la app.

En esta página, se explica brevemente qué sucede durante cada etapa de la canalización y se analizan los problemas que pueden causar los cuellos de botella. Antes de leer esta página, debes familiarizarte con la información presentada en Cómo crear un perfil de velocidad de procesamiento de la GPU. Además, para comprender cómo se relacionan todas las etapas, puedes consultar Cómo funciona la canalización de renderización..

Representación visual

La herramienta Profile GPU Rendering muestra las etapas y sus tiempos relativos en el formato de un gráfico: un histograma con código de color. En la Figura 1, se muestra un ejemplo de esta pantalla.

Gráfico de Profile GPU Rendering
Figura 1. Gráfico de Profile GPU Rendering

Cada segmento de la barra vertical que se muestra en el gráfico de Profile GPU Rendering representa una etapa de la canalización y se resalta con un color específico en el gráfico de barras. En la Figura 2, se muestra una clave para el significado de cada color que se muestra.

Leyenda de gráficos de Profile GPU Rendering
Figura 2: Leyenda de gráficos de Profile GPU Rendering

Una vez que comprendas qué significa cada color, puedes enfocarte en aspectos específicos de la app para optimizar el rendimiento de la renderización.

Etapas y significados

En esta sección, se explica qué sucede durante cada etapa, así como también las causas del cuello de botella.

Control de entradas

La etapa de control de entradas de la canalización mide cuánto tiempo dedicó la app al control de eventos de entrada. Esta métrica indica cuánto tiempo dedicó la app a la ejecución de código llamado como resultado de devoluciones de llamadas de eventos de entrada.

Si este segmento es grande

Los valores altos en esta área suelen ser el resultado de demasiado trabajo o de trabajos muy complejos, que tienen lugar dentro de las devoluciones de llamada de eventos de controladores de entrada. Dado que estas devoluciones de llamada siempre ocurren en el subproceso principal, las soluciones a este problema se centran en optimizar el trabajo directamente o en descargar el trabajo en un subproceso diferente.

El desplazamiento por un LazyColumn o un LazyRow también puede aparecer en esta fase. Una vez que el toque del usuario se considera un desplazamiento, la lista diferida consume eventos táctiles para componer y diseñar elementos de forma dinámica. Si tu app realiza un trabajo personalizado en respuesta a los cambios en la posición de desplazamiento, es importante que esta operación sea lo más rápida posible para evitar la pérdida de fotogramas. Las herramientas de generación de perfiles, como el Generador de perfiles de CPU en Android Studio o Perfetto, pueden ayudarte a investigar más a fondo. Consulta la descripción general del registro del sistema para obtener más información.

Animaciones

La fase de animaciones muestra cuánto tiempo llevó evaluar todos los estados de animación que se ejecutaban en ese fotograma. Algunas APIs de animación comunes en Compose son animate*AsState, Transition y Animatable. Además, el recomponedor se ejecuta durante esta fase para procesar los cambios de estado de la instantánea y actualizar las composiciones. Esto significa que la sobrecarga de recomposición suele aparecer directamente en la etapa de animación.

Para las IU de Jetpack Compose, incluye la biblioteca de Compose Runtime Tracing para ver seguimientos de composición detallados junto con los eventos del sistema.

Si este segmento es grande

Los valores altos en esta área suelen ser el resultado de un trabajo que se está ejecutando debido a cambios de estado provocados por la animación. Por ejemplo, una animación de deslizamiento, que mueve tu LazyColumn o LazyRow, provoca una composición, una medición y una asignación rápidas de nuevos elementos de la lista.

Medir

Para dibujar tus elementos componibles en la pantalla, Android ejecuta tres fases en los nodos de diseño de tu árbol de IU.

Primero, el sistema mide los nodos de diseño. Cada elemento componible tiene restricciones y modificadores específicos que describen los límites de tamaño del objeto en la pantalla. Algunos elementos componibles pueden tener un tamaño fijo específico; otros tienen un tamaño que se adapta a las restricciones que pasa el contenedor de diseño principal.

En segundo lugar, el sistema coloca los nodos de diseño. Una vez que Compose calcula los tamaños de los nodos secundarios durante la fase de medición, puede continuar con la fase de colocación, en la que establece el tamaño y la posición de los nodos de diseño en la pantalla.

El sistema siempre realiza este diseño de una sola pasada para mayor eficiencia. Cuando se invalida un diseño componible, Compose mide ese nodo específico y solo propaga las actualizaciones de diseño a las jerarquías superiores si el elemento secundario cambia su tamaño o sus restricciones.

Si este segmento es grande

Un segmento grande en esta área significa que la app dedica demasiado tiempo a la fase de diseño, que consiste en posicionar y determinar el tamaño de los nodos de diseño. Estas operaciones incluyen la ejecución de modificadores de medición y posición para elementos componibles, lo que puede retrasar la preparación de los fotogramas si el árbol de diseño es demasiado complejo. En estos casos, abordar el rendimiento implica comparar tu app de Compose y seguir las prácticas recomendadas de rendimiento.

Usa el Generador de perfiles de CPU en Android Studio o Perfetto para inspeccionar los pases de diseño y detectar cuellos de botella. Consulta la Descripción general del registro del sistema para obtener más información.

Dibuja

La etapa de generación traduce las operaciones de renderización, como generar un fondo, una forma o un texto, en una secuencia de comandos de generación nativos. El sistema captura estos comandos en una lista de visualización para la ejecución de la GPU.

La barra de generación registra cuánto tiempo se tarda en completar la captura de los comandos en la lista de visualización para todos los nodos de diseño que debían actualizarse en la pantalla de este fotograma. El tiempo medido también se aplica a cualquier lógica de dibujo personalizada que puedas tener dentro de los modificadores de dibujo o un elemento componible de Canvas.

Si este segmento es grande

En términos simplificados, esta métrica muestra el tiempo que se tardó en ejecutar todos los comandos de dibujo para cada nodo de diseño invalidado. Esta medición incluye el tiempo dedicado a enviar estos comandos a los nodos secundarios y los elementos de diseño vectoriales. Por este motivo, cuando veas este pico de barra, la causa podría ser que muchos elementos componibles se invalidaron de repente. La invalidación hace que sea necesario volver a ejecutar los comandos de dibujo y regenerar las listas de visualización de los nodos de diseño. De forma alternativa, un tiempo prolongado puede ser el resultado de algunos elementos componibles o lienzos personalizados que tienen una lógica muy compleja en su implementación de DrawScope.

Además, Compose suele controlar sus pasos internos de medición y diseño dentro de lo que la plataforma considera la fase de dibujo. Por lo tanto, una barra de Draw elevada puede deberse a operaciones de diseño o medición internas costosas o excesivas, en lugar de solo a comandos de dibujo. Si tienes dudas, captura un registro de Perfetto para ver si la sobrecarga proviene de rutinas de dibujo o de pases de diseño y medición de Compose.

Subir

La métrica de carga representa el tiempo que se tarda en transferir los objetos del mapa de bits de la memoria de la CPU a la GPU en el fotograma actual.

Como procesadores diferentes, la CPU y la GPU tienen diferentes áreas de RAM dedicadas al procesamiento. Cuando generas un mapa de bits en Android, el sistema transfiere el mapa de bits a la memoria de la GPU antes de que la GPU pueda mostrarlo en la pantalla. Luego, la GPU almacena en caché el mapa de bits para que el sistema no necesite transferir los datos nuevamente, a menos que se expulse la textura de la caché de textura de la GPU.

Nota: En dispositivos Lollipop, esta etapa es morada.

Si este segmento es grande

Todos los recursos de un fotograma deben residir en la memoria de la GPU antes de que puedan usarse para generar un fotograma. Esto significa que un valor alto para esta métrica podría implicar una gran cantidad de pequeñas cargas de recursos o una pequeña cantidad de recursos muy grandes. Un caso común es cuando una app muestra un único mapa de bits aproximado al tamaño de la pantalla. Otro caso es cuando una app muestra una gran cantidad de miniaturas.

Para reducir esta barra, puedes emplear algunas técnicas, como las siguientes:

  • Asegúrate de que las resoluciones de mapa de bits no sean mucho más grandes que el tamaño en el que se mostrarán. Por ejemplo, evita mostrar una imagen de 1024 x 1024 como una imagen de 48 x 48.
  • Aprovecha las bibliotecas modernas, como Coil, para precargar asincrónicamente un mapa de bits antes de la siguiente fase de sincronización.

Ejecución de comandos

El segmento de ejecución de comandos representa el tiempo que lleva ejecutar todos los comandos necesarios para generar listas de visualización en la pantalla.

Para que el sistema genere listas de visualización en la pantalla, envía los comandos necesarios a la GPU. Por lo general, realiza esta acción a través de la API de OpenGL ES.

Este proceso lleva tiempo, ya que el sistema realiza la transformación final y el recorte para cada comando antes de enviar el comando a la GPU. Luego, se genera una sobrecarga adicional en el lado de la GPU, que calcula los comandos finales. Estos comandos incluyen transformaciones finales y recortes adicionales.

Si este segmento es grande

El tiempo invertido en esta etapa es una medición directa de la complejidad y la cantidad de listas de visualización que el sistema renderiza en un fotograma determinado. Por ejemplo, tener muchas operaciones de generación podría aumentar este tiempo, especialmente en casos en los que hay un pequeño costo inherente para cada generación. Por ejemplo:

for (i in 0 until 1000) {
    canvas.drawPoint()
}

Esta ejecución es mucho más costosa que la de:

canvas.drawPoints(thousandPointArray)

No siempre existe una correlación de 1:1 entre la ejecución de comandos y la generación de listas de visualización. A diferencia de la barra de comandos de ejecución, que captura el tiempo que lleva enviar comandos de dibujo a la GPU, la métrica de generación representa el tiempo que se tardó en capturar los comandos emitidos en la lista de visualización.

Esta diferencia surge porque el sistema almacena en caché las listas de visualización siempre que sea posible. Como resultado, hay situaciones en las que un desplazamiento, una transformación o una animación requieren que el sistema vuelva a enviar una lista de visualización, pero no tenga que reconstruirla realmente, es decir, recuperar los comandos de generación, desde cero. Como resultado, puedes ver una barra de ejecución de comandos alta sin ver una barra de comandos de generación alta.

Intercambio de búferes

Una vez que Android termina de enviar su lista de visualización a la GPU, el sistema ejecuta un comando final para indicarle al controlador de gráficos que se llevó a cabo con el fotograma actual. En este punto, el controlador finalmente puede presentar la imagen actualizada en la pantalla.

Si este segmento es grande

Es importante comprender que la GPU ejecuta el trabajo en paralelo con la CPU. El sistema Android emite comandos de generación a la GPU y, luego, pasa a la siguiente tarea. La GPU lee esos comandos de generación de una cola y los procesa.

En situaciones en las que la CPU emite comandos más rápido de lo que la GPU los consume, es posible que se llene la cola de comunicaciones entre los procesadores. Cuando esto ocurre, la CPU se bloquea y espera hasta que haya espacio en la cola para colocar el siguiente comando. Este estado de cola completa se produce con frecuencia durante la etapa de intercambio de búferes, porque en ese punto ya se ejecutaron comandos de un fotograma completo.

La clave para mitigar este problema es reducir la complejidad del trabajo que ocurre en la GPU, de manera similar a lo que se haría para la fase de emisión de comandos.

Varios

Además del tiempo que le toma al sistema de renderización realizar su trabajo, hay un conjunto adicional de trabajo que tiene lugar en el subproceso principal y no tiene nada que ver con la renderización. El tiempo que este trabajo consume se registra como tiempo misceláneo. El tiempo misceláneo generalmente representa el trabajo que podría estar teniendo lugar en el subproceso de IU entre dos fotogramas consecutivos de renderización.

Si este segmento es grande

Si este valor es alto, es probable que la app tenga devoluciones de llamada, intents u otro trabajo ejecutándose en otro subproceso. Las herramientas como el Generador de perfiles de CPU en Android Studio o Perfetto pueden proporcionar visibilidad de las tareas que se ejecutan en el subproceso principal. Esta información puede ayudarte a mejorar el rendimiento. Consulta la Descripción general del registro del sistema para obtener más información.