Comprender la memoria en Unity WebGL

Algunos usuarios ya están familiarizados con plataformas donde la memoria es limitada. Para otros, que vienen del escritorio o del WebPlayer, esto nunca ha sido un problema hasta ahora.
Apuntar a las plataformas de consola es relativamente fácil en este sentido, ya que sabes exactamente cuánta memoria está disponible. Esto le permite presupuestar su memoria y se garantiza que su contenido se ejecutará. En las plataformas móviles las cosas son un poco más complicadas debido a la gran cantidad de dispositivos diferentes que existen, pero al menos puedes elegir las especificaciones más bajas y decidir incluir en la lista negra los dispositivos de gama baja a nivel de mercado.
En la Web, simplemente no se puede. Lo ideal sería que todos los usuarios finales tuvieran navegadores de 64 bits y toneladas de memoria, pero eso está lejos de la realidad. Además de eso, no hay forma de saber las especificaciones del hardware en el que se ejecuta su contenido. Conoces el sistema operativo, el navegador y no mucho más. Por último, el usuario final podría ejecutar su contenido WebGL así como otras páginas web. Por eso es que éste es un problema difícil.
A continuación se muestra una descripción general de la memoria al ejecutar contenido WebGL de Unity en el navegador:

Esta imagen muestra que además del Unity Heap, el contenido de Unity WebGL requerirá asignaciones adicionales en la memoria del navegador. Es muy importante entender esto para poder optimizar el proyecto y así minimizar la tasa de abandono de los usuarios.
Como se puede ver en la imagen, hay varios grupos de asignaciones: DOM, Unity Heap, datos de activos y código que serán persistentes en la memoria una vez que se cargue la página web. Otros, como Asset Bundles, WebAudio y Memory FS, variarán dependiendo de lo que suceda en su contenido (por ejemplo: descarga de paquete de activos, reproducción de audio, etc.).
En el momento de la carga, también hay varias asignaciones temporales del navegador durante el análisis y la compilación de asm.js que a veces causan problemas de falta de memoria a algunos usuarios en navegadores de 32 bits.
En general, el Unity Heap es la memoria que contiene todos los objetos, componentes, texturas, sombreadores, etc. del juego específicos de Unity.
En WebGL, el tamaño del montón de Unity debe conocerse de antemano para que el navegador pueda asignarle espacio y, una vez asignado, el búfer no puede reducirse ni crecer.
El código responsable de asignar el Unity Heap es el siguiente:
buffer = nuevo ArrayBuffer(MEMORIA_TOTAL);
Este código se puede encontrar en el build.js generado y será ejecutado por la máquina virtual JS del navegador.
TOTAL_MEMORY se define por el tamaño de la memoria WebGL en la configuración del reproductor. El valor predeterminado es 256 MB, pero es solo un valor arbitrario que elegimos. De hecho, un proyecto vacío funciona con sólo 16 MB.
Sin embargo, el contenido del mundo real probablemente necesitará más, algo así como 256 o 386 MB en la mayoría de los casos. Tenga en cuenta que cuanto más memoria se necesite, menos usuarios finales podrán ejecutarla.
Antes de poder ejecutar el código, es necesario:
Descargado.
copiado en un blob de texto.
Compilado.
Tenga en cuenta que cada uno de estos pasos requerirá una gran cantidad de memoria:
- El buffer de descarga es temporal, pero el del código fuente y el del código compilado son persistentes en la memoria.
- El tamaño del buffer descargado y el código fuente son ambos del tamaño del js sin comprimir generado por Unity. Para estimar cuánta memoria será necesaria para ellos:
- hacer una compilación de lanzamiento
- Cambie el nombre de jsgz y datagz a *.gz y descomprímalos con una herramienta de compresión
- Su tamaño sin comprimir también será su tamaño en la memoria del navegador.
- El tamaño del código compilado depende del navegador.
Una optimización fácil es habilitar Strip Engine Code para que su compilación no incluya código de motor nativo que no necesita (por ejemplo: El módulo de física 2D se eliminará si no lo necesita). Nota: Nota: El código administrado siempre se elimina.
Tenga en cuenta que la compatibilidad con excepciones y los complementos de terceros contribuirán al tamaño de su código. Dicho esto, hemos visto usuarios que necesitan enviar sus títulos con controles nulos y de límites de matriz, pero no quieren incurrir en la sobrecarga de memoria (y rendimiento) del soporte completo de excepciones. Para hacer eso, puede pasar --emit-null-checks y --enable-array-bounds-check a il2cpp, por ejemplo a través del script del editor:
PlayerSettings.SetPropertyString("ArgumentosIl2Cppadicionales", "--emit-null-checks --enable-array-bounds-check");
Por último, recuerda que las compilaciones de desarrollo producirán un código más grande porque no está minimizado, aunque eso no es un problema ya que solo vas a enviar compilaciones de lanzamiento al usuario final... ¿verdad? ;-)
En otras plataformas, una aplicación puede simplemente acceder a archivos en el almacenamiento permanente (disco duro, memoria flash, etc.). En la web esto no es posible ya que no hay acceso a un sistema de archivos real. Por lo tanto, una vez que se descargan los datos de Unity WebGL (archivo .data), se almacenan en la memoria. La desventaja es que requerirá memoria adicional en comparación con otras plataformas (a partir de la versión 5.3, el archivo .data se almacena en la memoria comprimida lz4). Por ejemplo, esto es lo que me dice el generador de perfiles sobre un proyecto que genera un archivo de datos de ~40 MB (con un Unity Heap de 256 MB):

¿Qué hay en el archivo .data? Es una colección de archivos que genera Unity: data.unity3d (todas las escenas, sus recursos dependientes y todo en la carpeta Recursos), unity_default_resources y algunos archivos más pequeños que necesita el motor.
Para saber el tamaño total exacto de los activos, eche un vistazo a data.unity3d en Temp\StagingArea\Data después de haber creado para WebGL (recuerde que la carpeta Temp se eliminará cuando se cierre el editor de Unity). Alternativamente, puede mirar las compensaciones pasadas a DataRequest en UnityLoader.js:
nueva DataRequest(0, 39065934, 0, 0).open('GET', '/data.unity3d');
(este código puede cambiar dependiendo de la versión de Unity; esto es de la versión 5.4)
Aunque no existe un sistema de archivos real, como mencionamos anteriormente, el contenido WebGL de Unity aún puede leer/escribir archivos. La principal diferencia en comparación con otras plataformas es que cualquier operación de E/S de archivo realmente leerá/escribirá en la memoria. Lo importante es saber que este sistema de archivos de memoria no vive en el Unity Heap, por lo tanto, requerirá memoria adicional. Por ejemplo, digamos que escribo una matriz en un archivo:
var buffer = nuevo byte [10*1014*1024];
File.WriteAllBytes(Application.temporaryCachePath + "/buffer.bytes", buffer);
El archivo se escribirá en la memoria, lo que también se puede ver en el generador de perfiles del navegador:

Tenga en cuenta que el tamaño del montón de Unity es de 256 MB.
De manera similar, dado que el sistema de almacenamiento en caché de Unity depende del sistema de archivos, todo el almacenamiento de caché está respaldado en la memoria. ¿Qué significa eso? Esto significa que elementos como PlayerPrefs y Asset Bundles almacenados en caché también serán persistentes en la memoria, fuera del Unity Heap.
Una de las mejores prácticas más importantes para reducir el consumo de memoria en webgl es usar Asset Bundles (si no está familiarizado con ellos, puede consultar el manual o este tutorial para comenzar). Sin embargo, dependiendo de cómo se utilicen, puede haber un impacto significativo en el consumo de memoria (dentro del Unity Heap y también fuera de él) que potencialmente hará que su contenido no funcione en navegadores de 32 bits.
Ahora que sabes que realmente necesitas usar paquetes de activos, ¿qué haces? ¿Volcar todos sus activos en un único paquete de activos?
¡NO! Si bien eso reduciría la presión al momento de cargar la página web, aún necesitará descargar un paquete de activos (potencialmente muy grande), lo que provocará un pico de memoria. Veamos la memoria antes de descargar el AB:

Como puedes ver, se asignan 256 MB para el Unity Heap. Y esto es después de descargar un paquete de activos sin almacenamiento en caché:

Lo que ves ahora es un buffer adicional, aproximadamente del mismo tamaño del paquete en el disco (~65mb), que fue asignado por XHR. Este es solo un buffer temporal pero causará un pico de memoria durante varios cuadros hasta que se recolecte la basura.
¿Qué hacer entonces para minimizar los picos de memoria? ¿Crear un paquete de activos para cada activo? Aunque es una idea interesante, no es muy práctica.
La conclusión es que no existe una regla general y realmente debes hacer lo que tenga más sentido para tu proyecto.
Por último, recuerda descargar el paquete de activos a través de AssetBundle.Unload cuando hayas terminado de usarlo.
El almacenamiento en caché de Asset Bundle funciona como en otras plataformas, solo necesita usar WWW.LoadFromCacheOrDownload. Sin embargo, existe una diferencia bastante significativa: el consumo de memoria. En Unity WebGL, el almacenamiento en caché de AB se basa en IndexedDBpara almacenar datos de forma persistente; el problema es que las entradas en la base de datos también existen en el sistema de archivos de memoria.
Veamos una captura de memoria antes de descargar un paquete de activos usando LoadFromCacheOrDownload:

Como puede ver, se utilizan 512 MB para el Unity Heap y ~4 MB para otras asignaciones. Esto es después de cargar el paquete:

La memoria adicional requerida aumentó a ~167 MB. Esa es la memoria adicional que necesitamos para este paquete de activos (paquete comprimido de ~64 MB). Y esto es después de la recolección de basura de la máquina virtual js:

Es un poco mejor, pero todavía se requieren ~85 MB: la mayor parte se utiliza para almacenar en caché el paquete de activos en el sistema de archivos de memoria. Ese es un recuerdo que no vas a recuperar, ni siquiera después de descargar el paquete. También es importante recordar que cuando el usuario abre el contenido en el navegador por segunda vez, esa porción de memoria se asigna de inmediato, incluso antes de cargar el paquete.
Como referencia, esta es una instantánea de memoria de Chrome:

De manera similar, existe otra asignación temporal relacionada con el almacenamiento en caché fuera del Unity Heap, que es necesaria para nuestro sistema de paquetes de activos. La mala noticia es que recientemente descubrimos que es mucho más grande de lo previsto. La buena noticia, sin embargo, es que esto se solucionará en las próximas versiones Beta 4 de Unity 5.5, Parche 6 de 5.3.6 y Parche 2 de 5.4.1.
Para versiones anteriores de Unity, en caso de que su contenido WebGL de Unity ya esté activo o cerca de lanzarse y no desee actualizar su proyecto, una solución rápida es configurar la siguiente propiedad a través del script del editor:
El parámetro de configuración del reproductor se establece en el parámetro de configuración del reproductor.
Una solución a largo plazo para minimizar la sobrecarga de memoria de almacenamiento en caché del paquete de activos es utilizar WWW Constructor en lugar de LoadFromCacheOrDownload() o utilizar UnityWebRequest.GetAssetBundle() sin parámetro hash/versión si está utilizando la nueva API UnityWebRequest .
Luego, utilice un mecanismo de almacenamiento en caché alternativo en el nivel XMLHttpRequest, que almacena el archivo descargado directamente en indexedDB, sin pasar por el sistema de archivos de memoria. Esto es exactamente lo que hemos desarrollado recientemente y está disponible en la tienda de activos. Siéntete libre de usarlo en tus proyectos y personalizarlo si lo necesitas.
En 5.3 y 5.4, se admiten compresiones LZMA y LZ4. Sin embargo, aunque el uso de LZMA (predeterminado) da como resultado un tamaño de descarga más pequeño en comparación con LZ4/Sin comprimir, tiene un par de desventajas en WebGL: provoca bloqueos de ejecución notables y requiere más memoria. Por lo tanto, recomendamos encarecidamente utilizar LZ4 o ninguna compresión (de hecho, la compresión de paquetes de activos LZMA no estará disponible para WebGL a partir de Unity 5.5) y, para compensar el mayor tamaño de descarga en comparación con lzma, es posible que desees comprimir/comprimir en gzip/brotli tus paquetes de activos y configurar tu servidor en consecuencia.
Consulte el manual para obtener más información sobre la compresión de paquetes de activos.
El audio en Unity WebGL se implementa de forma diferente. ¿Qué significa esto para la memoria?
Unity creará objetos AudioBufferespecíficos en el espacio JavaScript, para que puedan reproducirse a través de WebAudio.
Dado que los buffers de WebAudio residen fuera del montón de Unity y, por lo tanto, el generador de perfiles de Unity no puede rastrearlos, es necesario inspeccionar la memoria con herramientas específicas del navegador para ver cuánta memoria se usa para el audio. He aquí un ejemplo (usando la página about:memory de Firefox):

Tenga en cuenta que estos búferes de audio contienen datos sin comprimir, lo que puede no ser ideal para grandes clips de audio (por ejemplo, música de fondo). Para aquellos que quieran considerar escribir su propio complemento js para poder usar etiquetas <audio> en su lugar. De esta manera los archivos de audio permanecen comprimidos, por lo tanto utilizan menos memoria.
He aquí un resumen:
Reducir el tamaño del montón de Unity:
Mantenga el 'tamaño de memoria WebGL' lo más pequeño posible
Reduce el tamaño de tu código:
Habilitar código de motor de banda Deshabilitar excepciones Intente evitar el uso de complementos de terceros
Reducir el tamaño de sus datos:
Utilice paquetes de activos Utilice la compresión de textura Crunch
Sí, la mejor estrategia sería utilizar el generador de perfiles de memoria y analizar cuánta memoria requiere realmente su contenido y luego cambiar el tamaño de la memoria WebGL en consecuencia.
Tomemos como ejemplo un proyecto vacío. El generador de perfiles de memoria me dice que el valor "Total utilizado" equivale a un poco más de 16 MB (este valor puede variar entre versiones de Unity): eso significa que debo configurar el tamaño de memoria WebGL en algo mayor que eso. Obviamente, el "Total utilizado" será diferente según su contenido.
Sin embargo, si por alguna razón no puede utilizar el Profiler, simplemente puede seguir reduciendo el valor del tamaño de memoria WebGL hasta encontrar la cantidad mínima de memoria necesaria para ejecutar su contenido.
También es importante tener en cuenta que cualquier valor que no sea múltiplo de 16 se redondeará automáticamente (en tiempo de ejecución) al siguiente múltiplo, ya que este es un requisito de Emscripten.
La configuración del tamaño de la memoria WebGL (mb) determinará el valor de TOTAL_MEMORY (bytes) en el HTML generado:

Por lo tanto, para iterar sobre el tamaño del montón sin reconstruir el proyecto, se recomienda modificar el html. Luego, una vez que encuentre un valor con el que esté satisfecho, puede cambiar el tamaño de la memoria WebGL en el proyecto Unity.
Afortunadamente, esta no es la única forma y la próxima publicación del blog sobre Unity intentará proporcionar una mejor respuesta a esta pregunta.
Por último, recuerda que el generador de perfiles de Unity utilizará parte de la memoria del montón asignado, por lo que es posible que debas aumentar el tamaño de la memoria WebGL al crear el perfil.
Depende de si Unity se está quedando sin memoria o del navegador. El mensaje de error indicará cuál es el problema y cómo solucionarlo: "Si usted es el desarrollador de este contenido, intente asignar más/menos memoria a su compilación WebGL en la configuración del reproductor WebGL". Luego puedes ajustar la configuración del tamaño de memoria WebGL según corresponda. Sin embargo, hay más cosas que puedes hacer para solucionar el problema OOM. Si recibe este mensaje de error:

Además de lo que dice el mensaje, también puedes intentar reducir el tamaño del código y/o los datos. Esto se debe a que cuando el navegador carga la página web, intentará encontrar memoria libre para varias cosas, las más importantes: código, datos, montón de unidad y asm.js compilado. Pueden ser bastante grandes, especialmente la memoria de almacenamiento dinámico de datos y Unity, lo que puede ser un problema para los navegadores de 32 bits.
En algunos casos, incluso aunque haya suficiente memoria libre, el navegador seguirá fallando porque la memoria está fragmentada. Es por eso que, a veces, es posible que tu contenido pueda cargarse después de reiniciar el navegador.
El otro escenario, cuando Unity se queda sin memoria, mostrará un mensaje como el siguiente:

En este caso necesitas optimizar tu proyecto Unity.
Para analizar la memoria del navegador utilizada por tu contenido, puedes utilizar la herramienta de memoria de Firefox o la instantánea de montónde Chrome. Sin embargo, tenga en cuenta que no le mostrarán la memoria de WebAudio, para eso puede usar la página about:memory en Firefox: tome una captura de pantalla y luego busque “webaudio”. Si necesita perfilar la memoria a través de JavaScript, pruebe window.performance.memory (solo Chrome).
Para medir el uso de memoria dentro del Unity Heap, utilice Unity Profiler. Sin embargo, tenga en cuenta que es posible que necesite aumentar el tamaño de la memoria WebGL para poder utilizar el generador de perfiles.
Además, hay una nueva herramienta en la que hemos estado trabajando que te permite analizar lo que hay en tu compilación: Para usarlo, cree una compilación WebGL y luego visite http://files.unity3d.com/build-report/. Si bien esto está disponible a partir de Unity 5.4, tenga en cuenta que esta funcionalidad es un trabajo en progreso y está sujeta a cambios o eliminación en cualquier momento. Pero por ahora lo ponemos a disposición para fines de prueba.
16 es el mínimo. El máximo es 2032, sin embargo, generalmente aconsejamos mantenerse por debajo de 512.
Esta es una limitación técnica: 2048 MB (o más) desbordarán el tamaño del entero con signo de 32 bits del TypeArray utilizado para implementar el montón de Unity en JavaScript.
Hemos estado considerando usar el indicador emscripten ALLOW_MEMORY_GROWTH para permitir que se cambie el tamaño del Heap, pero hasta ahora decidimos no hacerlo porque hacerlo deshabilitaría algunas optimizaciones en Chrome. Todavía tenemos que hacer una evaluación comparativa real sobre el impacto de esto. Creemos que usar esto podría en realidad empeorar los problemas de memoria. Si ha llegado a un punto en el que el montón de Unity es demasiado pequeño para acomodar toda la memoria requerida y necesita crecer, entonces el navegador tendrá que asignar un montón más grande, copiar todo del montón anterior y luego desasignar el montón anterior. Al hacerlo, necesita memoria tanto para el montón nuevo como para el antiguo al mismo tiempo (hasta que termina de copiar), lo que requiere más memoria total. Por lo tanto, el uso de memoria sería mayor que cuando se utiliza un tamaño de memoria fijo predeterminado.
Los navegadores de 32 bits se encontrarán con las mismas limitaciones de memoria independientemente de si el sistema operativo es de 64 o 32 bits.
La recomendación final es perfilar el contenido WebGL de Unity también utilizando herramientas específicas del navegador, porque como describimos, hay asignaciones fuera del Unity Heap que el generador de perfiles de Unity no puede rastrear.
Esperemos que alguna de esta información le resulte útil. Si tiene más preguntas, no dude en preguntarlas aquí o en el foro WebGL.
Actualizar:
Hablamos de la memoria utilizada para el código y mencionamos que el código JS fuente se copia en un blob de texto temporal. Lo que descubrimos es que el blob no se desasignó correctamente de manera tan efectiva que se convirtió en una asignación permanente en la memoria del navegador. En about:memory, está etiquetado como memory-file-data:

Su tamaño depende del tamaño del código y para proyectos complejos puede ser fácilmente de 32 o 64 MB. Afortunadamente, esto se ha solucionado en el parche 8 de 5.3.6, el parche 1 de 5.4.2 y el parche 5.5.
En términos de audio, sabemos que el consumo de memoria sigue siendo un problema: Actualmente no se admite la transmisión de audio y los recursos de audio se guardan en la memoria del navegador sin comprimir. Por eso sugerimos utilizar la etiqueta <audio> para reproducir archivos de audio grandes. Para este propósito, recientemente publicamos un nuevo paquete Asset Store para ayudarle a minimizar el consumo de memoria al transmitir fuentes de audio. ¡Conócelo!