C#
ASP.NET_Core
Profiling
MiniProfiler

MiniProfilerをASP.NET Core MVC以外で利用する

はじめに

注意: 間違ったプロファイラの使い方をしていたので、初版と比べてベンチマーク結果が大幅に変わりました。

ASP.NETでリクエストの応答速度等を測りたい場合に使えるプロファイラとして、MiniProfilerというものがある。
ASP.NET Coreに対応したバージョンはまだalpha版だが、使用は可能。
しかし、ドキュメントを見るだけでは不明な点がいくつかあったので、調べてわかったことを記事に書く。

ここでは例としてコンソールプロジェクトに組み込む方法を書く。途中までは公式ドキュメントを参考にしている。

なお、プロファイラと言えば、ものによってはメモリ使用量等も計測できる場合もあるが、MiniProfilerではそのような機能は存在せず、あくまで速度の計測のみができることに注意。

前提条件

  • dotnet sdk 2.0以降 をインストールしている
  • "dotnet"コマンドにパスが通っている

コンソールプロジェクトの準備

以下のコマンドで新しくプロジェクトを作成する。

dotnet new console --name [プロジェクト名]

成功すると、[プロジェクト名]で指定したフォルダにcsprojが作成される。

パッケージのインストール

作成したプロジェクトにカレントディレクトリを移し、以下のコマンドを実行する。

dotnet add package MiniProfiler.AspNetCore --version [パッケージバージョン]

記事作成時点ではパッケージはまだプレリリース段階のものしかないので、MiniProfilerのnugetページを参照して、バージョンを指定する必要がある。

成功すると、以下の項目がcsprojに追加される。
<PackageReference Include="MiniProfiler.AspNetCore" Version="[パッケージバージョン]"/>

プロファイリングの実装

特に表記が無い限り、StackExchange.Profiling名前空間にクラスは存在する。

プロファイルオプションインスタンスの準備

まずはMiniProfilerOptionsのインスタンスを生成する。プロファイラーはこのインスタンスから設定を使い回すような設計になっているようなので、グローバルに持つか、DIで注入するようにすると、後々楽になる。全てデフォルト設定で済ませるならば、そのまま引数なしでnewして使えば良い。

プロファイリングの開始

生成したオプションから、StartProfiler("[プロファイラ名]")を実行して、MiniProfilerインスタンスを生成する。
ここで生成するインスタンスを、計測の一区間(1リクエスト等)で共有することになる。
この時指定する名前は、後から表示用に使用するので、どこの区間の計測に使用したか、わかる名前にしておくこと。

階層化

一般的に処理というものは、下記のように大抵は階層構造になっている。

A-+-B
  +-C-D

MiniProfilerでは、Timingオブジェクトによってこれを制御しており、以下のようにして階層構造の表現をする。

// using StackExchange.Profiling;
MiniProfiler mp = new MiniProfilerOptions().StartProfiler("my_profile");
using(mp.Step("step_a"))
{
    // 処理A
    using(mp.Step("step_b"))
    {
        // 処理B
    }
    using(mp.Step("step_c"))
    {
        // 処理C
        using(mp.Step("step_d"))
        {
            // 処理D
        }
    }
}
// 計測を終了したい時は、Stop()を実行する
// 実行しないと、処理時間が大幅に変わってしまうので注意
mp.Stop()

Step()の引数には、区間名を指定する。これは後で表示用に使われる。

プロファイルを行うかどうかを実行時に決定したい場合、MiniProfilerインスタンスをnullにすれば、Stepで何も生成しなくなるので、ほぼオーバーヘッドは無くなる。(Stepは拡張メソッドなので、インスタンスがnullでも問題ない)

結果の取得

MiniProfilerインスタンスのTiming MiniProfiler.Rootというプロパティから再帰的に結果を取得できる。
Timingクラスで主に使用するのは以下。

  • Name: Step()等で付けた名前
  • Children: 子区間のリスト
  • DurationMilliseconds: 子も含めた経過時間
  • DurationWithoutChildrenMilliseconds: 子を含まない経過時間
  • Depth: ルートから数えた深さ(数字が大きいほど深い階層)
  • HasChildren: 子を持っているかどうか

よって、一つのプロファイリングに関しては、基本的には以下のようにして結果を処理することになる。

// MiniProfiler mp;
// 計測終了

void ProcessTiming(Timing t)
{
    // tに対して何らかの処理
    if(t.HasChildren)
    {
        foreach(var child in t.Children)
        {
            // 再帰的に処理する
            ProcessTiming(child);
        }
    }
}

ProcessTiming(mp.Root);

結果の保存と取得

プロファイリング結果をその場で処理する場合は上記で十分だが、各プロファイリング結果をまとめて後で閲覧したい場合、MiniProfilerではIAsyncStorageを使用する。
使い方の大まかな流れとしては以下。

  1. MiniProfilerOptions.StorageIAsyncStorageのインスタンスをセット(以後、このインスタンスを使い回す)
  2. MiniProfiler.Storage.Save(MiniProfiler)で、プロファイル結果を保存
  3. MiniProfilerOptions.Storage.List()で、保存したプロファイル結果を列挙(GUID)
  4. MiniProfilerOptions.Storage.Load(guid)で、列挙したGUIDを指定して、プロファイル結果を取り出す
  5. 列挙したプロファイル結果それぞれに対して処理を行う

IAsyncStorageの実装については、基本的なものはStackExchange.Profiling.Storage.MemoryCacheStorageとなる。(MiniProfiler.AspNetCore.Mvcでデフォルトで使われるのもこれ)
ただし、このMemoryCacheStorageの生成が柔軟な分若干回りくどいので、後述する。
なお、他のIAsyncStorageの実装は、NugetでMiniProfiler.Providersで検索すると出てくる(RedisやSqlServer実装が存在する)

MemoryCacheStorageの生成方法

このMemoryCacheStorage、newの引数は(IMemoryCache cache, TimeSpan cacheDuration)になっている。
IMemoryCacheがどこにあるかというと、Microsoft.Extensions.Caching.Memory以下に存在する。

なので、このIMemoryCacheを実装したインスタンスを生成すればいいわけだけど、デフォルト実装(MemoryCache)を使用するには、Microsoft.Extensions.Caching.Memoryパッケージを追加する必要がある。
パッケージの追加は以下のコマンドで可能。

dotnet add package Microsoft.Extensions.Caching.Memory

パッケージを追加すれば、Microsoft.Extensions.Caching.Memory.MemoryCacheが使用できるようになる。
更にこのMemoryCacheを生成するには、IOption<MemoryCacheOptions>がnew引数に必要になる。
これは、作成するだけならMicrosoft.Extensions.Options.Options.Create(new MemoryCacheOptions())でOK。
応用すれば、設定ファイルからメモリキャッシュオプションを生成することも可能らしいが、ここでは省略する。

まとめると、以下のようなコードになる。

// using Microsoft.Extensions.Options;
// using Microsoft.Extensions.Caching.Memory;
// using StackExchange.Profiling.Storage;
var memcacheopt = Options.Create(new MemoryCacheOptions());
var memcache = new MemoryCache(memcacheopt);
var memstorage = new MemoryCacheStorage(memcache, TimeSpan.FromMinutes(60));

注意点

プロファイリング自体のオーバーヘッドが無くもないので、可能ならば開始時の設定等で、取る、取らないを決定できるようにしておいた方が良い。
実際に測った結果をGistにアップロードしておく