Verstehen des Speichers in Unity WebGL

Einige Nutzer sind bereits mit Plattformen vertraut, bei denen der Speicherplatz begrenzt ist. Für andere, die vom Desktop oder dem WebPlayer kommen, war dies bisher nie ein Problem.
Die Ausrichtung auf Konsolenplattformen ist in dieser Hinsicht relativ einfach, da man genau weiß, wie viel Speicherplatz zur Verfügung steht. Auf diese Weise können Sie Ihren Arbeitsspeicher haushalten und Ihre Inhalte sind garantiert lauffähig. Bei mobilen Plattformen sind die Dinge etwas komplizierter, weil es so viele verschiedene Geräte gibt, aber zumindest können Sie die niedrigsten Spezifikationen auswählen und entscheiden, dass Geräte mit niedrigeren Spezifikationen auf der Marktplatz-Ebene auf die schwarze Liste gesetzt werden.
Im Internet geht das nicht. Im Idealfall hätten alle Endbenutzer 64-Bit-Browser und jede Menge Arbeitsspeicher, aber das ist weit von der Realität entfernt. Darüber hinaus gibt es keine Möglichkeit, die technischen Daten der Hardware zu erfahren, auf der Ihre Inhalte laufen. Sie kennen das Betriebssystem, den Browser und nicht viel mehr. Schließlich kann der Endbenutzer sowohl Ihre WebGL-Inhalte als auch andere Webseiten ausführen. Deshalb ist dies ein schwieriges Problem.
Hier finden Sie eine Übersicht über den Speicherbedarf bei der Ausführung von Unity WebGL-Inhalten im Browser:

Dieses Bild zeigt, dass Unity WebGL-Inhalte zusätzlich zum Unity Heap weitere Zuweisungen im Speicher des Browsers benötigen. Das ist wirklich wichtig zu verstehen, damit Sie Ihr Projekt optimieren und somit die Abbruchrate der Nutzer minimieren können.
Wie Sie auf dem Bild sehen können, gibt es mehrere Gruppen von Zuweisungen: DOM, Unity Heap, Asset-Daten und Code, die im Speicher verbleiben, sobald die Webseite geladen ist. Andere, wie Asset-Bündel, WebAudio und Memory FS, variieren je nachdem, was in Ihrem Inhalt passiert (z. B.: Download von Asset-Bündeln, Audiowiedergabe usw.).
Zur Ladezeit gibt es auch einige temporäre Zuweisungen des Browsers während des Parsens und Kompilierens von asm.js, die bei einigen Nutzern von 32-Bit-Browsern manchmal Probleme mit dem Speicherplatz verursachen.
Im Allgemeinen ist der Unity Heap der Speicher, der alle Unity-spezifischen Spielobjekte, Komponenten, Texturen, Shader usw. enthält.
Bei WebGL muss die Größe des Unity-Heaps im Voraus bekannt sein, damit der Browser den Speicherplatz zuweisen kann, und sobald er zugewiesen ist, kann der Puffer weder schrumpfen noch wachsen.
Der Code, der für die Zuweisung des Unity Heap verantwortlich ist, lautet wie folgt:
buffer = new ArrayBuffer(TOTAL_MEMORY);
Dieser Code ist in der generierten build.js zu finden und wird von der JS VM des Browsers ausgeführt.
TOTAL_MEMORY wird durch die WebGL-Speichergröße in den Player-Einstellungen definiert. Der Standardwert ist 256 MB, aber das ist nur ein willkürlicher Wert, den wir gewählt haben. Ein leeres Projekt arbeitet sogar mit nur 16 MB.
Für reale Inhalte wird jedoch wahrscheinlich mehr benötigt, in den meisten Fällen etwa 256 oder 386 MB. Denken Sie daran, dass je mehr Speicherplatz benötigt wird, desto weniger Endnutzer in der Lage sein werden, das Programm auszuführen.
Bevor der Code ausgeführt werden kann, muss er erstellt werden:
heruntergeladen.
in einen Textblob kopiert.
zusammengestellt.
Bedenken Sie, dass jeder dieser Schritte ein Stückchen Speicherplatz benötigt:
- Der Download-Puffer ist temporär, aber der Quelltext und der kompilierte Code bleiben im Speicher erhalten.
- Die Größe des heruntergeladenen Puffers und des Quellcodes entspricht der Größe des unkomprimierten, von Unity generierten js. Abzuschätzen, wie viel Speicherplatz für sie benötigt wird:
- einen Release-Build erstellen
- jsgz und datagz in *.gz umbenennen und mit einem Komprimierungsprogramm entpacken
- ihre unkomprimierte Größe entspricht auch ihrer Größe im Speicher des Browsers.
- Die Größe des kompilierten Codes hängt vom jeweiligen Browser ab.
Eine einfache Optimierung ist die Aktivierung von Strip Engine Code, so dass Ihr Build keinen nativen Engine Code enthält, den Sie nicht benötigen (z.B.: 2d-Physikmodul wird entfernt, wenn Sie es nicht benötigen). Anmerkung: Anmerkung: Verwalteter Code wird immer gestrichen.
Denken Sie daran, dass die Unterstützung von Exceptions und Plugins von Drittanbietern zur Größe Ihres Codes beitragen werden. Allerdings haben wir auch schon Benutzer gesehen, die ihre Titel mit Null-Checks und Array Bounds Checks ausliefern müssen, aber nicht den Speicher- (und Leistungs-)Overhead einer vollständigen Ausnahmeunterstützung in Kauf nehmen wollen. Dazu kann man --emit-null-checks und --enable-array-bounds-check an il2cpp übergeben, z.B. per Editor-Skript:
PlayerSettings.SetPropertyString("additionalIl2CppArgs", "--emit-null-checks --enable-array-bounds-check");
Schließlich sollten Sie bedenken, dass Entwicklungs-Builds einen größeren Code erzeugen, weil er nicht minifiziert ist, obwohl das kein Problem darstellt, da Sie nur Release-Builds an den Endbenutzer ausliefern werden... richtig? ;-)
Auf anderen Plattformen kann eine Anwendung einfach auf Dateien auf dem permanenten Speicher (Festplatte, Flash-Speicher usw.) zugreifen. Im Web ist dies nicht möglich, da es keinen Zugriff auf ein echtes Dateisystem gibt. Sobald die Unity WebGL-Daten (.data-Datei) heruntergeladen sind, werden sie im Speicher abgelegt. Der Nachteil ist, dass im Vergleich zu anderen Plattformen zusätzlicher Speicher benötigt wird (ab 5.3 wird die .data-Datei im Speicher lz4-komprimiert gespeichert). Zum Beispiel, hier ist, was der Profiler sagt mir über ein Projekt, das eine ~40mb Datendatei (mit 256mb Unity Heap) generiert:

Was steht in der .data-Datei? Es ist eine Sammlung von Dateien, die Unity generiert: data.unity3d (alle Szenen, ihre abhängigen Assets und alles im Resources-Ordner), unity_default_resources und ein paar kleinere Dateien, die von der Engine benötigt werden.
Um die genaue Gesamtgröße der Assets zu erfahren, werfen Sie einen Blick auf data.unity3d in Temp\StagingArea\Data, nachdem Sie für WebGL gebaut haben (denken Sie daran, dass der Temp-Ordner gelöscht wird, wenn der Unity-Editor geschlossen wird). Alternativ können Sie sich die Offsets ansehen, die an den DataRequest in UnityLoader.js übergeben werden:
new DataRequest(0, 39065934, 0, 0).open('GET', '/data.unity3d');
(dieser Code kann sich je nach Unity-Version ändern - dies ist der Code von Version 5.4)
Obwohl es, wie bereits erwähnt, kein echtes Dateisystem gibt, kann Ihr Unity WebGL-Inhalt dennoch Dateien lesen und schreiben. Der Hauptunterschied zu anderen Plattformen besteht darin, dass jede Datei-E/A-Operation tatsächlich im Speicher gelesen/geschrieben wird. Es ist wichtig zu wissen, dass dieses Speicherdateisystem nicht im Unity Heap lebt und daher zusätzlichen Speicher benötigt. Nehmen wir zum Beispiel an, ich schreibe ein Array in eine Datei:
var buffer = new byte [10*1014*1024];
File.WriteAllBytes(Application.temporaryCachePath + "/buffer.bytes", buffer);
Die Datei wird in den Speicher geschrieben, was auch im Profiler des Browsers zu sehen ist:

Beachten Sie, dass die Heap-Größe von Unity 256 MB beträgt.
Da das Cache-System von Unity vom Dateisystem abhängt, wird der gesamte Cache-Speicher im Speicher gesichert. Was soll das bedeuten? Das bedeutet, dass Dinge wie PlayerPrefs und zwischengespeicherte Asset-Bündel auch außerhalb des Unity-Heaps im Speicher persistent sind.
Eine der wichtigsten Best Practices, um den Speicherverbrauch von Webgl zu reduzieren, ist die Verwendung von Asset-Bundles (wenn Sie damit nicht vertraut sind, können Sie im Handbuch oder in diesem Tutorial nachsehen, um den Einstieg zu finden). Je nachdem, wie sie verwendet werden, kann dies jedoch erhebliche Auswirkungen auf den Speicherverbrauch haben (sowohl innerhalb des Unity-Heaps als auch außerhalb), was dazu führen kann, dass Ihre Inhalte auf 32-Bit-Browsern nicht funktionieren.
Jetzt, wo Sie wissen, dass Sie wirklich Asset-Bündel verwenden müssen, was tun Sie dann? Alle Vermögenswerte in ein einziges Vermögensbündel packen?
NEIN! Auch wenn dies den Druck beim Laden der Webseite verringern würde, müssen Sie immer noch ein (potenziell sehr großes) Asset-Bündel herunterladen, was zu einer Speichererhöhung führt. Schauen wir uns den Speicher an, bevor das AB heruntergeladen wird:

Wie Sie sehen können, werden 256 MB für den Unity Heap zugewiesen. Und das nach dem Herunterladen eines Asset-Pakets ohne Zwischenspeicherung:

Was Sie jetzt sehen, ist ein zusätzlicher Puffer, der ungefähr die gleiche Größe wie das Bündel auf der Festplatte hat (~65 MB) und von XHR zugewiesen wurde. Dies ist nur ein temporärer Puffer, aber es wird eine Speicherspitze für mehrere Frames verursachen, bis es Müll gesammelt wird.
Was ist dann zu tun, um Speicherspitzen zu minimieren? ein Asset-Bundle für jedes Asset erstellen? Das ist zwar eine interessante Idee, aber nicht sehr praktisch.
Unterm Strich gibt es keine allgemeingültige Regel, und Sie müssen wirklich das tun, was für Ihr Projekt am sinnvollsten ist.
Vergessen Sie nicht, das Asset-Bundle mit AssetBundle.Unload zu entladen, wenn Sie es nicht mehr benötigen.
Die Zwischenspeicherung von Asset-Bündeln funktioniert wie auf anderen Plattformen, Sie müssen nur WWW.LoadFromCacheOrDownload verwenden. Es gibt jedoch einen ziemlich bedeutenden Unterschied, nämlich den Speicherverbrauch. Auf Unity WebGL, AB-Zwischenspeicherung verlässt sich auf IndexedDB für die Speicherung von Daten dauerhaft, das Problem ist, dass die Einträge in der DB auch im Speicher-Dateisystem existieren.
Schauen wir uns einen Speicherauszug an, bevor wir ein Asset-Bündel mit LoadFromCacheOrDownload herunterladen:

Wie Sie sehen können, werden 512 MB für den Unity Heap und ~4 MB für andere Zuweisungen verwendet. Dies ist nach dem Laden des Pakets:

Der zusätzlich benötigte Speicher stieg auf ~167 MB. Das ist zusätzlicher Speicher, den wir für dieses Asset-Bundle benötigen (~64mb komprimiertes Bundle). Und das ist nach der Garbage Collection von js vm:

Es ist ein bisschen besser, aber ~85MB sind immer noch erforderlich: das meiste davon wird für den Cache des Asset-Bündels im Speicher-Dateisystem verwendet. Diesen Speicherplatz werden Sie nicht zurückbekommen, auch nicht nach dem Entladen des Bündels. Es ist auch wichtig, daran zu denken, dass, wenn der Nutzer Ihre Inhalte ein zweites Mal im Browser öffnet, dieser Speicherplatz sofort zugewiesen wird, noch bevor das Bundle geladen wird.
Als Referenz ist dies ein Speicherauszug von Chrome:

In ähnlicher Weise gibt es eine weitere caching-bezogene temporäre Zuweisung außerhalb des Unity Heap, die von unserem Asset-Bundle-System benötigt wird. Die schlechte Nachricht ist, dass wir vor kurzem festgestellt haben, dass sie viel größer ist als vorgesehen. Die gute Nachricht ist jedoch, dass dies in der kommenden Unity 5.5 Beta 4, 5.3.6 Patch 6 und 5.4.1 Patch 2 behoben wird.
Für ältere Versionen von Unity, für den Fall, dass Ihr Unity WebGL Inhalt bereits live ist oder kurz vor der Veröffentlichung steht und Sie Ihr Projekt nicht aktualisieren möchten, gibt es einen schnellen Workaround, um die folgende Eigenschaft per Editor-Skript zu setzen:
PlayerSettings.SetPropertyString("emscriptenArgs", " -s MEMFS_APPEND_TO_TYPED_ARRAYS=1", BuildTargetGroup.WebGL);
Eine längerfristige Lösung zur Minimierung des Speicher-Overheads für das Asset-Bundle-Caching ist die Verwendung von WWW Constructor anstelle von LoadFromCacheOrDownload() oder die Verwendung von UnityWebRequest.GetAssetBundle() ohne Hash/Versionsparameter, wenn Sie die neue UnityWebRequest-API verwenden.
Dann verwenden Sie einen alternativen Caching-Mechanismus auf der XMLHttpRequest-Ebene, der die heruntergeladene Datei direkt in indexedDB speichert und so das Speicherdateisystem umgeht. Genau das haben wir kürzlich entwickelt und es ist im Asset Store verfügbar. Sie können es gerne in Ihren Projekten verwenden und bei Bedarf anpassen.
In 5.3 und 5.4 werden sowohl die LZMA- als auch die LZ4-Kompression unterstützt. Obwohl die Verwendung von LZMA (Standard) zu einer geringeren Downloadgröße im Vergleich zu LZ4/Unkomprimiert führt, hat sie für WebGL einige Nachteile: Sie führt zu spürbaren Verzögerungen bei der Ausführung und benötigt mehr Speicher. Daher empfehlen wir dringend, LZ4 oder gar keine Komprimierung zu verwenden (tatsächlich wird die LZMA-Komprimierung von Asset-Bündeln für WebGL ab Unity 5.5 nicht mehr verfügbar sein). Um die größere Downloadgröße im Vergleich zu lzma zu kompensieren, können Sie Ihre Asset-Bündel mit gzip/brotli versehen und Ihren Server entsprechend konfigurieren.
Weitere Informationen zur Komprimierung von Asset-Bündeln finden Sie im Handbuch.
Audio auf Unity WebGL ist anders implementiert. Was bedeutet das für das Gedächtnis?
Unity erstellt spezifische AudioBuffer-Objektein JavaScript-Land, so dass sie über WebAudio abgespielt werden können.
Da WebAudio-Puffer außerhalb des Unity-Heaps liegen und daher nicht vom Unity-Profiler verfolgt werden können, müssen Sie den Speicher mit browserspezifischen Tools untersuchen, um zu sehen, wie viel Speicher für Audio verwendet wird. Hier ein Beispiel (unter Verwendung der Firefox-Seite about:memory ):

Beachten Sie, dass diese Audiopuffer unkomprimierte Daten enthalten, was für große Audioclips (z. B. Hintergrundmusik) möglicherweise nicht ideal ist. Für diese sollten Sie ein eigenes js-Plugin schreiben, damit Sie stattdessen <audio>-Tags verwenden können. Auf diese Weise bleiben die Audiodateien komprimiert und verbrauchen daher weniger Speicherplatz.
Hier ist eine Zusammenfassung:
Verringern Sie die Größe des Unity Heap:
Halten Sie die 'WebGL-Speichergröße' so klein wie möglich
Verringern Sie die Größe Ihres Codes:
Aktivieren von Strip Engine Code Ausnahmen deaktivieren Versuchen Sie, die Verwendung von Drittanbieter-Plugins zu vermeiden
Verringern Sie Ihre Datengröße:
Asset-Bündel verwenden Crunch Texturkompression verwenden
Ja, die beste Strategie wäre es, den Speicher-Profiler zu verwenden und zu analysieren, wie viel Speicher Ihr Inhalt tatsächlich benötigt, und dann die WebGL-Speichergröße entsprechend zu ändern.
Nehmen wir ein leeres Projekt als Beispiel. Der Memory Profiler sagt mir, dass "Total Used" etwas mehr als 16 MB beträgt (dieser Wert kann sich zwischen den verschiedenen Versionen von Unity unterscheiden): Das bedeutet, dass ich die WebGL-Speichergröße auf etwas Größeres als das einstellen muss. Natürlich ist "Total Used" je nach Ihrem Inhalt unterschiedlich.
Wenn Sie jedoch aus irgendeinem Grund den Profiler nicht verwenden können, können Sie einfach den Wert für die WebGL-Speichergröße so lange verringern, bis Sie die minimale Speichermenge gefunden haben, die für die Ausführung Ihrer Inhalte erforderlich ist.
Es ist auch wichtig zu beachten, dass jeder Wert, der kein Vielfaches von 16 ist, automatisch (zur Laufzeit) auf das nächste Vielfache gerundet wird, da dies eine Anforderung von Emscripten ist.
Die Einstellung WebGL Memory Size (mb) bestimmt den Wert von TOTAL_MEMORY (Bytes) in der generierten HTML:

Um die Größe des Heaps zu ermitteln, ohne das Projekt neu zu erstellen, empfiehlt es sich also, die html-Datei zu ändern. Wenn Sie dann einen Wert gefunden haben, mit dem Sie zufrieden sind, können Sie die WebGL-Speichergröße im Unity-Projekt ändern.
Zum Glück ist dies nicht der einzige Weg, und der nächste Blogbeitrag über den Unity-Haufen wird versuchen, eine bessere Antwort auf diese Frage zu geben.
Schließlich ist zu beachten, dass der Profiler von Unity etwas Speicher aus dem zugewiesenen Heap verwendet, so dass Sie möglicherweise die WebGL-Speichergröße beim Profiling erhöhen müssen.
Es hängt davon ab, ob Unity zu wenig Speicher hat oder der Browser. In der Fehlermeldung wird angegeben, was das Problem ist und wie es gelöst werden kann: "Wenn Sie der Entwickler dieses Inhalts sind, versuchen Sie, Ihrem WebGL-Build in den Einstellungen des WebGL-Players mehr/weniger Speicher zuzuweisen." Dann können Sie die Einstellung für die WebGL-Speichergröße entsprechend anpassen. Sie können jedoch noch mehr tun, um das OOM-Problem zu lösen. Wenn Sie diese Fehlermeldung erhalten:

Zusätzlich zu dem, was in der Meldung steht, können Sie auch versuchen, die Größe des Codes und/oder der Daten zu reduzieren. Das liegt daran, dass der Browser beim Laden der Webseite versucht, freien Speicher für verschiedene Dinge zu finden, vor allem für Code, Daten, Unity Heap und kompilierte asm.js. Sie können recht groß sein, insbesondere der Heap-Speicher von Data und Unity, was für 32-Bit-Browser ein Problem darstellen kann.
In manchen Fällen schlägt der Browser trotz ausreichend freiem Speicher fehl, weil der Speicher fragmentiert ist. Aus diesem Grund kann es vorkommen, dass Ihr Inhalt nach dem Neustart des Browsers nicht geladen wird.
Im anderen Szenario, wenn Unity keinen Speicher mehr hat, wird eine Meldung wie diese angezeigt:

In diesem Fall müssen Sie Ihr Unity-Projekt optimieren.
Um den von Ihren Inhalten genutzten Speicher des Browsers zu analysieren, können Sie das Firefox Memory Tool oder den Chrome Heap Snapshot verwenden. Beachten Sie jedoch, dass sie Ihnen den WebAudio-Speicher nicht anzeigen. Dafür können Sie die Seite about:memory in Firefox verwenden: Machen Sie einen Schnappschuss und suchen Sie dann nach "webaudio". Wenn Sie ein Speicherprofil über JavaScript erstellen müssen, versuchen Sie window.performance.memory (nur Chrome).
Um die Speichernutzung innerhalb des Unity Heap zu messen, verwenden Sie den Unity Profiler. Beachten Sie jedoch, dass Sie möglicherweise die WebGL-Speichergröße erhöhen müssen, um den Profiler nutzen zu können.
Darüber hinaus gibt es ein neues Tool, an dem wir gearbeitet haben und mit dem Sie analysieren können, was in Ihrem Build enthalten ist: Um es zu verwenden, erstellen Sie ein WebGL-Build und besuchen Sie dann http://files.unity3d.com/build-report/. Obwohl diese Funktion ab Unity 5.4 verfügbar ist, ist zu beachten, dass sie noch in Arbeit ist und jederzeit geändert oder entfernt werden kann. Wir stellen sie jedoch vorerst zu Testzwecken zur Verfügung.
16 ist das Minimum. Der Höchstwert liegt bei 2032, wir raten jedoch generell dazu, unter 512 zu bleiben.
Dies ist eine technische Einschränkung: 2048 MB (oder mehr) lassen die 32-Bit-Ganzzahl mit Vorzeichen des TypeArray, das zur Implementierung des Unity-Heap in JavaScript verwendet wird, überlaufen.
Wir haben in Erwägung gezogen, das emscripten-Flag ALLOW_MEMORY_GROWTH zu verwenden, um eine Größenänderung des Heaps zu ermöglichen, haben uns aber bisher dagegen entschieden, da dies einige Optimierungen in Chrome deaktivieren würde. Wir müssen noch ein echtes Benchmarking der Auswirkungen durchführen. Wir gehen davon aus, dass dies die Speicherprobleme noch verschlimmern könnte. Wenn Sie einen Punkt erreicht haben, an dem der Unity-Heap zu klein ist, um den gesamten benötigten Speicher aufzunehmen, und er wachsen muss, dann muss der Browser einen größeren Heap allozieren, alles aus dem alten Heap kopieren und dann den alten Heap deallozieren. Dadurch wird gleichzeitig Speicher für den neuen und den alten Heap benötigt (bis der Kopiervorgang abgeschlossen ist), so dass insgesamt mehr Speicher benötigt wird. Der Speicherverbrauch wäre also höher als bei Verwendung einer vorgegebenen festen Speichergröße.
32-Bit-Browser stoßen auf die gleichen Speicherbeschränkungen, unabhängig davon, ob es sich um ein 64- oder 32-Bit-Betriebssystem handelt.
Abschließend wird empfohlen, den Unity-WebGL-Inhalt auch mit browserspezifischen Tools zu profilieren, da es, wie beschrieben, Zuweisungen außerhalb des Unity-Heaps gibt, die der Unity-Profiler nicht verfolgen kann.
Wir hoffen, dass einige dieser Informationen für Sie nützlich sind. Wenn Sie weitere Fragen haben, zögern Sie bitte nicht, diese hier oder im WebGL-Forum zu stellen.
Aktualisierung:
Wir haben über den für den Code verwendeten Speicher gesprochen und erwähnt, dass der JS-Quellcode in einen temporären Textblob kopiert wird. Wir haben festgestellt, dass der Blob nicht ordnungsgemäß freigegeben wurde, so dass es sich um eine permanente Zuordnung im Browser-Speicher handelte. In about:memory wird sie als memory-file-data bezeichnet:

Seine Größe ist abhängig von der Codegröße und kann bei komplexen Projekten leicht 32 oder 64 MB betragen. Zum Glück wurde dies in 5.3.6 Patch 8, 5.4.2 Patch 1 und 5.5 behoben.
Was Audio betrifft, so wissen wir, dass der Speicherverbrauch immer noch ein Problem darstellt: Audio-Streaming wird derzeit nicht unterstützt, und Audio-Assets werden derzeit unkomprimiert im Browser-Speicher gehalten. Deshalb haben wir vorgeschlagen, den <audio>-Tag zu verwenden, um große Audiodateien abzuspielen. Zu diesem Zweck haben wir vor kurzem ein neues Asset Store-Paket veröffentlicht, das Ihnen hilft, den Speicherverbrauch durch Streaming-Audioquellen zu minimieren. Probieren Sie es aus!