Que recherchez-vous ?
Engine & platform

Comprendre la mémoire dans Unity WebGL

MARCO TRIVELLATO / UNITY TECHNOLOGIESContributor
Sep 20, 2016|16 Min
Comprendre la mémoire dans Unity WebGL
Cette page a été traduite automatiquement pour faciliter votre expérience. Nous ne pouvons pas garantir l'exactitude ou la fiabilité du contenu traduit. Si vous avez des doutes quant à la qualité de cette traduction, reportez-vous à la version anglaise de la page web.
Depuis la sortie de Unity WebGL, nous avons fait beaucoup d'efforts pour optimiser la consommation de mémoire. Nous avons également expliqué le fonctionnement de la mémoire dans WebGL dans le manuel et dans nos présentations à Unite Europe 2015 et Unite Boston 2015. Cependant, comme il s'agit toujours d'un sujet brûlant dans nos conversations avec les clients, nous avons réalisé que nous devions en parler davantage. J'espère que ce billet répondra à certaines des questions les plus fréquemment posées.
En quoi Unity WebGL est-il différent des autres plateformes ?

Certains utilisateurs sont déjà familiarisés avec les plateformes où la mémoire est limitée. Pour d'autres, qui viennent de l'ordinateur de bureau ou du WebPlayer, cela n'a jamais été un problème jusqu'à présent.

Il est relativement facile de cibler les plates-formes de console à cet égard, puisque l'on connaît exactement la quantité de mémoire disponible. Cela vous permet de budgétiser votre mémoire et de garantir le bon fonctionnement de votre contenu. Sur les plateformes mobiles, les choses sont un peu plus compliquées en raison du grand nombre d'appareils différents qui existent, mais au moins vous pouvez choisir les spécifications les plus basses et décider de mettre sur liste noire les appareils bas de gamme au niveau de la place de marché.

Sur le web, c'est tout simplement impossible. Dans l'idéal, tous les utilisateurs finaux disposeraient de navigateurs 64 bits et de tonnes de mémoire, mais c'est loin d'être le cas. De plus, il n'y a aucun moyen de connaître les spécifications du matériel sur lequel votre contenu est exécuté. Vous connaissez le système d'exploitation, le navigateur et pas grand-chose d'autre. Enfin, l'utilisateur final peut exécuter votre contenu WebGL ainsi que d'autres pages web. C'est pourquoi il s'agit d'un problème difficile.

Vue d'ensemble

Voici un aperçu de la mémoire lors de l'exécution d'un contenu Unity WebGL dans le navigateur :

image04

Cette image montre qu'en plus du Unity Heap, le contenu Unity WebGL nécessitera des allocations supplémentaires dans la mémoire du navigateur. Il est très important de comprendre cela, afin d'optimiser votre projet et de minimiser le taux d'abandon des utilisateurs.

Comme vous pouvez le voir sur l'image, il y a plusieurs groupes d'allocations : DOM, Unity Heap, Asset Data et Code qui seront conservés en mémoire une fois la page web chargée. D'autres, comme les lots de ressources, WebAudio et Memory FS, varient en fonction de ce qui se passe dans votre contenu (par exemple : téléchargement de lots de ressources, lecture audio, etc.)

Au moment du chargement, il y a également plusieurs allocations temporaires du navigateur pendant l'analyse et la compilation d'asm.js qui causent parfois des problèmes de sortie de mémoire pour certains utilisateurs de navigateurs 32 bits.

Tas d'unité

En général, le tas Unity est la mémoire contenant tous les objets, composants, textures, shaders, etc. spécifiques au jeu Unity.

Avec WebGL, la taille du tas Unity doit être connue à l'avance pour que le navigateur puisse lui allouer de l'espace et, une fois allouée, la mémoire tampon ne peut ni diminuer ni augmenter.

Le code responsable de l'allocation du tas Unity est le suivant :

buffer = new ArrayBuffer(TOTAL_MEMORY);

Ce code se trouve dans le fichier build.js généré et sera exécuté par la VM JS du navigateur.

TOTAL_MEMORY est défini par la taille de la mémoire WebGL dans les paramètres du lecteur. La valeur par défaut est de 256 Mo, mais c'est une valeur arbitraire que nous avons choisie. En fait, un projet vide ne nécessite que 16 Mo.

Cependant, le contenu réel nécessitera probablement plus, quelque chose comme 256 ou 386 mégaoctets dans la plupart des cas. Gardez à l'esprit que plus la mémoire nécessaire est importante, moins les utilisateurs finaux seront en mesure de l'utiliser.

Mémoire du code source/compilé

Avant que le code ne puisse être exécuté, il faut qu'il le soit :

téléchargée.

copié dans un bloc de texte.

compilés.

Il faut tenir compte du fait que chacune de ces étapes nécessitera une certaine quantité de mémoire :

  • Le tampon de téléchargement est temporaire, mais le code source et le code compilé sont conservés en mémoire.
  • La taille du tampon téléchargé et du code source correspond à la taille des fichiers js non compressés générés par Unity. Pour estimer la quantité de mémoire nécessaire à leur fonctionnement :
  • créer une version de compilation (release build)
  • renommer jsgz et datagz en *.gz et les décompresser avec un outil de compression
  • leur taille non comprimée sera également leur taille dans la mémoire du navigateur.
  • La taille du code compilé dépend du navigateur.

Une optimisation facile est d'activer l'option Strip Engine Code pour que votre compilation n'inclue pas le code natif du moteur dont vous n'avez pas besoin (par exemple : Le module physique 2d sera supprimé si vous n'en avez pas besoin). Remarque : Remarque : Le code géré est toujours dépouillé.

Gardez à l'esprit que la prise en charge des exceptions et les plugins tiers vont contribuer à la taille de votre code. Cela dit, nous avons vu des utilisateurs qui ont besoin de livrer leurs titres avec des contrôles de nullité et des contrôles de limites de tableaux, mais qui ne veulent pas encourir la surcharge de mémoire (et de performance) d'une prise en charge complète des exceptions. Pour ce faire, vous pouvez passer --emit-null-checks et --enable-array-bounds-check à il2cpp, par exemple via le script de l'éditeur :

PlayerSettings.SetPropertyString("additionalIl2CppArgs", "--emit-null-checks --enable-array-bounds-check") ;

Enfin, n'oubliez pas que les builds de développement produiront un code plus volumineux parce qu'il n'est pas minifié, bien que ce ne soit pas un problème puisque vous n'allez livrer que des builds de version à l'utilisateur final... n'est-ce pas ? ;-)

Données sur les actifs

Sur d'autres plateformes, une application peut simplement accéder à des fichiers sur le stockage permanent (disque dur, mémoire flash, etc.). Sur le web, cela n'est pas possible car il n'y a pas d'accès à un véritable système de fichiers. Par conséquent, une fois que les données Unity WebGL (fichier .data) sont téléchargées, elles sont stockées en mémoire. L'inconvénient est qu'il nécessitera plus de mémoire que les autres plateformes (à partir de la version 5.3, le fichier .data est stocké en mémoire avec une compression lz4). Par exemple, voici ce que le profiler me dit à propos d'un projet qui génère un fichier de données de ~40mb (avec 256mb Unity Heap) :

Unity Profiler

Que contient le fichier .data ? C'est une collection de fichiers que unity génère : data.unity3d (toutes les scènes, leurs assets dépendants et tout ce qui se trouve dans le dossier Resources), unity_default_resources et quelques petits fichiers nécessaires au moteur.

Pour connaître la taille totale exacte des ressources, jetez un oeil à data.unity3d dans Temp\StagingArea\Data après avoir construit pour WebGL (rappelez-vous que le dossier Temp sera supprimé lorsque l'éditeur Unity sera fermé). Vous pouvez également examiner les décalages transmis à DataRequest dans UnityLoader.js :

new DataRequest(0, 39065934, 0, 0).open('GET', '/data.unity3d') ;

(ce code peut changer en fonction de la version de Unity - il s'agit de la version 5.4)

Système de fichiers de la mémoire

Bien qu'il n'y ait pas de véritable système de fichiers, comme nous l'avons mentionné précédemment, votre contenu Unity WebGL peut toujours lire/écrire des fichiers. La principale différence par rapport aux autres plates-formes est que toute opération d'E/S de fichier est en fait une lecture/écriture en mémoire. Ce qu'il est important de savoir, c'est que ce système de fichiers en mémoire ne vit pas dans le tas d'Unity, et qu'il nécessitera donc de la mémoire supplémentaire. Par exemple, disons que j'écris un tableau dans un fichier :

var buffer = new byte [10*1014*1024] ;

File.WriteAllBytes(Application.temporaryCachePath + "/buffer.bytes", buffer) ;

Le fichier sera écrit dans la mémoire, ce qui est également visible dans le profileur du navigateur :

Profileur de navigateur

Notez que la taille du tas d'Unity est de 256 mégaoctets.

De même, comme le système de cache d'Unity dépend du système de fichiers, l'ensemble du stockage du cache est sauvegardé en mémoire. Qu'est-ce que cela signifie ? Cela signifie que des choses comme les PlayerPrefs et les Asset Bundles mis en cache seront également persistants en mémoire, en dehors du Heap d'Unity.

Ensembles d'actifs

L'une des meilleures pratiques les plus importantes pour réduire la consommation de mémoire sur webgl, est d'utiliser les Asset Bundles (si vous n'êtes pas familier avec eux, vous pouvez consulter le manuel ou ce tutoriel pour commencer). Cependant, selon la manière dont ils sont utilisés, il peut y avoir un impact significatif sur la consommation de mémoire (à l'intérieur du tas Unity et à l'extérieur également) qui fera que votre contenu ne fonctionnera pas sur les navigateurs 32 bits.

Maintenant que vous savez qu'il est indispensable d'utiliser les asset bundles, que faites-vous ? Regrouper tous vos actifs en un seul ensemble d'actifs ?

NON ! Même si cela permet de réduire la pression au moment du chargement de la page web, vous devrez toujours télécharger un ensemble de ressources (potentiellement très volumineux), ce qui provoquera un pic de mémoire. Examinons la mémoire avant le téléchargement de l'AB :

Examinons la mémoire avant le téléchargement de l'ensemble des actifs.

Comme vous pouvez le voir, 256 mégaoctets sont alloués au tas Unity. Et ce, après avoir téléchargé un ensemble d'actifs sans mise en cache :

Après avoir téléchargé un ensemble d'actifs sans mise en cache

Ce que vous voyez maintenant est un tampon supplémentaire, approximativement de la même taille que le paquet sur le disque (~65mb), qui a été alloué par XHR. Ce n'est qu'un tampon temporaire, mais il provoquera un pic de mémoire pendant plusieurs images jusqu'à ce qu'il soit ramassé.

Que faire alors pour minimiser les pics de mémoire ? Créer un ensemble d'actifs pour chaque actif ? Bien que l'idée soit intéressante, elle n'est pas très pratique.

En fin de compte, il n'y a pas de règle générale et vous devez vraiment faire ce qui est le plus logique pour votre projet.

Enfin, n'oubliez pas de décharger le groupe d'actifs via AssetBundle.Unload lorsque vous avez terminé.

Mise en cache de l'ensemble des actifs

La mise en cache de l'Asset Bundle fonctionne comme sur les autres plateformes, il suffit d'utiliser WWW.LoadFromCacheOrDownload. Il y a cependant une différence assez importante, à savoir la consommation de mémoire. Sur Unity WebGL, la mise en cache AB s'appuie sur IndexedDB pour stocker les données de manière persistante, le problème étant que les entrées dans la base de données existent également dans le système de fichiers de la mémoire.

Examinons une capture de mémoire avant le téléchargement d'un ensemble d'actifs à l'aide de LoadFromCacheOrDownload :

capture de la mémoire avant le téléchargement d'un ensemble d'actifs à l'aide de LoadFromCacheOrDownload

Comme vous pouvez le voir, 512mb sont utilisés pour le Heap d'Unity et ~4mb pour d'autres allocations. Ceci après avoir chargé le paquet :

Voici ce qui se passe après le chargement de la liasse

La mémoire supplémentaire requise est passée à ~167mb. C'est la mémoire supplémentaire dont nous avons besoin pour ce paquet de ressources (~64mb paquet compressé). Et ceci après la collecte des déchets de la vm js :

Et ceci après le ramassage des ordures de la vm js

C'est un peu mieux, mais ~85 Mo sont encore nécessaires : la plupart sont utilisés pour mettre en cache le paquet d'actifs dans le système de fichiers en mémoire. C'est de la mémoire que vous ne récupérerez pas, même après avoir déchargé le paquet. Il est également important de se rappeler que lorsque l'utilisateur ouvre votre contenu dans le navigateur une deuxième fois, cette partie de la mémoire est allouée immédiatement, avant même le chargement du paquet.

Pour référence, il s'agit d'un instantané de la mémoire de Chrome :

instantané de la mémoire à partir de Chrome

De même, il existe une autre allocation temporaire liée à la mise en cache, en dehors de la pile Unity, qui est nécessaire à notre système de liasses d'actifs. La mauvaise nouvelle, c'est que nous avons récemment découvert qu'il était beaucoup plus grand que prévu. La bonne nouvelle est que ce problème est corrigé dans les prochaines versions d'Unity 5.5 Beta 4, 5.3.6 Patch 6 et 5.4.1 Patch 2.

Pour les anciennes versions de Unity, dans le cas où votre contenu Unity WebGL est déjà en ligne ou proche de la sortie et que vous ne voulez pas mettre à jour votre projet, une solution de contournement rapide consiste à définir la propriété suivante via un script de l'éditeur :

PlayerSettings.SetPropertyString("emscriptenArgs", " -s MEMFS_APPEND_TO_TYPED_ARRAYS=1", BuildTargetGroup.WebGL) ;

Une solution à plus long terme pour minimiser l'encombrement de la mémoire cache du pack d'actifs est d'utiliser WWW Constructor au lieu de LoadFromCacheOrDownload() ou d'utiliser UnityWebRequest.GetAssetBundle() sans paramètre de hachage/version si vous utilisez la nouvelle API UnityWebRequest.

Utilisez ensuite un autre mécanisme de mise en cache au niveau XMLHttpRequest, qui stocke le fichier téléchargé directement dans indexedDB, en contournant le système de fichiers en mémoire. C'est exactement ce que nous avons développé récemment et qui est disponible sur le magasin d'actifs. N'hésitez pas à l'utiliser dans vos projets et à le personnaliser si nécessaire.

Compression d'un ensemble d'actifs

Dans les versions 5.3 et 5.4, les compressions LZMA et LZ4 sont toutes deux prises en charge. Cependant, même si l'utilisation de LZMA (par défaut) permet de réduire la taille des téléchargements par rapport à LZ4/Uncompressed, elle présente quelques inconvénients pour WebGL : elle provoque des blocages d'exécution notables et nécessite plus de mémoire. Par conséquent, nous recommandons fortement d'utiliser LZ4 ou aucune compression du tout (en fait, la compression LZMA ne sera pas disponible pour WebGL à partir de Unity 5.5), et pour compenser la taille de téléchargement plus importante par rapport à LZMA, vous pouvez vouloir gzip/brotli votre asset bundles et configurer votre serveur en conséquence.

Consultez le manuel pour plus d'informations sur la compression des asset bundle.

WebAudio

L'audio sur Unity WebGL est implémenté différemment. Qu'est-ce que cela signifie pour la mémoire ?

Unity créera des objets AudioBufferspécifiques en JavaScript, afin qu'ils puissent être joués via WebAudio.

Étant donné que les tampons WebAudio vivent en dehors de la pile Unity et ne peuvent donc pas être suivis par le profileur Unity, vous devez inspecter la mémoire à l'aide d'outils spécifiques au navigateur pour voir quelle quantité de mémoire est utilisée pour l'audio. Voici un exemple (en utilisant la page about:memory de Firefox) :

Voici un exemple (en utilisant la page about:memory de Firefox)

Il faut tenir compte du fait que ces tampons audio contiennent des données non compressées, ce qui peut ne pas être idéal pour des clips audio de grande taille (par exemple, de la musique de fond). Pour ceux-là, vous pouvez envisager d'écrire votre propre plugin js afin d'utiliser les balises <audio> à la place. De cette manière, les fichiers audio restent compressés et utilisent donc moins de mémoire.

FAQ
Quelles sont les meilleures pratiques pour réduire l'utilisation de la mémoire ?

En voici un résumé :

Réduire la taille du tas Unity :

Gardez la "taille de la mémoire WebGL" aussi petite que possible.

Réduisez la taille de votre code :

Activer le code moteur à bandes Désactiver les exceptions Essayez d'éviter l'utilisation de plugins tiers

Réduisez la taille de vos données :

Utiliser des regroupements d'actifs Utiliser la compression de texture Crunch

Existe-t-il une stratégie pour déterminer la taille de la mémoire WebGL ?

Oui, la meilleure stratégie est d'utiliser le profileur de mémoire et d'analyser la quantité de mémoire nécessaire à votre contenu, puis de modifier la taille de la mémoire WebGL en conséquence.

Prenons l'exemple d'un projet vide. Le Memory Profiler m'indique que le "Total Used" s'élève à un peu plus de 16MB (cette valeur peut varier selon les versions de Unity) : cela signifie que je dois régler la taille de la mémoire WebGL sur quelque chose de plus grand que cela. Il est évident que le "Total utilisé" sera différent en fonction de votre contenu.

Cependant, si pour une raison quelconque vous ne pouvez pas utiliser le Profiler, vous pouvez simplement continuer à réduire la valeur de la taille de la mémoire WebGL jusqu'à ce que vous trouviez la quantité minimale de mémoire nécessaire pour exécuter votre contenu.

Il est également important de noter que toute valeur qui n'est pas un multiple de 16 sera automatiquement arrondie (au moment de l'exécution) au multiple suivant, car il s'agit d'une exigence de l'Emscripten.

La taille de la mémoire WebGL (mb) déterminera la valeur de TOTAL_MEMOIRE (octets) dans le html généré :

La taille de la mémoire WebGL (mb) déterminera la valeur de TOTAL_MEMOIRE (octets) dans le code html généré.

Ainsi, pour itérer sur la taille du tas sans recompiler le projet, il est recommandé de modifier le code html. Ensuite, une fois que vous avez trouvé une valeur qui vous convient, vous pouvez modifier la taille de la mémoire WebGL dans le projet Unity.

Heureusement, ce n'est pas la seule façon de procéder et le prochain article de blog sur le tas d'Unity tentera d'apporter une meilleure réponse à cette question.

Enfin, rappelez-vous que le profileur d'Unity utilisera une partie de la mémoire du tas allouée, vous devrez donc augmenter la taille de la mémoire WebGL lors du profilage.

Mon programme de construction manque de mémoire, comment puis-je y remédier ?

Cela dépend si c'est Unity qui manque de mémoire ou le navigateur. Le message d'erreur indique quel est le problème et comment le résoudre : "Si vous êtes le développeur de ce contenu, essayez d'allouer plus/moins de mémoire à votre build WebGL dans les paramètres du lecteur WebGL." Vous pouvez ensuite ajuster la taille de la mémoire WebGL en conséquence. Cependant, vous pouvez faire plus pour résoudre le problème de l'OOM. Si vous obtenez ce message d'erreur :

Le message d'erreur indique la nature du problème et la manière de le résoudre.

En plus de ce que dit le message, vous pouvez également essayer de réduire la taille du code et/ou des données. En effet, lorsque le navigateur charge la page web, il essaie de trouver de la mémoire libre pour plusieurs éléments, dont les plus importants sont : le code, les données, le tas de l'unité et le fichier asm.js compilé. Ils peuvent être assez volumineux, en particulier la mémoire de tas de Data et Unity, ce qui peut poser un problème pour les navigateurs 32 bits.

Dans certains cas, même s'il y a suffisamment de mémoire libre, le navigateur échoue parce que la mémoire est fragmentée. C'est pourquoi, parfois, votre contenu peut réussir à se charger après le redémarrage du navigateur.

Dans l'autre cas, lorsque Unity manque de mémoire, un message du type suivant s'affiche

Dans l'autre cas, lorsque Unity n'a plus de mémoire, un message du type

Dans ce cas, vous devez optimiser votre projet Unity.

Comment mesurer la consommation de mémoire ?

Pour analyser la mémoire du navigateur utilisée par votre contenu, vous pouvez utiliser Firefox Memory Tool ou Chrome Heap snapshot. Cependant, sachez qu'ils ne vous montreront pas la mémoire de WebAudio. Pour cela, vous pouvez utiliser la page about:memory dans Firefox : prenez un instantané, puis recherchez "webaudio". Si vous avez besoin de profiler la mémoire via JavaScript, essayez window.performance.memory (Chrome uniquement).

Pour mesurer l'utilisation de la mémoire dans le tas d'Unity, utilisez le profileur d'Unity. Cependant, sachez que vous devrez peut-être augmenter la taille de la mémoire WebGL, afin de pouvoir utiliser le profileur.

En outre, nous avons travaillé sur un nouvel outil qui vous permet d'analyser ce qui se trouve dans votre construction : Pour l'utiliser, créez une version WebGL, puis visitez http://files.unity3d.com/build-report/. Bien que cette fonctionnalité soit disponible depuis Unity 5.4, il convient de noter qu'il s'agit d'un travail en cours et qu'elle est susceptible d'être modifiée ou supprimée à tout moment. Mais pour l'instant, nous le mettons à disposition à des fins de test.

Quelle est la valeur minimale/maximale de WebGL Memory Size ?

Le minimum est de 16. Le maximum est de 2032, mais nous conseillons généralement de rester en dessous de 512.

Est-il possible d'allouer plus de 2032 Mo à des fins de développement ?

Il s'agit d'une limitation technique : 2048 Mo (ou plus) dépassera la taille de l'entier signé 32 bits du TypeArray utilisé pour implémenter le tas Unity en JavaScript.

Pourquoi ne pouvez-vous pas redimensionner le Unity Heap ?

Nous avons envisagé d'utiliser le drapeau emscripten ALLOW_MEMORY_GROWTH pour permettre au Heap d'être redimensionné, mais nous avons décidé de ne pas le faire car cela désactiverait certaines optimisations dans Chrome. Nous devons encore procéder à une véritable analyse comparative de cet impact. Nous pensons que l'utilisation de cette méthode pourrait en fait aggraver les problèmes de mémoire. Si vous avez atteint un point où le tas d'Unity est trop petit pour contenir toute la mémoire requise, et qu'il doit s'agrandir, le navigateur devra allouer un plus grand tas, copier tout ce qui se trouve dans l'ancien tas, et ensuite désallouer l'ancien tas. Ce faisant, il a besoin de mémoire pour le nouveau et l'ancien tas en même temps (jusqu'à ce qu'il ait terminé la copie), ce qui nécessite plus de mémoire totale. L'utilisation de la mémoire sera donc plus importante que lors de l'utilisation d'une taille de mémoire fixe prédéterminée.

Pourquoi un navigateur 32 bits manque-t-il de mémoire sur un système d'exploitation 64 bits ?

Les navigateurs 32 bits se heurtent aux mêmes limitations de mémoire, que le système d'exploitation soit 64 ou 32 bits.

Conclusions

La dernière recommandation est de profiler votre contenu Unity WebGL en utilisant également des outils spécifiques au navigateur, car comme nous l'avons décrit, il y a des allocations en dehors du tas Unity que le profiler d'Unity ne peut pas suivre.

Nous espérons que certaines de ces informations vous seront utiles. Si vous avez d'autres questions, n'hésitez pas à les poser ici ou dans le forum WebGL.

Mise à jour :

Nous avons parlé de la mémoire utilisée pour le code et nous avons mentionné que le code JS source est copié dans un bloc de texte temporaire. Nous avons découvert que le blob n'avait pas été correctement désalloué et qu'il s'agissait donc d'une allocation permanente dans la mémoire du navigateur. Dans about:memory, il est étiqueté comme memory-file-data :

 Dans about:memory, il est étiqueté comme memory-file-data.

Sa taille dépend de la taille du code et, pour les projets complexes, elle peut facilement atteindre 32 ou 64 Mo. Heureusement, ce problème a été corrigé dans les versions 5.3.6 Patch 8, 5.4.2 Patch 1 et 5.5.

En ce qui concerne l'Audio, nous savons que la consommation de mémoire reste un problème : Le streaming audio n'est pas pris en charge actuellement et les fichiers audio sont conservés dans la mémoire du navigateur en tant que fichiers non compressés. Nous avons donc suggéré d'utiliser la balise <audio> pour lire des fichiers audio volumineux. À cette fin, nous avons récemment publié un nouveau paquet Asset Store pour vous aider à minimiser la consommation de mémoire par les sources audio en continu. A voir !