investigating-performance について
ソフトウェアの遅さを調べるエンジニア向けのパフォーマンス調査スキルです。問題を再現し、症状に合うプロファイリングを行い、一度に一つ変更して効果を測る手順を示します。
主な機能
- 01指標、統計値、負荷、目標の定義
- 02遅い挙動の安定した再現
- 03症状に応じたプロファイリング選択
- 04データベース、アプリ、ネットワークのボトルネック特定
- 05一度に一つ変更して再測定
- 06テールレイテンシと負荷テストの偏りへの対応
活用シーン
- 01リクエスト遅延の原因特定
- 02本番環境だけで起こる遅延の調査
- 03最適化による目標指標の改善確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ osmontero/ opencode-skills --skill investigating-performanceインストール後、Claude Code で/investigating-performanceを入力して読み込まれたか確認します。
同梱ファイルreferences/profiling-tools.md
詳細
パフォーマンスの調査
概要
パフォーマンス改善では、特有の失敗が起こります。エンジニアがコードを読み、遅いに違いない箇所について直感を働かせ、そこを最適化し、測定しても何も変わらない変更をリリースしてしまうのです。ボトルネックは別の場所にあります。なぜなら、ボトルネックはほとんどの場合、別の場所にあるからです。
基本原則: 測定し、場所を特定し、1つだけ変更して、もう一度測定します。直感で測定対象を選ぶことはできますが、修正対象を選ぶことはできません。
関連スキル: systematic-debugging — 遅さではなく、誤りに対して同じ規律を適用します。どちらも、原因を特定する前に行動することを拒みます。
鉄則
NO OPTIMIZATION WITHOUT A MEASUREMENT THAT NAMES THE BOTTLENECK
X を変更する前に、「X が時間の N%を占めている」と言える必要があります。「X はコストが高そうだ」ではありません。「X は O(n²) だ」でもありません。数値を伴う測定が必要です。
例外はありません:
- 「ここが遅い部分なのは明らかだ」も例外ではありません。明らかだからこそ、測定する価値があります。
- 「修正は安く済む」も例外ではありません。ボトルネックではない箇所への安価な修正は、見返りがゼロの純粋なリスクです。
- いずれにせよスタイル上の理由で行う変更は別です。変更してかまいません。ただし、パフォーマンス改善とは呼ばないでください。
当然の帰結: 変更後に測定しない改善は、最適化ではなく、リリースされた推測です。
ステップ 0:目標を定義する
「遅い」はバグ報告ではありません。何かに手を付ける前に、次の4項目をすべて書き出してください。
| 例 | |
|---|---|
| 指標 | POST /documents のエンドツーエンドのレイテンシ |
| 統計値 | 平均ではなく p99 — 平均では、ユーザーが不満を訴える問題が隠れてしまいます |
| 負荷 | 本番環境に近いデータ量で、同時接続するクライアント 200 個 |
| 目標 | 現在の 2.1s から 300ms 未満へ |
目標がなければ、いつ完了したか判断できず、パフォーマンス改善に自然な終了点はありません。統計値がなければ平均を最適化するだけで、不満の原因となった裾の部分には手を付けないままです。
ステップ 1:再現する
遅い動作を必要なときに、繰り返し再現できるハーネスが必要です。
- 実データに近い形状。 開発環境の 1,000 行ではインデックスの欠落を再現できませんが、1,000 万行なら再現できます。
- まずウォームアップしてから測定します。JIT コンパイル、コネクションプール、ページキャッシュの影響で、初回実行は別のプログラムのように動作します。
- 繰り返し実行し、分布を確認します。 1回の計測結果はノイズです。1つの数値ではなく、最小値/p50/p99 を報告してください。
- 環境を固定します。 同じマシン、同じビルドフラグ、ほかの負荷なしで実行します。特に、本番ビルドを測定してください。開発モードのフレームワークビルドは10倍遅いことがあり、実際にリリースするものとは異なります。
ローカルで再現できない場合は、発生している環境で測定してください。サンプルレートを低くした本番環境のプロファイリングは安全であり、再現できない動作を忠実に再現しようとするより優れています。
ステップ 2:プロファイリングの前に特性を把握する
ここで2分かければ、誤った観点のプロファイリングに1時間費やさずに済みます。
| 観察結果 | 示唆される原因 |
|---|---|
| 初回の呼び出しは遅いが、その後は速い | コールドキャッシュ、遅延初期化、接続の確立、JIT |
| 常に一様に遅い | アルゴリズム、または呼び出しごとに発生する固定コスト |
| 低負荷では速いが、高負荷では急激に遅くなる | 競合、プールの枯渇、またはキュー — コードパスが原因ではありません |
| 入力サイズの増加に対して処理時間が超線形に増加する | 計算量の不具合 — プロファイリングの前にネストしたループを探してください |
| p50 は問題ないが、p99 がひどい | GC 停止、ロックの集中、リトライ、まれに発生する低速パス |
| CPU 使用率が 100% | 計算律速 → CPU プロファイル |
| CPU 使用率は低いが、レイテンシは高い | 待ち状態 — I/O、ネットワーク、ロック、または sleep |
| メモリが増え続け、解放されない | 割り当て率ではなく保持が原因 → 2つのスナップショットを使ったヒーププロファイル |
| 本番環境でのみ遅い | データ量、同時実行数、ネットワークトポロジー、または設定 |
簡単な妥当性チェック: 同時実行数 ≈ スループット × レイテンシ(リトルの法則)。1秒あたり 100 リクエストを処理し、各リクエストに 2 秒かかる場合、処理中のリクエストはおよそ 200 件です。プールの接続数が 20 なら、プールがボトルネックであり、クエリをどれだけ調整しても解決しません。
ステップ 3:適切な指標をプロファイリングする
特性評価で問題が示された指標をプロファイリングします。I/O バウンドのプログラムを CPU プロファイリングしても、プロセスがアイドル状態であることがわかるだけで、何も学べません。
| 指標 | タイミング | わかること |
|---|---|---|
| CPU | CPU 使用率が張り付き、計算負荷が高い場合 | どの関数が CPU サイクルを消費しているか |
| 割り当て | GC 時間が長く、メモリの入れ替わりが激しい場合 | ガベージがどこから発生しているか |
| ヒープ(使用中) | メモリが際限なく増える場合 | 何が、誰によって保持されているか |
| ブロック/待機 | CPU 使用率が低く、レイテンシが高い場合 | プログラムが何を待っているか |
| ミューテックス/ロック | 並行処理で性能が低下する場合 | 競合しているロック |
| 実行トレース | 明確な原因が見当たらないレイテンシの急上昇がある場合 | GC の一時停止、スケジューラーの停滞、シリアル化 |
| クエリプラン | データベースが少しでも関与している場合 | スキャン、適切でない結合順序、インデックスの欠如 |
Go、Node、Python、ブラウザー、SQL、システムレベルなど、スタックごとの具体的な実行方法:references/profiling-tools.md。
プロファイルは累積時間の大きい順に上から確認し、フラットなプロファイルには注意してください。ピークがないプロファイルは、たいていコストが呼び出し回数の多すぎる処理に分散していることを意味します。時間を計るだけでなく、呼び出し回数を数えてください。
ステップ 4:よくある原因
問題箇所を特定できたら、原因は通常、次のいずれかです。
| レイヤー | 原因 | 手がかり |
|---|---|---|
| データベース | N+1 クエリ | クエリ数が結果数に比例して増える |
| データベース | インデックスの欠如 | プランに大きなテーブルの Seq Scan がある |
| データベース | インデックスはあるが使われていない | 型の不一致、列に対する関数の使用、または先頭にワイルドカードがある LIKE |
| データベース | 幅の広い行に対する SELECT * | 転送バイト数が必要な行数に対して桁違いに多い |
| アプリケーション | ループ内の処理をループの外に移せる | 同じ呼び出しが同じ引数で n 回実行される |
| アプリケーション | シリアル化/デシリアル化 | CPU プロファイルで JSON のエンコード/デコードが大半を占める |
| アプリケーション | 上限のない並行処理 | 負荷に応じて Goroutine/プロミスの数が増え、メモリ使用量も増える |
| アプリケーション | I/O の間もロックを保持している | ミューテックスプロファイルに多数の取得ではなく、長時間の保持が現れる |
| アプリケーション | ホットパスでのメモリ割り当て | 割り当てプロファイルで、1 つの呼び出し箇所が大半を占める |
| ネットワーク | チャットtyなプロトコル | 1 回のバッチで済む処理で、小さなラウンドトリップが多数発生する |
| ネットワーク | 接続が再利用されていない | トレースに TLS ハンドシェイクが現れる |
| フロントエンド | レンダリングをブロックするウォーターフォール | ネットワークパネルに、依存関係のあるリクエストが順次実行されている様子が現れる |
| フロントエンド | メインスレッド上での処理 | パフォーマンストレースに長時間タスクが現れる |
| フロントエンド | バンドル全体を配信している | 初回の JS ペイロードが大きく、その大半が未使用 |
最適化する前に、その処理自体を完全に回避できないか考えてください。 キャッシュ、バッチ処理、ページネーション、非同期での実行、あるいは処理をしないことは、いずれも高速化より大きな効果があり、通常はよりシンプルです。
ステップ 5:一度に 1 つ変更し、再度計測する
one change → re-measure → keep it or revert it → repeat
- 一度に変数は 1 つだけ変更します。 2 つ同時に変更すると、結果の原因を特定できません。一方の改善ともう一方の悪化が同じ程度だった場合も同様です。
- 計測可能な改善がない変更は元に戻します。 最適化はどれも可読性を損ないます。見返りのない複雑さは、恒久的な負担になります。
- 改善がノイズではなく、実際のものだと確認します。 2 つの単一の数値ではなく、複数回実行した結果の分布を比較してください(
benchstatなどの同等ツールを使うと適切に比較できます)。 - 修正と一緒にベンチマークもコミットします。 リグレッションが再発したときに、それに気づけるのはベンチマークだけです。
- 改善のたびに再プロファイリングします。 ボトルネックは移ります。2 番目に大きかったコストが最大になり、多くの場合、まったく別の場所にあります。
ステップ 0 で設定した目標に達したら終了してください。それ以上続けると、誰も求めていない数値のために、実際の保守性を犠牲にすることになります。
テールレイテンシ
p99 についての指摘なら、平均値はかえって誤解を招きます。平均値を改善する修正が、テールを悪化させることはよくあります。
頻度の高い順に挙げた、一般的な原因は次のとおりです。
- キューイング。 使用率が約 70% を超えると、待ち時間は急激に増加します。対策は、より高速なコードではなく、容量の拡大かバックプレッシャーです。
- GC の一時停止。 GC の合計時間ではなく、一時停止時間の分布を確認します。割り当て率を下げるか、保持されているデータ量を減らします。
- ロックの集中。 1 つの処理がロックを長時間保持すると、待機中のすべての処理が遅くなります。レイテンシの急増は、負荷ではなく競合と相関します。
- リトライ。 タイムアウトにリトライが加わると、遅いリクエストが 3 つに増えます。しかも、それはシステムが最も処理能力を発揮できないときに起こります。
- コールドパス。 キャッシュミス、アイドル状態からの最初のリクエスト、接続の再確立などです。
- ヘッドオブラインブロッキング。 1 つの遅い項目によって、同じ接続、バッチ、パーティション内で後続のすべての処理が滞ります。
負荷生成ツールで協調的省略が発生していないか確認してください。 各レスポンスを待ってから次のリクエストを送るハーネスでは、システムが最も遅いときに負荷の送信が止まります。その結果、測定しようとしているテールが消えてしまいます。一定のレートで送信するジェネレーターを使うか、協調的省略を明示的に補正するジェネレーターを使ってください。
危険信号
- 「ここが明らかに遅い部分です」――プロファイルを確認していない
- プロファイルで実行時間の 0.4% と示された関数を最適化している
- 開発ビルド、または空のデータベースでベンチマークを実施している
- 1 回の測定を根拠にしている
- 1 つのコミットに 2 つの変更があり、どちらもパフォーマンス改善とされている
- マイクロベンチマークは改善しているのに、エンドツーエンドの指標が変化していない
- キャッシュを使わない処理のコストを測定する前にキャッシュを追加している
- 「これで速くなるはずです」
どれも、戻って測定し直すべきというサインです。
よくある間違い
理解している部分を最適化すること。 慣れていることは根拠になりません。ボトルネックは、自分が書いていないコードに偏って存在します。
実際の使われ方を反映していないマイクロベンチマーク。 アムダールの法則は容赦ありません。実行時間の 5% を占める部分を 2 倍速くしても、得られる改善は 2.5% です。
割り当てと保持を混同すること。 割り当て率が高い場合は、GC への負荷が問題です。使用中のヒープが増え続ける場合は、メモリーリークです。それぞれ必要なプロファイルも対策も異なります。
バグを隠すためにキャッシュすること。 意図せず二次時間になる関数の前にキャッシュを置くと、最も必要なときにキャッシュミスが発生するまで、問題が見えなくなります。
ばらつきを無視すること。 p50 が改善して p99 が悪化する変更は、通常、リグレッションです。ユーザーが報告するのはテールだからです。
変更後に測定しないこと。 それをしなければ、リリースしたのは修正ではなく差分です。どちらだったのかも分かりません。