Cómo Perfilamos Aplicaciones WebXR/WebGL
Este artículo abarca métodos para perfilar aplicaciones WebXR y WebGL (independientes del framework subyacente). El perfilado es el primer paso para optimizar tu aplicación. Optimizar sin perfilar es un esfuerzo inútil, como se describe en Cuellos de Botella.
Ten en cuenta que este post no aborda cómo resolver los problemas de rendimiento encontrados. Hay muchos posts de blog y charlas dirigidas a cuellos de botella específicos, pero una vez que sepas cuál es el problema, encontrarás recursos para ello.
Las métricas que vamos a perfilar son el tiempo de CPU, tiempo de GPU, memoria heap de JavaScript, y tiempo de carga de la aplicación. El objetivo es proporcionar la colección más completa de información sobre perfilado de forma gratuita. Y no: “conteo de polígonos” no es una métrica que nos importe.
Contenidos
Cuellos de Botella
Sin encontrar el cuello de botella de tu aplicación, tus esfuerzos por optimizar no serán efectivos.
Ejemplo
Imagina que estás renderizando una escena grande con muchos objetos. Tu aplicación apenas alcanza 60 fps. Puedes optimizar el conteo de vértices de cada objeto y aún así permanecer en 60 fps.
El CPU es responsable de enviar al GPU el comando para renderizar cada objeto (“Draw Call”). En este caso, es mucho más probable que el CPU esté sobrecargado con el trabajo de enviar las llamadas de dibujo, mientras que el GPU simplemente está esperando más trabajo del CPU.
Pero no lo sabemos… hasta que medimos. Eso es para lo que sirve el Perfilado.
Tipos de Cuellos de Botella
Cualquier parte de una aplicación puede causar un cuello de botella. Aquí hay algunos ejemplos de cuellos de botella comunes en orden de frecuencia:
CPU Draw Calls: Demasiadas llamadas al controlador causan demasiado overhead.
Basura: La aplicación está funcionando bien, pero produce basura que provoca interrupciones en intervalos regulares o irregulares.
Lógica del CPU: Tu aplicación no puede producir trabajo para el GPU lo suficientemente rápido. A menudo causado por simulaciones físicas intensas o lanzamientos de rayos.
Sombreado de Fragmentos de GPU: El costo de rendimiento por fragmento es demasiado alto.
Procesamiento de Vértices de GPU: El requerimiento de procesamiento por vértice es muy alto.
Resolución de GPU: Causado por post-procesamiento en GPUs móviles.
Extracción de Vértices de GPU: La memoria para los vértices no se lee lo suficientemente rápido.
Cada uno podría tener sub-cuellos de botella.
Métricas
Las métricas que vamos a perfilar son:
Tiempo de CPU
El trabajo para cualquier aplicación XR se divide entre CPU y GPU. El CPU es responsable de preparar el trabajo gráfico para que el GPU lo renderice y ejecuta la lógica de la aplicación.
Cualquier código JavaScript se ejecutará en el CPU para preparar las llamadas de dibujo, realizar la carga de recursos, simular la física, renderizar audio y calcular las transformaciones del grafo de la escena.
Tiempo de GPU
El GPU es responsable de la carga pesada gráfica. Mientras que el CPU enviará una llamada de dibujo con algo como “dibujar malla X con shader Y con textura Z y parámetros de material W”, el GPU realizará la rasterización real y las transformaciones por vértice para producir los píxeles en la pantalla.
El GPU también es responsable de enviar la imagen final a la pantalla, esperando “V-Sync” que sincroniza la tasa de refresco de la pantalla con la de la aplicación.
Tasa de Fotogramas y V-Sync
Nosotros no usamos la tasa de fotogramas (fotogramas por segundo = fps) para perfilar nuestra aplicación.
Esta métrica solo se enumera para explicar la complejidad de entender cómo el rendimiento interactúa con V-Sync. Es demasiado tosca para hacer juicios útiles con ella.
Considera V-Sync como plazos fijos: 60, 72, 90 o más plazos por segundo (la tasa de fotogramas). Nos referiremos a “hacer V-Sync” como haber renderizado y enviado el fotograma a tiempo para cumplir el plazo.
Cuando no cumples el plazo, todo tu trabajo se descarta y necesitas intentar alcanzar el siguiente plazo en su lugar. En entornos con tasa abierta de fotogramas, esto puede significar que tus controladores te bajen a la mitad de la tasa de fotogramas.
Si tu aplicación es apenas demasiado lenta, esto significa que estarías viendo por ejemplo 30 fps en lugar de 59 fps, contando una historia completamente diferente. Tal vez tu aplicación esté funcionando decentemente con un tiempo de CPU de 3ms y de GPU de 3ms, pero porque el trabajo empieza tarde, te pierdes la V-Sync para tu tasa de fotogramas objetivo–lo que reduce tu tasa de fotogramas efectiva a la mitad. Ningún conteo de vértices, llamada de dibujo, o ninguna optimización clásica te ayudará aquí.
Latencia
La latencia, el tiempo entre la entrada (por ejemplo, el movimiento de la cabeza) y el fotograma terminado, es especialmente importante para VR. Generalmente, las implementaciones de WebXR se encargan de esto por nosotros y podrían programar los callbacks de cuadro un poco más tarde, si no usamos nuestro presupuesto de fotogramas, para reducir la latencia. Tenemos un control limitado sobre esto desde WebXR y, por lo tanto, no lo cubriremos en esta publicación de blog.
Recolección de Basura
JavaScript viene con gestión de memoria que te permite tratar las asignaciones despreocupadamente y sin pensar. De ahí que sea común hacerlo.
Sin la necesidad de gestionar la memoria, pierdes el control para especificar cuándo quieres que se gestione tu memoria. Este proceso se llama “Recolección de Basura” y se producirá en momentos aleatorios en el ciclo de vida de tu aplicación–por ejemplo, cuando acabas de cumplir con V-Sync y estás apenas por enviar tu fotograma.
La recolección de basura puede tomar de 0.1 a 10 ms. Teniendo en cuenta que tu presupuesto usual de fotograma de VR es de 11 ms, absolutamente no quieres que esto suceda en momentos aleatorios.
Entonces, ¿cómo lo evitamos? La única forma es evitar cualquier basura que necesite limpieza. Cuanto menos produzcas, más pequeñas y raras serán las interrupciones de recolección de basura.
Tiempo de Carga de la Aplicación
Al igual que con la carga de sitios web, el compromiso del usuario con el uso de tu aplicación disminuye con cada segundo que pasa esperando por ella. Dado que las aplicaciones WebXR son bastante grandes, podría haber un poco más de buena disposición aquí en comparación con un sitio web, pero no hay necesidad de esto.
Iniciando la aplicación temprano y cargando recursos que no se necesitan inmediatamente más tarde, podemos reducir el tiempo de carga percibido.
Al optimizar los recursos y asegurarnos de que la configuración de nuestro servidor sea óptima, podemos reducir el tiempo de carga en general.
Y usando formatos que necesitan menos análisis, podemos reducir la cantidad de trabajo que el CPU necesita hacer después de descargar los recursos.
Herramientas
En este post, cubriremos las siguientes herramientas:
- Chrome Profiler, [CPU, GPU, Basura]
- Pestaña de Red de Chrome, [Tiempo de Carga]
- OVR Profiler Tool, [CPU, GPU]
- Spector.js extensión del navegador, [CPU, GPU]
- WebGL Disjoint Timer Query, [GPU]
- Wonderland Editor Profiler (solo para Wonderland Engine). [CPU, GPU]
Chrome Profiler
El profiler incorporado de Chrome te permitirá perfilar tu tiempo de CPU de JavaScript, Recolección de Basura, y muy aproximadamente tu tiempo de cuadro de GPU.
Puedes encontrar la pestaña Rendimiento navegando a cualquier sitio web y presionando Ctrl + Shift + C
(Command + Shift + C
en MacOS). Encuentra “Rendimiento” allí.
Para grabar una sesión de perfil, pulsa el botón de grabar en la parte superior izquierda (Ctrl/Command + E
).
3-5 segundos suelen ser completamente suficientes ya que generalmente nos interesan cuadros individuales.
Esto también funciona cuando Depuración Remota en Dispositivos Android como Meta Quest o tu smartphone.
Safari tiene un profiler similar para dispositivos Mac, iOS, y el Apple Vision PRO (por ejemplo, con Safari ejecutándose en el simulador de Apple Vision PRO).
Chrome Memory Profiler
En Recolección de Basura describimos el tartamudeo en aplicaciones que de otro modo funcionarían suavemente.
Para encontrar ese cuello de botella, los navegadores proporcionan una forma de tomar muestras del Heap de JavaScript. Así es como lo habilitas en Chrome desde el Chrome Profiler descrito anteriormente:

Ejemplo
Lo siguiente es un perfil en Meta Quest 2 de Elysian, que se basa en Three.js:
Puedes ver que el “JS Heap” fluctúa entre 14.1 MB y 23.3 MB.
Dondequiera que la memoria caiga repentinamente, encontramos que ocurre la Recolección de Basura. En este caso, el GC es tan grande que retrasa el inicio del siguiente cuadro, causando un cuadro caído:

Como resultado, tenemos que esperar que el cuadro caído sea reproyectado desde el último cuadro, de lo contrario, ocurrirá un tartamudeo significativo. Dado que las animaciones no pueden ser reproyectadas, habrá algo de tartamudeo de cualquier manera.
Pestaña de Red
La pestaña de red de cualquier navegador es una gran herramienta para perfilar el tiempo de carga de tu aplicación.
Puedes encontrar la pestaña de Red navegando a cualquier sitio web y presionando Ctrl + Shift + C
(Command + Shift + C
en MacOS). Encuentra “Red” allí.
Para grabar la actividad de red, recarga la página mientras tienes la pestaña abierta.
Descargas Bloqueadas
Si un recurso no se necesita inmediatamente al iniciar la aplicación, debe cargarse más tarde, ya que los navegadores solo ejecutarán solicitudes paralelas limitadas a la vez (por ejemplo, 6 para Chrome).
Meta Quest Performance HUD
Al perfilar la representación VR en el Meta Quest, puedes usar el Performance HUD.
Se instala más fácilmente a través del Meta Quest Developer Hub.
Esto es especialmente útil para depurar problemas de rendimiento dependientes de la vista, ya que puedes usar el casco mientras obtienes comentarios continuos sobre el rendimiento.
Ejemplo
En el ejemplo a continuación, Ayushman Johri muestra el excelente rendimiento de su “Vintage Study Room” basado en Wonderland Engine utilizando el Meta Quest Performance HUD:
Aquí hay una prueba de rendimiento con el @MetaQuestVR ¡Gráfico de FPS!
— Ayushman Johri ✨ (@AyushmanJohri) 19 de agosto de 2023
Mencionaré que observé más cuadros obsoletos que los que obtendría sin grabación del sistema~
También por elección artística, decidí usar texturas sin comprimir para algunos activos extra detallados ✨
No perfecto pero bastante cercano! pic.twitter.com/MLzWVLHItn
OVR GPU Profiler
Si te encuentras con cuellos de botella de sombreado de vértices o fragmentos, apreciarás una comprensión más clara de lo que toma más tiempo en tus shaders.
El ovrgpuprofiler es una herramienta muy precisa. Si tienes algo de comprensión sobre la memoria y arquitectura del GPU, te brinda mucha perspectiva.
Se ejecuta instalándolo en tu casco Meta Quest a través de adb
y ejecutándolo mediante adb shell
:
1 47 métricas soportadas:
2 1 Relojes / Segundo
3 2 GPU % Bus Ocupado
4 3 % Estancamiento de Extracción de Vértices
5 4 % Estancamiento de Extracción de Textura
6 5 Fallos de Caché de Textura L1 por Píxel
7 6 % Fallo de L1 de Textura
8 7 % Fallo de L2 de Textura
9 8 % Estancado en Memoria del Sistema
10 9 Polígonos Pre-recortados/Segundo
11 10 % Prims Rechazados Trivialmente
12 11 % Prims Recortados
(Fuente: developer.oculus.com)
El ejemplo de salida se verá de la siguiente manera:
1$ adb shell ovrgpuprofiler -r"4,5,6"
2
3% Estancamiento de Extracción de Textura : 2.449
4Fallos de Caché de Textura L1 por Píxel : 0.124
5% Fallo de L1 de Textura : 20.338
6
7% Estancamiento de Extracción de Textura : 2.369
8Fallos de Caché de Textura L1 por Píxel : 0.122
9% Fallo de L1 de Textura : 20.130
10
11% Estancamiento de Extracción de Textura : 2.580
12Fallos de Caché de Textura L1 por Píxel : 0.127
13% Fallo de L1 de Textura
14
15...
(Modificado de: developer.oculus.com)
A menudo leerás que leer y escribir en memoria son generalmente las operaciones más costosas en un programa de shader. Sin embargo, si esto es cierto, depende de qué tan amigables con el caché sean tus operaciones de memoria.
La memoria de caché L1 (“Nivel 1”) es increíblemente rápida. Para asegurarte de que la estás usando, asegúrate de obedecer el principio de localidad: accede a la memoria que está cerca de la memoria que accesaste previamente.
Spector.js
Spector.js es una extensión para navegadores de Chrome y Firefox que permite capturar trazas de marcos WebGL. La herramienta puede mostrar la lista completa de comandos y resumir estadísticas como contadores de vértices y llamadas de dibujo.
Instálalo desde la Tienda de Extensiones de Chrome o la Biblioteca de Complementos de Firefox. Alternativamente, puedes insertar la herramienta mediante una etiqueta HTML para navegadores sin soporte de plugins.

Primero habilita la herramienta a través del botón de complementos en la parte superior, luego graba un cuadro haciendo clic en el botón de grabar rojo.

Puedes ver que Wonderland Engine está dibujando muchos objetos en un total de 11 llamadas de dibujo aquí. Para cualquier otro marco, este número será de 10 a 100 veces más alto.
Disjoint Timer Query
La extensión EXT_disjoint_timer_query es una extensión de WebGL que permite medir el tiempo de GPU de un conjunto de comandos WebGL. Solo es bien soportado en Chrome [agosto de 2023] y si la bandera de “Extensiones de Depuración de WebGL” está habilitada en Chrome.
Dado que todos los comandos se ejecutan de manera asincrónica en el GPU, las mediciones de tiempo también deben programarse de manera asincrónica.
Profiler del Editor de Wonderland
El Editor de Wonderland viene con una herramienta de perfilado incorporada que ayuda a entender dónde podría estar sufriendo el rendimiento de tu aplicación WebXR.
Palabras de Cierre
Cada framework tiene sus propias características de rendimiento particulares. La mayoría están atados a llamadas de dibujo antes que cualquier otra cosa, y luego ligados a fragmentos–¡no hay necesidad de ir a bajo polígono si no estás en 90 fps!
Diseñamos Wonderland Engine desde cero para evitar la mayoría de los cuellos de botella antes mencionados– es gratis hasta 120k USD de ingresos por año. Contáctanos aquí para Licencias Empresariales y Soporte.
Prueba Wonderland Engine ahora y comienza a ahorrar tiempo optimizando.
