Claude Codeにpy-spyのdumpを読ませて詰まる関数を絞る
稼働中のPythonプロセスをpy-spyで採取し、dumpをClaude Codeに読ませて詰まる関数を絞る手順。dumpだけを許可する権限の切り方と、手元での検算まで。
稼働中のPythonプロセスが詰まっているとき、コードを書き換えずに採取できるのがpy-spyです。その出力をClaude Codeに読ませると、疑わしい関数の候補を短時間で絞れます。ただし本番プロセスに触れる操作なので、Claude Codeに渡す権限はpy-spy dumpだけに絞るのが無難です。本記事は、何を採取して何を渡すか、権限をどう切るか、絞った結果をどう検算するかをまとめます。
py-spyは何を採取し、どの出力が使えるか
py-spyはPythonプログラム向けのサンプリングプロファイラーです。対象プロセスとは別のプロセスとして動き、コードの変更も再起動も要りません。READMEは、低オーバーヘッドで本番のPythonコードに対して使っても安全だと説明しています。
Claude Codeに渡す候補は3種類あります。
| 出力 | コマンド | 向いている場面 |
|---|---|---|
| コールスタックのスナップショット | コマンドpy-spy dump --pid <PID> | 向いている場面今まさに止まっている場所を知りたい |
| フレームグラフ(SVG) | コマンドpy-spy record -o profile.svg --pid <PID> | 向いている場面時間を食う関数を人が眺めたい |
| speedscope形式・raw形式 | コマンドpy-spy record --format ... | 向いている場面ツールやスクリプトで加工したい |
recordの既定の出力はSVGのフレームグラフで、--formatでspeedscopeやrawデータに変えられます。READMEは各形式の中身までは説明していません。実際にどんな行が並ぶかは、手元で一度headで確かめてから渡す形式を決めてください。
迷ったらdumpから始める
dumpは各スレッドのコールスタックを標準出力に出します。READMEは「単一のコールスタックでプログラムがどこで固まっているかを知りたい場合に便利」と位置づけています。出力が短いので、Claude Codeのコンテキストを圧迫しません。
--localsを付けると各フレームのローカル変数も出ます。変数には認証情報や顧客データが入ることがあるため、Claude Codeに渡す採取では--localsを付けない運用が安全です。
フレームグラフは「詰まり」より「遅さ」向き
dumpは1回の瞬間写真です。たまたま待機中の場面を撮ると、本当に遅い関数は写りません。時間を食う関数を探すなら、一定時間サンプリングするrecordの結果のほうが向いています。詰まりの有無を見るならdumpを数回、継続的な遅さを見るならrecord、という使い分けです。
採取には権限が要る
py-spyは別プロセスのメモリを読んで動きます。LinuxではPIDを指定して既存プロセスへ接続すると、通常はroot権限が必要です。READMEによると、macOSは常にroot権限を要求します。py-spyに子プロセスとして起動させる形(py-spy record -- python myprogram.py)なら、root無しで採取できます。
コンテナでは追加の設定が要ります。
- Docker:
--cap-add SYS_PTRACEで起動するか、docker-composeのcap_addにSYS_PTRACEを足す - Kubernetes: コンテナの
securityContextにSYS_PTRACEを加える。あるいはkubectl debugでエフェメラルコンテナを付ける - Dockerでは、ホストOS側のpy-spyからコンテナ内のプロセスを採取する方法もある
どれもコンテナを作り直す変更なので、本番でいきなり試さず、手順を決めてから進めます。
--nonblockingを付けると、対象プロセスを一時停止せずに読み取れます。代わりに読み取りが原子的でないため、サンプリングエラーや不完全なスタックが混ざることがあります。
本番に触れるBashだけを許可する
Claude Codeの権限は、.claude/settings.jsonのpermissionsにallowとdenyのルールで書きます。Bashルールはコマンド文字列に対して一致を見ます。
{
"permissions": {
"allow": [
"Bash(sudo py-spy dump --pid 4182)"
],
"deny": [
"Bash(sudo py-spy record *)",
"Bash(py-spy record *)",
"Bash(py-spy top *)",
"Bash(sudo py-spy top *)"
]
}
}この書き方には理由が3つあります。
*を含まないルールは、そのコマンドと完全に一致したものだけを通します。PIDまで固定すれば、別のPIDへの接続や--locals付きの実行は一致しません。sudoは、Claude Codeが自動で取り除くラッパー(timeout、time、nice、nohup、stdbufなど)に含まれません。root権限が要る環境では、sudo込みでルールを書く必要があります。- 複合コマンドは、
&&、;、|などで区切られた各部分がそれぞれルールに一致しなければ通りません。py-spy dumpに別のコマンドをつなげても、後ろの部分は別に判定されます。
PIDはプロセスを再起動するたびに変わります。固定がつらければ、Bash(sudo py-spy dump --pid *)のように末尾を*にする選択肢もありますが、そのときは任意のPIDに接続できると理解したうえで使ってください。
引数で絞る権限は破れやすい
公式ドキュメントは、引数を制約するBashルールは壊れやすいと警告しています。denyルールは、/usr/bin/py-spyのようなフルパス指定やsh -c '...'経由の実行を止めません。コマンド文字列の一致は、安全境界ではなく「Claudeが普通に書く形」を止める仕組みです。確実に止めたいなら、サンドボックスでファイルシステムとネットワークをOSレベルで制限するか、PreToolUseフックでコマンド全文を検査します。
出力はリダイレクトでワークスペースへ
採取結果はファイルに残し、Claude Codeに読ませます。リダイレクト先は、ファイル書き込みのルールと作業ディレクトリに照らして別に検査されます。~で始まるパスやグロブ文字を含むパスは承認が必要になります。作業ディレクトリ内で、Editルールに抵触しないパスに出すと、追加の承認が出にくくなります。保護されたパスや自分のEditルール次第で結果は変わるため、最初の1回は表示を確かめてください。
mkdir -p profiling
sudo py-spy dump --pid 4182 > profiling/dump-1.txt
sleep 5
sudo py-spy dump --pid 4182 > profiling/dump-2.txt間隔を空けて2〜3回取ると、毎回同じ場所にいるのか、場所が動いているのかが分かります。
Claude Codeに読ませる
dumpのテキストを渡し、次のように頼みます。
profiling/dump-1.txt と dump-2.txt は、同じPythonプロセスを5秒空けて
採取した py-spy dump です。
- 各スレッドの現在位置を表にしてください
- 2回とも同じ位置にいるスレッドを疑わしい順に挙げてください
- 断定せず、確認するための追加採取かコードの読み方も示してください見てほしいのは次の点です。
- 2回の採取で動いていないスレッドがあるか
- 待機(I/Oやロック)と計算のどちらに見えるか
- 自分のコードの関数か、ライブラリ内部か
READMEは、スリープ中などアイドルのスレッドは既定で除外されると説明しています。含めたいときは--idleを付けます。ただしIO待ちが「アイドル」と判定されない場合もあり、スレッドの状態は完全ではありません。Claudeの読みは仮説として扱ってください。
recordの出力を読ませる場合、READMEには各形式の行の構成が載っていません。手元でheadを使い、関数名と回数が読める形式を選んでから渡します。本記事の手順はdumpを前提にしています。
症状別にrecordのオプションを選ぶ
recordを人が回すとき、症状によって足すフラグが変わります。READMEにある用途を並べます。
| 症状 | 足すフラグ | READMEの説明 |
|---|---|---|
| gunicornやmultiprocessingのワーカーが遅い | 足すフラグ--subprocesses | READMEの説明子プロセスのPythonも自動で追跡し、親の下の子として出力に含める |
| CPU計算が遅いのか待っているだけか分からない | 足すフラグ--gil | READMEの説明GILを保持しているスレッドのトレースだけを含める |
| C/C++やCythonの拡張が怪しい | 足すフラグ--native | READMEの説明拡張のフレームも含める。記号付きでコンパイルすると結果が良い |
| 待機中のスレッドも見たい | 足すフラグ--idle | READMEの説明アイドルと判断したスレッドも含める |
--gilには注意が要ります。GILを解放したまま動く拡張の活動は見逃します。時間の大半が拡張の内部にあるなら、--gilで絞ると本命が消えかねません。フラグ有無の2通りを比べると安全です。
--nativeのサポートはプラットフォームで異なります。OSとCPUの組み合わせごとに対応表があるため、自分の環境が対象かをREADMEで確かめてください。Cythonは、元の.pyxの行番号を得るために生成されたC/C++ファイルが要ります。
macOSの/usr/bin/pythonは採取できない
macOSのSystem Integrity Protectionは、root権限でも/usr/bin配下のバイナリのメモリ読み取りを禁じます。システム付属のPythonは対象外です。READMEは、別のPythonディストリビューションを入れる、virtualenvで動かす、SIPを無効にするという3つの回避を挙げています。SIPの無効化は、開発機でも影響が大きい選択です。
採取ファイルの扱いも権限で決める
採取したdumpには、関数名やファイルパスが並びます。ローカル変数を付けなくても、社内のパス構造やモジュール名は含まれます。共有リポジトリに誤ってコミットしないよう、profiling/は.gitignoreに入れておきます。
Claude Codeに読ませる側のルールは次のとおりです。Readルールは、ファイル読み取りを対象にします。
- 採取結果の置き場を
profiling/の1か所に決める - そこだけを読ませ、本番の設定ファイル(
.envなど)の読み取りは別途denyにする - 採取を実行するシェルと、解析するClaude Codeのセッションを分けられるなら分ける
denyの書き方は次のとおりです。.claude/settings.jsonに足します。
{
"permissions": {
"deny": [
"Read(./.env)",
"Read(./secrets/**)"
]
}
}パスはgitignore形式で、./始まりと素のパスは現在のディレクトリからの相対、//始まりはファイルシステムのルートからの絶対パス、~/始まりはホームからの相対になります。profiling/だけを読ませたい場合も、読ませたくない側をdenyで列挙するのが確実です。
最後の分け方は、採取だけを権限のある人が実行し、ファイルを渡して解析する運用です。Claude Codeにsudoを許可せずに済みます。
絞った関数を手元で検算する
Claudeが「この関数が怪しい」と言っても、サンプリングは確率的な観測です。次の順で確かめます。
- 疑われた関数を、本番ではなく手元で再現する入力で呼び、時間を測る
- 同じ入力で
py-spy record -- python myprogram.pyを実行する。子プロセスとして起動する形ならroot権限が要らない - 上位に出る関数がdumpと一致するか比べる
- 関数を直した後、同じ手順で順位が下がったかを見る
2の実行はClaude Codeに任せず、人が回したほうが権限を絞れます。結果のファイルだけを読ませる構成です。
CodSpeedのベンチ結果を読む流れはClaude CodeのCodSpeedプラグインでベンチ結果とフレームグラフを読むにあります。こちらはCIのベンチ比較が主題で、稼働中のプロセスの採取とは入口が違います。コードを計装して計測する別の手段は、Claude CodeのLogfireプラグインでPythonアプリを計装するにあります。
つまずきやすい点
- 「Permission Denied」が出る: root権限、macOSの制限、コンテナのSYS_PTRACEのどれかが原因として挙がっています
- 空に近い出力になる: 対象がアイドルだった可能性があります。
--idleの有無を見直してください - コマンドが毎回確認される: ルールがコマンドと完全一致しているか、
sudoを含めたかを確認してください
まとめ
稼働中のプロセスは、まずdumpを数回取るのが安全です。Claude Codeには、PIDまで固定したdumpだけを許可し、recordは人が回す構成にすると、読み取りと権限の範囲を小さく保てます。Claudeが挙げた関数は、手元の再実行とrecordの結果で確かめるまで仮説にとどめてください。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。