Version: 2018.2
WebGL のパフォーマンスについて
WebGL: ブラウザースクリプトとの相互作用

WebGL のメモリ

Unity WebGL のメモリは、実行できるコンテンツの複雑さを制限する制約要因になる可能性があります。そのため、ここでは WebGL でメモリがどのように使われるかについて説明します。

WebGL のコンテンツはブラウザー内で実行されます。そのため、メモリはすべてブラウザーによってブラウザー内部のメモリ空間に割り当てられることになります。利用可能なメモリの総量は使用しているブラウザー、OS、デバイスによって大きく変動する可能性があります。決定要因には他にもブラウザーが 32 ビット 64 ビットのどちらか、ブラウザーがタブごとに分離したプロセスを使用するのか、それとも開かれているタブがすべて同じメモリ空間を共有するのか、ブラウザーの JavaScript エンジンがゲームのコードを変換するのにどれだけのメモリを要するか、などがあります。

Unity WebGL コンテンツがブラウザーに著しく大きなメモリの割り当てを必要とするエリアが、複数あります。

Unity のヒープ

これは Unity がすべての状況、管理されたオブジェクトやネイティブオブジェクト、現在読み込まれているアセットやシーンを保存するためのメモリです。他のプラットフォームで Unity プレイヤーが使用するメモリと似たようなものです。使用するメモリ量は Unity WebGL player settings から設定することができます(何度も書き出すのが嫌であれば、生成された HTML ファイルに記載されている TOTAL_MEMORY の値を編集することで同じ内容の操作をすることもできます)。 Unity Profiler を用いてこのメモリ量をサンプリングすることもできます。このメモリは JavaScript コードでバイト配列の TypedArray として生成され、このサイズのメモリが一続きの集まりとして割り当てできることを要求します。(メモリが断片化していてもブラウザーが割り当てできるように)この空間はできるだけ小さくしたいかと思われますが、ゲームに含まれるすべてのシーンに属するデータを再生できるだけの大きさが必要です。

アセットデータ

Unity WebGL のビルドを作成する際、 Unity はコンテンツに必要なすべてのシーンとアセットを持つ .data ファイルを書き出します。WebGL には実際のファイルシステムがないため、このファイルはコンテンツの開始前にダウンロードされ、コンテンツが実行されている間はずっと非圧縮状態のデータがブラウザーメモリの連続したブロックに保持され続けることになります。そのためダウンロード時間とメモリ使用量の削減を両立するには、このデータをできる限り少なくするよう心掛けるといいでしょう。アセットのビルドサイズを最適化する方法についての情報は ファイルサイズの削減 を参照してください。

読み込み時間とメモリ使用量を減らすために他にできることは、アセットデータを アセットバンドル にまとめることです。そうすることでアセット読み込みのタイミングを完全に管理することができ、必要なくなったタイミングで破棄して使用されていたメモリを解放することもできます。 AssetBundle は直接 Unity ヒープに読み込まれ、ブラウザーによる追加の割り当てが発生することはありません。(WWW.LoadFromCacheOrDownload を使用して AssetBundle をキャッシングする場合はこの限りではありません。WWW.LoadFromCacheOrDownload は、ブラウザーの IndexedDB によるメモリにマッピングされた仮想ファイルシステムを使用するためです。)

コードをパースするのに必要なメモリ

メモリに関係するもう 1 つの問題はブラウザーの JavaScript エンジンに求められるメモリです。Unity は何百万行という膨大な JavaScript コードファイルを発行しますが、これは通常ブラウザーで扱われる JavaScript コード量よりも多いのです。JavaScript エンジンによってはこのコードをパース、最適化するためにより大きなデータ構造体を割り当てる可能性もあり、それによってコンテンツ読み込み時にメモリスパイクが数ギガバイトに及ぶ、という可能性もあります。WebAssembly などの将来的な技術が行く行くはこの問題を解決してくれることを期待していますが、そのときまでにできる最善のアドバイスは発行するコードサイズを少なくすること、です。サイズの削減についてのより詳しい情報と実践方法については こちら を参照してください。

メモリに関する問題の処理

Unity WebGL ビルドでメモリ関係のエラーが出た場合、ブラウザーがメモリの割り当てに失敗しているのか、Unity WebGL ランタイムが Unity ヒープの事前に割り当てられたブロック内で空いているブロックへの割り当てに失敗しているのかを理解することが重要です。ブラウザーがメモリの割り当てに失敗している場合、例えば Unity ヒープのサイズを削減するなどして、上記のメモリエリアのサイズ削減を試みるといいかもしれません。一方で、 Unity ランタイムが Unity ヒープ内のブロックへの割り当てに失敗している場合、逆にそのサイズを増やすといいかもしれません。

Unity は、エラーメッセージが上のどちらなのかを理解しようと試みます (そしてどうしたらいいかを提案します)。ブラウザーごとにメッセージの表示方法が違うため、常に簡単というわけではなく、すべてを理解できないかもしれません。“Out of memory” (メモリ不足) エラーがブラウザーで表示された場合、実行中のブラウザーがメモリ不足の問題を抱えている可能性があります (この場合 Unity ヒープのサイズを小さくするといいかもしれません)。また、 Unity コンテンツを読み込んでいるときに、人間に理解可能なエラーメッセージの表示なしにブラウザーがクラッシュする場合があります。これには多くの理由が考えられますが、よくあるものとしては JavaScript エンジンが生成されたコードをパースし、最適化するためにあまりに多くのメモリが必要だということです。

Large-Allocation Http ヘッダー

サーバーはコンテンツの Large-Allocation http ヘッダーを生成することがあります。これはサポートされているブラウザー (現在 Firefox のみ) にメモリが必要としているものを伝え、分割されていないメモリ空間で新しい処理を発生させたり、大きなメモリ割り当てが成功するように雑多な作業を行ったりします。こうすることにより、特に 32 ビットブラウザーで Unity ヒープを割り当てようとするときに、ブラウザーがメモリ不足になる問題を解決できます。

ガベージコレクションへの配慮

Unity にマネージオブジェクトを割り当てる場合、使われなくなったものはガベージコレクトされる必要があります。詳しい情報はドキュメントの automatic memory management を参照してください。 WebGL でも同じことが言えます。マネージオブジェクト、ガベージコレクトされたメモリともに Unity ヒープ内に割り当てられます。

しかし、WebGL で気を付けなければいけないのは、ガベージコレクション (GC) を実行するタイミングに注意が必要なことです。ガベージコレクションを実行するために、通常 GC はすべてのスレッドの実行を一時停止し、スタックを精査し読み込み済みオブジェクトの参照を登録します。これは現在の JavaScript では不可能なことです。このため WebGL で GC はスタックが空になった場合にのみ実行されます(現在これは毎フレーム後に 1 度行われます)。この仕様はほとんどのマネージメモリを保守的に扱うコンテンツでは問題になりません。そして、相対的に多めの GC 割り当てがフレームごとに行われます (Unity プロファイラーを用いてデバッグすることができます)。

ただし、下のコードは WebGL 上で実行すると失敗します。なぜなら、ループのイテレーションの間に GC を実行する機会を得ることができず、すべての中間の文字列オブジェクトによって使用されるメモリを解放するからです。それは、結局、Unity のヒープのメモリが不足する原因になります。

string hugeString = "";

for (int i = 0; i < 100000; i++)
{
    hugeString += "foo";
}

参考資料


2018–08–23 編集レビュー 無しに修正されたページ - ページのフィードバックを残す

WebGL のパフォーマンスについて
WebGL: ブラウザースクリプトとの相互作用