Поддержка Python для профилировщика Linux perf

автор:

Пабло Галиндо

The Linux perf profiler - это очень мощный инструмент, позволяющий профилировать и получать информацию о производительности вашего приложения. perf также имеет очень яркую экосистему инструментов, которые помогают анализировать данные, которые он производит.

Основная проблема при использовании профилировщика perf с приложениями Python заключается в том, что perf получает информацию только о родных символах, то есть именах функций и процедур, написанных на C. Это означает, что имена и названия файлов функций Python в вашем коде не появятся в выводе perf.

Начиная с Python 3.12, интерпретатор может работать в специальном режиме, который позволяет функциям Python отображаться в выводе perf профилировщика. Когда этот режим включен, интерпретатор вставляет небольшой фрагмент кода, скомпилированный на лету, перед выполнением каждой функции Python и обучает perf взаимосвязи между этим фрагментом кода и связанной с ним функцией Python с помощью perf map files.

Примечание

Поддержка профилировщика perf в настоящее время доступна только для Linux на некоторых архитектурах. Проверьте вывод шага сборки 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 для выборки трассировки стека процессора на частоте 9999 герц:

$ 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 script.py
$ perf report -g -i perf.data

Пример, используя опцию -X:

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

Пример, использующий sys API в файле example.py:

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

Как добиться наилучших результатов

Для достижения наилучших результатов Python следует компилировать с CFLAGS="-fno-omit-frame-pointer -mno-omit-leaf-frame-pointer", поскольку это позволяет профилировщикам разворачивать код, используя только указатель кадра, а не отладочную информацию DWARF. Это связано с тем, что код, который вставляется для обеспечения поддержки perf, генерируется динамически и не имеет доступной отладочной информации DWARF.

Вы можете проверить, была ли ваша система скомпилирована с этим флагом, выполнив команду:

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

Если вы не видите никакого вывода, это означает, что ваш интерпретатор не был скомпилирован с указателями кадров, и поэтому он может не показывать функции Python в выводе perf.

Как работать без указателей кадров

Если вы работаете с интерпретатором Python, который был скомпилирован без указателей кадров, вы все равно можете использовать профилировщик perf, но накладные расходы будут немного выше, потому что Python должен генерировать информацию о разворачивании для каждого вызова функции Python на лету. Кроме того, perf потребует больше времени на обработку данных, поскольку для разворачивания стека ему придется использовать отладочную информацию DWARF, а это медленный процесс.

Чтобы включить этот режим, вы можете использовать переменную окружения PYTHON_PERF_JIT_SUPPORT или опцию -X perf_jit, которая включит режим JIT для профилировщика perf.

Примечание

Из-за ошибки в инструменте 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 --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, автоматически заберет файл perf dump, который создает Python (в /tmp/perf-$PID.dump), а затем создаст perf.jit.data, который объединит всю JIT-информацию вместе. Она также должна создать множество файлов jitted-XXXX-N.so в текущем каталоге, которые являются ELF-образами для всех JIT-батутов, созданных Python.

Предупреждение

Обратите внимание, что при использовании --call-graph dwarf инструмент perf будет делать снимки стека профилируемого процесса и сохранять информацию в файле perf.data. По умолчанию размер дампа стека составляет 8192 байта, но пользователь может изменить размер, указав его через запятую, как в --call-graph dwarf,4096. Размер дампа стека очень важен, так как при слишком маленьком размере perf не сможет развернуть стек и вывод будет неполным. С другой стороны, если размер будет слишком большим, то perf не сможет выполнять выборку процесса так часто, как хотелось бы, поскольку накладные расходы будут выше.