Некоторое время назад с коллегой обсуждали вопрос профилирования программ на Python (haters gonna hate, flamers gonna flame). Он сказал, что использует gprof2dot для пост‑обработки данных cProfile, в ответ я высказал мысль, о том, что было бы хорошо использовать более современные средства профилирования. И задумался, а можно ли какие‑то из существующих инструментов приспособить для работы с данными в формате pstat (это внутренний недокументированный формат cProfile), но которые более удобные и дают больше возможностей для анализа.

Disclaimer

Всё это в равной степени относится и к profile и поэтому я буду упоминать только cProfile, но иметь в виду их оба два.

Я подразумеваю, что у вас есть какой-то опыт использования профайлеров и профайлеров в Python, поэтому какие-то моменты опущу в описании.

Оглавление

Введение

В чём, на мой взгляд, проблема с gprof2dot. Основное его предназначение в создании изображения графа вызовов. В случае, если граф содержит большое количество функций, то изображение, либо должно иметь очень высокое разрешение, либо на нём будет скрыта часть вызовов функций. В обоих случаях это доставляет неудобство и замедляет процесс профилирования.

Для работы с данными pstat есть различные инструменты, которые в той или иной мере пытаются упростить процесс анализа результатов профилирования, за счёт повышения уровня интерактивности. Условно их можно разделить на три класса:

  1. Инструменты, использующие dot.exe/GraphViz для построения графа вызовов.

  2. Инструменты, строящие иерархические карты.

  3. Инструменты, строящие 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мс:

Пример ошибочной визуализации в SnakeViz
Пример ошибочной визуализации в SnakeViz

flameprof просто создаёт файл, на основе которого можно построить FlameGraph и в его случае информация просто суммируется, в результате вышележащие функции получают дополнительный «вес», что опять же не позволяет делать нам правильные выводы об интересующих нас функциях.

Пример визуализации FlameGraph в плагине austin-vscode
Пример визуализации FlameGraph в плагине austin‑vscode

Отдельно стоит упомянуть рекурсивные вызовы, которые появляются при импортах пакетов и пронизывают многие вызовы и от которых сложно избавиться.

Так в чём же проблема?

На мой взгляд, проблема в том, что в силу ограниченности формата хранения данных, встроенные профайлеры 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)


  1. OcMaRUS
    06.01.2025 07:20

    Спасибо за статью. Тема, не то чтоб новая, но точно не на таком уровне погружения у меня. Поэтому спрошу "ламерский" вопрос - а в чем практическая ценность профилирования? Удивило замечание "в повседневных задачах". Если дебаг какой или оптимизация, могу представить, а какие еще бизнез задачи решаются?


    1. molnij
      06.01.2025 07:20

      оптимизация вполне может быть бизнес-задачей. ну и для меня например такая задача определенно не является задачей каждого дня, но возникает достаточно регулярно, чтобы озадачится некоторым инструментарием для неё.


    1. zzzzzzerg Автор
      06.01.2025 07:20

      Выше уже ответили — различные оптимизации. Мне часто приходится решать задачи улучшения производительности в довольно большой кодовой базе, и там с ходу не понятно куда надо нанести «удар кувалдой».


    1. gamerforEA
      06.01.2025 07:20

      Ну вот, например, я годами плевался от производительности анализа Heap Dump'ов в VisualVM - даже для сравнительно небольших дампов размером в 8-11 GB приходилось ждать подсчёта Retained Sizes порядка 10 часов. Соответственно, когда нужно просмотреть не один дамп, а множество, процесс растягивался на дни.
      Но недавно, наконец, решил попрофилировать и, оказалось, что причины тормозов там довольно тривиальны (в основном IO и работа с хэш-таблицами) и решаются без особых усилий. После небольших правок процесс ускорился до примерно часа. Если бы я заглянул в профайлер несколько лет назад, сэкономил бы себе кучу времени.
      Уверен, есть множество приложений, которые можно было бы заметно ускорить, если бы хоть кто-нибудь не поленился и посмотрел бы в профайлер (вспоминаем долгие загрузки GTA Online из-за парсера JSON).


  1. Z55
    06.01.2025 07:20

    ИМХО, всё это профилирование - для ванильных примеров синтетических задач, поэтому и польза от этих инструментов сомнительна. В реальных приложениях приходится использовать трейсинг, либо обвешивать всё логами/декораторами, чтобы понять, а где собственно проседает твоя приложуха. Очень жаль, что у штатного профайлера нет опции "не въедаться в байт код, а дать информацию по верхам", т.е. дать отчёт о том, какие функции (моего кода, а байт кода) были вызваны, сколько раз, с какими параметрами, как долго они выполнялись итд. Это очень сильно помогло бы в оптимизации приложений.


    1. PrinceKorwin
      06.01.2025 07:20

      Flamegraph диаграммы удобны под это дело. Может и для питона есть, не интересовался.


      1. zzzzzzerg Автор
        06.01.2025 07:20

        Для питона есть, в статье есть несколько ссылок, которые точно умеют - это austin, py-spy, pyinstrument.


    1. zzzzzzerg Автор
      06.01.2025 07:20

      Если вы говорите про трейсинг, то возможно вам будет интересно посмотреть на pyroscope, сам его я не использовал (у нас немного другое направление), но выглядит он интересно.

      Также большинство указанных в статье инструментов (в разделе Другие инструменты) умеют подцепляться к запущенным приложениям и строить либо FlameGraph, либо выгружать данные в формате Google Trace Event.

      А по поводу ванильных примеров - я применял austin на реальном большом десктопном приложении, применял с пользой. С неванильными примерами есть другая проблема - ниже об этом писали - это получающиеся большие логи, которые надо как то предобрабатывать.


  1. 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 к нему есть, ну или еще разок потыкать в альтернативы


    1. zzzzzzerg Автор
      06.01.2025 07:20

      Попробуйте что-то на основе flamegraph, он вроде как должен существенно снижать размер профиля (но у меня все равно получались профили по несколько Гб с примерно таким же результатом как у вас). У того же остина довольно простой формат, котороый можно фильтровать перед анализом "глазками".