← システム性能分析のしくみ コース
10. フレームグラフの読み方 — 横軸は時間ではない
フレームグラフの縦軸と横軸が何を表しているのかを図で追っていきましょう。
①材料は「大量のスタックのスナップショット」
perf record -F 99 -a -g -- sleep 6099Hz で全CPUのスタックを取る
サンプルごとに1本のスタックが残る
サンプル1main → parse → memcpy
サンプル2main → parse → memcpy
サンプル3main → write → sys_write
…数千本60秒 × 99Hz × CPU数
1本1本を読み解くと
「その瞬間に何を実行中だったか」の記録
- ・-F は取得周波数、-a は全CPU対象、-g はコールグラフ(スタック)の記録を有効にするオプションです(perf-record(1))
- ・1つのサンプルは時間の長さを持たず、ある瞬間の一枚の写真にすぎません
フレームグラフの入力は、プロファイラが一定周波数で撮った大量のスタックのスナップショットです。ここではまだ図の形はしていません。
②同じスタックを併合して1本の箱にする
main;parse;memcpyサンプル1
main;parse;memcpyサンプル2
main;write;sys_writeサンプル3
完全に同じスタック文字列をまとめて数える
main;parse;memcpy 2
main;write;sys_write 1
flamegraph.pl が箱に描く
memcpy の箱は幅2サンプル2本ぶんの幅
sys_write の箱は幅1サンプル1本ぶんの幅
- ・公開されている手順は perf record → perf script → stackcollapse-perf.pl → flamegraph.pl の順です
- ・折りたたみ後の各行は「セミコロン区切りのスタック」と「出現回数」の組になります
同一のスタックが何本あったかを数え、その本数がそのまま箱の幅になります。幅は回数であって時間ではありません。
③縦軸はスタックの深さ、下が呼び出し元
memcpy深さ2 = 葉(実行中の関数)
上が呼び出し先
parse深さ1
下が呼び出し元
main深さ0 = 一番下
- ・y軸はスタックの深さを表し、一番下をゼロとして数えます
- ・一番上の縁(top edge)がその瞬間にCPU上で実行されていた関数で、その下はすべて祖先(呼び出し元)です
下から上へ呼び出しが進みます。図の上端に並ぶ関数こそが、実際にCPUを握っていた関数です。
④横軸は時間ではない — 文字列順に並べているだけ
よくある誤解左が先に実行され、右が後
これは誤り実際は
x軸はスタックの母集団をアルファベット順に並べたもの
なぜ文字列順に並べるのか
同じ関数名が必ず隣同士になる
隣り合った同じフレームを1つの広い箱に併合できる
結果として全体像が読めるようになる
- ・公式の説明では x軸はスタックプロファイルの母集団をアルファベット順に並べたものであり、時間の経過ではないと明記されています
- ・x軸に時間を置いた似た見た目の図は flame chart と呼ばれ、フレームグラフとは別物です
並び順は併合しやすさのためだけに決まっています。左右の位置に実行順の意味はないので「左から順に処理された」と読んではいけません。
⑤読むべきは「横に広い」フレーム、特に上端の平らな部分
上端が平らで横に広い例: memcpy が全体の40%
最優先で調べる上端が細かくギザギザ多数の葉に分散
広い理由をたどる
多くのサンプルでその関数が葉だった
= 実際にCPUを消費していた
対比
縦に深いだけの細い塔呼び出しが深いことしか示さない
CPU消費とは限らない- ・箱の幅は、そのフレームがスタック群の中に現れた頻度に比例します
- ・深さは呼び出しの入れ子の段数であって、消費量ではありません
面積ではなく幅を見ます。深い塔は構造が複雑なだけのことが多く、幅の広い上端こそが実際の消費者です。
⑥色には意味がない
オリジナルのフレームグラフの配色ランダムな暖色
目的
隣り合う箱を目で区別しやすくするだけ
つまり
赤いから遅い、といった読み方はできない
色相に意味を持たせた派生版は別物カーネル/ユーザなど種別で塗り分ける版もある
- ・オリジナル版はランダムな色を使い、隣接するフレームを見分けやすくすることが目的だと説明されています
色から性能を読み取ろうとしないでください。判断材料は幅と、上端に何がいるかだけです。
⑦[unknown] だらけの図はフレームポインタ省略が原因
図に [unknown] が並ぶ / 塔が途中で切れる祖先がたどれていない
原因
最適化でフレームポインタが保持されていないgcc の -fomit-frame-pointer 相当
配布パッケージで起きやすいperf 側の既定
--call-graph の既定はユーザ空間で fpフレームポインタでスタックを歩く
対処は2通り
-fno-omit-frame-pointer で再ビルド
perf record --call-graph dwarfDWARF の CFI を使って巻き戻す
- ・perf-record(1) は --call-graph の既定がユーザ空間では fp であると記載しています
- ・同マニュアルは、フレームポインタを省いてビルドされたバイナリでは fp 方式が誤ったコールグラフを生むため、libunwind か libdw とリンクされていれば dwarf を使うべきだと述べています
- ・dwarf 方式ではスタックダンプの既定サイズは 8192 バイトです
スタックが途切れている図は、遅い場所を隠したまま見せてきます。読み方を工夫する前に、まず取り方を直してください。