mcpField
D
dotnet-profiling

dotnet-profiling

github.com/wshaddix開発8677 回インストール35 週間前に更新
リポジトリ

dotnet-profiling について

dotnet-profilingは、.NETアプリケーションのパフォーマンス問題を診断するスキルです。dotnet-counters、dotnet-trace、dotnet-dumpの使い方を扱い、メトリクス、フレームグラフ、ヒープダンプの読み解きを重視します。

主な機能

  1. 01dotnet-countersによるランタイムメトリクス監視
  2. 02イベントトレースとCPUプロファイルの収集
  3. 03フレームグラフ分析向けのトレース変換
  4. 04割り当ての追跡とGCデータの関連付け
  5. 05マネージドメモリダンプの取得と調査

活用シーン

  1. 01遅延の原因がCPU、メモリ、I/Oのどれかを切り分け
  2. 02CPUフレームグラフからホットパスを特定
  3. 03メモリ割り当てとGC活動を調査
  4. 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.RuntimeCPU 使用率、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
フレームグラフの読み方

フレームグラフは、次のようにコールスタックを表示します。

  • フレームの幅は、その関数で費やされたサンプル時間の合計に対する割合を表します(幅が広いほど、時間を多く費やしています)。
  • 高さはコールスタックの深さを表します(スタックが高いほど、呼び出しチェーンが深くなります)。
  • 色は通常、任意のものであり、特定の配色を使用するツールでない限り、意味はありません。

分析ワークフロー:

  1. 幅の広い平坦部、つまりサンプルの大部分を消費する関数を探します。
  2. 最も幅の広いフレームを上方向にたどり、どの呼び出し元が最も時間を費やしているかを確認します。
  3. 予想外に幅が広い箇所を特定します。本来高速であるべきフレームワークメソッドの幅が広い場合は、誤用が示唆されます。
  4. 最適化前後のトレースを比較し、対象関数の幅が縮小したことを確認します。

.NET のフレームグラフでよく見られるパターン:

パターン考えられる原因調査方法
幅の広い System.Linq フレームデリゲートのオーバーヘッドを伴う LINQ 中心のホットパスforeach ループまたは Span ベースの処理に置き換える
幅の広い JIT_New / gc_heap::allocateGC を引き起こす過剰な割り当て--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.nettracePerfView、VS、dotnet-trace convertはい(キャプチャ);Windows(PerfView)
Speedscope.speedscope.jsonhttps://www.speedscope.app/はい
Chromium.chromium.jsonChrome 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[]

分析の進め方:

  1. アプリケーションの型で、想定外に件数やサイズが大きいものを探します
  2. 件数を想定されるカーディナリティと比較します(例:OrderEntity オブジェクトが 2,100 個ある場合、現在の負荷で想定される数でしょうか?)
  3. 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 -gcGC ヒープのセグメント構成を表示しますLOH の断片化を調査する場合
gcwhere <address>オブジェクトがどの GC 世代にあるかを表示しますオブジェクトが固定されているか、LOH にあるかを判定する場合
dumpmt <MT>メソッドテーブルの詳細を表示します型のメタデータを調査する場合
threadsスタックトレース付きですべてのマネージドスレッドを一覧表示しますデッドロックやブロッキングを特定する場合
clrstack現在のスレッドのマネージドコールスタックを表示しますスレッドの状態をヒープデータと照合する場合
メモリリーク調査のワークフロー
  1. ベースライン: アプリケーションの起動と初期ウォームアップ後にダンプを取得します
  2. 負荷: メモリリークが疑われるワークロードのシナリオを実行します
  3. 比較: ワークロードの完了後に 2 つ目のダンプを取得します
  4. 差分: 2 つのダンプの dumpheap -stat の出力を比較し、件数または合計サイズが大幅に増加した型を探します
  5. ルート: 増加した型のインスタンスに対して gcroot を使い、保持チェーンを特定します
  6. 修正: 保持チェーンを断ち切ります(静的コレクションから削除する、イベントサブスクリプションを破棄する、非同期処理のライフタイムに関する問題を修正する)
# 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] を使って、修正による改善を定量化するための対象を絞ったベンチマークを作成します。


エージェントが陥りやすい点

  1. dotnet-trace ではなく、まず dotnet-counters を使ってください -- カウンターはオーバーヘッドがほぼゼロで、問題のカテゴリ(CPU、メモリ、スレッド)を特定できます。カウンターで調査対象を絞り込んでから、トレースまたはダンプを使用してください。
  2. 本番環境ではインストルメンテーションではなく、CPU サンプリングを使用してください -- サンプリングのオーバーヘッドは 2~5% で、本番環境でも安全に使用できます。インストルメンテーションは 10~50% 以上のオーバーヘッドが発生するため、開発環境に限定してください。
  3. 分析の際は、必ずトレースをフレームグラフに変換してください -- 生の .nettrace イベントログを読むのは現実的ではありません。dotnet-trace convert --format Speedscope を使用して、https://www.speedscope.app/ で開き、視覚的に分析してください。
  4. メモリリークの調査では、ダンプを 2 つ取得してください -- 1 つのダンプでは現在の状態はわかりますが、通常の常駐オブジェクトとリークしたオブジェクトを区別できません。リークが疑われるシナリオの前後に取得した 2 つのダンプ間で、ヒープ統計を比較してください。
  5. LOH オブジェクトを見つけるには、dumpheap を -min 85000 でフィルターしてください -- 85,000 バイト以上のオブジェクトはラージオブジェクトヒープ(LOH)に格納され、Gen 2 GC でのみ回収されます。LOH のオブジェクト数が多い場合は、断片化の可能性があります。
  6. GC カウンターデータの解釈には [skill:dotnet-observability] を使用してください -- ランタイムの GC/スレッドプールカウンターは OpenTelemetry メトリクスと重複します。プロファイリングの結果を分散トレースのコンテキストと関連付けるには、オブザーバビリティスキルを使用してください。
  7. dotnet-trace gc-collect と dotnet-dump を混同しないでください -- gc-collect は時間経過に伴う割り当てイベント(どのメソッドが割り当てを行うか)をトレースし、dotnet-dump は特定時点のヒープスナップショット(どのオブジェクトが存在するか)を取得します。割り当て率の分析には gc-collect を、保持やメモリリークの分析には dotnet-dump を使用してください。

コメント