instrumentation.md
1> **Источник:** https://python-all.ru/3.15/howto/instrumentation.html2>3> «Документация Python на русском» – неофициальный перевод официальной документации Python: версии от 2.6 до 3.16, полнотекстовый поиск, английский оригинал рядом с переводом. Эта Markdown-версия страницы предназначена для работы с LLM: вставьте её в ChatGPT, Claude или Cursor.45---67# Инструментирование CPython с помощью DTrace и SystemTap89DTrace и SystemTap – это инструменты мониторинга, каждый из которых предоставляет способ проверки того, что делают процессы в компьютерной системе. Оба используют предметно-ориентированные языки, позволяющие пользователю писать сценарии, которые:1011- фильтровать, какие процессы наблюдать12- собирать данные из интересующих процессов13- создавать отчёты по данным1415Начиная с Python 3.6, CPython можно собрать со встроенными «маркерами», также известными как «зонды», которые могут наблюдаться скриптом DTrace или SystemTap, что упрощает мониторинг того, что делают процессы CPython в системе.1617**Особенность реализации CPython:** Маркеры DTrace являются деталями реализации интерпретатора CPython. Никаких гарантий совместимости зондов между версиями CPython не даётся. Скрипты DTrace могут перестать работать или работать некорректно без предупреждения при смене версий CPython.1819## Включение статических маркеров2021macOS имеет встроенную поддержку DTrace. В Linux для сборки CPython со встроенными маркерами для SystemTap необходимо установить средства разработки SystemTap.2223На машине с Linux это можно сделать с помощью:2425```console26$ yum install systemtap-sdt-devel27```2829или:3031```console32$ sudo apt-get install systemtap-sdt-dev33```3435Затем CPython должен быть [`configured with the --with-dtrace option`](https://python-all.ru/3.15/using/configure.html#cmdoption-with-dtrace):3637```text38checking for --with-dtrace... yes39```4041В macOS можно вывести список доступных зондов DTrace, запустив процесс Python в фоне и перечислив все зонды, предоставляемые провайдером Python:4243```console44$ python3.6 -q &45$ sudo dtrace -l -P python$! # или: dtrace -l -m python3.64647 ID PROVIDER MODULE FUNCTION NAME4829564 python18035 python3.6 _PyEval_EvalFrameDefault function-entry4929565 python18035 python3.6 dtrace_function_entry function-entry5029566 python18035 python3.6 _PyEval_EvalFrameDefault function-return5129567 python18035 python3.6 dtrace_function_return function-return5229568 python18035 python3.6 collect gc-done5329569 python18035 python3.6 collect gc-start5429570 python18035 python3.6 _PyEval_EvalFrameDefault line5529571 python18035 python3.6 maybe_dtrace_line line56```5758В Linux можно проверить, присутствуют ли статические маркеры SystemTap в собранном бинарном файле, посмотрев, содержит ли он секцию «.note.stapsdt».5960```console61$ readelf -S ./python | grep .note.stapsdt62[30] .note.stapsdt NOTE 0000000000000000 00308d7863```6465Если Python собран как разделяемая библиотека (с опцией конфигурации [`--enable-shared`](https://python-all.ru/3.15/using/configure.html#cmdoption-enable-shared)), то искать нужно в разделяемой библиотеке. Например:6667```console68$ readelf -S libpython3.3dm.so.1.0 | grep .note.stapsdt69[29] .note.stapsdt NOTE 0000000000000000 00365b6870```7172Достаточно современный readelf может вывести метаданные:7374```console75$ readelf -n ./python7677Displaying notes found at file offset 0x00000254 with length 0x00000020:78 Owner Data size Description79 GNU 0x00000010 NT_GNU_ABI_TAG (ABI version tag)80 OS: Linux, ABI: 2.6.328182Displaying notes found at file offset 0x00000274 with length 0x00000024:83 Owner Data size Description84 GNU 0x00000014 NT_GNU_BUILD_ID (unique build ID bitstring)85 Build ID: df924a2b08a7e89f6e11251d4602022977af26708687Displaying notes found at file offset 0x002d6c30 with length 0x00000144:88 Owner Data size Description89 stapsdt 0x00000031 NT_STAPSDT (SystemTap probe descriptors)90 Provider: python91 Name: gc__start92 Location: 0x00000000004371c3, Base: 0x0000000000630ce2, Semaphore: 0x00000000008d6bf693 Arguments: -4@%ebx94 stapsdt 0x00000030 NT_STAPSDT (SystemTap probe descriptors)95 Provider: python96 Name: gc__done97 Location: 0x00000000004374e1, Base: 0x0000000000630ce2, Semaphore: 0x00000000008d6bf898 Arguments: -8@%rax99 stapsdt 0x00000045 NT_STAPSDT (SystemTap probe descriptors)100 Provider: python101 Name: function__entry102 Location: 0x000000000053db6c, Base: 0x0000000000630ce2, Semaphore: 0x00000000008d6be8103 Arguments: 8@%rbp 8@%r12 -4@%eax104 stapsdt 0x00000046 NT_STAPSDT (SystemTap probe descriptors)105 Provider: python106 Name: function__return107 Location: 0x000000000053dba8, Base: 0x0000000000630ce2, Semaphore: 0x00000000008d6bea108 Arguments: 8@%rbp 8@%r12 -4@%eax109```110111Приведённые выше метаданные содержат информацию для SystemTap, описывающую, как он может модифицировать стратегически размещённые инструкции машинного кода, чтобы включить хуки трассировки, используемые скриптом SystemTap.112113## Статические зонды DTrace114115Следующий пример скрипта DTrace можно использовать для отображения иерархии вызовов/возвратов скрипта Python, трассируя только внутри вызова функции с именем «start». Иными словами, вызовы функций во время импорта отображаться не будут:116117```text118self int indent;119120python$target:::function-entry121/copyinstr(arg1) == "start"/122{123 self->trace = 1;124}125126python$target:::function-entry127/self->trace/128{129 printf("%d\t%*s:", timestamp, 15, probename);130 printf("%*s", self->indent, "");131 printf("%s:%s:%d\n", basename(copyinstr(arg0)), copyinstr(arg1), arg2);132 self->indent++;133}134135python$target:::function-return136/self->trace/137{138 self->indent--;139 printf("%d\t%*s:", timestamp, 15, probename);140 printf("%*s", self->indent, "");141 printf("%s:%s:%d\n", basename(copyinstr(arg0)), copyinstr(arg1), arg2);142}143144python$target:::function-return145/copyinstr(arg1) == "start"/146{147 self->trace = 0;148}149```150151Его можно вызвать так:152153```console154$ sudo dtrace -q -s call_stack.d -c "python3.6 script.py"155```156157Вывод выглядит так:158159```text160156641360502280 function-entry:call_stack.py:start:23161156641360518804 function-entry: call_stack.py:function_1:1162156641360532797 function-entry: call_stack.py:function_3:9163156641360546807 function-return: call_stack.py:function_3:10164156641360563367 function-return: call_stack.py:function_1:2165156641360578365 function-entry: call_stack.py:function_2:5166156641360591757 function-entry: call_stack.py:function_1:1167156641360605556 function-entry: call_stack.py:function_3:9168156641360617482 function-return: call_stack.py:function_3:10169156641360629814 function-return: call_stack.py:function_1:2170156641360642285 function-return: call_stack.py:function_2:6171156641360656770 function-entry: call_stack.py:function_3:9172156641360669707 function-return: call_stack.py:function_3:10173156641360687853 function-entry: call_stack.py:function_4:13174156641360700719 function-return: call_stack.py:function_4:14175156641360719640 function-entry: call_stack.py:function_5:18176156641360732567 function-return: call_stack.py:function_5:21177156641360747370 function-return:call_stack.py:start:28178```179180## Статические маркеры SystemTap181182Низкоуровневый способ использования интеграции с SystemTap заключается в прямом использовании статических маркеров. Для этого необходимо явно указать бинарный файл, содержащий их.183184Например, следующий скрипт SystemTap можно использовать для отображения иерархии вызовов/возвратов скрипта Python:185186```text187probe process("python").mark("function__entry") {188 filename = user_string($arg1);189 funcname = user_string($arg2);190 lineno = $arg3;191192 printf("%s => %s in %s:%d\\n",193 thread_indent(1), funcname, filename, lineno);194}195196probe process("python").mark("function__return") {197 filename = user_string($arg1);198 funcname = user_string($arg2);199 lineno = $arg3;200201 printf("%s <= %s in %s:%d\\n",202 thread_indent(-1), funcname, filename, lineno);203}204```205206Его можно вызвать так:207208```console209$ stap \210 show-call-hierarchy.stp \211 -c "./python test.py"212```213214Вывод выглядит так:215216```text21711408 python(8274): => __contains__ in Lib/_abcoll.py:36221811414 python(8274): => __getitem__ in Lib/os.py:42521911418 python(8274): => encode in Lib/os.py:49022011424 python(8274): <= encode in Lib/os.py:49322111428 python(8274): <= __getitem__ in Lib/os.py:42622211433 python(8274): <= __contains__ in Lib/_abcoll.py:366223```224225где столбцы означают:226227- время в микросекундах с момента запуска скрипта228- имя исполняемого файла229- PID процесса230231а остальная часть показывает иерархию вызовов/возвратов по мере выполнения скрипта.232233Для сборки CPython [`--enable-shared`](https://python-all.ru/3.15/using/configure.html#cmdoption-enable-shared) маркеры находятся внутри разделяемой библиотеки libpython, и точечный путь зонда должен это отражать. Например, следующая строка из приведённого выше примера:234235```text236probe process("python").mark("function__entry") {237```238239должна выглядеть так:240241```text242probe process("python").library("libpython3.6dm.so.1.0").mark("function__entry") {243```244245(предполагается [отладочная сборка](https://python-all.ru/3.15/using/configure.html#debug-build) CPython 3.6)246247## Доступные статические маркеры248249**function\_\_entry(str filename, str funcname, int lineno)**250251Этот маркер указывает, что началось выполнение функции Python. Он срабатывает только для чистых Python-функций (байткод).252253Имя файла, имя функции и номер строки передаются обратно скрипту трассировки в качестве позиционных аргументов, к которым необходимо обращаться через `$arg1`, `$arg2`, `$arg3`:254255> - `$arg1` : `(const char *)` filename, доступно через `user_string($arg1)`256> - `$arg2` : `(const char *)` имя функции, доступно через `user_string($arg2)`257> - `$arg3` : `int` номер строки258259**function\_\_return(str filename, str funcname, int lineno)**260261Этот маркер противоположен `function__entry()` и указывает, что выполнение функции Python завершилось (либо через `return`, либо через исключение). Он срабатывает только для чистых Python-функций (байткод).262263Аргументы такие же, как для `function__entry()`264265**line(str filename, str funcname, int lineno)**266267Этот маркер указывает, что строка Python скоро будет выполнена. Это эквивалент построчной трассировки с помощью профилировщика Python. Он не срабатывает внутри C-функций.268269Аргументы такие же, как для `function__entry()`.270271**gc\_\_start(int generation)**272273Срабатывает, когда интерпретатор Python запускает цикл сборки мусора. `arg0` – поколение для сканирования, например [`gc.collect()`](https://python-all.ru/3.15/library/gc.html#gc.collect).274275**gc\_\_done(long collected)**276277Срабатывает, когда интерпретатор Python завершает цикл сборки мусора. `arg0` – количество собранных объектов.278279**import\_\_find\_\_load\_\_start(str modulename)**280281Срабатывает до того, как [`importlib`](https://python-all.ru/3.15/library/importlib.html#module-importlib) попытается найти и загрузить модуль. `arg0` – имя модуля.282283Добавлено в версии 3.7.284285**import\_\_find\_\_load\_\_done(str modulename, int found)**286287Срабатывает после вызова функции find\_and\_load объекта [`importlib`](https://python-all.ru/3.15/library/importlib.html#module-importlib). `arg0` – имя модуля, `arg1` указывает, был ли модуль успешно загружен.288289Добавлено в версии 3.7.290291**audit(str event, void \*tuple)**292293Срабатывает при вызове [`sys.audit()`](https://python-all.ru/3.15/library/sys.html#sys.audit) или [`PySys_Audit()`](https://python-all.ru/3.15/c-api/sys.html#c.PySys_Audit). `arg0` – имя события в виде строки C, `arg1` – указатель [`PyObject`](https://python-all.ru/3.15/c-api/structures.html#c.PyObject) на объект кортежа.294295Добавлено в версии 3.8.296297### Точки входа в C298299Чтобы упростить запуск маркеров DTrace, C API Python содержит ряд вспомогательных функций, повторяющих каждый статический маркер. В сборках Python без поддержки DTrace эти функции ничего не делают.300301В общем случае вызывать их вручную не требуется – Python сделает это сам.302303| Функция C API | Статический маркер | Примечания |304| --- | --- | --- |305| void PyDTrace\_LINE(const char \*arg0, const char \*arg1, int arg2) | `line()` | |306| void PyDTrace\_FUNCTION\_ENTRY(const char \*arg0, const char \*arg1, int arg2) | `function__entry()` | |307| void PyDTrace\_FUNCTION\_RETURN(const char \*arg0, const char \*arg1, int arg2) | `function__return()` | |308| void PyDTrace\_GC\_START(int arg0) | `gc__start()` | |309| void PyDTrace\_GC\_DONE([Py\_ssize\_t](https://python-all.ru/3.15/c-api/intro.html#c.Py_ssize_t) arg0) | `gc__done()` | |310| void PyDTrace\_INSTANCE\_NEW\_START(int arg0) | `instance__new__start()` | Не используется в Python |311| void PyDTrace\_INSTANCE\_NEW\_DONE(int arg0) | `instance__new__done()` | Не используется в Python |312| void PyDTrace\_INSTANCE\_DELETE\_START(int arg0) | `instance__delete__start()` | Не используется в Python |313| void PyDTrace\_INSTANCE\_DELETE\_DONE(int arg0) | `instance__delete__done()` | Не используется в Python |314| void PyDTrace\_IMPORT\_FIND\_LOAD\_START(const char \*arg0) | `import__find__load__start()` | |315| void PyDTrace\_IMPORT\_FIND\_LOAD\_DONE(const char \*arg0, int arg1) | `import__find__load__done()` | |316| void PyDTrace\_AUDIT(const char \*arg0, void \*arg1) | `audit()` | |317318### C-проверки зондов319320#### `int PyDTrace_LINE_ENABLED(void)`321322#### `int PyDTrace_FUNCTION_ENTRY_ENABLED(void)`323324#### `int PyDTrace_FUNCTION_RETURN_ENABLED(void)`325326#### `int PyDTrace_GC_START_ENABLED(void)`327328#### `int PyDTrace_GC_DONE_ENABLED(void)`329330#### `int PyDTrace_INSTANCE_NEW_START_ENABLED(void)`331332#### `int PyDTrace_INSTANCE_NEW_DONE_ENABLED(void)`333334#### `int PyDTrace_INSTANCE_DELETE_START_ENABLED(void)`335336#### `int PyDTrace_INSTANCE_DELETE_DONE_ENABLED(void)`337338#### `int PyDTrace_IMPORT_FIND_LOAD_START_ENABLED(void)`339340#### `int PyDTrace_IMPORT_FIND_LOAD_DONE_ENABLED(void)`341342#### `int PyDTrace_AUDIT_ENABLED(void)`343344Все вызовы функций `PyDTrace` должны быть защищены вызовом одной из этих функций. Это позволяет Python минимизировать влияние на производительность, когда зондирование отключено.345346В сборках без включённого DTrace эти функции ничего не делают и возвращают `0`.347348## Тэпсеты SystemTap349350Более высокоуровневый способ использования интеграции SystemTap – это использование «тэпсета»: аналога библиотеки в SystemTap, который скрывает некоторые низкоуровневые детали статических маркеров.351352Вот файл тэпсета, основанный на неразделяемой сборке CPython:353354```text355/*356 Provide a higher-level wrapping around the function__entry and357 function__return markers:358 \*/359probe python.function.entry = process("python").mark("function__entry")360{361 filename = user_string($arg1);362 funcname = user_string($arg2);363 lineno = $arg3;364 frameptr = $arg4365}366probe python.function.return = process("python").mark("function__return")367{368 filename = user_string($arg1);369 funcname = user_string($arg2);370 lineno = $arg3;371 frameptr = $arg4372}373```374375Если этот файл установлен в каталог тэпсетов SystemTap (например, `/usr/share/systemtap/tapset`), то становятся доступны следующие дополнительные точки зондирования:376377**python.function.entry(str filename, str funcname, int lineno, frameptr)**378379Эта точка зондирования указывает, что выполнение функции Python началось. Она срабатывает только для чистых Python-функций (байт-код).380381**python.function.return(str filename, str funcname, int lineno, frameptr)**382383Эта точка зондирования является обратной к `python.function.return` и указывает, что выполнение функции Python завершено (либо через `return`, либо через исключение). Она срабатывает только для чистых Python (байт-код) функций.384385## Примеры386387Этот скрипт SystemTap использует приведённый выше тэпсет для более чистой реализации примера трассировки иерархии вызовов функций Python, без необходимости называть статические маркеры напрямую:388389```text390probe python.function.entry391{392 printf("%s => %s in %s:%d\n",393 thread_indent(1), funcname, filename, lineno);394}395396probe python.function.return397{398 printf("%s <= %s in %s:%d\n",399 thread_indent(-1), funcname, filename, lineno);400}401```402403Следующий скрипт использует тэпсет выше, чтобы предоставить представление типа top для всего выполняющегося кода CPython, показывая 20 наиболее часто входящих байт-код фреймов каждую секунду во всей системе:404405```text406global fn_calls;407408probe python.function.entry409{410 fn_calls[pid(), filename, funcname, lineno] += 1;411}412413probe timer.ms(1000) {414 printf("\033[2J\033[1;1H") /* clear screen \*/415 printf("%6s %80s %6s %30s %6s\n",416 "PID", "FILENAME", "LINE", "FUNCTION", "CALLS")417 foreach ([pid, filename, funcname, lineno] in fn_calls- limit 20) {418 printf("%6d %80s %6d %30s %6d\n",419 pid, filename, lineno, funcname,420 fn_calls[pid, filename, funcname, lineno]);421 }422 delete fn_calls;423}424```425