ツナ缶雑記

ぐうたらSEのブログです。主にマイクロソフト系技術を中心に扱います。

Blazor サーバーのメモリ利用量を確認してみた

以前の記事で、Blazor サーバーのアプリケーションは、システムリソースの消費量が気になる、といったことを書きました。

tsuna-can.hateblo.jp

これがどういうことなのか、実際にサンプルのアプリケーションを通して、確認してみようと思います。 比較のため、ASP.NET Core 3.0 の MVC アプリケーションでも同様のアプリケーションを作成して、どのような差があるのか確認してみます。

サンプルアプリケーション

今回は動作の結果をわかりやすくするため、リクエストを受け付けたら 1 MBの領域を確保して、その確保したバイト数を画面表示するようなものにしておきます。 同じようなアプリケーションということで、ASP.NET Core の MVC アプリケーションと、Blazor サーバーで以下のような画面を作成しました。

ASP.NET Core の MVC アプリケーション

まず ViewModel はこんな感じ(いろいろ適当ですが。。。)。

using System.Collections.Generic;

namespace AspNetMvcApp.Models.Large
{
    public class IndexViewModel
    {
        public List<byte> Data { get; set; }
        public int Capacity { get; set; }
    }
}

コントローラーはこんな感じ。 こちらも適当です。

using System.Collections.Generic;
using AspNetMvcApp.Models.Large;
using Microsoft.AspNetCore.Mvc;

namespace AspNetMvcApp.Controllers
{
    public class LargeController : Controller
    {
        public IActionResult Index()
        {
            var data = new List<byte>(1024 * 1024);
            var model = new IndexViewModel { Data = data, Capacity = data.Capacity };
            return View(model);
        }
    }
}

View はスキャフォールドで ViewModel から生成しただけなので割愛します。

Blazor サーバーのアプリケーション

Blazor コンポーネントは以下のような感じ。

@page "/large"
<h3>Large</h3>
<dl>
    <dt>バイト数</dt>
    <dd>@capacity</dd>
</dl>
@code {
    private List<byte> data;
    private int capacity;

    protected override async Task OnInitializedAsync()
    {
        this.data = new List<byte>(1024 * 1024);
        this.capacity = this.data.Capacity;
    }
}

やっていることは MVC アプリケーションの方とまったく同じです。

動作確認

今回はサーバーメモリの状態に着目して、システムリソースの消費量を計測していきます。 Visual Studio 2019 の診断ツールを使うと、簡単にメモリダンプを取得できるので、これを使ってメモリ消費量を確認することにします。

テストシナリオは極めてシンプルです。 以下の手順でアプリケーションを実行し、各ステップの実行後にメモリダンプを取得します。

  1. 上記の画面とは無関係のホーム画面をデバッグモードで起動(ブラウザーChrome を使います)
  2. 作成した上記の画面に同一画面内で画面遷移(ブラウザー画面は開きっぱなし)
  3. ブラウザーのタブを新たに作成して、作成した上記の画面に直接アクセス(ブラウザー画面は開きっぱなし)
  4. 異なるブラウザー(今回は Edge)を起動して、作成した上記の画面に直接アクセス(ブラウザー画面は開きっぱなし)

Blazor サーバーのアプリケーションは、上記の手順後、さらに追加で以下を実行します。

  1. Edge で開いた画面を閉じる
  2. Chrome の別タブで開いた画面を閉じる

結果

ASP.NET Core MVC アプリケーション

f:id:masatsuna:20191105000316p:plain
ASP.NET Core MVC アプリケーションのメモリ消費量

初回起動時に約 2 MBのメモリが確保され、その後の通常アクセス、別タブ、別ブラウザーでのアクセスで、若干メモリ消費量が上がるものの、それほど大きな消費にはなっていません。 実際にメモリダンプの中身を見ても、 IndexViewModel や LargeController のオブジェクトはリクエストごとに破棄されています。 私たちが普通に Web アプリケーションの動作として想像するものそのままの結果です。

Blazor サーバーアプリケーション

f:id:masatsuna:20191105000647p:plain
Blazor サーバーアプリケーションのメモリ消費量

続いて Blazor サーバーアプリケーションの方を見ると、アクセスのたびに約 1 MB のメモリが積みあがっているのがわかります。 そして、開きっぱなしにしていたブラウザー画面を閉じると、閉じるたびに約 1 MB のメモリが解放されています。 これが、 Blazor サーバーアプリケーションの特徴的な動作なのです。

Single Page Application は、ブラウザーのプロセス上に、入力途中の情報や、表示する情報を保持ししています。 これにより、ユーザーの入力やサーバーから受け取った情報を部分的にレンダリングしなおして、素早く画面更新を行っています。 しかし、 Blazor サーバーアプリケーションは、サーバー内でレンダリングを行い、その結果を SignalR でクライアントに通知するように動作します。 要するに、入力途中の情報や、表示する情報をサーバーメモリ内に保存しているのです。 当然ながら、その保存の単位は、ブラウザーの画面単位になります。 同一マシンから複数のタブやブラウザーを用いてアクセスしても、その画面数分だけ Blazor サーバー内でオブジェクトが生成されます。

実際に 4 の時点で Blazor サーバーアプリケーションは、以下のような状態になっています。

f:id:masatsuna:20191105001930p:plain
4 の時点でのメモリダンプ

今回作成した画面は、 Large という名前の Blazor コンポーネントです。 4 の時点では 3 つのブラウザー画面が Large にアクセスしているので、 Blazor サーバー内にも 3 つの Large オブジェクトが存在しています。 そしてこのオブジェクトには 1 MB のオブジェクトを保持しているので、このようにメモリが積みあがっています*1

なお今回 MainLayout をはじめ、他の Blazor コンポーネントも 3 つずつオブジェクトが存在しています。 これは Large 画面が他の Blazor コンポーネント内に配置されているからです。

まとめ

今回は Blazor サーバーアプリケーションの動作をメモリの観点から眺めてみました。 Single Page Application をサーバー内で構築してしまうという「アーキテクチャ上の無理」を感じるのは私だけでしょうか。。。 生産性は非常に魅力的なのですが、こういった問題にどのようにアプローチすればいいのか、まだあまりわかっていません。

*1:以前の Web アプリケーションの感覚で言えば、InProc に設定した Session にオブジェクトを突っ込みまくっているのと近く、ヤバいにおいを感じます。