Rustで PerformanceEvidenceProbe という小さなツールを作りました。
名前が長いので、以下では PEP と呼びます。
PEPは、実行中のアプリケーションを外から観測して、CPU、メモリ、プロセスなどの情報を記録するためのツールです。
Windows、Linux、macOSに対応しています。
GitHub
ソースコード、README、実行例、Windows / Linux / macOS向けのreleaseをGitHubで公開しています。
PerformanceEvidenceProbe
MIT OR Apache-2.0です。
なぜ作ったか
きっかけは単純で、
「性能についてAgentに調べてもらいたいけど、毎回対象アプリに計測コードを埋め込むのは面倒だな」
と思ったことでした。
既存アプリに計測コードを追加して、ログを増やして、計測して、また外す。
これを毎回やりたくありませんでした。
そこで、
対象アプリ
↓
PEPが外から観測
↓
計測結果を保存
↓
Agentに渡す
↓
Agentが内容を読む
という形にしました。
人間向けのきれいなグラフを作ることより、まずはAgentにも人間にも後から読める材料を残すことを重視しています。
何が測れるのか
PEPは対象コマンドを起動して観測する run と、すでに動いているPIDを観測する attach を持っています。
観測できるものはOSによって異なりますが、主に以下のような情報を扱います。
- プロセスのidentityとlifecycle
- user / kernel CPU time
- thread数
- 経過時間やsampling間隔
- working set
- private bytes
- read / writeのbytes、operations
- 観測対象プロセス群の集計
- collector自身のCPUなどの観測値
- 実行開始、終了、degradationなどのevent
Windowsの run ではJob Objectを使ったaccountingも行うため、子プロセスを含めた集計情報をより多く取得できます。
一方でLinux / macOSでは、現在は直接のroot processを中心とした、意図的に狭い観測範囲にしています。
これは、
「取れそうな似た値を無理に同じmetricとして扱わない」
ためです。
たとえばWindowsで得られるmemory metricに似た値が別OSに存在していても、意味が同じだと保証できなければ代用品として入れません。
Agentに説明もやらせる
PEP自身は、できるだけ観測に徹します。
たとえばCPU使用量が高かったとしても、
「これは性能問題です」
とは判断しません。
観測した値を保存して、その意味を考えるのは別の層に任せます。
自分の場合は、その出力をAgentへ渡して、
- どのプロセスが動いていたか
- CPUがどの程度使われていたか
- メモリ使用量がどう変化したか
- 想定外の子プロセスが存在しないか
- I/Oが増えていないか
- どこを追加調査した方がよいか
などを説明させる使い方を想定しています。
つまりPEPは、
性能を説明するツールというより、性能を説明するための材料を集めるツール
です。
なぜ「Evidence」と呼んでいるのか
PEPでは、単に最後の数字だけをJSONに書いて終わりにはしていません。
1回の観測ごとに独立したevidence bundleを作り、その中へraw dataを保存します。
中心になるのはこの3つです。
processes.ndjson
samples.ndjson
events.ndjson
processes.ndjson には、観測したプロセスのidentityや取得結果。
samples.ndjson には、時刻付きのCPU、memory、I/Oなどのraw sample。
events.ndjson には、起動、終了、collector degradationなどのevent。
という形で、何を観測して最終結果が出たのかを後から辿れるようにしています。
そのraw evidenceから、
summary.json
を生成します。
ここで重要なのは、summary.json が新しく計測した結果ではないことです。
保存済みのraw dataから決定的に再構築できる、読みやすい派生物として扱っています。
つまり、
実行
↓
raw evidence
↓
summary
↓
Agentによる解釈
という順番です。
Agentの説明が気に入らなければ、raw evidenceへ戻れます。
summaryの数字が気になれば、元sampleまで戻れます。
PEPが何かを「速い」「遅い」と判定することもありません。
このあたりを、自分は「単なるログ」より少し強い意味でevidenceと呼んでいます。
もちろん、PEPの記録があるからといって、
- このbenchmarkが正しい
- このworkloadが代表的である
- この性能主張が正しい
ことまで証明するわけではありません。
PEPが証拠として残すのは、あくまで
「この観測境界の中で、collectorが何を観測したか」
です。
測れなかった値を0にしない
作っていてかなり気になったのが、OSごとの差でした。
Windows、Linux、macOSでは、同じ名前で取れそうに見える値でも意味が完全には一致しません。
環境や観測状態によって取得できない値もあります。
そこでPEPでは、取得不能なmetricを勝手に 0 にしません。
たとえば、
{
"record_type": "metric_unavailable",
"metric": "process.working_set_bytes",
"subject_kind": "RUN",
"reason": "semantic_mismatch"
}
のように、
値が存在しない理由そのものを記録します。
逆に、本当に観測値が0だった場合は、
{
"user_cpu_time_ns": 0,
"kernel_cpu_time_ns": 2071,
"thread_count": 1
}
のように、普通の数値 0 として残ります。
この区別はAgentに読ませる場合にもかなり重要です。
0だった
と
測れなかった
は全く違います。
入力時点でこの意味を潰してしまうと、その後どれだけAgentが賢く考えても復元できません。
完全に測れたふりもしない
PEPは、観測そのものの状態も記録します。
たとえばsamplingの一部が欠けた場合、それを隠したまま完全な測定結果として出すのではなく、measurement validityやcompletenessへ反映します。
workload自体が終了したのか。
collectorは正常だったのか。
一部のmetricだけ取得できなかったのか。
そもそもそのOSでは同じ意味の値を取得できないのか。
これらをなるべく混ぜないようにしています。
性能データでは、数字そのものと同じくらい
「その数字をどこまで信用してよいのか」
が重要だと思ったからです。
Rustで作った理由
この種のツールは、対象プロセスを監視したり、OS固有APIを触ったりするので、Rustとはかなり相性が良いと感じました。
特に、
- 単体のCLIとして配布しやすい
- 実行時依存を増やさずに済む
- OS固有処理と共通処理を分離しやすい
- 型で「値」と「取得不能」を分けやすい
- raw recordを比較的厳密に扱いやすい
あたりは便利でした。
一方、クロスプラットフォーム対応では、
「全部のOSで同じ値を返す」
より、
「同じ意味だと確認できるものだけ同じものとして扱う」
方が重要だと感じています。
LinuxやmacOSで取れないものが多く見えても、それを似た値で埋めて見栄えを良くするより、取得できなかった理由を残す方を選びました。
使い方のイメージ
自分がやりたかったのは、だいたいこんな使い方です。
Agent
↓
アプリを実行
↓
PEPで観測
↓
evidence bundleを保存
↓
Agentが結果を読む
↓
仮説を立てる
↓
条件を変えて再実行
この流れにしておけば、対象アプリ側へ計測コードを埋め込まなくても、Agentが何度か実験しながら性能を調べられます。
ベンチマークの数字を一つ取って終わりではなく、
実行時に何が起きていたかを、後から再確認できる形で残す
ための道具です。
まだ小さいツールです
PEPはまだ小さなツールです。
万能なprofilerでもなければ、既存のperformance toolを置き換えるものでもありません。
call stack profilerでもありませんし、benchmark結果を自動的に認証してくれるものでもありません。
狙っているのはもっと狭くて、
Agentや人間が後から検証できるruntime evidenceを、対象アプリの外側から残すこと
です。
元々は「Agentに性能調査をやらせたいけど、計測コードを毎回埋め込むの面倒だな」から始めたものですが、作っていくと、
測ることより、測ったという主張をどう残すか
の方が面白い問題になってきました。
Rustでこういう道具を作るのはなかなか楽しかったので、しばらく育ててみようと思っています。