Unity:WebGLでメモリエラーに苦しんだ話
目次
はじめに
こんにちは、のんびりエンジニアのたっつーです。
Twitter(@kingyo222)で Unity情報 を発信しているのでよければフォローしてください!
Unity+WebGLでビッグなデータを扱うと発生するエラーの対策を書いてみました!
Unity のWebGLプラットフォームでのメモリエラーについてだいぶ悩まされたので、このエラーの解決方法を共有させていただきます。
具体的なエラーの内容
エラー1
「memory access out of bounds」のエラーが表示されて、Javascript から SendMessage を使用して Unity 側の関数呼び出しがエラーになって動かない場合。
Invoking error handler due to
Uncaught RuntimeError: memory access out of bounds
Uncaught RuntimeError: memory access out of bounds
at wasm-function[8336]:324
at wasm-function[8331]:257
at wasm-function[1315]:929
at wasm-function[1391]:124
at wasm-function[6601]:898
at wasm-function[6598]:117
at wasm-function[8314]:478
at wasm-function[26579]:17
at Object.dynCall_iiii (blob:http://127.0.0.1:8887/df084535-121f-44bd-9351-339a7a3e5c5c:2:422103)
at Object.handlerFunc (blob:http://127.0.0.1:8887/df084535-121f-44bd-9351-339a7a3e5c5c:2:208347)
at jsEventHandler (blob:http://127.0.0.1:8887/df084535-121f-44bd-9351-339a7a3e5c5c:2:204833)
エラー2
Unityのビルド設定で「Development Build」にチェックが付いているとこのエラーが表示されます。
「Stack overflow! Attempted to allocate ~」のエラーが表示されて、Javascript から SendMessage を使用して Unity 側の関数呼び出しがエラーになって動かない場合。
Stack overflow! Attempted to allocate 5242881 bytes on the stack, but stack has only 5242721 bytes available!
Invoking error handler due to
Uncaught abort("Stack overflow! Attempted to allocate 5242881 bytes on the stack, but stack has only 5242721 bytes available!") at Error
at jsStackTrace (App.wasm.framework.unityweb:693:12)
at stackTrace [Object.stackTrace] (App.wasm.framework.unityweb:707:11)
at Object.onAbort (http://127.0.0.1:8887/Build/UnityLoader.js:1039:50)
at abort (App.wasm.framework.unityweb:25388:20)
at abortStackOverflow (App.wasm.framework.unityweb:752:2)
at stackAlloc (wasm-function[590]:33)
at blob:http://127.0.0.1:8887/cd9afda3-5ef3-4581-837c-a22f63982caa:24136:37
at stringToC (App.wasm.framework.unityweb:368:10)
at ccall [Object.ccall] (App.wasm.framework.unityweb:388:16)
at SendMessage [Object.SendMessage] (App.wasm.framework.unityweb:72:151)
at Object.SendMessage (http://127.0.0.1:8887/Build/UnityLoader.js:1057:50)
at http://127.0.0.1:8887/:19:22
再現ソースコード
Javascript側からUnity側にデータを受け渡す際非情に大きいデータに、このエラーが発生します。そのため、再現ソースコードではJavascipt側でループ文でわざと巨大な文字列を作成してそのデータをUnity側に送信しようとしてエラーを発生させています。
Unity側
単純なソースコードですね、Javascript からデータが受け取れるように MyFunction 関数を定義して引数でstring をもらうようにしています。
using UnityEngine;
public class TestObject : MonoBehaviour
{
public void MyFunction(string data)
{
// Debug.Log(data);
}
}
Javascript側
setInterval で 1秒おきにタイマーを設定します。
タイマー内ではループにより巨大なデータを作成して最終的にはUnityのgameInstance.SendMessage を使って Unity側に送信を試みますが、エラーとなります。
ここでループの階数「5*1024*1024=5242880」の数値はこの後の説明でよく登場するので覚えておいてください。
<script>
var gameInstance = UnityLoader.instantiate("gameContainer", "Build/App.json", {onProgress: UnityProgress});
setInterval(function() {
var text = "";
for(var i = 0; i < 5*1024*1024 / 4; i++) {
text += "*";
}
gameInstance.SendMessage('MyGameObject', 'MyFunction', text);
}, 1000);
</script>
適当な解説
WebGLのビルド仕組み
WebGLのビルドの仕組みについてはここに書くと長くなってしまうので、今回は公式ページの必要な部分だけ説明させていただきます。
何が言いたいかと言えば、各コンパイラを通して、Webで実行可能なJavascriptのソースコードが出力されるということです。
C# —(IL2CPP)—> C++ —(Emscripten)–> Javascript に変換されます。
WebGL で実行するには、すべてのコードが JavaScript で書かれている必要があります。Unity では Emscripten コンパイラーツールチェーンを使用して Unity ランタイムコード (C および C++ で記述されています) を asm.js JavaScript にクロスコンパイルしています。asm.js は最適化可能な JavaScript のサブセットで、asm.js コードを JavaScript エンジンによって非常に効率のよいネイティブコードに AOT コンパイルできるようにするものです。
https://docs.unity3d.com/ja/current/Manual/webgl-gettingstarted.html
Unity では、.NET ゲームコード(C# や UnityScript スクリプト)を JavaScript に変換するのに IL2CPP と呼ばれる技術を使っています。IL2CPP は .NET バイトコードを、対応する C++ ソースファイルに変換します。それが Emscripten を使ってコンパイルされることでスクリプトが JavaScript に変換されます。
何が問題なのか?
今回発生している問題は、Javascript(WebAssembly)のメモリ関連のエラーだと思われます。
以下のエラーの内容を翻訳すると、「スタックオーバーフロー!スタックに5242881バイトを割り当てようとしましたが、スタックには5242721バイトしかありません。」となります。
Stack overflow! Attempted to allocate 5242881 bytes on the stack, but stack has only 5242721 bytes available!
つまり、Javascript から SendMesssage を使って呼び出した際に、スタックオーバーフローが発生して呼び出せなかったよ!スタック領域が足りないよ!って感じのエラーです。
そもそもスタック領域って?
メモリの領域には、C言語的に言うとスタック領域とヒープ領域が存在します。
それぞれを簡単に説明すると、以下のようになります。
スタック領域
関数の呼び出し時に、引数・戻り値・関数アドレス の情報を格納する一次領域、関数の呼び出しが行われるたびにメモリを消費して、関数が終了するとメモリを解放する。
ヒープ領域
関数の呼び出しとは関係なく、プログラマが明示的にメモリを確保した場合に使われる領域。
今回の問題切り分け
今回発生している問題は、 Javascript(WebAssembly) で実行する際に、関数呼び出しを行った際にスタック領域(一次領域)が確保できなくてエラーになったと考えられます。
ちなみにC言語では、スタック領域のサイズはコンパイル(リンカ)の段階で決まっており動的には変更できます。
今回でいえば、実行時に Javascript(WebAssembly)は、実行時にスタック領域が決められるためこのスタック領域のサイズが指定できれば問題が解決できます。
対策方法
解決のためのヒント1
emscripten の スタックの指定方法、ふむふむ MODULE[“TOTAL_STACK”] の値を指定できれば、スタックのメモリサイズを変更できるみたいですね。
TOTAL_STACK=5242880 // Could be set via MODULE[‘TOTAL_STACK’];
STACK_BASE = STACKTOP = alignMemory(STATICTOP);
STACK_MAX = STACK_BASE + TOTAL_STACK;
DYNAMIC_BASE = alignMemory(STACK_MAX);
HEAP32[DYNAMICTOP_PTR>>2] = DYNAMIC_BASE;
https://github.com/Sable/emscripten_malloc
解決のためのヒント2
下のブログで、このタイマーを設定するとログウィンドウに「Memory stats – used: 155M free: 37M」のように表示されて Unity が使っているメモリーがわかるよ!って説明されています。
しかしこのコードはUnity2018 / 2019 では実行できないためご注意ください。
なぜ使えないかといいますと、 TOTAL_MEMORY / TOTAL_STACK などの変数がグローバル領域に定義されていないからです。
setInterval(function() {
https://medium.com/@kongregate/unity-webgl-memory-and-performance-optimization-3939780a7e97
if (typeof TOTAL_MEMORY !== ‘undefined’) {
try {
var totalMem = TOTAL_MEMORY/1024.0/1024.0;
var usedMem = (TOTAL_STACK + (STATICTOP – STATIC_BASE) +
(DYNAMICTOP – DYNAMIC_BASE))/1024.0/1024.0;
console.log(‘Memory stats – used: ‘ + Math.ceil(usedMem) + ‘M’ +
‘ free: ‘ + Math.floor(totalMem – usedMem) + ‘M’);
} catch(e) {}
}
}, 5000);s
解決のためのヒント3
gameInstance.Module に TOTAL_MEMORY はあるけど、 TOTAL_STACK が無いけどどうすればいいの!!?
ほぉほぉ、 gameInstance.Module にそれぞれの値を設定できるのか。
They use TOTAL_MEMORY, TOTAL_STACK, STATICTOP, STATIC_BASE, DYNAMICTOP and DYNAMIC_BASE to calculate how much memory is actually used.
https://answers.unity.com/questions/1434716/unity-webgl-heap-memory-usage-tracking.html
In my case I found access to gameInstance.Module.TOTAL_MEMORY, but don’t know where to find other variables.
解決したソースコード
だいぶもったいぶりましたが、以下のソースコードが解決編になります。
インスタンスを生成する際に「Module: { TOTAL_STACK: 6 * 1024 * 1024 }」を指定しましょう、次は正常に動作しましたね!
ちなみにですが、 TOTAL_MEMORY / TOTAL_STACK の2つの領域を調整する事を推奨します。
メモリの指定サイズは、TOTAL_MEMORY > TOTAL_STACK になるように指定します。
<script>
var gameInstance = UnityLoader.instantiate(
"gameContainer",
"Build/App.json",
{
onProgress: UnityProgress,
Module: { TOTAL_STACK: 6 * 1024 * 1024 }
});
setInterval(function() {
var text = "";
for(var i = 0; i < 5*1024*1024 / 4; i++) {
text += "*";
}
gameInstance.SendMessage('MyGameObject', 'MyFunction', text);
}, 1000);
</script>
よければ、SNSにシェアをお願いします!
「解決したソースコード」は、どこに配置すればよいのでしょうか。
Unity プロジェクトのAssetフォルダ内でしょうか?
Jさん
コメントありがとうございます。
WebGLのテンプレート機能で、自分のカスタムテンプレートを作成して、そのIndex.htmlの中身になりますね。
https://docs.unity3d.com/ja/2019.4/Manual/webgl-templates.html
もしくは、WebGLビルド生成後のIndex.htmlの中身を書き換えてください。