Claude Media
Claude Codeでpprofを解析 — top出力を渡しflatとcumの読み違いを防ぐ

Claude Codeでpprofを解析 — top出力を渡しflatとcumの読み違いを防ぐ

Goのpprofが出すtopとlistの出力をテキストでClaude Codeに渡し、ボトルネックを絞る手順です。flatとcumの読み分けと、-diff_baseで改修前後を引く依頼文をまとめます。

Goのボトルネック調査でClaude Codeに任せやすいのは、pprofが出すtopとlistのテキストを読ませて「どの関数のどの行を疑うか」を絞る部分です。プロファイルのファイル(.prof)は圧縮されたバイナリ形式なので、go tool pprofでテキストに落としてから渡します。そうすれば、普通のログと同じように扱えます。

ただし、渡すだけでは読み違いが起きます。pprofの表にはflatとcumの2つの列があり、取り違えると「呼び出し元を最適化する」という的外れな提案につながります。この記事では、テキストの取り出し方、読み違いを避ける指示、-diff_baseで改修前後を引く依頼文を順に扱います。

pprofの出力は「テキストに落として渡す」のが速い

pprofは、Goの実行時プロファイル(CPU使用、メモリ割り当てなど)を可視化するツールです。runtime/pprofパッケージが書き出すデータを、go tool pprofが読みます。

Claude Codeに渡す形は、バイナリのままよりテキストの方が向いています。理由は単純で、-topや-listの出力は人間が読むための表と注釈付きソースなので、そのままClaude Codeが読める文字列だからです。画面に出す-webのグラフを経由する必要もありません。

渡し方は2通りあります。

  • パイプで標準入力に流す(claude -pを使う)
  • テキストをファイルに保存し、パスを依頼文に書く

標準入力に渡せる上限は10MBです。超えるとClaude Codeはエラーで終了し、非ゼロのステータスを返します。topは-nodecountで行数を絞れるので上限に届くことはまずありませんが、-listを広い正規表現で回すと膨らみます。大きいときは、ファイルに書いてパスを渡す方に切り替えます。

プロファイルを取る

まず、手元でプロファイルを作ります。ベンチマークがあるなら、go testにフラグを付けるのが最短です。

go test -bench . -cpuprofile cpu.prof -memprofile mem.prof

runtime/pprofのドキュメントにある書き方で、CPUプロファイルとメモリプロファイルがそれぞれcpu.profとmem.profに書かれます。単体のプログラムなら、flagで-cpuprofileと-memprofileを受け取り、pprof.StartCPUProfileとpprof.StopCPUProfileで囲むのが公式の例です。稼働中のサーバーなら、net/http/pprofをimport _するだけで/debug/pprof/以下に取得用のハンドラーが入ります。

メモリ側は、書き出す前にruntime.GC()を呼んで統計を最新にするのが公式サンプルの手順です。ここを抜くと、直前のガベージが残った状態の数字を読ませることになります。

topとlistをテキストにする

go tool pprofには、対話モードを使わずレポートだけを出力するフラグがあります。まずCPUのtopです。

go tool pprof -top -nodecount=25 cpu.prof > cpu-top.txt

-nodecountは出力する行数の上限です。pprofは「どれを切るかを経験則で選ぶ」ので、上位25行が単純な降順とは限りません。topを見て気になった関数があれば、次に-listで行単位の内訳を取ります。

go tool pprof -list 'report\.buildRows' cpu.prof > cpu-list.txt

-listには正規表現を渡します。一致した関数のソースが、行ごとのflatとcum付きで出ます。ドットはエスケープしておくと、意図しない関数まで拾いません。

ヒープは、どの値で並べるかを先に決めます。ヒーププロファイルには保持中のメモリと、プログラム開始からの割り当て総量の両方が入っていて、-inuse_space(既定)、-inuse_objects、-alloc_space、-alloc_objectsで切り替えます。allocsプロファイルは同じデータの既定表示を-alloc_spaceにしたものです。

go tool pprof -top -nodecount=25 -alloc_space mem.prof > mem-alloc-top.txt
go tool pprof -top -nodecount=25 -inuse_space mem.prof > mem-inuse-top.txt

メモリリークの疑いならinuse、GC負荷の疑いならallocと、問いによって見る値が違います。依頼文では、どちらを渡したかを必ず書き添えます。

flatとcumを読み違えると何が起きるか

pprofのレポートは、プロファイルのサンプルから呼び出し階層を組み立てて、各位置に2つの値を持たせます。

  • flat: その位置自身の値
  • cum: その位置と、そこから呼ばれた先すべてを足した値

次はcpu-top.txtの例です。関数名と数値は説明用に作った架空のもので、実在のプログラムの出力ではありません。

Showing nodes accounting for 3.96s, 66.00% of 6.00s total
      flat  flat%   sum%        cum   cum%
     1.80s 30.00% 30.00%      1.80s 30.00%  regexp.(*Regexp).doExecute
     1.20s 20.00% 50.00%      1.20s 20.00%  runtime.memmove
     0.90s 15.00% 65.00%      2.10s 35.00%  runtime.mallocgc
     0.06s  1.00% 66.00%      4.20s 70.00%  example.com/app/report.buildRows

この表を、そのままClaude Codeに「遅い関数を直して」と渡すと、buildRowsがcum 70%で最大なので、そこを書き換える案が返りがちです。しかしbuildRowsのflatは1%しかありません。時間は自分の中ではなく、呼び出した先で使われています。

読み分けの目安は次のとおりです。

flatcum読み方
大cum大(flatとほぼ同じ)読み方葉に近い関数。この関数の中身そのものが重い
小cum大読み方呼び出し元。重いのは呼び出し先なので-listで内訳を見る
大cumflatより明らかに大読み方自身も重く、呼び先にも時間を使っている
小cum小読み方今回の調査では対象外

pprofのWeb UIの説明でも同じ構図が示されていて、flatもcumも小さい関数、両方大きい関数、flatが小さくcumが大きい関数(Flushの例)に分けて説明されています。

読み違いを防ぐ依頼文

渡し方の工夫で、読み違いはかなり減らせます。ポイントは、列の意味を先に書くことと、結論の前に根拠の行を引用させることです。

cat cpu-top.txt | claude -p \
  'Goのpprof(CPU)のtop出力です。flatは関数自身、cumは呼び先を含む値です。
cumが大きくflatが小さい関数は呼び出し元として扱い、最適化対象にしないでください。
flatの大きい順に上位3件について、根拠の行をそのまま引用し、
次に-listで見るべき関数名を1つだけ挙げてください。'

claude -pは非対話で動き、標準入力を読みます。ビルドログを流して原因を説明させる使い方が、公式のヘッドレスモードの例にも載っています。

同じ指示を毎回書くのが面倒なら、プロジェクトのCLAUDE.mdに置いておきます。次は書き方の例です。

## pprof出力の読み方
- flatは関数自身の値、cumは呼び先を含む値。最適化の第一候補はflatが大きい関数
- cumだけが大きい関数は呼び出し元。-listで行ごとの内訳を取ってから判断する
- 提案には必ずtop出力の該当行を引用する。引用できない関数名は挙げない
- ヒープは-inuse_spaceか-alloc_spaceかを確認してから結論を出す

CIなどの自動実行でclaude -pを使うなら、--bareを付けると、フックやプラグイン、自動メモリ、CLAUDE.mdを読み込まずに起動します。この場合はCLAUDE.mdに書いた規則が効かないので、指示は依頼文の側に入れます。

listの出力から行を特定させる

topで候補の関数が決まったら、cpu-list.txtを渡して、行単位に落とします。これも例用の架空の出力です。

ROUTINE ======================== example.com/app/report.buildRows
      60ms      4.20s (flat, cum) 70.00% of Total
         .          .     41:	for _, r := range rows {
         .      2.34s     42:		re := regexp.MustCompile(pattern)
         .      1.80s     43:		if re.MatchString(r.Name) {

左の2列が行ごとのflatとcumです。42行目の正規表現のコンパイルが、ループの中で毎回走っていることが読み取れます。ここまで来ると、Claude Codeへの依頼は「該当行の修正案をパッチで出して」と具体的に書けます。

claude -p 'cpu-list.txt を読み、cumの大きい行を3つ挙げて理由を書いてください。
その後、最も大きい行だけをループの外に出す修正案を差分で示してください。' \
  --allowedTools "Read"

--allowedTools "Read"は、ファイルを読む許可だけを事前に与える指定です。編集を許可していないので、提案は差分として画面に返るだけで、ソースは書き換わりません。実際に直させるときは、Editを足すか対話セッションに切り替えます。

インライン展開された関数が絡むと、行の帰属が分かりにくくなることがあります。pprofの-noinlinesは、インライン展開された関数の値を、最初に呼び出した関数の側に寄せて表示します。-listの結果が不自然なときは、このフラグを付けた版も渡して比べると状況がつかめます。

-diff_baseで改修前後を比べる

修正の効果は、感覚でなく差分で見ます。pprofには、2つのプロファイルを引き算する-baseと-diff_baseがあります。

go tool pprof -top -nodecount=25 -diff_base before.prof after.prof > diff-top.txt

2つのオプションの違いは、パーセントの基準と、負の値の出方です。

オプション向く場面パーセントの基準
-base向く場面同じプログラムで後から取った累積プロファイルから、先に取ったものを引く。値はすべて正になるパーセントの基準ソースとbaseの差
-diff_base向く場面改修前後など2つのプロファイルの比較。減った分は負の値になるパーセントの基準baseプロファイルの合計

-diff_baseでは、改善した関数の値が負になることがあります。負の値の意味は表を見ただけでは伝わりにくいので、依頼文に「負の値は減少を表す」と書いておきます。負荷の量が違う2回の計測を比べるなら、-normalizeでソース側の合計をbase側に合わせてから引く方法もあります。

差分を読ませる依頼文は、次のようにします。

before.prof(改修前)を基準に after.prof(改修後)を引いた pprof の top 出力です。
-diff_base を使っているので、負の値は減少、正の値は増加を表します。
1. 最も減少した関数と、最も増加した関数をそれぞれ3つ挙げてください
2. 増加した関数が、今回の変更で説明できるものか判断してください
3. 説明できない増加があれば、-list で調べるべき関数名を挙げてください

「増加した関数を必ず確認させる」のがこの依頼文のポイントです。改善だけを褒めて終わると、別の場所に押し出された負荷を見落とします。

検証はベンチマークの再計測で閉じる

Claude Codeが示した原因は、あくまで仮説です。修正を入れたら、同じ条件でプロファイルを取り直し、上の-diff_baseの表で数値を確かめます。比較用に取るときは、ベンチマークの入力と実行時間を固定します。条件が揺れると、差分に出るのは修正の効果ではなく計測のばらつきになります。

長い出力を絞って渡すという考え方は、他の言語でも同じです。Rustのテスト出力を失敗分だけに絞る方法はClaude Codeでclap製CLIをnextestで回す手順に、Bazelの出力を対象だけに絞る方法はClaude CodeでBazelのテストを回す記事にあります。基準期間との差から兆候を探す考え方は、Claude Codeで振動データをFFT解析する記事と共通です。

よくあるつまずき

  • 上位がruntime関数ばかりで、自分のコードが見えない。-focusで自分のパッケージの名前に一致するサンプルだけに絞ります。-focusは、正規表現に一致する項目を含むサンプルだけを残すオプションです
  • 渡したtopが途中で切れている。-nodecountで上限を決めた結果で、pprofは切る行を経験則で選びます。必要なら値を増やして取り直します
  • -listが何も出さない。正規表現が関数名に一致していません。topの関数名を、ドットをエスケープしてそのまま使います
  • 重い関数の呼び出し元と呼び先を一度に見たい。-peekに正規表現を渡すと、一致した項目を前後の関係ごと、切り詰めずに出力します。cumだけが大きい関数の周辺を渡すときに役立ちます
  • ヒープの数字が毎回違う。取得前にruntime.GC()を呼んでいるか、inuseとallocのどちらを見ているかを確かめます
  • CPUプロファイルの合計が短すぎる。サンプルが少ないと順位が不安定になります。計測時間を延ばすか、ベンチマークの-benchtimeを長くします
  • 渡した出力が10MBを超える。claude -pの標準入力の上限です。ファイルに保存して、パスを依頼文に書く方法に切り替えます

まとめ

pprofの解析で、Claude Codeに任せるのは「表を読んで次に見る関数を絞る」ところまでです。flatとcumの意味を依頼文に書き、根拠の行を引用させ、-diff_baseで改修前後を比べて検証すれば、読み違いの大半を防げます。

Webのグラフや対話モードを使う調査は人の目が速い場面もあります。topの1画面で原因が見えるときは、そのまま手で直した方が早いこともあります。テキストを渡す方法が効くのは、候補が多くて絞り込みに時間がかかるときと、毎回同じ手順で回帰を確かめたいときです。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. pkg.go.dev/runtime/pprof
  2. github.com/google/pprof/blob/main/doc/README.md
  3. go.dev/doc/diagnostics
  4. code.claude.com/docs/en/headless
この記事を共有:XはてブLinkedIn