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 saber qué optimizar para mejorar 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 allí. Antes de leer esta página, debes familiarizarte con la información presentada en Profile GPU Rendering. 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.

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.

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

Una vez que comprendas lo que significa cada color, puedes enfocarte en aspectos específicos de tu app para intentar optimizar el rendimiento de su renderización.

Etapas y significados

En esta sección, se explica qué sucede durante cada etapa correspondiente a un color en la Figura 2, 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.

También vale la pena destacar que el desplazamiento RecyclerView puede aparecer en esta fase. RecyclerView se desplaza inmediatamente cuando consume el evento táctil. Como resultado, puedes aumentar o propagar nuevas vistas de elementos. Por este motivo, es importante que esta operación sea lo más rápida posible. Las herramientas de creación de perfiles, como Traceview o Systrace, pueden ayudarte a obtener más detalles.

Animación

La fase de animaciones muestra cuánto tiempo llevó evaluar todos los animadores que se ejecutaban en ese fotograma. Los animadores más comunes son ObjectAnimator, ViewPropertyAnimator y las transiciones.

Si este segmento es grande

Los valores altos en esta área, en general, son el resultado de un trabajo que se está ejecutando debido a algún cambio en la propiedad de la animación. Por ejemplo, una animación de desplazamiento, que mueve tu ListView o RecyclerView, provoca un gran aumento y una gran propagación de vistas.

Medición/diseño

Para generar los elementos de la vista en la pantalla, Android ejecuta dos operaciones específicas en los diseños y las vistas de la jerarquía de vistas.

Primero, el sistema mide los elementos de la vista. Cada vista y diseño tiene datos específicos que describen el tamaño del objeto en la pantalla. Algunas vistas pueden tener un tamaño específico; otras tienen un tamaño que se adapta al tamaño del contenedor de diseño de nivel superior.

En segundo lugar, el sistema presenta los elementos de la vista. Una vez que el sistema calcula los tamaños de las vistas secundarias, puede continuar con el diseño, el tamaño y el posicionamiento de las vistas en la pantalla.

El sistema realiza la medición y el diseño no solo para las vistas que se dibujarán, sino también para las jerarquías principales de esas vistas, hasta la vista raíz.

Si este segmento es grande

Si la app dedica mucho tiempo por fotograma en esta área, en general, suele deberse al gran volumen de vistas que se deben mostrar o a problemas como la doble imposición en el lugar equivocado de tu jerarquía. En cualquiera de estos casos, abordar el rendimiento implica mejorar el rendimiento de las jerarquías de vistas.

El código que hayas agregado a onLayout(boolean, int, int, int, int) o onMeasure(int, int) también puede causar problemas de rendimiento. Traceview y Systrace pueden ayudarte a examinar las pilas de llamadas para identificar los problemas que pueda tener tu código.

Generación

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

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 todas las vistas que debían actualizarse en la pantalla de este fotograma. El tiempo promedio se aplica a cualquier código que hayas agregado a los objetos de la IU en tu app. Algunos ejemplos de este tipo de código son onDraw(), dispatchDraw() y los diversos draw ()methods que pertenecen a las subclases de la clase Drawable.

Si este segmento es grande

En términos simplificados, esta métrica muestra el tiempo que se tardó en ejecutar todas las llamadas a onDraw() para cada vista invalidada. Esta medición incluye el tiempo dedicado a enviar los comandos de generación a los elementos secundarios y los elementos de diseño que puedan estar presentes. Por este motivo, cuando veas este aumento repentino, la causa podría ser que muchas vistas se invalidaron de repente. La invalidación hace que sea necesario regenerar las listas de visualización de las vistas. De forma alternativa, un tiempo prolongado puede ser el resultado de algunas vistas personalizadas que tienen una lógica muy compleja en sus métodos onDraw().

Sincronización/carga

La métrica de sincronización y carga representa el tiempo que se tarda en transferir los objetos del mapa de bits de la memoria de la CPU a la GPU durante 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, tu app debe evitar mostrar una imagen de 1024 x 1024 como una imagen de 48 x 48.
  • Aprovecha prepareToDraw() 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:

Kotlin

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

Java

for (int i = 0; i < 1000; i++) {
    canvas.drawPoint()
}

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

Kotlin

canvas.drawPoints(thousandPointArray)

Java

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 los comandos de ejecución, que capturan el tiempo que lleva enviar comandos de ejecución 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 la barra de "ejecución de comandos" sin ver la barra de comandos de generación.

Búferes de procesamiento/intercambio

Una vez que Android termina de enviar toda 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 “ejecució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 la 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 seguimiento de métodos o Systrace pueden proporcionar visibilidad de las tareas que se ejecutan en el subproceso principal. Esta información puede ayudarte a mejorar el rendimiento.