EventPipe(EventSource)から流れてくるデータを処理する

# 始めに

[前回の記事で、DiagnosticSourceからEventSourceへ出力する記事](https://qiita.com/skitoy4321/items/848889d76a0b6e697d6a) を書いたが、今回は、そのEventSourceのデータを処理するためのライブラリの使い方を書く。

これによって、コマンドラインで来たデータを加工したり、あるいはWPFで流す等の処理も書けるようになる。

[今回調査に使ったソースをgistに貼っておく。](https://gist.github.com/itn3000/69560c6b889677d6a42e24f289396f1c)

# 前提条件

* dotnet sdk-3.0以降が必要
* 情報収集対象のアプリが、netcoreapp3.0以降

# 準備(イベント出力側の用意)

前回の記事でも作成したソースを使用する。

```csharp
using System.Diagnostics;
using System;
using System.Threading.Tasks;
class C1
{
    public DiagnosticSource _D = new DiagnosticListener("Diag1");
    public void A()
    {
        if(_D.IsEnabled("ev1"))
        {
            _D.Write("ev1", new { X = "str" });
        }
    }
}
class Program
{
    static void Main(string[] args)
    {
        while(true)
        {
            // 1秒に一回"ev1"イベントが発生する
            // 停止機能はないので、Ctrl+C等を使用して止めること
            new C1().A();
            Task.Delay(1000).Wait();
        }
    }
}
```

# 必要なライブラリ

[`Microsoft.Diagnostics.NETCore.Client`](https://www.nuget.org/packages/Microsoft.Diagnostics.NETCore.Client/)と[`Microsoft.Diagnostics.Tracing.TraceEvent`](https://www.nuget.org/packages/Microsoft.Diagnostics.Tracing.TraceEvent/)が必要。

## `Microsoft.Diagnostics.NETCore.Client`について

EventPipeプロトコルでdotnetプロセスにアタッチするためのライブラリ。dotnetプロセスが用意するEventPipe読み込みの口へ、PIDをキーにしてアクセスして、データを受け取る。

主に使用するクラスは以下(名前空間は`Microsoft.Diagnostics.NETCore.Client`からの相対位置)

* EventPipeProvider
    * EventSourceの出力元の情報を保持するクラス
    * 捕捉するイベントの種類、レベル、各種フラグ等を設定する
* DiagnosticClient
    * EventProviderとPIDを元に、EventPipe接続を行う
    * `StartEventPipeSession`
    * `public static IEnumerable<int> DiagnosticClient.GetPublishedProcesses()`で、アタッチ可能なPIDの一覧が取得可能
* EventPipeSession
    * `DiagnosticClient.StartEventPipeSession`から生成される、セッションオブジェクト
    * `EventPipeSession.EventStream`で生のバイトデータが取得できる
        * フォーマットは [perfviewリポジトリのドキュメントに記載がある](https://github.com/microsoft/perfview/blob/master/src/TraceEvent/EventPipe/EventPipeFormat.md)
    * **使用後は、必ず`Stop()`メソッドで動作を停止すること**

さて、これだけではEventPipeの生データが取得できるだけで、中身はバイナリデータなので人に読めるものではない(いや、もしかしたら読める人もいるかもしれないが)。
これを解析してくれるライブラリが、`Microsoft.Diagnostics.Tracing.TraceEvent`となる

## `Microsoft.Diagnostics.Tracing.TraceEvent`について

前述のEventPipeSessionから得たデータを解釈して、プログラム的に解析するためのライブラリ。起点はStreamから読み込むので、先のEventPipeSession.EventStreamをの内容をそのまま出力したファイルを使っても良い

EventStreamの解析に使用する場合、主に使用するクラスは`Microsoft.Diagnostics.Tracing.EventPipeEventSource`である。

### EventPipeEventSourceについて

`System.IO.Stream`をコンストラクタにとる。Streamには、`EventPipeSession.EventStream`から得たデータが入っている想定。
データ解析手順には非同期イベントパターン(EAP)を採用しているので、EventPipeEventSourceに付随するイベントを購読する。
また、`IObservable<T>`を返すインターフェイスも提供している。

イベントは、共通のヘッダ部分以外はイベント固有のデータが流れてくるため、そのイベント用の解析クラスを用意する必要がある。
このイベント解析用ベースクラスが`TraceEventParser`で、解析クラスはこれを継承して各イベントの解析処理を実装している。

例えば、CLRのイベント(GCの回収イベントとか)は、専用のパーサーが用意されており、EventPipeEventSource.Clrで専用のイベントが用意されている。

なお、汎用的な用途に使える`DynamicTraceEventParser`も存在しており、ライブラリ側で用意されていない場合はこちらを使用することになるだろう。

そして、一通りのイベント設定が終わったら、`EventPipeEventSource.Process`で解析を開始する。
注意点として、`Process`を実行した時点でスレッドがロックされるため、途中キャンセル等をしたい場合は、
別スレッドを展開して、別のスレッドから、`EventPipeEventSource.StopProcessing`と、元のストリームのクローズ(EventPipeSessionからStreamを取得している場合は、`EventPipeSession.Stop`を実行する
具体的には以下

```csharp
// EventPipeSession session;
// 汎用的なイベントを購読
evsrc.Dynamic.All += (TraceEvent evt) =>
{
    // 解析処理
};
await Task.WhenAll(
  // 解析スレッド
  Task.Run(() => evsrc.Process()),
  // 停止スレッド
  // エンターキーを押したら終了する
  Task.Run(() =>
  {
    Console.ReadLine();
    evsrc.StopProcessing();
    session.Stop();
  }
);
```

なお、これを通さない`Action<TraceEvent> EventPipeEventSource.AllEvents`なるイベントもあるが、これを通すとPayloadNames(イベントデータプロパティの名前)など、主にPayload関連について、セットされないプロパティが出てくる。その場合は、`TraceEvent.EventData()`でバイトデータを取得して、自力でパースする必要がある。

# `EventPipeEventSource.Dynamic.All`の中の解析処理について

`EventPipeEventSource.Dynamic.All`の中では、`TraceEvent.PayloadNames`と`TraceEvent.PayloadByName(string name)`の組み合わせでデータを取得する。具体的には以下のようになる。

```csharp
// TracEvent traceevt;
foreach(var pname in traceevt.PayloadNames)
{
    object pobj = traceevt.PayloadByName(pname);
    Console.WriteLine($"{pname} = {pobj}");
}
```

pobjが何になるか、というところは、プリミティブ型(数値、文字列)ならばそのままキャストすればOK。しかし、それ以外のオブジェクト等がイベント発生元で渡された場合は少々工夫が必要。

## ペイロードデータにオブジェクトで来た場合

この場合、実際の型はinternalで隠蔽されているが、取り出す時には`IDictionary<string, object>[]`で取り出すことができる(`[]`なことに注意)。
`IDictionary<string, object>`には二つ要素が格納されており、それぞれのキーは`Key`と`Value`で、値の方にそれぞれプロパティ名と、実際の値が格納されている
具体的には以下のように値を取り出す。

```csharp
// object pobj = traceevt.PayloadByName(pname);
if(pobj is IDictionary<string, object>[] pdicar)
{
    foreach(var pdic in pdicar)
    {
        string key = (string)pdic["Key"];
        object value = pdic["Value"];
        // 解析処理
    }
}
```

より複雑な型の場合はネストして処理が必要になるだろうが、そもそもEventSourceでそこまで複雑なデータは作らない方が良いと思う。
パーサー自体についての詳細なドキュメントは、 [githubのmicrosoft/perfviewのドキュメントにある](https://github.com/microsoft/perfview/blob/P2.0.61/documentation/TraceEvent/TraceEventProgrammersGuide.md)

## 解析処理の注意点

### コールバック内の例外処理

解析コールバックの中で例外をキャッチし損ねると、以後のイベントが流れなくなり、更に、StopProcessingも受け付けなくなった。
なので、現時点ではコールバックの中ではtry-catchで囲った方が良い。

### 一つもイベントが来ない条件で指定したらフリーズする

少なくともMicrosoft.Diagnostics.Tracing.TraceEvent-2.0.61時点では、一つも該当するイベントが無いようなEventPipeProviderを設定すると、EventPipeEventSourceのコンストラクタで処理が止まってしまうという不具合があるらしい。
詳しい内容は下記issue

* [EventPipeEventSource constructor blocks until the first event is read](https://github.com/microsoft/perfview/issues/1172)

# 終わりに

前回の記事の派生で、EventPipeについて調べてみたけど、取っ掛かりは思ったより簡単だなという感想だった。
ここから、自分なりに使いやすいモニタリングツールを作ってみるのもいいかもしれない。

また、EventPipe自体はローカル限定なので、ここから外部ネットワークに流す口を作るというのも面白い発想かもしれない。

今回紹介したのはイベントの取得のみだが、その他にもメモリダンプを取ることもできる等、トラブルシューティングに役立つ機能があるので、うまく活用していきたい。

## 参考資料

* [dotnet/diagnostics](https://github.com/dotnet/diagnostics): EventPipe等の記述など
    * [dotnet/diagnostics/documentation/design-docs/diagnostics-client-library.md](https://github.com/dotnet/diagnostics/blob/3.1.141901/documentation/design-docs/diagnostics-client-library.md): クライアントライブラリの使い方
* [microsoft/perfview](https://github.com/microsoft/perfview): 解析ツールのperfviewや、そのライブラリの`Microsoft.Diagnostics.Tracing.TraceEvent`のソース等
    * [microsoft/perfview/documentation/TraceEvent](https://github.com/microsoft/perfview/tree/P2.0.61/documentation/TraceEvent): もう少し詳細なドキュメント
* [Microsoft.Diagnostics.NETCore.Clientに関するドキュメント](https://github.com/dotnet/diagnostics/blob/3.1.141901/documentation/diagnostics-client-library-instructions.md)