Blazor WebAssemblyで2GBの壁を超え4GBへ

この記事は [Qiita Advent Calendar 2021 Blazor](https://qiita.com/advent-calendar/2021/blazor) の15日目の記事です。

[2日目の記事](https://qiita.com/yamachu/items/5596169ff19cca7a21ff) でもお伝えしたとおり、Native Dependenciesがサポートされたので、メモリをもりもり使う様（？）なネイティブライブラリもブラウザで動作させられるようになりました。
流石にそんなデカイメモリを使うことはあるのか、というところではありますが、例えば音声合成や音声認識において、辞書ファイルや探索グラフをオンメモリに展開すると500MB~以上なんてことはザラにあったりします。
他にも動画ファイルを読み込んで処理したり、画像の加工をしたり…

そんなアプリケーションを動作させたいのであれば、Blazor WebAssemblyで広いメモリ空間を使えるようにする必要があります。
果たしてどこまでメモリを使うことが出来るのでしょうか？実験で試してみましょう。

## 忙しい人向けに3行まとめ

- Native依存を含めて
- MAXIMUM_MEMORYのビルドフラグを使えば
- 約4GBまで使える（はず

## 環境

- Intel mac mem 32 GB
- Google Chrome  96.0.4664.110
- .NET 6.0.100

## 検証コード

検証に使ったBlazor WebAssemblyのRazorファイルは以下の様な構成です。

```csharp
@page "/"
@using System.Runtime.InteropServices

<label>
    Allocate input GB 
    <input @bind-value="@InputValue" />
</label>

<button onclick="@DoAllocate">Allocate</button>

@code {
    private string? InputValue { get; set; }

    public void DoAllocate()
    {
        var gb = double.Parse(InputValue);
        var totalAllocMem = (nuint)(1_073_741_824 * gb);
        Console.WriteLine(totalAllocMem);

        unsafe
        {
            var buffer = (byte*)NativeMemory.Alloc(totalAllocMem);
            buffer[totalAllocMem - 1] = 1; // Can access!

            NativeMemory.Free(buffer);
        }
    }
}
```

.NET 6から追加されたメモリ管理のAPIである [`NativeMemory.Alloc`](https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.nativememory.alloc?view=net-6.0&WT.mc_id=DT-MVP-5002987) をメモリの確保に使用しています。

`byte*`型にキャストなどを行っていることからも、これは`unsafe`な処理なので、csproj側にも追加しておきます。

`PropertyGroup` の要素として

```xml
<PropertyGroup>
  <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
```

を追加し、unsafeコードをビルド出来る様にしておきましょう。

これでコードの準備が完了しました。
実際に何GBまで使用できるのか確かめてみます。

## デフォルトの状態での検証

![スクリーンショット 2021-12-14 22.38.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104085/50980d10-9326-debb-728d-8e0d3732fa65.png)

順に `1, 1.5, 1.9, 2` を入力しメモリの確保と解放を行っています。
画像から見て分かるように、`2` を入力した段階でメモリ不足でクラッシュしてしまいました。

通常では2GBまでが制約のようです。

## Emscriptenのビルドフラグを使用し限界突破を目指す

さて、本当に限界は2GBなのでしょうか？
32bitで表現できるアドレス空間は4GB程度あるはずと睨み、Emscriptenでどうにかならないか調べてみます。

そうすると見つかりました、Release Notesです！
https://emscripten.org/docs/introducing_emscripten/release_notes.html

この `2.0.13: 01/19/2021` のところを見ると

> - Added support for -s MALLOC=emmalloc when -s MAXIMUM_MEMORY is more than 2GB.

.NET 6のwasm-toolsで使われているEmscriptenは2.0.17なので、このリリースは含まれています。
どうもこの `MAXIMUM_MEMORY` フラグを使うことで2GBの壁を突破できそうです。
これを試してみましょう。

Native Dependenciesを利用した際にEmscriptenのタスクが走りましたが、その実態は Dotnet Workloadの `wasm-tools` が正体です。
これ経由でビルドフラグを渡せないか探ってみると

```xml
<!--
  // 略
  Public properties (optional):
  // 略
  - $(EmccFlags)                        - Emcc flags used for both compiling native files, and linking
-->
```

とまさにこれといったものが見つかります。
このファイルは macOS だと `/usr/local/share/dotnet/packs/Microsoft.NET.Runtime.WebAssembly.Sdk/6.0.0/Sdk/WasmApp.targets` に存在するのですが、GitHubのリポジトリで管理されているもので言うと

https://github.com/dotnet/runtime/blob/a8a10f81bfc289a346ed815f9d064967f9537a30/src/mono/wasm/build/WasmApp.targets#L50

これが該当します。

プロパティもわかったところでcsprojの書き換えを行いましょう。

```xml
  <PropertyGroup>
    <EmccFlags>-s MAXIMUM_MEMORY=4GB</EmccFlags>
  </PropertyGroup>
```

この状態でビルドし同様に確保するメモリを入力してみます。

`1.5, 2` の順番で入力してみると…

![スクリーンショット 2021-12-14 23.09.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104085/a66627af-cc02-69dd-65ff-d52b09d63be5.png)

何ということでしょう、4GBどころか先程の2GBも使い切れていません！
さて原因の究明です。

そういう時にはまずはビルドログということで、おもむろに

```sh
$ dotnet build -v d
```

を叩きビルドを始め、`MAXIMUM_MEMORY` でgrepしてみましょう。
見つかりましたか？

そのとおり、このビルドフラグは使われていません。
そう、これは `dotnet.wasm` をリビルドするタイミングでしか使われないのです。

どうにかして `dotnet.wasm` をリビルドさせるにはどうしたら良いでしょうか？
そう、2日目の記事で紹介した`Native Dependencies`を無理やり使って何かしらの外部ソースをリンクさせてしまえば良いのです。

ということでこんなCファイルをでっち上げます。
`Test.c` とでも名付けておきましょう。

```c
int main()
{
    return 0;
}
```

見るからに何もしないコードですね。

これをcsprojに追加します。

```xml
  <ItemGroup>
    <NativeFileReference Include="Test.c" ScanForPInvokes="false" />
  </ItemGroup>
```

何もしないコードなのでPInvokeの検索テーブルに追加する必要もありません、falseを設定しておきます。

この状態で再度ビルドログが流れるようにビルドすると無事 `MAXIMUM_MEMORY` が使われていることが確認できると思います。
さてこれでメモリは使えるのでしょうか、試してみましょう。

順に`1.5, 2, 3, 4, 3.99` を入力しています。

![スクリーンショット 2021-12-14 23.18.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104085/521f6c32-c0a6-8e42-5252-0690753d0574.png)

4だと桁あふれしてしまっていますね…
ということで3.99ぐらいを試してみましたがこの辺りは駄目なようですね。

では刻んでみましょう。

まずは`3`ぐらいを確保してみましょう。

![スクリーンショット 2021-12-14 23.22.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104085/5fed4952-48fc-1899-1633-6615786c65ad.png)

3GB!?確保できたはずでは！？
はい、ということで急に3GBは確保できないようです。
調査を進めてみた所、まず2GB確保、および開放し、その後3GBを確保であれば行うことが出来ました。

暖機運転が必要なようです。
この様な暖機運転後メモリを確保してみると `3.9GB` 近くまでは確保ができました。

## まとめ

Nativeな依存を含め `MAXIMUM_MEMORY` のビルドフラグを追加することで約 `4GB` までのメモリがBlazor WebAssemblyで使用できることがわかりました。
また段階的にメモリ確保の量を上げてGROWTHさせることが必要なことも併せて確認できました。

とは言えブラウザの1ページがこんなデカイメモリを確保したらどう考えてもユーザが困惑するので、必要なメモリを少しずつ確保するなどの対応は必要そうですね。

---

## あとがき

とここまで書いたあとでIssueを探してみるとありました。

https://github.com/dotnet/runtime/issues/58382

読んでみると

> I guess it would be multiple things which would have to be fixed, not just this one.

ｵｯ…実は何かしらあるかもしれませんね？
今回はAllocateして最後のメモリアドレスに対してアクセスが出来るかだけの検証でした。
実際にデカめのファイルを読ませたりなどすると実は動かない、とかは出てくるのかもしれません。

なにか見つけたらIssueにコメントなりして様子を見ることにします。

また4GB以上のメモリをビルドフラグに渡せるのか実験もしてみましたが、

> wasm-ld : error : maximum memory too large, cannot be greater than 4294967296

あっ、はい。

ということで4GB Maxだけど、実際本当に使えるか神のみぞ知るというオチになってしました。