Skip to main content

Include/internal/pycore_instruments.h

cpython 3.14 @ ab2d84fe1023/Include/internal/pycore_instruments.h

PEP 669 (low-impact monitoring for CPython) landed in 3.12 and this header is the internal half of that feature. It defines the event bit constants that tools register interest in, the per-thread and per-code-object bookkeeping that tracks which events are active, and the _PyMonitoring_Fire* functions that the eval loop calls when an instrumented instruction is reached. The design goal is that zero-overhead applies when no tool has registered: the fast path is a single flag check before any event dispatch logic runs.

Map

LinesSymbolRolegopy
~10-40_Py_MONITORING_* constantsBit positions for each event kindnot ported
~42-55_PyMonitoringEventSetBitmask typedef combining event bitsnot ported
~57-75_PyCoMonitoringDataPer-code instrumentation arrays (local monitors, per-instruction opcodes/tools)not ported
~77-100_Py_Instrumented_InstructionsGlobal flag array: which opcodes have any active toolnot ported
~102-120_PyMonitoring_FireCallEventFire a CALL event toward registered toolsnot ported
~122-135_PyMonitoring_FireReturnEventFire a RETURN eventnot ported
~137-150_PyMonitoring_FireLineEventFire a LINE event for line-trace toolsnot ported

Reading

Event bit constants (lines 10 to 40)

cpython 3.14 @ ab2d84fe1023/Include/internal/pycore_instruments.h#L10-40

Each observable event has a dedicated bit so that tools can subscribe to any combination with a single OR:

#define _Py_MONITORING_EVENT_PY_START 0
#define _Py_MONITORING_EVENT_PY_RESUME 1
#define _Py_MONITORING_EVENT_PY_RETURN 2
#define _Py_MONITORING_EVENT_PY_YIELD 3
#define _Py_MONITORING_EVENT_CALL 4
#define _Py_MONITORING_EVENT_LINE 5
#define _Py_MONITORING_EVENT_INSTRUCTION 6
#define _Py_MONITORING_EVENT_JUMP 7
#define _Py_MONITORING_EVENT_BRANCH 8
#define _Py_MONITORING_EVENT_STOP_ITERATION 9
#define _Py_MONITORING_EVENT_RAISE 10
#define _Py_MONITORING_EVENT_EXCEPTION_HANDLED 11
#define _Py_MONITORING_EVENT_PY_UNWIND 12
#define _Py_MONITORING_EVENT_PY_THROW 13
#define _Py_MONITORING_EVENT_RERAISE 14

typedef uint32_t _PyMonitoringEventSet;

A tool registers via sys.monitoring.set_events(tool_id, event_set). CPython then rewrites the affected bytecode instructions to their _INSTRUMENTED_* variants so the fast path for un-monitored code remains a straight opcode dispatch with no branch.

Per-code instrumentation data (lines 57 to 75)

cpython 3.14 @ ab2d84fe1023/Include/internal/pycore_instruments.h#L57-75

Each PyCodeObject carries a _PyCoMonitoringData * pointer that is NULL until the first tool subscribes to any event for that code object:

typedef struct {
/* Bitmask of events any tool wants for this code object as a whole. */
_PyMonitoringEventSet local_monitors;
/* Per-instruction original opcodes (before replacement with INSTRUMENTED_*). */
uint8_t *per_instruction_opcodes;
/* Per-instruction bitmask of which tools want INSTRUCTION events here. */
uint8_t *per_instruction_tools;
} _PyCoMonitoringData;

The per_instruction_opcodes array is allocated lazily at the same length as co_code and stores the original opcode that was replaced so that the Fire functions can report the logical instruction to the tool, not the sentinel INSTRUMENTED_* variant.

Fire functions and the eval-loop check (lines 100 to 150)

cpython 3.14 @ ab2d84fe1023/Include/internal/pycore_instruments.h#L100-150

The three main fire functions share a common signature shape: they receive the current frame, the current instruction pointer, and event-specific arguments, then fan out to each registered tool:

int _PyMonitoring_FireCallEvent(
PyMonitoringState *state,
PyObject *tstate, /* actually _PyThreadState* */
_PyInterpreterFrame *frame,
PyObject *callable,
PyObject *arg0);

int _PyMonitoring_FireReturnEvent(
PyMonitoringState *state,
PyObject *tstate,
_PyInterpreterFrame *frame,
PyObject *retval);

int _PyMonitoring_FireLineEvent(
PyMonitoringState *state,
PyObject *tstate,
_PyInterpreterFrame *frame,
int line);

The eval loop calls these only after confirming that _Py_Instrumented_Instructions[opcode] is non-zero, keeping the hot path a single indexed byte load followed by a conditional branch.

gopy mirror

gopy does not yet port the monitoring subsystem. The eval loop in vm/eval_gen.go has no INSTRUMENTED_* opcode cases and no sys.monitoring module. Adding monitoring would require: allocating _PyCoMonitoringData during code-object construction, rewriting bytecode instructions on tool registration, and wiring the fire functions into every relevant eval-loop ceval point. This is tracked as future work after the core eval loop stabilizes.

CPython 3.14 changes

3.14 extends the event set with BRANCH_RIGHT and BRANCH_LEFT to let coverage tools distinguish taken vs not-taken branches without instrumenting every conditional jump. The _PyCoMonitoringData struct gains a branches array parallel to per_instruction_opcodes to record branch direction history. The fire-function signatures are otherwise unchanged from 3.12.