Skip to content
469 changes: 469 additions & 0 deletions Lib/test/test_free_threading/test_monitoring.py

Large diffs are not rendered by default.

163 changes: 143 additions & 20 deletions crates/compiler-core/src/bytecode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -823,43 +823,96 @@ impl CodeUnits {
unsafe { &*self.units.get() }.as_ptr()
}

/// `read_unit` from a base pointer previously obtained with `units_ptr`.
/// Read an instruction from a base pointer previously obtained with `units_ptr`.
///
/// # Safety
/// `base` must come from `units_ptr` on a live `CodeUnits`, and `index`
/// must be within that array (the dispatch loop only ever reads indices
/// the code object itself produced).
/// `base` must belong to a live `CodeUnits`, and `index` must identify an
/// in-bounds instruction start, never an inline cache. Construction fixup
/// must be complete. Runtime opcode replacements preserve the operand and
/// base family, so the two bytes need not be read as one atomic value.
/// This accessor does not validate malformed jump targets.
#[inline(always)]
#[must_use]
pub unsafe fn read_unit_from(base: *const CodeUnit, index: usize) -> CodeUnit {
let ptr = base.wrapping_add(index) as *const AtomicU16;
let [op, arg] = unsafe { &*ptr }.load(Ordering::Acquire).to_ne_bytes();
// SAFETY: only valid Instruction values are ever stored into the
// instruction array (see `read_op`).
let ptr = base.wrapping_add(index).cast::<AtomicU8>();
let op = unsafe { &*ptr }.load(Ordering::Acquire);
let arg = unsafe { &*ptr.add(1) }.load(Ordering::Relaxed);
// SAFETY: instruction starts contain valid opcodes; cache words are
// excluded by the caller. Match replace_op's byte-sized atomic access.
CodeUnit {
op: unsafe { mem::transmute::<u8, Instruction>(op) },
arg: OpArgByte::from(arg),
}
}

/// Atomically read the opcode and its arg byte at `index` as a single
/// Acquire-ordered 16-bit load.
///
/// Equivalent to `read_op` followed by `read_arg`, but the one access
/// keeps the eval loop from re-loading the instruction array pointer
/// across the acquire barrier, and it is what the dispatch loop uses.
/// Acquire pairs with `replace_op` (Release) exactly as `read_op` does.
///
/// `to_ne_bytes` yields the bytes in memory order, so `[0]` is the `op`
/// field and `[1]` the `arg` field of the `repr(C)` unit on either
/// endianness.
/// Read an instruction start using separate opcode and operand byte loads.
/// Runtime specialization preserves its operand and base family.
#[inline(always)]
#[must_use]
pub fn read_unit(&self, index: usize) -> CodeUnit {
// SAFETY: `index` is in bounds for every caller of this method.
// SAFETY: callers supply an in-bounds instruction start.
unsafe { Self::read_unit_from(self.units_ptr(), index) }
}

/// Walk bounded base instructions for structural analysis.
///
/// Like CPython's `_Py_GetBaseCodeUnit`, derive base opcodes from execution
/// units. Virtual monitoring never writes wrappers into these units, and
/// specialization preserves each base family, operand and cache width.
/// Read only instruction starts with byte-sized atomic accesses, skipping
/// all caches. Stop before a wrapper without owning metadata or an
/// incomplete extent. This is optional analysis, not constructor or jump
/// validation, and is not a decoder for physically instrumented code.
pub fn base_instructions(&self) -> impl Iterator<Item = (usize, CodeUnit)> + '_ {
let mut index = 0usize;
let mut stopped = false;
core::iter::from_fn(move || {
if stopped || index >= self.len() {
return None;
}
let op = self.read_op(index);
if matches!(
op,
Instruction::InstrumentedLine
| Instruction::InstrumentedInstruction
| Instruction::EnterExecutor
| Instruction::TraceRecord
| Instruction::Reserved
| Instruction::InterpreterExit
) {
stopped = true;
return None;
}
let base = op.deoptimize();
let Some(end) = index.checked_add(1 + base.cache_entries()) else {
stopped = true;
return None;
};
if end > self.len() {
stopped = true;
return None;
}
let unit = CodeUnit::new(base, self.read_arg(index));
let at = index;
index = end;
Some((at, unit))
})
}

/// Owned scalar snapshot for analyses indexed by code-unit offset.
/// Cache positions are newly generated zeroes, never live cache reads.
pub fn base_instruction_prefix(&self) -> Vec<CodeUnit> {
let mut result = Vec::new();
for (index, unit) in self.base_instructions() {
result.push(unit);
result.resize(
index + 1 + unit.op.cache_entries(),
CodeUnit::new(Instruction::Cache, 0.into()),
);
}
result
}

/// Atomically read the arg byte at `index` with Relaxed ordering.
pub fn read_arg(&self, index: usize) -> OpArgByte {
let units = unsafe { &*self.units.get() };
Expand Down Expand Up @@ -1474,6 +1527,76 @@ mod tests {
use super::*;
use alloc::{vec, vec::Vec};

#[test]
fn base_instruction_walk_skips_cache_payloads() {
// Scalar storage only: this fixture is never executed.
let code = CodeUnits::from([
CodeUnit::new(Opcode::LoadGlobal.as_instruction(), 7.into()),
CodeUnit::new(Instruction::Cache, 0.into()),
CodeUnit::new(Instruction::InstrumentedLine, 9.into()),
CodeUnit::new(Instruction::InstrumentedInstruction, 11.into()),
CodeUnit::new(Instruction::Cache, 0.into()),
CodeUnit::new(Instruction::ReturnValue, 0.into()),
]);
// SAFETY: a sequential same-family replacement at a known start.
unsafe { code.replace_op(0, Instruction::LoadGlobalModule) };
let starts = code.base_instructions().collect::<Vec<_>>();
assert_eq!(starts.len(), 2);
assert_eq!(starts[0].0, 0);
assert_eq!(starts[0].1.op.as_opcode(), Opcode::LoadGlobal);
assert_eq!(u8::from(starts[0].1.arg), 7);
assert_eq!(starts[1].0, 5);
assert_eq!(starts[1].1.op.as_opcode(), Opcode::ReturnValue);
let snapshot = code.base_instruction_prefix();
assert_eq!(snapshot.len(), 6);
for unit in &snapshot[1..5] {
assert_eq!(unit.op.as_opcode(), Opcode::Cache);
assert_eq!(u8::from(unit.arg), 0);
}

// Optional analysis stops before unavailable structure. Neither scalar
// fixture is executed or used to change constructor admission policy.
for tail in [
Instruction::InstrumentedLine,
Opcode::LoadGlobal.as_instruction(),
] {
let unavailable = CodeUnits::from([
CodeUnit::new(Instruction::Nop, 0.into()),
CodeUnit::new(tail, 0.into()),
]);
let starts = unavailable.base_instructions().collect::<Vec<_>>();
assert_eq!(starts.len(), 1);
assert_eq!(starts[0].0, 0);
assert_eq!(starts[0].1.op.as_opcode(), Opcode::Nop);
assert_eq!(unavailable.base_instruction_prefix().len(), 1);
}
}

#[test]
fn base_instruction_families_preserve_cache_width() {
for value in 0..=u8::MAX {
if let Ok(op) = Opcode::try_from(value) {
assert_eq!(op.cache_entries(), op.deoptimize().cache_entries());
}
}
}

#[test]
fn instruction_byte_reader_preserves_operand() {
let code = CodeUnits::from([
CodeUnit::new(Opcode::JumpBackward.as_instruction(), 13.into()),
CodeUnit::new(Instruction::Cache, 0.into()),
]);
// No bytecode execution or concurrent access: test scalar read results.
unsafe { code.replace_op(0, Instruction::JumpBackwardNoJit) };
let unit = code.read_unit(0);
assert_eq!(unit.op.as_opcode(), Opcode::JumpBackwardNoJit);
assert_eq!(u8::from(unit.arg), 13);
let base = code.base_instructions().next().unwrap();
assert_eq!(base.1.op.as_opcode(), Opcode::JumpBackward);
assert_eq!(u8::from(base.1.arg), 13);
}

#[test]
fn exception_table_encode_decode() {
let entries = vec![
Expand Down
57 changes: 18 additions & 39 deletions crates/vm/src/builtins/code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ mod const_cache;

use super::descriptor::{MemberKind, MemberLayout};
use super::{PyBytesRef, PyStrRef, PyTupleRef, PyType, set::PyFrozenSet};
use crate::common::lock::{OnceCell, PyMutex};
use crate::common::lock::OnceCell;
use crate::common::rc::PyRc;
#[cfg(feature = "host_env")]
use crate::convert::ToPyException;
use crate::{
Expand All @@ -22,7 +23,7 @@ use alloc::fmt;
use core::{
borrow::Borrow,
ops::Deref,
sync::atomic::{AtomicPtr, AtomicU64, Ordering},
sync::atomic::{AtomicPtr, Ordering},
};
use malachite_bigint::BigInt;
use num_traits::Zero;
Expand Down Expand Up @@ -420,18 +421,6 @@ impl<B: AsRef<[u8]>> IntoCodeObject for frozen::FrozenCodeObject<B> {
}
}

/// Per-code-object monitoring data (_PyCoMonitoringData).
/// Stores original opcodes displaced by INSTRUMENTED_LINE / INSTRUMENTED_INSTRUCTION.
pub struct CoMonitoringData {
/// Original opcodes at positions with INSTRUMENTED_LINE.
/// Indexed by instruction index. 0 = not instrumented for LINE.
pub line_opcodes: Vec<u8>,

/// Original opcodes at positions with INSTRUMENTED_INSTRUCTION.
/// Indexed by instruction index. 0 = not instrumented for INSTRUCTION.
pub per_instruction_opcodes: Vec<u8>,
}

/// `_PyCoCached`: objects created on first use.
#[derive(Default)]
struct CoCached {
Expand All @@ -441,6 +430,8 @@ struct CoCached {
freevars: OnceCell<PyTupleRef>,
}

pub(crate) struct MonitoringCodeIdentity;

#[pyclass(module = false, name = "code", traverse = "manual", clear = false)]
pub struct PyCode {
#[pymember(name = "co_argcount", path = "arg_count")]
Expand Down Expand Up @@ -469,11 +460,8 @@ pub struct PyCode {
source_path: AtomicPtr<PyStrInterned>,
#[pymember(name = "co_nlocals")]
nlocals: i32,
/// Version counter for lazy re-instrumentation.
/// Compared against `PyGlobalState::instrumentation_version` at RESUME.
pub instrumentation_version: AtomicU64,
/// Side-table for INSTRUMENTED_LINE / INSTRUMENTED_INSTRUCTION.
pub monitoring_data: PyMutex<Option<CoMonitoringData>>,
// Native lifetime witness, separate from Python-visible weak references.
monitoring_identity: OnceCell<PyRc<MonitoringCodeIdentity>>,
/// Whether adaptive counters have been initialized (lazy quickening).
pub quickened: core::sync::atomic::AtomicBool,
/// Whether the bytecode contains any instruction that mutates the current
Expand Down Expand Up @@ -605,6 +593,15 @@ impl MemberLayout for CodeFlags {
}

impl PyCode {
pub(crate) fn monitoring_identity(&self) -> &PyRc<MonitoringCodeIdentity> {
self.monitoring_identity
.get_or_init(|| PyRc::new(MonitoringCodeIdentity))
}

pub(crate) fn existing_monitoring_identity(&self) -> Option<&PyRc<MonitoringCodeIdentity>> {
self.monitoring_identity.get()
}

pub fn new(mut code: CodeObject, ctx: &Context) -> Self {
code.instructions.fixup_get_iter(code.flags);
let sp = code.source_path as *const PyStrInterned as *mut PyStrInterned;
Expand Down Expand Up @@ -668,8 +665,7 @@ impl PyCode {
localsplus_names,
source_path: AtomicPtr::new(sp),
nlocals,
instrumentation_version: AtomicU64::new(0),
monitoring_data: PyMutex::new(None),
monitoring_identity: OnceCell::new(),
quickened: core::sync::atomic::AtomicBool::new(false),
has_exc_handling,
}
Expand Down Expand Up @@ -751,24 +747,7 @@ impl PyCode {
/// `deopt_code`: the instructions with each opcode replaced by its base
/// opcode (`_Py_GetBaseCodeUnit`) and the inline caches zeroed.
fn deopt_code(&self) -> Vec<u8> {
let monitoring_data = self.monitoring_data.lock();
self.code.instructions.original_bytes_with(|i, op| {
let Some(data) = monitoring_data.as_ref() else {
return op;
};
let op = match op {
Instruction::InstrumentedLine => Instruction::try_from(data.line_opcodes[i])
.expect("INSTRUMENTED_LINE without original opcode"),
op => op,
};
match op {
Instruction::InstrumentedInstruction => {
Instruction::try_from(data.per_instruction_opcodes[i])
.expect("INSTRUMENTED_INSTRUCTION without original opcode")
}
op => op,
}
})
self.code.instructions.original_bytes()
}

pub fn source_path(&self) -> &'static PyStrInterned {
Expand Down
Loading
Loading