Найти в Дзене

Профилирование асинхронного Python

Оглавление

Общие слова

Профилирование приложений — это процесс анализа программы для определения её характеристик: времени выполнения различных частей кода и использования ресурсов.

Основные этапы профилирования всегда более-менее одинаковы:

  • Измерение времени выполнения. Cколько времени требуется для выполнения различных частей кода?
  • Анализ использования памяти. Сколько памяти потребляется различными частями программы?
  • Выявление узких мест. Какие части кода замедляют работу программы и/или используют слишком много ресурсов?
  • Оптимизация производительности. Принятие мер для улучшения скорости выполнения и эффективности использования ресурсов на основе полученных данных.

А как вообще работает профилировщик?

Детальному обзору будет посвящена отдельная статья, пока можно ограничится базовой классификацией:

  • Детерминированные (deterministic) профилировщики. Главный представитель - встроенный cProfile. Такой профилировщик считает количество вызовов каждой функции и потраченное функцией время. Проблема в том, что время ожидания асинхронных вызовов не учитывается.
  • Статистические (statistical) профилировщики. Распространённые представители - scalene, py-spy, yappi, pyinstrument, austin. Такие профилировщики с некоторой частотой снимают "слепок" с процесса и применяют методы статистического анализа для поиска узких мест.

Основные типы узких мест в асинхронном Python-коде

Для асинхронного кода существует небольшое количество специфических "узких мест", которые лучше перечислить заранее.

Каждому типу сопоставим пример кода.

Список допущений

  • Python 3.12
  • Используется один и только один event-loop
  • Яндекс.Дзен не умеет отображать код текстом, поэтому пришлось вставлять его картинками.

Блокирующие операции

Async Python bottleneck #1

Последовательный вызов асинхронных задач

-2
Async Python bottleneck #2

Слишком частое переключение контекста

-3
Async Python bottleneck #3

Неравномерное распределение ресурсов

В англоязычной литературе такой сценарий называется "Resource Starvation".

-4
Async Python bottleneck #4

Чрезмерный расход памяти

-5
Async Python bottleneck #5

Использование "scalene" для профилирования

Почему scalene? Потому что этот инструмент позволяет профилировать и CPU, и GPU, и память; 10k+ звёзд на гитхабе, проект активно развивается.

Посмотрим что скажет scalene для каждого "проблемного" кода из списка выше.

Запускать будем в режиме scalene --cpu --memory --cli script_name.py

Блокирующие операции

-6

Проблемную строку с блокирующим вызовов видно сразу - 2% времени на Python, 98% - на системные вызовы.

Последовательный вызов асинхронных задач

-7

Здесь чуть сложнее. Видно, что 90% времени уходит на системные вызовы, но поменялась строка - теперь это сам asyncio.run(). Такой паттерн вывода профилировщика лучше всего просто запомнить.

Слишком частое переключение контекста

-8

Видим, как растёт потребление памяти в asyncio.gather() - делаем вывод о слишком сильном "дроблении" задач.

Неравномерное распределение ресурсов

-9

И снова соотношение времени system vs python не в пользу python-операций.

Чрезмерный расход памяти

-10

Здесь профилировщик сделал всё за нас и сразу показал проблемный код.

Заключение

Надо обратить внимание, что для трёх случаев - "блокирующие операции", "последовательный вызов асинхронных задач" и "неравномерное распределение ресурсов" профилировщик показал нам одну и ту же картину - system % >> python %. Для уточнения причины требуется, собственно, разработчик.

Профилировать Python - достаточно несложная задача, если знать основные типы узких мест и быть готовым внимательно читать вывод профилировщика.