Unity:WebGLでメモリエラーに苦しんだ話

最終更新日

たっつー(管理人)
たっつー(管理人)

こんにちは、のんびりエンジニアのたっつーです。
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 コンパイルできるようにするものです。


Unity では、.NET ゲームコード(C# や UnityScript スクリプト)を JavaScript に変換するのに IL2CPP と呼ばれる技術を使っています。IL2CPP は .NET バイトコードを、対応する C++ ソースファイルに変換します。それが Emscripten を使ってコンパイルされることでスクリプトが JavaScript に変換されます。

https://docs.unity3d.com/ja/current/Manual/webgl-gettingstarted.html

何が問題なのか?

今回発生している問題は、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() {
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

https://medium.com/@kongregate/unity-webgl-memory-and-performance-optimization-3939780a7e97

解決のためのヒント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.
In my case I found access to gameInstance.Module.TOTAL_MEMORY, but don’t know where to find other variables.

https://answers.unity.com/questions/1434716/unity-webgl-heap-memory-usage-tracking.html

解決したソースコード

だいぶもったいぶりましたが、以下のソースコードが解決編になります。
インスタンスを生成する際に「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にシェアをお願いします!

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

コメントする