パフォーマンスプロファイリング手法

HtmlAgilityPack で“大規模ドキュメント”を扱う際のパフォーマンスプロファイリング手法

— .NET 環境における計測戦略の体系的ガイド —


1. なぜ専用のプロファイリングが必要か

  • メモリ使用量が加速度的に増加
    DOM 全体をメモリに展開するため、100 MB 超の HTML では数百 MB〜数 GB のヒープを占有しやすい。

  • CPU 時間が線形以上に伸びる処理がある
    無効なタグ修正・子孫ノード走査・XPath クエリなどが複雑度を押し上げる。

  • I/O とパースの並列最適化が難しい
    逐次読み込みと構文木構築が密結合しており、ボトルネック特定には粒度の細かい計測が不可欠。


2. 計測の粒度と目的を決める

粒度 主目的 主な指標
マクロ(アプリ全体) エンドツーエンド時間の把握 処理完了までの経過時間、ピークワーキングセット
ミドル(HAP 呼び出し単位) パース・変換・検索ごとのコスト比較 メソッド別 CPU/メモリ、ガベコレ頻度
ミクロ(内部ループ) ホットスポットの特定 IL 命令数、割込み/サンプリング頻度

3. 推奨プロファイラと特徴

ツール 特徴 適合する粒度
dotnet-trace / dotnet-cpu-usage (.NET CLI) ETW ベースの軽量サンプリング。CI でも自動収集可能 マクロ〜ミドル
Visual Studio Diagnostic Tools Timeline ビューで CPU・メモリ・GC を同時可視化。ヒープダンプも取得 ミドル
PerfView ETL 解析が強力。関数別 CPU 比率や GC Pause を詳細分析 マクロ〜ミクロ
JetBrains dotTrace / dotMemory 呼び出しツリーとアロケーションパスを GUI で深掘り ミドル〜ミクロ
BenchmarkDotNet マイクロベンチマークを統計処理し、回帰検知に適用 ミクロ

4. 代表的ワークフロー

  1. 計測対象コードを最小化

    • 不要なログや副作用コードを preprocessor directive (#if DEBUG) で除外。

  2. リリースビルド & PDB 付きで実行

    • JIT 最適化後の実挙動を測定する。

  3. dotnet-trace を 1 st pass で収集

    bash
    dotnet-trace collect -p <pid> --format Speedscope
    • “Parse” 処理全体の CPU 消費を捕捉。

  4. ホットスポットを Visual Studio / PerfView で深掘り

    • HtmlDocument.Load() 直後の _ParseDocument()HtmlNode.SelectNodes() 直後の XPathEvaluator が高頻度でヒットしやすい。

  5. メモリリーク/過剰アロケーション検証

    • dotMemory で世代別ヒープと HtmlNode オブジェクト数を追跡。

    • 世代 2 に残る String が多い場合は OptionUseIdAttribute や文字列 interning 戦略を見直す。

  6. 高速ループを BenchmarkDotNet 化

    • 例:XPath vs LINQ to XPath vs 手書き走査の平均実行時間比較。

    • CI に組み込み、パフォーマンス回帰を検知。


5. 測定時のベストプラクティス

チェック項目 説明
GC 設定 GCSettings.LatencyMode = LowLatency を短時間で試し、GC Stop-the-World 時間を測る
スレッドプールと async I/O 解析前の HttpClient ダウンロードを ConfigureAwait(false) で切り離す
Span<byte>/SequenceReader 独自ストリーム解析を部分導入し、クラシックパースと分離計測
Stopwatch ロギング 主要メソッド入り口・出口で ElapsedMilliseconds をローカル出力し、プロファイラ結果と突合
データセットの再現性 実案件 HTML と同サイズのサンプルをリポジトリに固定し、測定差異を排除

6. よくあるボトルネックと対処パターン

症状 主因 改善策
GC Gen2 が頻繁に発生し、Pause が長い HtmlNode が大量残存 Streaming Parsing + Clear() で部分解放
CPU 使用率が 100 % 固定 XPath の多回呼び出し XPath キャッシュ、LINQ 変換、HtmlNode.ElementsFlags 見直し
メモリピーク > 1 GB 画像・スクリプトタグを含む巨大文字列 OptionOutputOriginalCase = false で属性複製を抑制
プロファイラで string.Concat が上位 文字列結合による断片化 StringBuilderCache パターン・Span<char> 置換

7. CI/自動化の組み込み例

yaml
# .github/workflows/perf.yml(抜粋) - name: Run microbenchmarks run: dotnet run -c Release -f net8.0 --project tests/HapPerfBenchmarks - name: Collect trace for large-doc test run: | dotnet build -c Release dotnet-trace collect --output trace.nettrace -- \ dotnet run -c Release -- large.html - name: Upload artifact uses: actions/upload-artifact@v4 with: name: perf-trace path: trace.nettrace
  • ベンチ結果や .nettrace を Pull Request に添付し、性能劣化をレビューフローで可視化。


8. まとめと次のステップ

  1. マクロ→ミクロの段階的プロファイリングでボトルネックを漏れなく洗い出す。

  2. ETW 系ツールとヒープ解析ツールを併用して CPU とメモリの相関を確認。

  3. BenchmarkDotNet によるリグレッション検知で長期的な性能維持を自動化。

これらをパイプラインに組み込むことで、大規模 HTML 解析でも スループットの安定化メモリフットプリントの抑制が可能になります。

ChatGPT4o 生成日:2025/06/23