Общие слова
Профилирование приложений — это процесс анализа программы для определения её характеристик: времени выполнения различных частей кода и использования ресурсов.
Основные этапы профилирования всегда более-менее одинаковы:
- Измерение времени выполнения. Cколько времени требуется для выполнения различных частей кода?
- Анализ использования памяти. Сколько памяти потребляется различными частями программы?
- Выявление узких мест. Какие части кода замедляют работу программы и/или используют слишком много ресурсов?
- Оптимизация производительности. Принятие мер для улучшения скорости выполнения и эффективности использования ресурсов на основе полученных данных.
А как вообще работает профилировщик?
Детальному обзору будет посвящена отдельная статья, пока можно ограничится базовой классификацией:
- Детерминированные (deterministic) профилировщики. Главный представитель - встроенный cProfile. Такой профилировщик считает количество вызовов каждой функции и потраченное функцией время. Проблема в том, что время ожидания асинхронных вызовов не учитывается.
- Статистические (statistical) профилировщики. Распространённые представители - scalene, py-spy, yappi, pyinstrument, austin. Такие профилировщики с некоторой частотой снимают "слепок" с процесса и применяют методы статистического анализа для поиска узких мест.
Основные типы узких мест в асинхронном Python-коде
Для асинхронного кода существует небольшое количество специфических "узких мест", которые лучше перечислить заранее.
Каждому типу сопоставим пример кода.
Список допущений
- Python 3.12
- Используется один и только один event-loop
- Яндекс.Дзен не умеет отображать код текстом, поэтому пришлось вставлять его картинками.
Блокирующие операции
Последовательный вызов асинхронных задач
Слишком частое переключение контекста
Неравномерное распределение ресурсов
В англоязычной литературе такой сценарий называется "Resource Starvation".
Чрезмерный расход памяти
Использование "scalene" для профилирования
Почему scalene? Потому что этот инструмент позволяет профилировать и CPU, и GPU, и память; 10k+ звёзд на гитхабе, проект активно развивается.
Посмотрим что скажет scalene для каждого "проблемного" кода из списка выше.
Запускать будем в режиме scalene --cpu --memory --cli script_name.py
Блокирующие операции
Проблемную строку с блокирующим вызовов видно сразу - 2% времени на Python, 98% - на системные вызовы.
Последовательный вызов асинхронных задач
Здесь чуть сложнее. Видно, что 90% времени уходит на системные вызовы, но поменялась строка - теперь это сам asyncio.run(). Такой паттерн вывода профилировщика лучше всего просто запомнить.
Слишком частое переключение контекста
Видим, как растёт потребление памяти в asyncio.gather() - делаем вывод о слишком сильном "дроблении" задач.
Неравномерное распределение ресурсов
И снова соотношение времени system vs python не в пользу python-операций.
Чрезмерный расход памяти
Здесь профилировщик сделал всё за нас и сразу показал проблемный код.
Заключение
Надо обратить внимание, что для трёх случаев - "блокирующие операции", "последовательный вызов асинхронных задач" и "неравномерное распределение ресурсов" профилировщик показал нам одну и ту же картину - system % >> python %. Для уточнения причины требуется, собственно, разработчик.
Профилировать Python - достаточно несложная задача, если знать основные типы узких мест и быть готовым внимательно читать вывод профилировщика.