Некоторое время назад с коллегой обсуждали вопрос профилирования программ на Python (haters gonna hate, flamers gonna flame). Он сказал, что использует gprof2dot для пост‑обработки данных cProfile, в ответ я высказал мысль, о том, что было бы хорошо использовать более современные средства профилирования. И задумался, а можно ли какие‑то из существующих инструментов приспособить для работы с данными в формате pstat
(это внутренний недокументированный формат cProfile
), но которые более удобные и дают больше возможностей для анализа.
Disclaimer
Всё это в равной степени относится и к profile
и поэтому я буду упоминать только cProfile
, но иметь в виду их оба два.
Я подразумеваю, что у вас есть какой-то опыт использования профайлеров и профайлеров в Python
, поэтому какие-то моменты опущу в описании.
Оглавление
Введение
В чём, на мой взгляд, проблема с gprof2dot
. Основное его предназначение в создании изображения графа вызовов. В случае, если граф содержит большое количество функций, то изображение, либо должно иметь очень высокое разрешение, либо на нём будет скрыта часть вызовов функций. В обоих случаях это доставляет неудобство и замедляет процесс профилирования.
Для работы с данными pstat
есть различные инструменты, которые в той или иной мере пытаются упростить процесс анализа результатов профилирования, за счёт повышения уровня интерактивности. Условно их можно разделить на три класса:
Инструменты, использующие
dot.exe/GraphViz
для построения графа вызовов.Инструменты, строящие иерархические карты.
Инструменты, строящие FlameGraph.
К первому классу можно отнести xdot.py от автора gprof2dot
. Это десктопное приложение, с довольно ограниченным набором функциональности, кроме операций масштабирования, не предоставляет никаких других функций.
KCacheGrind (или QCacheGrind под Windows
) также имеет функциональность для работы с графом вызовов. Позволяет проваливаться в функции (так называемый drill-down
), для выбранной функции строит цепочку вызовов.
KCacheGrind
также относится и ко второму классу, который основывается на построении иерархических карт. Для выбранной функции строит карту, на которой, чем больше площадь у фигуры, тем большее время эта функция занимает в статистике. Есть статья Профилирование python приложений, в которой рассказано, как используя pyprof2calltree подготовить данные для KCacheGrind
. Есть и другие приложения со сходной функциональностью (например, snakerunner), но KCacheGrind
проверенный временем инструмент.
К третьему классу можно отнести SnakeViz, который позволяет построить Icicle‑график (это аналог FlameGraph
, но растёт не вверх, а вниз). Предоставляет навигацию по этому графику, позволяет проваливаться вниз по стеку и подниматься обратно. Имеет всю необходимую функциональность, но не очень удобный пользовательский интерфейс (на мой взгляд). Требует минимум зависимостей для установки.
Если SnakeViz
может создать иерархический график, то и мы можем сделать тоже самое и использовать более удобные средства визуализации и анализа. Например, есть доступные онлайн speedscope, flamegraph.com или плагины для VSCode
— vscode‑flamegraph, speedscope‑in‑vscode, austin‑vscode.
Если немного поискать в интернете, то можно найти flameprof, с помощью которого можно сконвертировать данные и получить svg
изображение или текстовый файл, пригодный для загрузки в перечисленные выше приложения.
Казалось бы, задача решена и найдены подходящие инструменты, но нет.
Что не так с cProfile
Помимо того, что cProfile
заметно замедляет выполнение программ при профилировании, данные, которые он при этом собирает и хранит, содержат ограниченное количество информации.
В pstat
для каждой вызываемой функции хранится статистика, которая содержит следующее:
— количество вызовов (не считая рекурсивных вызовов);
— общее количество вызовов (с учётом рекурсивных вызовов);
— время, проведённое в самой функции (называется totaltime
);
— время, выполнения функции с учётом вызовов других функций (cum_time
, всегда больше или равно totaltime
).
А также список функций, из которых вызывалась данная функция и такой же набор данных.
Используя эту информацию, gprof2dot
строит граф вызовов, SnakeViz
и flameprof
иерархические графики, KCacheGrind
иерархические карты. В этом и проблема — эти данные непригодны для этих целей.
И если в случае gprof2dot
мы не имеем никаких ожиданий относительно иерархии вызовов (хотя получаемый граф и является направленным), то в случае SnakeViz
, flameprof
и аналогичных инструментов, мы ожидаем определённую иерархическую структуру у графиков. FlameGraph
предполагает, что отображаемые на графике функции, потребляют тем больше времени выполнения, чем ниже они находятся по оси Y. Тогда, как информация, восстановленная из pstat
, не позволяет это сделать.
Покажу на примере:
Пусть у нас есть две функции F1
и F2
. Каждая функция предназначена для тестирования различных вариантов реализации бизнес‑функциональности. В данных функциях вызываются библиотечные функции, которые вызывают какие‑то свои функции и т. д. Граф вызовов выглядит примерно вот так:
pstat
в этом случае будет хранить статистику для следующих пар функций:
F1 -> LF
F2 -> LF
LF -> CF
И имея эти данные, мы не можем определить, какая доля вызовов функции CF
относится к F1
, а какая к F2
. Таким образом, все представления в иерархическом виде показывают недостоверные данные. Ни SnakeViz
, ни flameprof
, ни pyprof2calltree
никаких корректировок не делают (что, наверное, разумно).
В SnakeViz
вполне можно получить изображение, в котором вложенная функция потребляет больше времени, чем вызывающая. Здесь ничего удивительного, SnakeViz
просто считывает информацию из pstat
.
Например, на изображении ниже видно, что __setitem__
имеет накопленное время 70мс, тогда как вызывающая функция update
— 37мс:
flameprof
просто создаёт файл, на основе которого можно построить FlameGraph
и в его случае информация просто суммируется, в результате вышележащие функции получают дополнительный «вес», что опять же не позволяет делать нам правильные выводы об интересующих нас функциях.
Отдельно стоит упомянуть рекурсивные вызовы, которые появляются при импортах пакетов и пронизывают многие вызовы и от которых сложно избавиться.
Так в чём же проблема?
На мой взгляд, проблема в том, что в силу ограниченности формата хранения данных, встроенные профайлеры profile/cProfile
пригодны только для узкого круга задач, в основном (имхо), для профилирования приложений с несложным графом вызовов. Также отсутствует какая‑либо поддержка многопоточного и асинхронного выполнения.
В случае если используется профилирование всего приложения (через python -m cProfile file.py
) информация о тяжёлых вызовах размывается — да, мы увидим, что какая‑то функция потребляет бОльшую часть времени, но кто, откуда, с какими параметрами её вызывал мы не увидим, что приведёт нас к необходимости искать другие инструменты или делать какие‑то наколенные решения.
В случае использования декоратора для профилирования отдельных функций, мы также можем столкнуться с проблемой, описанной выше — не будет достоверной информации о вызовах, если «тяжёлая» функция вызывается через несколько цепочек. В результате мы либо опять же вынуждены использовать другие инструменты, либо вручную смещать декоратор вниз по стеку, что сильно удлиняет процесс профилирования.
А если мы всё равно вынуждены использовать другие инструменты, то зачем пользоваться profile/cProfile
вообще?
cProfile не является профайлером
Если посмотреть на историю изменения файла _lsprof.c
(в котором находится реализация cProfile
), то можно увидеть не очень большое количество коммитов. Впервые он был добавлен в 2006 году и с тех пор принципиально не менялся до 2023 года, когда был выполнен переход на новое API для профилирования (sys.monitoring). Все коммиты, в основном космитические, связанные с изменениями самого Python
. Формат хранения данных при этом каких‑то существенных изменений не претерпел. Функционального развития тоже не было.
Поэтому я думаю, что хотя cProfile
и планировался как профайлер, но вместо этого получился пример использования внутреннего API для построения инструментирующих (или детерминированных) профайлеров. Впрочем, даже в этом случае он не покрывает все необходимы варианты, например, асинхронные вызовы, многопоточное выполнение никак не разделяется в результатах профилирования.
Другие инструменты
В повседневной работе я использую austin и viztracer.
austin
является статистическим (или семплирующим) профайлером (т. е. он периодически собирает информацию о выполняющихся в данный момент функциях, мелкие функции поэтому может пропустить), строит FlameGraph
и имеет удобные инструменты для визуализации, в том числе и плагин для vscode
. Его данные можно загрузить в speedscope
или аналогичные инструменты.
К семплирующим профайлерам также относятся py‑spy, pyinstrument, scalene и другие. Обещается, что такие профайлеры вносят меньше задержек в профилируемое приложение.
viztracer
является детерминированным профайлером (т. е. собирает информацию о каждом вызове функций), сохраняет данные в формате Google Trace Event, визуализация построена на основе Perfetto (расширенной версии профайлера, встроенного в Google Chrome
и Edge
). Данные также можно сконвертировать в формат, пригодный для построения FlameGraph
. Также есть yappi, но с ним опыта не имел.
А что с gprof2dot?
Интересным итогом стало то, что после, того как я разобрался с форматом хранения pstat
, я теперь склоняюсь к тому, что gprof2dot
(и аналоги) лучше всего подходят для графического представления результатов профилирования cProfile
.
Спасибо за внимание.
Комментарии (10)
Z55
06.01.2025 07:20ИМХО, всё это профилирование - для ванильных примеров синтетических задач, поэтому и польза от этих инструментов сомнительна. В реальных приложениях приходится использовать трейсинг, либо обвешивать всё логами/декораторами, чтобы понять, а где собственно проседает твоя приложуха. Очень жаль, что у штатного профайлера нет опции "не въедаться в байт код, а дать информацию по верхам", т.е. дать отчёт о том, какие функции (моего кода, а байт кода) были вызваны, сколько раз, с какими параметрами, как долго они выполнялись итд. Это очень сильно помогло бы в оптимизации приложений.
PrinceKorwin
06.01.2025 07:20Flamegraph диаграммы удобны под это дело. Может и для питона есть, не интересовался.
zzzzzzerg Автор
06.01.2025 07:20Для питона есть, в статье есть несколько ссылок, которые точно умеют - это austin, py-spy, pyinstrument.
zzzzzzerg Автор
06.01.2025 07:20Если вы говорите про трейсинг, то возможно вам будет интересно посмотреть на pyroscope, сам его я не использовал (у нас немного другое направление), но выглядит он интересно.
Также большинство указанных в статье инструментов (в разделе Другие инструменты) умеют подцепляться к запущенным приложениям и строить либо FlameGraph, либо выгружать данные в формате Google Trace Event.
А по поводу ванильных примеров - я применял austin на реальном большом десктопном приложении, применял с пользой. С неванильными примерами есть другая проблема - ниже об этом писали - это получающиеся большие логи, которые надо как то предобрабатывать.
molnij
06.01.2025 07:20Опишу свой опыт
Когда переходил в питон, попробовал профилировать инструментами based on [c]profile - точно был остин, что-то еще из плагинов для vscode, что-то из внешних инструментов пытающихся засунуть гигантский файл в браузер. И vscode и браузер отчаянно сопротивлялись вплоть до падения, или до реакции в несколько минут на каждый клик.
Какое-то время жил на профайлере VS (из плюсов для меня - относительно привычный вид a-la flame tree) - оно хотя бы работало, даже на многогигабайтных трейсах. Но MS остановили его развитие кажется на 3.9 питоне и новостей о продолжении не попадлось. Но есть ощущение что майки почему-то вообще на нормальную студию забили болт, что кмк невероятно странно.
Сейчас живу на line_profiler/kernprof. Не сказать, что предел мечтаний. Была надежда, что если натравить какойнибудь из гуи cvs - получится сравнивать выводы, но пока безрезультно, поэтому по старинке, глазками. Кстати надо будет снова посмотреть какие gui к нему есть, ну или еще разок потыкать в альтернативы
zzzzzzerg Автор
06.01.2025 07:20Попробуйте что-то на основе flamegraph, он вроде как должен существенно снижать размер профиля (но у меня все равно получались профили по несколько Гб с примерно таким же результатом как у вас). У того же остина довольно простой формат, котороый можно фильтровать перед анализом "глазками".
OcMaRUS
Спасибо за статью. Тема, не то чтоб новая, но точно не на таком уровне погружения у меня. Поэтому спрошу "ламерский" вопрос - а в чем практическая ценность профилирования? Удивило замечание "в повседневных задачах". Если дебаг какой или оптимизация, могу представить, а какие еще бизнез задачи решаются?
molnij
оптимизация вполне может быть бизнес-задачей. ну и для меня например такая задача определенно не является задачей каждого дня, но возникает достаточно регулярно, чтобы озадачится некоторым инструментарием для неё.
zzzzzzerg Автор
Выше уже ответили — различные оптимизации. Мне часто приходится решать задачи улучшения производительности в довольно большой кодовой базе, и там с ходу не понятно куда надо нанести «удар кувалдой».
gamerforEA
Ну вот, например, я годами плевался от производительности анализа Heap Dump'ов в VisualVM - даже для сравнительно небольших дампов размером в 8-11 GB приходилось ждать подсчёта Retained Sizes порядка 10 часов. Соответственно, когда нужно просмотреть не один дамп, а множество, процесс растягивался на дни.
Но недавно, наконец, решил попрофилировать и, оказалось, что причины тормозов там довольно тривиальны (в основном IO и работа с хэш-таблицами) и решаются без особых усилий. После небольших правок процесс ускорился до примерно часа. Если бы я заглянул в профайлер несколько лет назад, сэкономил бы себе кучу времени.
Уверен, есть множество приложений, которые можно было бы заметно ускорить, если бы хоть кто-нибудь не поленился и посмотрел бы в профайлер (вспоминаем долгие загрузки GTA Online из-за парсера JSON).