sys.monitoring
sys.monitoring (PEP 669) is the low-overhead instrumentation
API. Tools register callbacks for events (call, line, branch,
exception, ...); the runtime fires them by swapping the relevant
bytecodes to INSTRUMENTED_* variants and stashing per-event
state in side tables on the code object. Cold code pays nothing;
hot code with no registered events pays only the cost of the
event check (typically a single load and branch).
Where the code lives
| File | Role |
|---|---|
Python/instrumentation.c | The whole subsystem. |
Include/internal/pycore_instruments.h | Event ids, per-code instrumentation data. |
Include/cpython/monitoring.h | Public PyMonitoring_* entry points (C extensions). |
Lib/sys.py, Python/sysmodule.c | The sys.monitoring Python API. |
Python/bytecodes.c | INSTRUMENTED_* opcode bodies and DISPATCH hooks. |
Events
PEP 669 defines a fixed set of events:
PY_START, PY_RESUME, PY_RETURN, PY_YIELD, PY_THROW, PY_UNWIND
CALL, C_RETURN, C_RAISE
RAISE, RERAISE, EXCEPTION_HANDLED, STOP_ITERATION
LINE, INSTRUCTION, JUMP, BRANCH_LEFT, BRANCH_RIGHT
Each event has a numeric id. Tools register a callback per
(tool_id, event_id) pair. Up to six tools can be active at
once (debuggers, coverage, profilers, ...); each gets its own
ids so they do not collide.
Tools and event sets
/* Include/internal/pycore_instruments.h _PyMonitoringEventSet */
typedef uint32_t _PyMonitoringEventSet;
/* Tools registered with the runtime */
typedef struct {
PyObject *callbacks[PY_MONITORING_EVENTS];
} _Py_GlobalMonitors;
The interpreter keeps three layers of event sets:
- Global. Events enabled for everything.
- Per code object. Events enabled for a specific function.
- Local (per-instruction). Events enabled for one bytecode
offset (for
LINEandINSTRUCTION, which need finer control).
The effective set at an instruction is the union of the three; the instrumentation pass uses it to decide which opcodes to swap.
The INSTRUMENTED opcode mirror
Every event-emitting opcode has an INSTRUMENTED_* sibling
generated from the same bytecodes.c DSL. Examples:
CALL -> INSTRUMENTED_CALL
RETURN_VALUE -> INSTRUMENTED_RETURN_VALUE
RESUME -> INSTRUMENTED_RESUME
JUMP_BACKWARD -> INSTRUMENTED_JUMP_BACKWARD
LOAD_SUPER_ATTR -> INSTRUMENTED_LOAD_SUPER_ATTR
END_FOR -> INSTRUMENTED_END_FOR
The body of an INSTRUMENTED_X does what X does and, before
or after, calls into the instrumentation dispatcher to fire any
registered callbacks. When monitoring is enabled for an event
on a code object, the runtime rewrites the opcode byte from
X to INSTRUMENTED_X in co_code; when it is disabled, it
flips back. The flip is in place; no recompilation.
For LINE and INSTRUCTION, where the event fires at every
instruction, the runtime inserts a single
INSTRUMENTED_LINE / INSTRUMENTED_INSTRUCTION at the
relevant offsets; the opcode body checks the per-instruction
tooling array and fires only if the local event is enabled.
Side tables on the code object
Instrumentation does not change co_code shape. it changes the
opcode bytes plus a few side tables:
/* Include/internal/pycore_code.h _PyCoMonitoringData */
typedef struct _PyCoMonitoringData {
_PyMonitoringEventSet local_events;
uint8_t *per_instruction_opcodes; /* original opcodes before flip */
uint8_t *per_instruction_tools; /* bitmask: which tools want LINE here */
/* ... */
} _PyCoMonitoringData;
co->_co_monitoring is NULL for uninstrumented code (the
common case, no overhead). When monitoring activates on a code
object, the runtime allocates the struct lazily.
per_instruction_opcodesremembers the pre-flip opcode so the runtime can restore on deactivation.per_instruction_toolsis a per-offset bitmask: bit i set means tool i wantsLINE(orINSTRUCTION) at this offset. TheINSTRUMENTED_LINEbody checks the bitmask.
Firing a callback
When INSTRUMENTED_CALL fires the CALL event, the dispatcher
walks the registered tools in id order and calls each one whose
event bit is set in local_events:
/* Python/instrumentation.c (sketch) */
for (int tool = 0; tool < PY_MONITORING_TOOL_IDS; tool++) {
if (!(local_events & (1 << tool))) continue;
PyObject *cb = tstate->interp->monitors.tools[tool].callbacks[event];
if (cb == NULL) continue;
PyObject *res = PyObject_CallOneArg(cb, args);
if (res == DISABLE) {
/* turn this event off at this site permanently */
}
}
A callback can return the sentinel sys.monitoring.DISABLE to
turn its event off at this code location for the rest of the
process; the rewrite logic flips the offending bytecodes back
to their uninstrumented form. This is how a coverage tool can
mark a line as covered and never pay again.
Interaction with the specializer
Specialised opcodes can be instrumented. The flow:
- The site starts generic.
- The specializer rewrites it.
- Monitoring is enabled; the rewrite flips the specialised
opcode to
INSTRUMENTED_<specialised>. - If monitoring is disabled, the byte flips back to the specialised opcode.
The cases generator emits INSTRUMENTED_* variants for every
opcode that needs them, including the specialised ones; the
metadata header records the mapping.
Interaction with Tier-2
Tier-2 cannot project through INSTRUMENTED_* opcodes. an
instrumented site forces the trace projector to stop. This
means heavily instrumented code stays in Tier-1; uninstrumented
loops with the same shape still get traced.
Legacy tracing
sys.settrace and sys.setprofile still work; they are
implemented on top of the same machinery. The legacy API is
thinner: one callback, all events, no DISABLE. The new API
should be preferred for new tooling because it can be selective.
CPython 3.14 changes
BRANCH_LEFTandBRANCH_RIGHTevents (replacing the earlierBRANCHevent) distinguish which side of a conditional jump was taken, which matters for branch coverage tools.STOP_ITERATIONevent added so coverage tools can see generator and iterator termination without instrumenting everyfor.- Per-instruction events were rationalised; the
INSTRUCTIONevent now fires after the instruction, not before, which lines up better with how debuggers expect step semantics.
PEP touchpoints
- PEP 669. Low-impact monitoring for CPython.
- PEP 626. Location table;
LINEevents depend on it.
Reference
Python/instrumentation.c,Include/internal/pycore_instruments.h,Include/cpython/monitoring.h.- PEP 669. Low impact monitoring.
Doc/library/sys.monitoring.rst.