以前の記事で、Blazor サーバーのアプリケーションは、システムリソースの消費量が気になる、といったことを書きました。
これがどういうことなのか、実際にサンプルのアプリケーションを通して、確認してみようと思います。 比較のため、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 の診断ツールを使うと、簡単にメモリダンプを取得できるので、これを使ってメモリ消費量を確認することにします。
テストシナリオは極めてシンプルです。 以下の手順でアプリケーションを実行し、各ステップの実行後にメモリダンプを取得します。
- 上記の画面とは無関係のホーム画面をデバッグモードで起動(ブラウザーは Chrome を使います)
- 作成した上記の画面に同一画面内で画面遷移(ブラウザー画面は開きっぱなし)
- ブラウザーのタブを新たに作成して、作成した上記の画面に直接アクセス(ブラウザー画面は開きっぱなし)
- 異なるブラウザー(今回は Edge)を起動して、作成した上記の画面に直接アクセス(ブラウザー画面は開きっぱなし)
Blazor サーバーのアプリケーションは、上記の手順後、さらに追加で以下を実行します。
- Edge で開いた画面を閉じる
- Chrome の別タブで開いた画面を閉じる
結果
ASP.NET Core MVC アプリケーション
初回起動時に約 2 MBのメモリが確保され、その後の通常アクセス、別タブ、別ブラウザーでのアクセスで、若干メモリ消費量が上がるものの、それほど大きな消費にはなっていません。 実際にメモリダンプの中身を見ても、 IndexViewModel や LargeController のオブジェクトはリクエストごとに破棄されています。 私たちが普通に Web アプリケーションの動作として想像するものそのままの結果です。
Blazor サーバーアプリケーション
続いて Blazor サーバーアプリケーションの方を見ると、アクセスのたびに約 1 MB のメモリが積みあがっているのがわかります。 そして、開きっぱなしにしていたブラウザー画面を閉じると、閉じるたびに約 1 MB のメモリが解放されています。 これが、 Blazor サーバーアプリケーションの特徴的な動作なのです。
Single Page Application は、ブラウザーのプロセス上に、入力途中の情報や、表示する情報を保持ししています。 これにより、ユーザーの入力やサーバーから受け取った情報を部分的にレンダリングしなおして、素早く画面更新を行っています。 しかし、 Blazor サーバーアプリケーションは、サーバー内でレンダリングを行い、その結果を SignalR でクライアントに通知するように動作します。 要するに、入力途中の情報や、表示する情報をサーバーメモリ内に保存しているのです。 当然ながら、その保存の単位は、ブラウザーの画面単位になります。 同一マシンから複数のタブやブラウザーを用いてアクセスしても、その画面数分だけ Blazor サーバー内でオブジェクトが生成されます。
実際に 4 の時点で Blazor サーバーアプリケーションは、以下のような状態になっています。
今回作成した画面は、 Large という名前の Blazor コンポーネントです。 4 の時点では 3 つのブラウザー画面が Large にアクセスしているので、 Blazor サーバー内にも 3 つの Large オブジェクトが存在しています。 そしてこのオブジェクトには 1 MB のオブジェクトを保持しているので、このようにメモリが積みあがっています*1。
なお今回 MainLayout をはじめ、他の Blazor コンポーネントも 3 つずつオブジェクトが存在しています。 これは Large 画面が他の Blazor コンポーネント内に配置されているからです。
まとめ
今回は Blazor サーバーアプリケーションの動作をメモリの観点から眺めてみました。 Single Page Application をサーバー内で構築してしまうという「アーキテクチャ上の無理」を感じるのは私だけでしょうか。。。 生産性は非常に魅力的なのですが、こういった問題にどのようにアプローチすればいいのか、まだあまりわかっていません。
*1:以前の Web アプリケーションの感覚で言えば、InProc に設定した Session にオブジェクトを突っ込みまくっているのと近く、ヤバいにおいを感じます。