UnityのWebGLでメモリを理解する

一部のユーザーは、メモリが制限されているプラットフォームにすでに慣れ親しんでいる。デスクトップやウェブプレーヤーから来た他の人にとっては、これまで問題にはならなかった。
この点、コンソール・プラットフォームをターゲットにするのは比較的簡単だ。そのため、メモリを節約することができ、コンテンツの実行も保証される。モバイルプラットフォームでは、さまざまなデバイスが存在するため、物事はもう少し複雑になるが、少なくとも最低スペックを選択し、マーケットプレイスレベルで下位デバイスのブラックリストを決定することはできる。
ウェブでは、それができない。理想を言えば、すべてのエンドユーザーが64ビットのブラウザと大量のメモリを持っていることだが、現実にはほど遠い。その上、コンテンツが動作しているハードウェアのスペックを知る方法はない。OS、ブラウザー、それ以上のものはない。最後に、エンドユーザーはあなたのWebGLコンテンツだけでなく、他のWebページも実行しているかもしれません。だからこれは難しい問題なんだ。
ブラウザでUnity WebGLコンテンツを実行する際のメモリの概要を示します:

この画像は、Unityヒープの上に、Unity WebGLコンテンツがブラウザのメモリに追加の割り当てを必要とすることを示しています。プロジェクトを最適化し、ユーザーの離脱率を最小限に抑えるためには、このことを理解することが本当に重要です。
画像からわかるように、いくつかの割り当てグループがある:DOM、ユニティ・ヒープ、アセット・データ、コードは、ウェブ・ページがロードされると、メモリ上で永続化される。その他、アセットバンドル、WebAudio、メモリーFSなどは、コンテンツで何が起こっているか(アセットバンドルダウンロード、オーディオ再生など)によって変わります。
ローディング時に、asm.jsのパースとコンパイルの間にブラウザの一時的な割り当てがいくつかあり、32ビットブラウザの一部のユーザーにメモリ不足の問題を引き起こすことがあります。
一般的に、UnityヒープはUnity固有のゲームオブジェクト、コンポーネント、テクスチャ、シェーダなどをすべて含むメモリです。
WebGLでは、Unityのヒープサイズを事前に知っておく必要があるため、ブラウザはそのための領域を確保することができ、一度確保されたバッファは縮小も拡大もできない。
ユニティ・ヒープの割り当てを担当するコードは以下の通り:
buffer = new ArrayBuffer(TOTAL_MEMORY);
このコードは生成されたbuild.jsにあり、ブラウザのJS VMによって実行される。
TOTAL_MEMORYは、Player SettingsのWebGL Memory Sizeで定義されます。デフォルト値は256mbだが、これは私たちが任意に選んだ値に過ぎない。実際、空のプロジェクトはわずか16MBで動く。
しかし、現実のコンテンツはもっと必要で、たいていの場合は256MBか386MBといったところだろう。必要なメモリが多ければ多いほど、それを実行できるエンドユーザーは少なくなることを覚えておいてほしい。
コードを実行する前に、そのコードが必要なのだ:
をダウンロードした。
をテキスト塊にコピーした。
をコンパイルした。
これらの各ステップは、メモリの塊を必要とすることを考慮に入れてください:
- ダウンロード・バッファは一時的なものだが、ソースとコンパイルされたコードのものはメモリ上に永続的に存在する。
- ダウンロードされたバッファのサイズとソースコードのサイズは、どちらもUnityによって生成された圧縮されていないjsのサイズです。そのために必要なメモリの量を見積もる:
- リリースビルドを作る
- jsgzとdatagzを*.gzにリネームし、圧縮ツールで解凍する。
- 圧縮されていないサイズは、ブラウザのメモリ上のサイズでもある。
- コンパイルされたコードのサイズはブラウザに依存する。
簡単な最適化は、ビルドに不要なネイティブ・エンジン・コードが含まれないように、エンジン・コードのストリップを有効にすることだ(例::2D物理モジュールは必要なければ取り除かれる)。注意:注意:マネージドコードは常に取り除かれる。
Exceptionsのサポートやサードパーティプラグインがコードサイズに寄与することに留意してください。とはいえ、NULLチェックや配列境界チェックをタイトルに搭載する必要があるにもかかわらず、完全な例外サポートによるメモリ(およびパフォーマンス)オーバーヘッドを発生させたくないというユーザーを見かけることもある。そのためには、例えばエディタースクリプトを使って、--emit-null-checksと --enable-array-bounds-checksをil2cppに渡せばよい:
PlayerSettings.SetPropertyString("additionalIl2CppArgs", "--emit-null-checks --enable-array-bounds-check");
最後に、Developmentビルドは最小化されていないため、より大きなコードを生成することを覚えておいてほしい。)
他のプラットフォームでは、アプリケーションは永久記憶装置(ハードドライブ、フラッシュメモリーなど)上のファイルにアクセスするだけです。ウェブ上では、実際のファイルシステムにアクセスできないので、これは不可能だ。そのため、Unity WebGLのデータ(.dataファイル)がダウンロードされると、メモリに保存されます。欠点は、他のプラットフォームに比べてメモリが必要になることだ(5.3現在、.dataファイルはlz4圧縮されてメモリに保存される)。例えば、(256MBのUnityヒープを持つ)約40MBのデータファイルを生成するプロジェクトについて、プロファイラーが教えてくれることは以下の通りだ:

.dataファイルの中身は?それはunityが生成するファイルのコレクションです: data.unity3d(すべてのシーン、それらに依存するアセット、およびResourcesフォルダ内のすべて)、unity_default_resources、およびエンジンが必要とするいくつかの小さなファイル。
アセットの正確な合計サイズを知るには、WebGL用にビルドした後、TempStagingAreaDataのdata.unity3dを見てください(Unityエディタを閉じるとTempフォルダが削除されることを覚えておいてください)。あるいは、UnityLoader.jsのDataRequestに渡されたオフセットを見ることもできます:
new DataRequest(0, 39065934, 0, 0).open('GET', '/data.unity3d');
(このコードはUnityのバージョンによって変わるかもしれません - これは5.4のものです)
本当のファイルシステムは存在しませんが、先に述べたように、Unity WebGLコンテンツはファイルを読み書きすることができます。他のプラットフォームとの主な違いは、ファイルI/O操作が実際にメモリ内で読み書きされることだ。重要なことは、このメモリー・ファイル・システムはUnityのヒープには存在しないため、追加のメモリーが必要になるということです。例えば、配列をファイルに書き出すとしよう:
var buffer = new byte [10*1014*1024];
File.WriteAllBytes(Application.temporaryCachePath + "/buffer.bytes", buffer);
ファイルはメモリーに書き込まれ、ブラウザーのプロファイラーでも確認できる:

なお、Unityのヒープサイズは256mbである。
同様に、Unityのキャッシュシステムはファイルシステムに依存しているため、キャッシュストレージ全体がメモリにバックアップされます。これは何を意味するのでしょうか。これは、PlayerPrefsやキャッシュされたAsset Bundlesのようなものも、Unity Heap以外のメモリに永続的に存在することを意味します。
webglのメモリ消費量を減らすための最も重要なベストプラクティスの1つは、アセット・バンドルを使うことです(アセット・バンドルについてよく知らない場合は、マニュアルやこのチュートリアルを参考にしてください)。しかし、これらの使用方法によっては、メモリ消費に大きな影響を与える可能性があり(Unityのヒープ内部と外部でも)、32ビットブラウザでコンテンツが動作しなくなる可能性があります。
アセット・バンドルを使う必要があることは分かったが、ではどうすればいいのか?すべての資産を1つの資産バンドルに捨てるのか?
いいえ!ウェブページのローディング時のプレッシャーが軽減されるとはいえ、(潜在的に非常に大きな)アセットバンドルをダウンロードする必要があり、メモリが急増する。ABをダウンロードする前のメモリを見てみよう:

ご覧のように、256mbがUnityヒープに割り当てられています。そしてこれは、キャッシュなしでアセットバンドルをダウンロードした後の話だ:

現在表示されているのは、XHRによって割り当てられた、ディスク上のバンドルとほぼ同じサイズ(~65MB)の追加バッファです。これは単なる一時的なバッファだが、ガベージコレクションされるまでの数フレームの間、メモリが急増する。
各アセットに1つのアセットバンドルを作成するか?面白いアイデアだが、あまり現実的ではない。
要するに、一般的なルールはなく、あなたのプロジェクトにとってより理にかなった方法を取る必要があるということだ。
最後に、アセットバンドルを使い終わったら、AssetBundle.Unloadを使ってアセットバンドルをアンロードすることを忘れないでください。
Asset Bundleのキャッシュは他のプラットフォームと同じように動作します。WWW.LoadFromCacheOrDownloadを使用するだけです。しかし、ひとつだけかなり大きな違いがある。Unity WebGLでは、ABキャッシュはデータを永続的に保存するためにIndexedDBに依存していますが、問題はDB内のエントリがメモリファイルシステムにも存在することです。
LoadFromCacheOrDownload を使ってアセットバンドルをダウンロードする前のメモリキャプチャを見てみましょう:

ご覧の通り、512mbがUnityヒープに、そして~4mbがその他のアロケーションに使用されています。これはバンドルをロードした後だ:

追加で必要なメモリは〜167mbに跳ね上がった。これは、このアセット・バンドル(~64MBの圧縮バンドル)に必要な追加メモリだ。そして、これはjs vmのガベージコレクションの後である:

少しはマシになったが、それでも~85MBは必要だ。そのほとんどは、アセットバンドルをメモリ・ファイルシステムにキャッシュするために使われている。そのメモリは、バンドルから降ろした後でも取り戻せない。また、ユーザーがブラウザであなたのコンテンツを2度目に開いたとき、そのメモリの塊は、バンドルをロードする前であっても、すぐに割り当てられることを覚えておくことも重要だ。
参考までに、これはクロームのメモリースナップショットである:

同様に、Unityヒープ以外にもキャッシュ関連の一時的な割り当てがあり、これはアセットバンドルシステムで必要となります。悪いニュースは、それが意図したよりもはるかに大きいことが最近わかったことだ。しかし、良いニュースは、今後のUnity5.5 Beta 4、5.3.6 Patch 6、5.4.1 Patch 2で修正されることです。
古いバージョンのUnityの場合、Unity WebGLコンテンツがすでにリリースされているか、リリース間近で、プロジェクトをアップグレードしたくない場合は、エディタスクリプトで次のプロパティを設定することで簡単に回避できます:
PlayerSettings.SetPropertyString("emscriptenArgs", " -s MEMFS_APPEND_TO_TYPED_ARRAYS=1", BuildTargetGroup.WebGL);
アセットバンドルのキャッシュメモリのオーバーヘッドを最小限に抑える長期的な解決策は、LoadFromCacheOrDownload()の代わりにWWW Constructor を使用するか、新しいUnityWebRequestAPI を使用している場合は、ハッシュ/バージョンパラメータなしでUnityWebRequest.GetAssetBundle() を使用することです。
そして、XMLHttpRequestレベルの代替キャッシュメカニズムを使用し、ダウンロードしたファイルを直接indexedDBに格納し、メモリファイルシステムをバイパスします。これはまさに私たちが最近開発したもので、アセットストアで入手できる。必要であればカスタマイズして、ご自由にプロジェクトにお使いください。
5.3と5.4では、LZMAとLZ4の両方の圧縮がサポートされています。しかし、LZMA(デフォルト)を使用すると、LZ4/Uncompressedに比べてダウンロードサイズが小さくなるとはいえ、WebGLではいくつかの欠点があります。そのため、LZ4を使用するか、まったく圧縮しないことを強く推奨します(実のところ、Unity 5.5ではLZMAによるアセットバンドル圧縮はWebGLでは使用できません)。また、lzmaに比べて大きくなるダウンロードサイズを補うために、アセットバンドルをgzip/brotliし、それに応じてサーバーを設定することもできます。
アセットバンドル圧縮の詳細については、マニュアルを参照してください。
Unity WebGLのオーディオは実装が異なります。それはメモリーにとって何を意味するのか?
Unityは特定のAudioBufferオブジェクトをJavaScriptで作成し、WebAudioで再生できるようにします。
WebAudioバッファはUnityのヒープ外にあるため、Unityのプロファイラでは追跡できないため、ブラウザ固有のツールでメモリを検査し、オーディオに使用されているメモリの量を確認する必要があります。以下はその例である(Firefoxのabout:memoryページを使用):

これらのオーディオバッファは非圧縮データを保持するため、大きなオーディオクリップのアセット(BGMなど)には適していない可能性があることを考慮してください。そのような場合は、代わりに<audio>タグを使用できるように、独自のjsプラグインを書くことを検討するとよいでしょう。こうすることで、オーディオファイルは圧縮されたままとなり、メモリ使用量が少なくなる。
以下はその要約である:
ユニティヒープのサイズを小さくする:
WebGL Memory size'をできるだけ小さくしてください。
コードサイズを小さくする:
ストリップ・エンジン・コードを有効にする 例外を無効にする サードパーティプラグインの使用を避ける
データサイズを小さくする:
アセット・バンドルの使用 クランチ・テクスチャ圧縮を使う
はい、最善の方法は、メモリ・プロファイラを使用して、コンテンツが実際に必要とするメモリ量を分析し、それに応じてWebGL Memory Sizeを変更することでしょう。
空のプロジェクトを例にとってみよう。Memory Profilerによると、"Total Used "は16MB強です(この値はUnityのリリースによって異なるかもしれません)。もちろん、"Total Used "はコンテンツによって異なる。
しかし、何らかの理由でプロファイラーを使用できない場合は、コンテンツを実行するのに必要な最小限のメモリ量が見つかるまで、WebGL Memory Sizeの値を減らし続けるだけです。
また、Emscriptenの要件として、16の倍数でない値は(実行時に)自動的に次の倍数に丸められることにも注意する必要がある。
WebGL Memory Size (mb)の設定は、生成されるhtmlのTOTAL_MEMORY (bytes)の値を決定します:

そのため、プロジェクトを再ビルドすることなくヒープのサイズを反復するには、htmlを修正することをお勧めする。そして、満足のいく値が見つかったら、UnityプロジェクトのWebGL Memory Sizeを変更します。
ありがたいことに、これだけが唯一の方法ではない。ユニティ・ヒープに関する次のブログ記事では、この疑問に対するより良い答えを提供しようと考えている。
最後に、Unity のプロファイラーが割り当てられたヒープからいくらかのメモリーを使用することを覚えておいてください。
Unityのメモリ不足なのか、ブラウザのせいなのかによる。エラーメッセージには、問題の内容と解決方法が表示されます:"もしあなたがこのコンテンツの開発者であれば、WebGLプレーヤーの設定でWebGLビルドにより多くの/より少ないメモリを割り当ててみてください。"その後、WebGL Memory Sizeの設定を適宜調整してください。しかし、OOMを解決するためにできることは他にもある。このエラーメッセージが表示されたら

メッセージに書かれていることに加えて、コードやデータのサイズを小さくすることもできる。それは、ブラウザがウェブページをロードするとき、コード、データ、unityヒープ、コンパイルされたasm.jsなど、最も重要ないくつかのもののために空きメモリを見つけようとするからだ。特にDataとUnityのヒープ・メモリはかなり大きくなる可能性があり、32ビットのブラウザでは問題になることがある。
十分な空きメモリがあるにもかかわらず、メモリが断片化されているためにブラウザが失敗する場合もある。そのため、ブラウザを再起動してもコンテンツが読み込まれないことがある。
もうひとつのシナリオは、Unityがメモリ不足になると、次のようなメッセージが表示される:

この場合、Unityプロジェクトを最適化する必要があります。
コンテンツによって使用されるブラウザのメモリを分析するには、FirefoxMemory ToolまたはChromeHeap snapshotを使用できます。Firefoxのabout:memoryページでスナップショットを取り、"webaudio "を検索してください。JavaScriptでメモリをプロファイリングする必要がある場合は、window.performance.memory(Chromeのみ)をお試しください。
Unityヒープ内のメモリ使用量を測定するには、Unity Profilerを使用します。ただし、プロファイラーを使用できるようにするには、WebGLのメモリサイズを増やす必要があるかもしれないことに注意してください。
さらに、私たちが取り組んでいる新しいツールがあり、ビルドの中身を分析することができます:使用するには、WebGLビルドを作成し、http://files.unity3d.com/build-report/。これはUnity 5.4で利用可能ですが、この機能は進行中であり、いつでも変更または削除される可能性があることに注意してください。しかし、今のところテスト用として公開している。
16が最低ラインだ。最大値は2032だが、一般的には512以下に抑えるようアドバイスしている。
これは技術的な限界である:2048 MB (またはそれ以上) は、JavaScript で Unity ヒープを実装するために使用される TypeArray の 32 ビット符号付き整数サイズをオーバーフローします。
ALLOW_MEMORY_GROWTH emscriptenフラグを使用して、ヒープのサイズを変更できるようにすることを検討しましたが、そうすることでChromeのいくつかの最適化が無効になるため、今のところ使用しないことにしました。この影響について、私たちはまだ本格的なベンチマークを行っていない。これを使うと、メモリの問題がかえって悪化する可能性がある。Unityのヒープが小さすぎて、必要なメモリをすべて収めることができず、大きくする必要がある場合、ブラウザはより大きなヒープを割り当て、古いヒープからすべてをコピーし、古いヒープをデアロケートする必要があります。そうすることで、(コピーが終わるまで)新しいヒープと古いヒープの両方のメモリを同時に必要とするため、より多くの総メモリを必要とする。そのため、あらかじめ決められた固定メモリーサイズを使用する場合よりも、メモリー使用量は多くなる。
32ビットのブラウザは、OSが64ビットでも32ビットでも、同じメモリ制限にぶつかります。
最後の推奨は、ブラウザ固有のツールを使用してUnity WebGLコンテンツをプロファイリングすることです。なぜなら、説明したように、Unityのプロファイラが追跡できないUnityヒープ外の割り当てがあるからです。
この情報のいくつかが役に立つことを願っている。さらに質問がある場合は、遠慮なくここかWebGLフォーラムで質問してください。
アップデート情報:
コードに使われるメモリについて話し、JSのソースコードが一時的なテキスト塊にコピーされることを述べた。私たちが発見したのは、このブロブが適切に割り当て解除されなかったため、事実上、ブラウザのメモリに永久に割り当てられていたことです。about:memoryでは、memory-file-dataと表記されている:

そのサイズはコードサイズに依存し、複雑なプロジェクトでは32MBや64MBになることもある。ありがたいことに、これは5.3.6パッチ8、5.4.2パッチ1、5.5で修正されている。
オーディオに関しては、メモリ消費量が依然として問題であることが分かっている:現在、オーディオストリーミングはサポートされておらず、オーディオアセットは非圧縮のままブラウザのメモリに保持されます。そこで、大きな音声ファイルを再生するために<audio>タグを使うことを提案しました。この目的のために、私たちは最近、ストリーミングオーディオソースによるメモリ消費を最小限に抑えるための新しいアセットストアパッケージを公開しました。ぜひチェックしてほしい!