dotnet-profiling について
dotnet-profilingは、.NETアプリケーションのパフォーマンス問題を診断するスキルです。dotnet-counters、dotnet-trace、dotnet-dumpの使い方を扱い、メトリクス、フレームグラフ、ヒープダンプの読み解きを重視します。
主な機能
- 01dotnet-countersによるランタイムメトリクス監視
- 02イベントトレースとCPUプロファイルの収集
- 03フレームグラフ分析向けのトレース変換
- 04割り当ての追跡とGCデータの関連付け
- 05マネージドメモリダンプの取得と調査
活用シーン
- 01遅延の原因がCPU、メモリ、I/Oのどれかを切り分け
- 02CPUフレームグラフからホットパスを特定
- 03メモリ割り当てとGC活動を調査
- 04メモリリーク疑いのオブジェクト参照を追跡
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill dotnet-profilingインストール後、Claude Code で/dotnet-profilingを入力して読み込まれたか確認します。
詳細
dotnet-profiling
.NET のパフォーマンス問題を調査するための診断ツールのガイダンスです。dotnet-counters によるリアルタイムのメトリック監視、dotnet-trace によるイベントトレースとフレームグラフの生成、dotnet-dump によるメモリダンプの取得と分析を扱います。単にツールを実行するだけでなく、プロファイリングデータの解釈(フレームグラフの読み取り、ヒープダンプの分析、GC メトリックの相関付け)に重点を置いています。
バージョンの前提: .NET SDK 8.0 以降を基準とします。3 つの診断ツール(dotnet-counters、dotnet-trace、dotnet-dump)はすべて .NET SDK に含まれており、別途インストールする必要はありません。
対象外: OpenTelemetry によるメトリック収集と分散トレーシングの設定については、[skill:dotnet-observability] を参照してください。マイクロベンチマークの設定(BenchmarkDotNet)については、このエピックの関連スキル [skill:dotnet-benchmarkdotnet] を参照してください。パフォーマンスアーキテクチャのパターン(Span<T>、ArrayPool、sealed による逆仮想化)については、このエピックの関連スキル [skill:dotnet-performance-patterns] を参照してください。CI での継続的なベンチマーク回帰検出については、[skill:dotnet-ci-benchmarking] を参照してください。アーキテクチャのパターン(キャッシュ、レジリエンス)については、[skill:dotnet-architecture-patterns] を参照してください。
相互参照:[skill:dotnet-observability] では、GC/スレッドプールのメトリックの解釈と OpenTelemetry との相関付けを扱います。[skill:dotnet-benchmarkdotnet] では、プロファイリングでホットパスを特定した後の構造化ベンチマークを扱います。[skill:dotnet-performance-patterns] では、プロファイリング結果に基づいて適用する最適化パターンを扱います。
dotnet-counters -- リアルタイムのメトリック監視
概要
dotnet-counters は、アプリケーションコードを変更せずに .NET ランタイムのメトリックをリアルタイムで監視できます。より負荷の高い計測を行う前に、パフォーマンス問題が CPU、メモリ、I/O のいずれに起因するかを特定するための初期トリアージツールとして使用してください。
実行中のプロセスの監視
# List running .NET processes
dotnet-counters ps
# Monitor default runtime counters for a process
dotnet-counters monitor --process-id <PID>
# Monitor with a specific refresh interval (seconds)
dotnet-counters monitor --process-id <PID> --refresh-interval 2
主な組み込みカウンタープロバイダー
| プロバイダー | カウンター | 分かること |
|---|---|---|
System.Runtime | CPU 使用率、GC ヒープサイズ、Gen 0/1/2 のコレクション、スレッドプールのキュー長、例外数 | ランタイム全体の健全性 |
Microsoft.AspNetCore.Hosting | リクエスト率、リクエスト時間、アクティブなリクエスト数 | HTTP リクエストのスループットとレイテンシ |
Microsoft.AspNetCore.Http.Connections | 接続時間、現在の接続数 | WebSocket/SignalR の接続負荷 |
System.Net.Http | 開始/失敗したリクエスト数、アクティブなリクエスト数、接続プールのサイズ | アウトバウンド HTTP クライアントの動作 |
System.Net.Sockets | 送受信バイト数、データグラム数、接続数 | ネットワーク I/O の量 |
特定のプロバイダーの監視
# Monitor runtime and ASP.NET counters together
dotnet-counters monitor --process-id <PID> \
--counters System.Runtime,Microsoft.AspNetCore.Hosting
# Monitor only GC-related counters
dotnet-counters monitor --process-id <PID> \
--counters System.Runtime[gc-heap-size,gen-0-gc-count,gen-1-gc-count,gen-2-gc-count]
カスタム EventCounters
アプリケーションは、ドメイン固有のメトリック用にカスタムカウンターを公開できます。
using System.Diagnostics.Tracing;
[EventSource(Name = "MyApp.Orders")]
public sealed class OrderMetrics : EventSource
{
public static readonly OrderMetrics Instance = new();
private EventCounter? _orderProcessingTime;
private IncrementingEventCounter? _ordersProcessed;
private OrderMetrics()
{
_orderProcessingTime = new EventCounter("order-processing-time", this)
{
DisplayName = "Order Processing Time (ms)",
DisplayUnits = "ms"
};
_ordersProcessed = new IncrementingEventCounter("orders-processed", this)
{
DisplayName = "Orders Processed",
DisplayRateTimeScale = TimeSpan.FromSeconds(1)
};
}
public void RecordProcessingTime(double milliseconds)
=> _orderProcessingTime?.WriteMetric(milliseconds);
public void RecordOrderProcessed()
=> _ordersProcessed?.Increment();
protected override void Dispose(bool disposing)
{
_orderProcessingTime?.Dispose();
_ordersProcessed?.Dispose();
base.Dispose(disposing);
}
}
カスタムカウンターを監視します。
dotnet-counters monitor --process-id <PID> --counters MyApp.Orders
カウンターデータの解釈
カウンターの値を使って、以降の調査の方向を決めます。これらのランタイムメトリックを OpenTelemetry トレースと相関付ける方法については、[skill:dotnet-observability] を参照してください。
| 症状 | カウンターの証拠 | 次のステップ |
|---|---|---|
| CPU 使用率が高い | cpu-usage > 80%、threadpool-queue-length が低い | dotnet-trace で CPU プロファイリングを行う |
| メモリの増加 | gc-heap-size が増加し、Gen 2 GC が頻発する | dotnet-dump でメモリダンプを取得する |
| スレッドの枯渇 | threadpool-queue-length が増加し、threadpool-thread-count が上限に達している | 同期処理による非同期処理の阻害やブロッキング呼び出しがないか確認する |
| リクエストのレイテンシ | request-duration が高く、active-requests は通常どおり | dotnet-trace で個々のリクエストをトレースする |
| GC の一時停止 | gen-2-gc-count が多く、time-in-gc > 10% | dotnet-trace gc-collect で割り当てをプロファイリングする |
カウンターデータのエクスポート
# Export to CSV for analysis
dotnet-counters collect --process-id <PID> \
--format csv \
--output counters.csv \
--counters System.Runtime
# Export to JSON for programmatic consumption
dotnet-counters collect --process-id <PID> \
--format json \
--output counters.json
dotnet-trace -- イベントトレースとフレームグラフ
概要
dotnet-trace は、実行中の .NET プロセスから詳細なイベントトレースを取得します。トレースをフレームグラフとして分析して CPU のホットパスを特定したり、割り当て追跡を設定して GC 負荷の原因を見つけたりできます。
CPU サンプリング
CPU サンプリングでは、一定間隔でスタックフレームを記録し、アプリケーションがどこで時間を費やしているかを統計的にプロファイルします。
# Collect a CPU sampling trace (default profile)
dotnet-trace collect --process-id <PID> --duration 00:00:30
# Collect with the cpu-sampling profile (explicit)
dotnet-trace collect --process-id <PID> \
--profile cpu-sampling \
--output cpu-trace.nettrace
CPU サンプリングとインストルメンテーションの比較
| アプローチ | オーバーヘッド | 最適な用途 | ツール |
|---|---|---|---|
| CPU サンプリング | 低(約2~5%) | 本番環境で CPU のホットパスを特定する | dotnet-trace --profile cpu-sampling |
| インストルメンテーション | 高(10~50%以上) | 正確な呼び出し回数、メソッドの開始/終了タイミング | Rider/VS プロファイラー、PerfView |
CPU サンプリングはオーバーヘッドが低いため、本番環境で安全に使用できます。デフォルトのアプローチとして使用してください。正確な呼び出し回数が重要な開発環境では、インストルメンテーションを使用してください。
フレームグラフの生成
トレースファイル(.nettrace)は、視覚的に分析するためにフレームグラフ形式に変換する必要があります。
Speedscope を使用する(ブラウザー上で動作、推奨):
# Convert to Speedscope format
dotnet-trace convert cpu-trace.nettrace --format Speedscope
# Opens cpu-trace.speedscope.json -- load at https://www.speedscope.app/
PerfView を使用する(Windows、.NET との高度な統合):
# Convert to Chromium trace format (also viewable in chrome://tracing)
dotnet-trace convert cpu-trace.nettrace --format Chromium
フレームグラフの読み方
フレームグラフは、次のようにコールスタックを表示します。
- フレームの幅は、その関数で費やされたサンプル時間の合計に対する割合を表します(幅が広いほど、時間を多く費やしています)。
- 高さはコールスタックの深さを表します(スタックが高いほど、呼び出しチェーンが深くなります)。
- 色は通常、任意のものであり、特定の配色を使用するツールでない限り、意味はありません。
分析ワークフロー:
- 幅の広い平坦部、つまりサンプルの大部分を消費する関数を探します。
- 最も幅の広いフレームを上方向にたどり、どの呼び出し元が最も時間を費やしているかを確認します。
- 予想外に幅が広い箇所を特定します。本来高速であるべきフレームワークメソッドの幅が広い場合は、誤用が示唆されます。
- 最適化前後のトレースを比較し、対象関数の幅が縮小したことを確認します。
.NET のフレームグラフでよく見られるパターン:
| パターン | 考えられる原因 | 調査方法 |
|---|---|---|
幅の広い System.Linq フレーム | デリゲートのオーバーヘッドを伴う LINQ 中心のホットパス | foreach ループまたは Span ベースの処理に置き換える |
幅の広い JIT_New / gc_heap::allocate | GC を引き起こす過剰な割り当て | --profile gc-collect による割り当てプロファイリング |
幅の広い Monitor.Enter / SpinLock | ロック競合 | 同期戦略を見直す |
幅の広い System.Text.RegularExpressions | 正規表現のバックトラッキング | RegexOptions.NonBacktracking を使用するか、正規表現をコンパイルする |
| 深い非同期ステートマシンのフレーム | タイトなループでの非同期オーバーヘッド | CPU バウンドの処理では同期パスを検討する |
gc-collect プロファイルによる割り当ての追跡
gc-collect プロファイルは割り当てイベントをキャプチャし、最も多くのメモリを割り当てるコードパスを特定します。
# Collect allocation data
dotnet-trace collect --process-id <PID> \
--profile gc-collect \
--duration 00:00:30 \
--output alloc-trace.nettrace
これにより、次の情報を示すトレースが生成されます。
- 最も多くのバイト数を割り当てるメソッド
- 最も頻繁に割り当てられる型
- 割り当てサイズと、それを引き起こすコールスタック
割り当てデータを dotnet-counters の GC カウンターの証拠と関連付けてください。gen-2-gc-count の値が高い場合、割り当てトレースから、Gen 2 まで生存する長寿命オブジェクトを生成するコードパスを確認できます。ホットな割り当て箇所を特定した後に適用できる、割り当てゼロのパターンについては、[skill:dotnet-performance-patterns] を参照してください。
カスタムトレースプロバイダー
対象を絞ったトレースを行うには、特定のイベントプロバイダーを指定します。
# Trace specific providers with keywords and verbosity
dotnet-trace collect --process-id <PID> \
--providers "Microsoft-Diagnostics-DiagnosticSource:::FilterAndPayloadSpecs=[AS]System.Net.Http"
# Trace EF Core queries (useful with [skill:dotnet-efcore-patterns])
dotnet-trace collect --process-id <PID> \
--providers Microsoft.EntityFrameworkCore
# Trace ASP.NET Core request processing
dotnet-trace collect --process-id <PID> \
--providers Microsoft.AspNetCore
トレースファイルの管理
| 形式 | 拡張子 | ビューアー | クロスプラットフォーム |
|---|---|---|---|
| NetTrace | .nettrace | PerfView、VS、dotnet-trace convert | はい(キャプチャ);Windows(PerfView) |
| Speedscope | .speedscope.json | https://www.speedscope.app/ | はい |
| Chromium | .chromium.json | Chrome DevTools(chrome://tracing) | はい |
dotnet-dump -- メモリダンプの分析
概要
dotnet-dump はプロセスメモリダンプをキャプチャして分析します。メモリリーク、大きなオブジェクトヒープの断片化、オブジェクト参照チェーンの調査に使用します。dotnet-trace とは異なり、ダンプはマネージヒープ全体のある時点でのスナップショットをキャプチャします。
ダンプの取得
# Capture a full heap dump
dotnet-dump collect --process-id <PID> --output app-dump.dmp
# Capture a minimal dump (faster, smaller, but less detail)
dotnet-dump collect --process-id <PID> --type Mini --output app-mini.dmp
取得するタイミング:
- メモリ使用量が想定されるベースラインを超えて増加した場合(dotnet-counters の
gc-heap-sizeと比較します) - アプリケーションが OOM に近づいている場合
- 負荷テスト後にメモリリークが疑われる場合
- ファイナライザーキューの滞留を調査する場合
SOS コマンドによるダンプの分析
対話型アナライザーでダンプを開きます。
dotnet-dump analyze app-dump.dmp
!dumpheap -- ヒープオブジェクトの概要
マネージドヒープ上のオブジェクトを型ごとにまとめ、合計サイズの降順で表示します。
> dumpheap -stat
Statistics:
MT Count TotalSize Class Name
00007fff2c6a4320 125 4,000 System.String[]
00007fff2c6a1230 8,432 269,824 System.String
00007fff2c7b5640 2,100 504,000 MyApp.Models.OrderEntity
00007fff2c6a0988 15,230 1,218,400 System.Byte[]
分析の進め方:
- アプリケーションの型で、想定外に件数やサイズが大きいものを探します
- 件数を想定されるカーディナリティと比較します(例:OrderEntity オブジェクトが 2,100 個ある場合、現在の負荷で想定される数でしょうか?)
System.Byte[]の件数が多い場合、上限のないバッファリングやストリーム処理の問題が原因であることがよくあります
型でフィルターします。
> dumpheap -type MyApp.Models.OrderEntity
> dumpheap -type System.Byte[] -min 85000
-min 85000 フィルターでは、ラージオブジェクトヒープのエントリー(85,000 バイト以上のオブジェクト)が表示されます。これらは Gen 2 GC の負荷を引き起こします。
!gcroot -- オブジェクトの保持元の特定
GC ルートから特定のオブジェクトまでの参照チェーンをたどり、そのオブジェクトが回収されない理由を明らかにします。
> gcroot 00007fff3c4a2100
HandleTable:
00007fff3c010010 (strong handle)
-> 00007fff3c3a1000 MyApp.Services.CacheService
-> 00007fff3c3a1020 System.Collections.Generic.Dictionary`2
-> 00007fff3c4a2100 MyApp.Models.OrderEntity
Found 1 unique root(s).
一般的なルートの種類とその意味:
| ルートの種類 | 意味 | 考えられる問題 |
|---|---|---|
strong handle | 静的フィールドまたは GC ハンドル | 解放されずに増え続ける静的コレクション |
pinned handle | ネイティブ相互運用のために固定されています | バッファが必要以上に長く固定されている |
async state machine | 非同期クロージャーにキャプチャされています | 長時間実行される非同期処理が参照を保持している |
finalizer queue | ファイナライザースレッドの処理待ちです | ファイナライザーの滞留が回収を妨げている |
threadpool | スレッドローカルストレージから参照されています | クリーンアップされないスレッド静的キャッシュ |
!finalizequeue -- ファイナライザーキューの分析
ファイナライズ待ちのオブジェクトを表示します。これらのオブジェクトは、少なくとも 1 回の GC サイクル分、回収が遅れます。
> finalizequeue
SyncBlocks to be cleaned up: 0
Free-Threaded Interfaces to be released: 0
MTA Interfaces to be released: 0
STA Interfaces to be released: 0
----------------------------------
generation 0 has 12 finalizable objects
generation 1 has 45 finalizable objects
generation 2 has 230 finalizable objects
Ready for finalization 8 objects
主な指標:
- 「Ready for finalization」の件数が多い場合、ファイナライザースレッドの処理が追いついていません
- Gen 2 のファイナライズ可能オブジェクトリストにあるオブジェクトはコストが高く、最低でも 2 回の GC サイクルを生き残ります(ファイナライズのスケジュールに 1 回、ファイナライズ実行後の回収に 1 回)
IDisposable.Dispose()を呼び出さずに~Destructor()を実装している型が、主な原因です
ヒープ分析に使うその他の SOS コマンド
| コマンド | 目的 | 使用するタイミング |
|---|---|---|
dumpobj <address> | 特定のオブジェクトのフィールド値を表示します | dumpheap で見つけた後にオブジェクトの状態を調べる場合 |
dumparray <address> | 配列の内容を表示します | ヒープ統計で見つかった大きな配列を調査する場合 |
eeheap -gc | GC ヒープのセグメント構成を表示します | LOH の断片化を調査する場合 |
gcwhere <address> | オブジェクトがどの GC 世代にあるかを表示します | オブジェクトが固定されているか、LOH にあるかを判定する場合 |
dumpmt <MT> | メソッドテーブルの詳細を表示します | 型のメタデータを調査する場合 |
threads | スタックトレース付きですべてのマネージドスレッドを一覧表示します | デッドロックやブロッキングを特定する場合 |
clrstack | 現在のスレッドのマネージドコールスタックを表示します | スレッドの状態をヒープデータと照合する場合 |
メモリリーク調査のワークフロー
- ベースライン: アプリケーションの起動と初期ウォームアップ後にダンプを取得します
- 負荷: メモリリークが疑われるワークロードのシナリオを実行します
- 比較: ワークロードの完了後に 2 つ目のダンプを取得します
- 差分: 2 つのダンプの
dumpheap -statの出力を比較し、件数または合計サイズが大幅に増加した型を探します - ルート: 増加した型のインスタンスに対して
gcrootを使い、保持チェーンを特定します - 修正: 保持チェーンを断ち切ります(静的コレクションから削除する、イベントサブスクリプションを破棄する、非同期処理のライフタイムに関する問題を修正する)
# Tip: save dumpheap output for comparison
# In dump 1:
> dumpheap -stat > /tmp/heap-before.txt
# In dump 2:
> dumpheap -stat > /tmp/heap-after.txt
# Compare externally:
# diff /tmp/heap-before.txt /tmp/heap-after.txt
プロファイリングワークフローの概要
診断ツールを使って、体系的な調査ワークフローを進めます。
1. dotnet-counters (triage)
├── CPU high? → dotnet-trace --profile cpu-sampling
│ → Convert to flame graph (Speedscope)
│ → Identify hot methods
├── Memory growing? → dotnet-dump collect
│ → dumpheap -stat (find large/numerous types)
│ → gcroot (find retention chains)
│ → Fix retention + verify with second dump
├── GC pressure? → dotnet-trace --profile gc-collect
│ → Identify allocation hot paths
│ → Apply zero-alloc patterns [skill:dotnet-performance-patterns]
└── Thread starvation? → dotnet-dump analyze
→ threads (list all managed threads)
→ clrstack (check for blocking calls)
プロファイリングでボトルネックを特定したら、[skill:dotnet-benchmarkdotnet] を使って、修正による改善を定量化するための対象を絞ったベンチマークを作成します。
エージェントが陥りやすい点
- dotnet-trace ではなく、まず dotnet-counters を使ってください -- カウンターはオーバーヘッドがほぼゼロで、問題のカテゴリ(CPU、メモリ、スレッド)を特定できます。カウンターで調査対象を絞り込んでから、トレースまたはダンプを使用してください。
- 本番環境ではインストルメンテーションではなく、CPU サンプリングを使用してください -- サンプリングのオーバーヘッドは 2~5% で、本番環境でも安全に使用できます。インストルメンテーションは 10~50% 以上のオーバーヘッドが発生するため、開発環境に限定してください。
- 分析の際は、必ずトレースをフレームグラフに変換してください -- 生の
.nettraceイベントログを読むのは現実的ではありません。dotnet-trace convert --format Speedscopeを使用して、https://www.speedscope.app/ で開き、視覚的に分析してください。 - メモリリークの調査では、ダンプを 2 つ取得してください -- 1 つのダンプでは現在の状態はわかりますが、通常の常駐オブジェクトとリークしたオブジェクトを区別できません。リークが疑われるシナリオの前後に取得した 2 つのダンプ間で、ヒープ統計を比較してください。
- LOH オブジェクトを見つけるには、dumpheap を
-min 85000でフィルターしてください -- 85,000 バイト以上のオブジェクトはラージオブジェクトヒープ(LOH)に格納され、Gen 2 GC でのみ回収されます。LOH のオブジェクト数が多い場合は、断片化の可能性があります。 - GC カウンターデータの解釈には [skill:dotnet-observability] を使用してください -- ランタイムの GC/スレッドプールカウンターは OpenTelemetry メトリクスと重複します。プロファイリングの結果を分散トレースのコンテキストと関連付けるには、オブザーバビリティスキルを使用してください。
- dotnet-trace gc-collect と dotnet-dump を混同しないでください -- gc-collect は時間経過に伴う割り当てイベント(どのメソッドが割り当てを行うか)をトレースし、dotnet-dump は特定時点のヒープスナップショット(どのオブジェクトが存在するか)を取得します。割り当て率の分析には gc-collect を、保持やメモリリークの分析には dotnet-dump を使用してください。