"perf map" 対応プロファイラのための Python のサポート
*****************************************************

Linux の perf プロファイラ と samply は、アプリケーションをプロファイ
リングして性能の情報を得るための強力なツールです。 どちらも、生成した
データの分析を助けるツール群が活発に開発されています。

これらを Python アプリケーションに使う際の主な問題は、C で書かれた関数
や手続きの名前、つまりネイティブシンボルの情報しか取得できないことです
。 そのため、Python コードの関数名やファイル名がプロファイラの出力に現
れません。

Python 3.12 以降では、対応するプロファイラの出力に Python 関数を表示す
るための特別なモードでインタープリタを実行できます。 このモードでは、
各 Python 関数の実行前に、その場でコンパイルした小さなコード片を挿入し
ます。 そして perf map ファイル を使い、そのコード片と Python 関数の対
応をプロファイラに伝えます。

注釈:

  このプロファイリング機能は、Linux と macOS の一部のアーキテクチャで
  利用できます。 perf は Linux で、samply は Linux と macOS の両方で使
  えます。 macOS での samply 対応は Python 3.15 からです。 お使いのシ
  ステムが対応しているかは、ビルド時の "configure" の出力、または
  "python -m sysconfig | grep HAVE_PERF_TRAMPOLINE" の出力で確認してく
  ださい。

例えば、次のスクリプトを考えます:

   def foo(n):
       result = 0
       for _ in range(n):
           result += 1
       return result

   def bar(n):
       foo(n)

   def baz(n):
       bar(n)

   if __name__ == "__main__":
       baz(1000000)

"perf" を実行して、CPU のスタックトレースを 9999 Hz でサンプリングでき
ます。

   $ perf record -F 9999 -g -o perf.data python my_script.py

続いて "perf report" でデータを分析します:

   $ perf report --stdio -n -g

   # Children      Self       Samples  Command     Shared Object       Symbol
   # ........  ........  ............  ..........  ..................  ..........................................
   #
       91.08%     0.00%             0  python.exe  python.exe          [.] _start
               |
               ---_start
               |
                   --90.71%--__libc_start_main
                           Py_BytesMain
                           |
                           |--56.88%--pymain_run_python.constprop.0
                           |          |
                           |          |--56.13%--_PyRun_AnyFileObject
                           |          |          _PyRun_SimpleFileObject
                           |          |          |
                           |          |          |--55.02%--run_mod
                           |          |          |          |
                           |          |          |           --54.65%--PyEval_EvalCode
                           |          |          |                     _PyEval_EvalFrameDefault
                           |          |          |                     PyObject_Vectorcall
                           |          |          |                     _PyEval_Vector
                           |          |          |                     _PyEval_EvalFrameDefault
                           |          |          |                     PyObject_Vectorcall
                           |          |          |                     _PyEval_Vector
                           |          |          |                     _PyEval_EvalFrameDefault
                           |          |          |                     PyObject_Vectorcall
                           |          |          |                     _PyEval_Vector
                           |          |          |                     |
                           |          |          |                     |--51.67%--_PyEval_EvalFrameDefault
                           |          |          |                     |          |
                           |          |          |                     |          |--11.52%--_PyCompactLong_Add
                           |          |          |                     |          |          |
                           |          |          |                     |          |          |--2.97%--_PyObject_Malloc
   ...

このように、Python 関数は出力に現れず、Python バイトコードを評価する関
数 "_PyEval_EvalFrameDefault" だけが表示されます。 すべての Python 関
数が同じ C 関数でバイトコードを評価するため、これだけではどの Python
関数に対応する評価処理か分からず、あまり役に立ちません。

一方、"perf" サポートを有効にして同じ実験を行うと、次のようになります:

   $ perf report --stdio -n -g

   # Children      Self       Samples  Command     Shared Object       Symbol
   # ........  ........  ............  ..........  ..................  .....................................................................
   #
       90.58%     0.36%             1  python.exe  python.exe          [.] _start
               |
               ---_start
               |
                   --89.86%--__libc_start_main
                           Py_BytesMain
                           |
                           |--55.43%--pymain_run_python.constprop.0
                           |          |
                           |          |--54.71%--_PyRun_AnyFileObject
                           |          |          _PyRun_SimpleFileObject
                           |          |          |
                           |          |          |--53.62%--run_mod
                           |          |          |          |
                           |          |          |           --53.26%--PyEval_EvalCode
                           |          |          |                     py::<module>:/src/script.py
                           |          |          |                     _PyEval_EvalFrameDefault
                           |          |          |                     PyObject_Vectorcall
                           |          |          |                     _PyEval_Vector
                           |          |          |                     py::baz:/src/script.py
                           |          |          |                     _PyEval_EvalFrameDefault
                           |          |          |                     PyObject_Vectorcall
                           |          |          |                     _PyEval_Vector
                           |          |          |                     py::bar:/src/script.py
                           |          |          |                     _PyEval_EvalFrameDefault
                           |          |          |                     PyObject_Vectorcall
                           |          |          |                     _PyEval_Vector
                           |          |          |                     py::foo:/src/script.py
                           |          |          |                     |
                           |          |          |                     |--51.81%--_PyEval_EvalFrameDefault
                           |          |          |                     |          |
                           |          |          |                     |          |--13.77%--_PyCompactLong_Add
                           |          |          |                     |          |          |
                           |          |          |                     |          |          |--3.26%--_PyObject_Malloc


samply プロファイラの利用
=========================

samply は perf の代わりに使えるモダンなプロファイラです。 Python が生
成する perf map ファイルを使うため、Python のプロファイリング機能に対
応しています。 perf が使えない macOS では特に便利です。

Python で samply を使うには、まず https://github.com/mstange/samply の
手順でインストールし、次を実行します。

   $ samply record PYTHONPERFSUPPORT=1 python my_script.py

Web インターフェースが開き、プロファイリングデータを対話的に分析できま
す。 samply の利点は、分析用のモダンな Web インターフェースを備え、
Linux と macOS の両方で動作することです。

macOS で samply を使うには Python 3.15 以降が必要です。 また、macOS の
制限により、署名された Python 実行ファイルはプロファイリングできません
。 自分でコンパイルしたものや、未署名またはローカル署名されたもの
(Homebrew でインストールしたものなど) を使ってください。 macOS で実行
中のプロセスにアタッチするには、最初に一度、また samply を更新するたび
に "samply setup" を実行し、samply バイナリを自己署名してください。


"perf" プロファイリングサポートを有効にする方法
===============================================

"perf" プロファイリングサポートは、環境変数 "PYTHONPERFSUPPORT" または
"-X perf" オプションで起動時から有効にできます。
"sys.activate_stack_trampoline()" と
"sys.deactivate_stack_trampoline()" で動的に切り替えることもできます。

"sys" の関数は "-X" オプションより優先され、"-X" オプションは環境変数
より優先されます。

環境変数を使う例です。

   $ PYTHONPERFSUPPORT=1 perf record -F 9999 -g -o perf.data python my_script.py
   $ perf report -g -i perf.data

"-X" オプションを使う例です。

   $ perf record -F 9999 -g -o perf.data python -X perf my_script.py
   $ perf report -g -i perf.data

"example.py" で "sys" API を使う例です:

   import sys

   sys.activate_stack_trampoline("perf")
   do_profiled_stuff()
   sys.deactivate_stack_trampoline()

   non_profiled_stuff()

続いて次を実行します。

   $ perf record -F 9999 -g -o perf.data python ./example.py
   $ perf report -g -i perf.data


最良の結果を得る方法
====================

最良の結果を得るには、フレームポインタを有効にしてください。 対応する
GCC 互換ツールチェーンでは、CPython は "-fno-omit-frame-pointer" など
のフラグを使ってビルドされます (詳細は "--without-frame-pointers" を参
照)。 これにより、プロファイラは DWARF デバッグ情報ではなくフレームポ
インタだけでスタックをたどれます。 "perf" 対応のために挿入するコードは
動的に生成され、DWARF デバッグ情報を持たないためです。

このフラグを使ってコンパイルされたかどうかは、次を実行して確認できます
。

   $ python -m sysconfig | grep 'no-omit-frame-pointer'

何も表示されなければ、インタープリタはフレームポインタなしでコンパイル
されています。 その場合、"perf" の出力に Python 関数を表示できない可能
性があります。


フレームポインタなしで使う方法
==============================

フレームポインタなしでコンパイルした Python インタープリタでも "perf"
は使えますが、オーバーヘッドは少し大きくなります。 Python 関数の呼び出
しごとに、スタックをたどるための情報をその場で生成する必要があるためで
す。 さらに "perf" は DWARF デバッグ情報を使ってスタックをたどる必要が
あり、この処理は遅いため、データの処理にも時間がかかります。

このモードを有効にするには、環境変数 "PYTHON_PERF_JIT_SUPPORT" または
"-X perf_jit" オプションを使います。 これにより "perf" プロファイラ用
の JIT モードが有効になります。

注釈:

  "perf" ツールのバグのため、JIT モードで動作するのは v6.8 より後のバ
  ージョンだけです。 修正は v6.7.2 にもバックポートされています。"perf
  version" でバージョンを確認するときは、一部のディストリビューション
  が "-" を含む独自のバージョン番号を追加する点に注意してください。
  "perf 6.7-3" は必ずしも "perf 6.7.3" を意味しません。

perf の JIT モードでは、"perf report" の前に一つ手順が必要です。 "perf
inject" で JIT 情報を "perf.data" ファイルに組み込みます。

   $ perf record -F 9999 -g -k 1 --call-graph dwarf -o perf.data python -Xperf_jit my_script.py
   $ perf inject -i perf.data --jit --output perf.jit.data
   $ perf report -g -i perf.jit.data

環境変数を使う場合は次のようになります。

   $ PYTHON_PERF_JIT_SUPPORT=1 perf record -F 9999 -g --call-graph dwarf -o perf.data python my_script.py
   $ perf inject -i perf.data --jit --output perf.jit.data
   $ perf report -g -i perf.jit.data

"perf inject --jit" command will read "perf.data", automatically pick
up the perf dump file that Python creates (in "/tmp/perf-$PID.dump"),
and then create "perf.jit.data" which merges all the JIT information
together. It should also create a lot of "jitted-XXXX-N.so" files in
the current directory which are ELF images for all the JIT trampolines
that were created by Python.

警告:

  "--call-graph dwarf" を指定すると、"perf" は対象プロセスのスタックの
  スナップショットを取得し、"perf.data" に保存します。 スタックダンプ
  のサイズはデフォルトで 8192 バイトですが、"--call-graph dwarf,16384"
  のようにカンマの後に指定すると変更できます。スタックダンプのサイズは
  重要です。 小さすぎると "perf" がスタックをたどれず、出力が不完全に
  なります。 大きすぎるとオーバーヘッドが増え、希望する頻度でサンプリ
  ングできなくなります。"-O0" のような低い最適化レベルでコンパイルした
  Python をプロファイリングする場合は、スタックフレームが大きくなりや
  すいため、特にサイズが重要です。 "-O0" でコンパイルした Python の関
  数が出力に現れない場合は、スタックダンプのサイズを最大の 65528 バイ
  トに増やしてみてください。

     $ perf record -F 9999 -g -k 1 --call-graph dwarf,65528 -o perf.data python -Xperf_jit my_script.py

  コンパイルフラグはスタックのサイズに大きく影響することがあります:

  * 通常、"-O0" のビルドは "-O1" 以上のビルドよりも、スタックフレーム
    がはるかに大きくなります。

  * 通常、最適化 ("-O1"、"-O2" など) を有効にするとスタックのサイズが
    小さくなります。

  * フレームポインタ ("-fno-omit-frame-pointer") を使うと、一般にスタ
    ックをより確実にたどれます。
