Skip to content

v0.11.0: adaptive specialization and sys.monitoring#19

Merged
tamnd merged 32 commits into
mainfrom
feat/v0.11.0-specialize-monitor
May 7, 2026
Merged

v0.11.0: adaptive specialization and sys.monitoring#19
tamnd merged 32 commits into
mainfrom
feat/v0.11.0-specialize-monitor

Conversation

@tamnd

@tamnd tamnd commented May 7, 2026

Copy link
Copy Markdown
Owner

The specializer and monitor drop. v0.11 lights up the two big
runtime features 3.12 / 3.13 added: PEP 659 adaptive
specialization that rewrites bytecode into typed variants on
first hit, and PEP 669 sys.monitoring instrumentation that
fans 19 events out to up to seven tools. The legacy
sys.settrace / sys.setprofile callbacks now ride on top of
the same monitor fan-out, so a single dispatch path drives both
shapes.

Shipped

specialize/ carries the PEP 659 port: backoff counter and
inline cache layouts (backoff.go, cache.go),
_PyCode_Quicken, the _PyOpcode_Caches and _PyOpcode_Deopt
tables, and per-family entry points for LOAD_ATTR,
STORE_ATTR, LOAD_GLOBAL, LOAD_SUPER_ATTR, BINARY_OP,
COMPARE_OP, CONTAINS_OP, TO_BOOL, STORE_SUBSCR,
UNPACK_SEQUENCE, FOR_ITER, SEND, CALL, and CALL_KW.
Each family lands with its hit / miss / deopt path and the
tp_version_tag / dk_version guards the fast variants assume.
vm/adaptive.go and dispatch.go wire the rewrite into the eval
loop.

monitor/ carries the PEP 669 port: InterpState per-interp
state slab, the 19 fire-event entry points, the _Py_Instrument
shadow walk that rewrites quickened bytecode to INSTRUMENTED_*
variants in place, the per-code tool-slot lifecycle, line
instrumentation driven by the PEP 626 line table, and the
shared callback runner that honours Disable / Missing. The
sys.monitoring builtin module surfaces every entry the user
needs (use_tool_id, register_callback, set_events,
set_local_events, restart_events, plus the constants).

vm/legacy_tracing.go bridges PEP 669 fan-out back to the
Py_tracefunc shape sys.settrace / sys.setprofile expect,
registering as PEP 669 tools 6 and 7. vm/sys_trace_builtins.go
exposes the four Python-visible builtins
(settrace, setprofile, gettrace, getprofile) on top of
the bridge. Per-frame TraceLines / TraceOpcodes / Lineno
slots on frame.Frame so the bridge can drive line and opcode
events on demand.

compile/opcodes_gen.go is regenerated to pick up the
specialized variants (LOAD_ATTR_INSTANCE_VALUE,
BINARY_OP_ADD_INT, ...) and the INSTRUMENTED_* mirror set.
The cache-count column lines up with _PyOpcode_Caches so the
dispatch loop strides over the inline cache slots correctly.

Test plan

  • go build ./... clean.
  • go test ./specialize/... green; cache_test.go pins
    every cache width against _PyOpcode_Caches[].
  • go test ./monitor/... green; install_test.go and
    line_test.go exercise the shadow walk and the line table
    decode.
  • go test ./vm/... green; adaptive_test.go drives a
    TO_BOOL rewrite and legacy_tracing_test.go drives the
    bridge.
  • vmtest/v011_gate_test.go end-to-end gate runs through
    the specializer, monitor fan-out, sys.settrace bridge, and
    sys.monitoring builtin surface in one go.
  • All pre-v0.11 tests still pass; the one failure
    (TestRunStringCPythonSmoke/comparison_eq) is pre-existing on
    main and unrelated to this drop.

tamnd added 30 commits May 7, 2026 17:33
Port pycore_backoff.h and the cache structs from pycore_code.h as
the foundation for v0.11 PEP 659 work. Cache widths are pinned by
test against the _PyOpcode_Caches[] table from CPython 3.14 so the
Go layout cannot silently drift from the C one.
The PEP 659 specializer needs to name LOAD_ATTR_INSTANCE_VALUE,
BINARY_OP_ADD_INT, CALL_PY_EXACT_ARGS, etc. by symbol. The
generator only walked the `opmap` dict; the specialized variants
live in `_specialized_opmap`. Read both and emit all 238 opcodes.
Quickened code now ticks the BackoffCounter on each adaptive opcode
and, when the counter triggers, calls into the per-family specializer
in specialize/. Specialized variants written into the bytecode are
folded back to their adaptive parent on entry to dispatch so the
generic body still runs (no fast-path arms yet). The cache-bearing
arms in eval_simple.go and eval_gen.go now use cacheAdvance to step
past the inline cache cells when running on Quickened code; raw
test bytecode without padding still advances one codeunit.
Bring across the PEP 669 event identifiers, the tool slot constants, and
the per-code _PyCoMonitoringData layout that the instrumentation runtime
will read from. No fire-event entry points yet; those come once the
shadow walk lands.
Brings across the cluster of fields CPython stamps on PyInterpreterState
for sys.monitoring: the global event mask, per-tool callback table, tool
names, version counters, and the tool-id / event-set lifecycle helpers
that back use_tool_id, free_tool_id, set_events, and register_callback.
state.Interpreter now allocates one of these per interpreter so the
shadow walk and fire-event entry points have somewhere to hang state.
Brings across the fan-out path tools call from the eval loop. EnterScope
refreshes the per-event MonState bitmask when the global version drifts;
fireEvent walks the active tools by most-significant-bit and invokes
each registered callback with the PEP 669 argument signature. Disable
returns clear an instrumented event from the per-MonState mask and
clear the slot on non-instrumented events so a tool that misuses the
sentinel does not get called again.

Also adds the Disable / Missing sentinels (bare singleton objects whose
identity callers compare against, mirroring _PyInstrumentation_DISABLE
and _MISSING).
Brings across set_local_events / get_local_events so a tool can
subscribe to instrumented events on a specific code object only. The
per-code monitoring slab (CoMonitoringData) hangs off objects.Code.MonitoringData
as an any to keep the objects package free of a monitor import.

The interpreter and code-local versions both bump on a real change so
the shadow walk knows to re-instrument; no-op writes leave them alone.
Brings across the bytecode-rewriting pass that turns subscribed events
into INSTRUMENTED_<X> opcodes. The walk diffs the new active-tool union
against the cached one so a no-op rebump skips the rewrite, and falls
back to the base opcode (deopting from any specialized form) when the
last tool drops off an offset. Per-codeunit tool bitmasks live on
CoMonitoringData.Tools so the dispatch fan-out can read them by
instruction index.

Skips line and per-instruction arms; those land with #406.
Hook fireInstrumented into the dispatch entry so an INSTRUMENTED_<X>
opcode resolves the active tool bitmask out of CoMonitoringData.Tools,
fans out through the matching FireXxx entry, and reflects any
Disable returns back into the per-codeunit mask before the base body
runs.
Allocate the per-code line table on first LINE subscription, walk
the bytecode to stamp original opcodes against PEP 626 line starts,
install / clear INSTRUMENTED_LINE through the shadow walk, and let
dispatch fire LINE plus recover the original opcode via
CallInstrumentationLine before falling through to the normal body.
Build the PEP 669 module surface from a per-interpreter InterpState:
use_tool_id / free_tool_id / clear_tool_id / get_tool, register_callback,
get_events / set_events, get_local_events / set_local_events,
restart_events, _all_events, plus the DISABLE / MISSING sentinels and
the events sub-namespace. set_events folds the grouped BRANCH bit into
BRANCH_LEFT | BRANCH_RIGHT so callers can subscribe with the public
name.
Bridges PEP 669 events back to the classic Py_tracefunc shape so
sys.settrace and sys.setprofile can keep working. Each tool slot
(SYS_PROFILE = 6, SYS_TRACE = 7) gets its own fan-out of small
handlers; SetProfile / SetTrace stamp the global event mask through
monitor.InterpState and bump the per-thread function pointer that
the handlers dispatch to.
Adds the four sys.* trace/profile builtins on top of the legacy
tracing bridge. Each one takes a Python callable, wraps it in a
trampoline that matches LegacyTraceFunc, and hands it to the
SetTrace/SetProfile entry points. RegisterSysTraceBuiltins drops
the four into a sys dict for the lifecycle wiring to pick up.
tamnd added 2 commits May 7, 2026 19:21
Drives the specializer, PEP 669 monitor, sys.settrace bridge, and
sys.monitoring builtin surface through their public entry points so we
catch regressions in the wiring between them.
@tamnd tamnd marked this pull request as ready for review May 7, 2026 15:50
@tamnd tamnd merged commit 4f9a38f into main May 7, 2026
2 of 6 checks passed
@tamnd tamnd deleted the feat/v0.11.0-specialize-monitor branch May 15, 2026 11:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant