Python support for the Linux "perf" profiler
********************************************

作者:
   Pablo Galindo

The Linux perf profiler is a very powerful tool that allows you to
profile and obtain information about the performance of your
application. "perf" also has a very vibrant ecosystem of tools that
aid with the analysis of the data that it produces.

The main problem with using the "perf" profiler with Python
applications is that "perf" only gets information about native
symbols, that is, the names of functions and procedures written in C.
This means that the names and file names of Python functions in your
code will not appear in the output of "perf".

Since Python 3.12, the interpreter can run in a special mode that
allows Python functions to appear in the output of the "perf"
profiler. When this mode is enabled, the interpreter will interpose a
small piece of code compiled on the fly before the execution of every
Python function and it will teach "perf" the relationship between this
piece of code and the associated Python function using perf map files.

备注:

  Support for the "perf" profiler is currently only available for
  Linux on select architectures. Check the output of the "configure"
  build step or check the output of "python -m sysconfig | grep
  HAVE_PERF_TRAMPOLINE" to see if your system is supported.

例如，考虑以下脚本：

   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" 以 9999 赫兹的频率来对 CPU 栈追踪信息进行采样:

   $ 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%--_PyLong_Add
                           |          |          |                     |          |          |
                           |          |          |                     |          |          |--2.97%--_PyObject_Malloc
   ...

如你所见，Python 函数不会显示在输出中，只有 "_PyEval_EvalFrameDefault"
(执行 Python 字节码的函数) 会显示出来。不幸的是，这并没有什么用处，因
为所有 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%--_PyLong_Add
                           |          |          |                     |          |          |
                           |          |          |                     |          |          |--3.26%--_PyObject_Malloc


如何启用 "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


如何获取最佳结果
================

For best results, Python should be compiled with "CFLAGS="-fno-omit-
frame-pointer -mno-omit-leaf-frame-pointer"" as this allows profilers
to unwind using only the frame pointer and not on DWARF debug
information. This is because as the code that is interposed to allow
"perf" support is dynamically generated it doesn't have any DWARF
debugging information available.

你可以通过运行以下命令来检查你的系统是否附带此标志来编译的:

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

如果你没有看到任何输出，则意味着你的解释器没有附带帧指针来编译，因而它
可能无法在 "perf" 的输出中显示 Python 函数。


如何在不带帧指针的情况下使用
============================

如果你使用在不带帧指针的情况下编译的 Python 解释器，你仍然可以使用
"perf" 性能分析器，但会有较高的资源开销，因为 Python 需要为每个 Python
函数即时生成展开信息。此外，"perf" 将花费更多时间来处理数据，因为它需
要使用 DWARF 调试信息来展开栈，而这是一个缓慢的过程。

要启用此模式，你可以使用环境变量 "PYTHON_PERF_JIT_SUPPORT" 或 "-X
perf_jit" 选项，它将为 "perf" 性能分析器启用 JIT 模式。

备注:

  由于 "perf" 工具的一个程序错误，只有 "perf" 版本号高于 v6.8 才能使用
  JIT 模式。修复也向下移植到了此工具的 v6.7.2 版。请注意，在检查
  "perf" 工具的版本时（这可通过运行 "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" 命令将读取 "perf.data"，自动获取 Python 创建的
perf 转储文件（在 "/tmp/perf-$PID.dump" 中），然后创建将所有 JIT 信息
合并到一起的 "perf.jit.data" 文件。 它还会在当前目录下创建大量
"jitted-XXXX-N.so" 文件，它们是 Python 所创建的所有 JIT 跳板的 ELF 映
像。

警告:

  当使用 "--call-graph dwarf" 时，"perf" 工具将对被分析进程的栈打快照
  并将信息保存在 "perf.data" 文件中。在默认情况下，栈转储的大小为 8192
  字节，但你可以通过额外传入一个逗号加数值如 "--call-graph
  dwarf,16384" 来改变这个大小。栈转储的大小很重要，因为如果这个值太小
  "perf" 将无法展开栈信息，而输出将不完整。另一方面，如果这个值太大，
  那么 "perf" 将无法按需以足够的频率对进程执行采样，因为那样资源开销会
  过高。栈大小在对使用较低优化级别 (如 "-O0") 编译的 Python 代码进行性
  能分析时更为重要，因为这类构建版往往有更大的栈帧。如果你是使用 "-O0"
  来编译 Python 并且没有在你的性能分析输出中看到 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") 通常会提供更可靠的栈展开
