Cómo crear perfiles de tu juego de WebGL con la marca about:tracing

Lilli Thompson
Lilli Thompson

Si no puedes medirlo, no puedes mejorarlo.

Lord Kelvin

Para que tus juegos HTML5 se ejecuten más rápido, primero debes identificar los cuellos de botella de rendimiento, pero esto puede ser difícil. Evaluar los datos de fotogramas por segundo (FPS) es un buen comienzo, pero para ver el panorama completo, debes comprender los matices de las actividades de Chrome.

La herramienta about:tracing proporciona las estadísticas que te ayudan a evitar soluciones apresuradas destinadas a mejorar el rendimiento, pero que, en esencia, son conjeturas bien intencionadas. Ahorrarás mucho tiempo y energía, obtendrás una imagen más clara de lo que hace Chrome con cada fotograma y podrás usar esta información para optimizar tu juego.

Hola about:tracing

La herramienta about:tracing de Chrome te brinda una vista de todas las actividades de Chrome durante un período con tanto detalle que podría parecerte abrumadora al principio. Muchas de las funciones de Chrome están instrumentadas para el seguimiento de forma predeterminada, por lo que, sin realizar ninguna instrumentación manual, puedes usar about:tracing para hacer un seguimiento de tu rendimiento. (consulta una sección posterior sobre cómo instrumentar manualmente tu JS)

Para ver la vista de seguimiento, simplemente escribe "about:tracing" en la barra omni (barra de direcciones) de Chrome.

Cuadro multifunción de Chrome
Escribe "about:tracing" en el cuadro multifunción de Chrome

Desde la herramienta de seguimiento, puedes comenzar a grabar, ejecutar el juego durante unos segundos y, luego, ver los datos de seguimiento. Este es un ejemplo de cómo podrían verse los datos:

Resultado de seguimiento simple
Resultado de seguimiento simple

Sí, es bastante confuso. Hablemos de cómo leerlo.

Cada fila representa un proceso al que se le está generando un perfil, el eje de izquierda a derecha indica el tiempo y cada cuadro de color es una llamada a función instrumentada. Hay filas para varios tipos de recursos. Los más interesantes para la generación de perfiles de juegos son CrGpuMain, que muestra lo que hace la unidad de procesamiento gráfico (GPU), y CrRendererMain. Cada registro contiene líneas de CrRendererMain para cada pestaña abierta durante el período de registro (incluida la pestaña about:tracing).

Cuando lees los datos de seguimiento, tu primera tarea es determinar qué fila de CrRendererMain corresponde a tu juego.

Resultado de seguimiento simple destacado
Resultado de seguimiento simple destacado

En este ejemplo, los dos candidatos son 2216 y 6516. Lamentablemente, por el momento, no hay una forma pulida de elegir tu aplicación, excepto buscar la línea que realiza muchas actualizaciones periódicas (o, si instrumentaste manualmente tu código con puntos de seguimiento, buscar la línea que contiene tus datos de seguimiento). En este ejemplo, parece que 6516 ejecuta un bucle principal a partir de la frecuencia de las actualizaciones. Si cierras todas las demás pestañas antes de iniciar el seguimiento, será más fácil encontrar el CrRendererMain correcto. Sin embargo, es posible que haya filas de CrRendererMain para procesos que no sean de tu juego.

Cómo encontrar tu marco

Una vez que hayas encontrado la fila correcta en la herramienta de seguimiento de tu juego, el siguiente paso es encontrar el bucle principal. El bucle principal se ve como un patrón repetitivo en los datos de seguimiento. Puedes navegar por los datos de seguimiento con las teclas W, A, S y D: A y D para moverte hacia la izquierda o la derecha (hacia atrás y hacia adelante en el tiempo) y W y S para acercar y alejar los datos. Se espera que el bucle principal sea un patrón que se repita cada 16 milisegundos si el juego se ejecuta a 60 Hz.

Parecen tres fotogramas de ejecución.
Parece tres fotogramas de ejecución

Una vez que hayas localizado el indicador de estado de tu juego, podrás analizar qué hace exactamente tu código en cada fotograma. Usa W, A, S y D para acercar la imagen hasta que puedas leer el texto en los cuadros de función.

Análisis detallado de un marco de ejecución
Análisis detallado de un marco de ejecución

Esta colección de cuadros muestra una serie de llamadas a función, cada una representada por un cuadro de color. El cuadro que está encima de cada función la llamó, por lo que, en este caso, puedes ver que MessageLoop::RunTask llamó a RenderWidget::OnSwapBuffersComplete, que, a su vez, llamó a RenderWidget::DoDeferredUpdate, y así sucesivamente. Cuando lees estos datos, puedes obtener una vista completa de qué llamó a qué y cuánto tiempo tardó cada ejecución.

Pero aquí es donde se complica un poco. La información que expone about:tracing son las llamadas a función sin procesar del código fuente de Chrome. Puedes hacer conjeturas fundamentadas sobre lo que hace cada función a partir del nombre, pero la información no es del todo fácil de usar. Es útil ver el flujo general de tu fotograma, pero necesitas algo más legible para comprender lo que sucede.

Cómo agregar etiquetas de seguimiento

Afortunadamente, hay una forma sencilla de agregar instrumentación manual a tu código para crear datos de seguimiento: console.time y console.timeEnd.

console.time("update");
update();
console.timeEnd("update");
console.time("render");
update();
console.timeEnd("render");

El código anterior crea cuadros nuevos en el nombre de la vista de seguimiento con las etiquetas especificadas, por lo que, si vuelves a ejecutar la app, verás cuadros de "actualización" y "renderización" que muestran el tiempo transcurrido entre las llamadas de inicio y finalización de cada etiqueta.

Etiquetas agregadas manualmente
Etiquetas agregadas manualmente

Con esto, puedes crear datos de seguimiento legibles por humanos para hacer un seguimiento de los puntos calientes en tu código.

¿GPU o CPU?

Con gráficos acelerados por hardware, una de las preguntas más importantes que puedes hacer durante la generación de perfiles es: ¿Este código está vinculado a la GPU o a la CPU? Con cada fotograma, realizarás un trabajo de renderización en la GPU y una lógica en la CPU. Para comprender qué hace que el juego sea lento, deberás ver cómo se equilibra el trabajo entre los dos recursos.

Primero, busca la línea en la vista de seguimiento llamada CrGPUMain, que indica si la GPU está ocupada en un momento determinado.

Registros de GPU y CPU

Puedes ver que cada fotograma del juego genera trabajo de CPU en CrRendererMain y en la GPU. El seguimiento anterior muestra un caso de uso muy simple en el que la CPU y la GPU están inactivas durante la mayor parte de cada fotograma de 16 ms.

La vista de seguimiento es muy útil cuando tienes un juego que se ejecuta con lentitud y no sabes qué recurso estás agotando. La clave para depurar es observar cómo se relacionan las líneas de la GPU y la CPU. Toma el mismo ejemplo que antes, pero agrega un poco de trabajo adicional en el bucle de actualización.

console.time("update");
doExtraWork();
update(Math.min(50, now - time));
console.timeEnd("update");

console.time("render");
render();
console.timeEnd("render");

Ahora verás un registro que se ve de la siguiente manera:

Registros de GPU y CPU

¿Qué nos dice este registro? Podemos ver que el fotograma en la imagen va de alrededor de 2270 ms a 2320 ms, lo que significa que cada fotograma tarda alrededor de 50 ms (una velocidad de fotogramas de 20 Hz). Puedes ver fragmentos de cuadros de colores que representan la función de renderización junto al cuadro de actualización, pero el marco está completamente dominado por la actualización.

A diferencia de lo que sucede en la CPU, puedes ver que la GPU sigue inactiva durante la mayor parte de cada fotograma. Para optimizar este código, puedes buscar operaciones que se puedan realizar en el código del sombreador y moverlas a la GPU para aprovechar mejor los recursos.

¿Qué sucede cuando el código del sombreador es lento y la GPU está sobrecargada? ¿Qué sucede si quitamos el trabajo innecesario de la CPU y, en su lugar, agregamos algo de trabajo en el código del sombreador de fragmentos? Este es un sombreador de fragmentos innecesariamente costoso:

#ifdef GL_ES
precision highp float;
#endif
void main(void) {
  for(int i=0; i<9999; i++) {
    gl_FragColor = vec4(1.0, 0, 0, 1.0);
  }
}

¿Cómo se ve un seguimiento de código que usa ese sombreador?

Registros de GPU y CPU cuando se usa un código de GPU lento
Registros de GPU y CPU cuando se usa un código de GPU lento

Una vez más, ten en cuenta la duración de un fotograma. Aquí, el patrón repetitivo va de alrededor de 2750 ms a 2950 ms, con una duración de 200 ms (tasa de fotogramas de alrededor de 5 Hz). La línea CrRendererMain está casi completamente vacía, lo que significa que la CPU está inactiva la mayor parte del tiempo, mientras que la GPU está sobrecargada. Esto es un indicador seguro de que tus sombreadores son demasiado pesados.

Si no tuvieras visibilidad sobre qué causaba exactamente la baja velocidad de fotogramas, podrías observar la actualización de 5 Hz y sentir la tentación de ingresar al código del juego y comenzar a intentar optimizar o quitar la lógica del juego. En este caso, eso no sería de ninguna ayuda, ya que la lógica del bucle de juego no es lo que consume tiempo. De hecho, lo que indica este seguimiento es que hacer más trabajo de CPU en cada fotograma sería, en esencia, “gratuito”, ya que la CPU está inactiva, por lo que darle más trabajo no afectará el tiempo que tarda el fotograma.

Ejemplos reales

Ahora veamos cómo se ven los datos de seguimiento de un juego real. Una de las ventajas de los juegos creados con tecnologías web abiertas es que puedes ver qué sucede en tus productos favoritos. Si quieres probar las herramientas de generación de perfiles, puedes elegir tu título de WebGL favorito de Chrome Web Store y generarle un perfil con about:tracing. Este es un ejemplo de seguimiento tomado del excelente juego de WebGL Skid Racer.

Cómo hacer un seguimiento de un juego real
Cómo hacer un seguimiento de un juego real

Parece que cada fotograma tarda alrededor de 20 ms, lo que significa que la velocidad de fotogramas es de alrededor de 50 FPS. Puedes ver que el trabajo está equilibrado entre la CPU y la GPU, pero la GPU es el recurso más demandado. Si quieres ver cómo es generar perfiles de ejemplos reales de juegos de WebGL, prueba algunos de los títulos de Chrome Web Store creados con WebGL, incluidos los siguientes:

Conclusión

Si quieres que tu juego se ejecute a 60 Hz, para cada fotograma, todas tus operaciones deben caber en 16 ms de CPU y 16 ms de GPU. Tienes dos recursos que se pueden usar en paralelo y puedes cambiar el trabajo entre ellos para maximizar el rendimiento. La vista about:tracing de Chrome es una herramienta invaluable para obtener estadísticas sobre lo que realmente hace tu código y te ayudará a maximizar tu tiempo de desarrollo abordando los problemas correctos.

Próximos pasos

Además de la GPU, también puedes hacer un seguimiento de otras partes del entorno de ejecución de Chrome. Chrome Canary, la versión preliminar de Chrome, está instrumentada para hacer un seguimiento de E/S, IndexedDB y varias otras actividades. Lee este artículo de Chromium para comprender mejor el estado actual de los eventos de seguimiento.

Si eres desarrollador de juegos web, asegúrate de mirar el siguiente video. Es una presentación del equipo de defensores de desarrolladores de juegos de Google en GDC 2012 sobre la optimización del rendimiento de los juegos de Chrome: