Использование профайлера или отладчика в CPython может серьезно повлиять на производительность. Замедления на порядок - обычное дело.
Python 3.12 представил новую структуру sys.monitoring, которая позволит осуществлять мониторинг при низких затратах. Это пространство имен обеспечивает доступ к функциям и константам, необходимым для активации и управления мониторингом событий. Обеспечивает детальный контроль, так что инструменты могут прослушивать только определенные события в определенных строках кода.
Мониторинг программ Python осуществляется путем регистрации функций обратного вызова для событий и активации набора событий. Активация событий и регистрация функций обратного вызова независимы друг от друга.
Как регистрация обратных вызовов, так и активация событий выполняются на основе каждого инструмента. Возможно иметь несколько инструментов, которые реагируют на различные наборы событий.
Обратите внимание, что в отличие от предшественника sys.settrace(), события и обратные вызовы относятся к интерпретатору, а не к потоку.
sys.monitoring.use_tool_id(tool_id: int, name: str, /) -> None:Необходимо вызвать функцию до того, как tool_id можно будет использовать. tool_id должен находиться в диапазоне от 0 до 5 включительно. Вызывает исключение ValueError, если tool_id используется.
sys.monitoring.clear_tool_id(tool_id: int, /) -> None:Отменяет регистрацию всех событий и функций обратного вызова, связанных с tool_id.
sys.monitoring.free_tool_id(tool_id: int, /) -> None:Следует вызывать, когда инструмент больше не нуждается в tool_id. Вызовет sys.monitoring.clear_tool_id() перед освобождением tool_id.
sys.monitoring.get_tool(tool_id: int, /) -> str | None:Возвращает название инструмента, если tool_id используется, в противном случае возвращает None. tool_id должен находиться в диапазоне от 0 до 5 включительно.
Все идентификаторы обрабатываются виртуальной машиной одинаково в отношении событий, но следующие идентификаторы предварительно определены, чтобы упростить взаимодействие инструментов:
sys.monitoring.DEBUGGER_ID = 0 sys.monitoring.COVERAGE_ID = 1 sys.monitoring.PROFILER_ID = 2 sys.monitoring.OPTIMIZER_ID = 5
sys.monitoring.register_callback(tool_id: int, event: int, func: Callable | None, /) → Callable | None:Функция sys.monitoring.register_callback() регистрирует вызов func для события event с данным tool_id.
Если для данного tool_id и события event был зарегистрирован другой обратный вызов, то он отменяется и возвращается. В противном случае register_callback() возвращает None.
Функции могут быть сняты с регистрации путем вызова sys.monitoring.register_callback(tool_id, event, None). Функции обратного вызова могут быть зарегистрированы или сняты с регистрации в любое время.
sys.monitoring.events.BRANCH:Берется (или нет) условная ветвь.
sys.monitoring.events.CALL:Вызов в коде Python (событие происходит перед вызовом).
sys.monitoring.events.C_RAISE:Исключение, возникающее при вызове любого объекта, кроме функций Python (событие происходит после завершения работы).
sys.monitoring.events.C_RETURN:Возврат из любого вызываемого объекта, кроме функций Python (событие происходит после возврата).
sys.monitoring.events.EXCEPTION_HANDLED:Обрабатывается исключение.
sys.monitoring.events.INSTRUCTION:Инструкция виртуальной машины будет выполнена.
sys.monitoring.events.JUMP:Выполнен безусловный скачок в графике потока управления
sys.monitoring.events.LINE:Будет выполнена инструкция, которая имеет другой номер строки из предыдущей инструкции.
sys.monitoring.events.PY_RESUME:Возобновление работы функции Python (для функций-генераторов и сопрограмм), за исключением вызовов throw().
sys.monitoring.events.PY_RETURN:Возврат из функции Python (происходит непосредственно перед возвратом, кадр вызываемой функции будет находиться в стеке).
sys.monitoring.events.PY_START:Начало функции Python (происходит сразу после вызова, кадр вызываемой функции будет находиться в стеке)
sys.monitoring.events.PY_THROW:Функция Python возобновляется вызовом throw().
sys.monitoring.events.PY_UNWIND:Выход из функции Python во время разматывания исключения.
sys.monitoring.events.PY_YIELD:Возврат из функции Python (происходит непосредственно перед возвратом, кадр вызываемой функции будет находиться в стеке).
sys.monitoring.events.RAISE:Возникает исключение, за исключением тех, которые вызывают событие STOP_ITERATION.
sys.monitoring.events.RERAISE:Исключение возникает повторно, например, в конце блока finally.
sys.monitoring.events.STOP_ITERATION:Вызывается искусственный StopIteration (см. событие STOP_ITERATION).
В будущем могут быть добавлены дополнительные события.
Эти события являются атрибутами пространства имён sys.monitoring.events. Каждое событие представлено в виде целочисленной константы, степень двойки которой равна 2. Чтобы определить набор событий, просто побитно объедините отдельные события. Например, чтобы указать события PY_RETURN и PY_START, используйте выражение PY_RETURN | PY_START.
An alias for 0 so users can do explicit comparisons like:
Copyif get_events(DEBUGGER_ID) == NO_EVENTS:...
Небольшой пример техники мониторинга за глобальным списком sys.path, который может изменился в процессе выполнения программы.
# example.py import sys # Контрольное значение длины списка `sys.path` path_length = len(sys.path) def monitor(code, line_number): """Функция мониторинга для выполнения каждой инструкции""" global path_length if len(sys.path) != path_length: print("🛎️ В `sys.path` изменилась длина") path_length = len(sys.path) breakpoint() else: # Отключим для данной инструкции, предполагая, что она # не изменит sys.path в случае повторного выполнения. return sys.monitoring.DISABLE # Регистрация отладчика sys.monitoring.use_tool_id(sys.monitoring.DEBUGGER_ID, "debugging") # Включим отладчик для учебных событий sys.monitoring.set_events( sys.monitoring.DEBUGGER_ID, sys.monitoring.events.INSTRUCTION, ) # запускаем `monitor()` для каждой инструкции sys.monitoring.register_callback( sys.monitoring.DEBUGGER_ID, sys.monitoring.events.INSTRUCTION, monitor, )
Разберем представленный код:
monitor() - функция для мониторинга sys.path. Она работает с каждой инструкцией с отдельными этапами выполнения кода Python.monitor() проверяет, изменилась ли длина sys.path для обнаружения мутаций. Функция не позволит обнаружить изменение элементов на месте, но для примера этого достаточно.monitor() выводит эмодзи колокольчика и открывает отладчик через breakpoint().sys.path не изменялась, то monitor() возвращается sys.monitoring.DISABLE. Эта константа указывает sys.monitoring избегать повторного вызова этой инструкции. Такое поведение открывает одно из ключевых преимуществ производительности sys.monitoring: это позволяет избежать повторных ненужных обращений к инструментам мониторинга. Без этого шага накладные расходы на функцию мониторинга значительно замедлили бы работу программы, возможно, до такой степени, что стали бы непрактичными.sys.monitoring.use_tool_id() включает инструмент с идентификатором DEBUGGER_ID и произвольным названием "debugging". sys.monitoring поддерживает только шесть активных инструментов, которые должны использовать уникальные идентификаторы. В примере используем идентификатор DEBUGGER_ID.sys.monitoring.set_events() указывает sys.monitoring на включение инструмента отладки для событий INSTRUCTION. Это означает, что инструмент включён только для этих событий.sys.monitoring.register_callback() сообщает sys.monitoring о вызове monitor() при каждом событии инструкции.Теперь посмотрим, как это работает. Ниже приведён небольшой пример, который изменяет sys.path. Код ниже необходимо вставить в конец предыдущего кода.
print("=> Старт") sys.path.insert(0, "animals") print("=> Стоп")
После вызова sys.path.insert() сразу откроется отладчик pdb:
$ python example.py => Старт 🛎️ В `sys.path` изменилась длина > /.../example.py(46)<module>() -> print("=> Стоп") (Pdb)
Обратите внимание, что отладчик pdb открывается на инструкции после произошедшей мутации, в следующей строке с print(). Это происходит потому, что sys.monitoring запускает функцию мониторинга перед каждой инструкцией.
Чтобы найти виновника, нужно вернуться на шаг назад. В данном случае это означает предыдущую строку, но после большого блока if/else или возврата функции, это может быть сложнее.
При открытом pdb можно использовать любые команды отладки. Например, pp для вывода sys.path в удобном для чтения виде.