impl sys.monitoring

This commit is contained in:
Jeong, YunWon committed 2026-02-27 09:06:55 +09:00
1 parent f35d0fde0b
commit 739f92e872
8 files changed
+1219 -31

No files matched your search

-2
View File
@@ -1012,8 +1012,6 @@ if 1:
code_lines = self.get_code_lines(genexp_code)
self.assertEqual(genexp_lines, code_lines)
# TODO: RUSTPYTHON; implicit return line number after async for
@unittest.expectedFailure
def test_line_number_implicit_return_after_async_for(self):
async def test(aseq):
+23 -2
View File
@@ -2865,6 +2865,9 @@ impl Compiler {
// Normal path jumps here to skip exception path blocks
let end_block = self.new_block();
// Emit NOP at the try: line so LINE events fire for it
emit!(self, Instruction::Nop);
// Setup a finally block if we have a finally statement.
// Push fblock with handler info for exception table generation
// IMPORTANT: handler goes to finally_except_block (exception path), not finally_block
@@ -3018,8 +3021,10 @@ impl Compiler {
type_,
name,
body,
range: handler_range,
..
}) = &handler;
self.set_source_range(*handler_range);
let next_handler = self.new_block();
// If we gave a typ,
@@ -3313,6 +3318,9 @@ impl Compiler {
};
let exit_block = self.new_block();
// Emit NOP at the try: line so LINE events fire for it
emit!(self, Instruction::Nop);
// Push fblock with handler info for exception table generation
if !finalbody.is_empty() {
emit!(
@@ -3743,6 +3751,9 @@ impl Compiler {
is_async: bool,
funcflags: bytecode::MakeFunctionFlags,
) -> CompileResult<()> {
// Save source range so MAKE_FUNCTION gets the `def` line, not the body's last line
let saved_range = self.current_source_range;
// Always enter function scope
self.enter_function(name, parameters)?;
self.current_code_info()
@@ -3796,6 +3807,9 @@ impl Compiler {
let code = self.exit_scope();
self.ctx = prev_ctx;
// Restore source range so MAKE_FUNCTION is attributed to the `def` line
self.set_source_range(saved_range);
// Create function object with closure
self.make_closure(code, funcflags)?;
@@ -5169,15 +5183,22 @@ impl Compiler {
if is_async {
emit!(self, Instruction::EndAsyncFor);
} else {
// END_FOR + POP_ITER pattern (CPython 3.14)
// FOR_ITER jumps to END_FOR, but VM skips it (+1) to reach POP_ITER
// END_FOR + POP_ITER are on the `for` line, not the body's last line
let saved_range = self.current_source_range;
self.set_source_range(iter.range());
emit!(self, Instruction::EndFor);
emit!(self, Instruction::PopIter);
self.set_source_range(saved_range);
}
self.compile_statements(orelse)?;
self.switch_to_block(after_block);
// Restore source range to the `for` line so any implicit return
// (LOAD_CONST None, RETURN_VALUE) is attributed to the `for` line,
// not the loop body's last line.
self.set_source_range(iter.range());
self.leave_conditional_block();
Ok(())
}
+42 -8
View File
@@ -250,15 +250,33 @@ impl CodeInfo {
let mut locations = Vec::new();
let mut linetable_locations: Vec<LineTableLocation> = Vec::new();
// Convert pseudo ops and remove resulting NOPs
// Convert pseudo ops and remove resulting NOPs (keep line-marker NOPs)
convert_pseudo_ops(&mut blocks, varname_cache.len() as u32);
for block in blocks
.iter_mut()
.filter(|b| b.next != BlockIdx::NULL || !b.instructions.is_empty())
{
block
// Collect lines that have non-NOP instructions in this block
let non_nop_lines: std::collections::HashSet<_> = block
.instructions
.retain(|ins| !matches!(ins.instr.real(), Some(Instruction::Nop)));
.iter()
.filter(|ins| !matches!(ins.instr.real(), Some(Instruction::Nop)))
.map(|ins| ins.location.line)
.collect();
let mut kept_nop_lines: std::collections::HashSet<OneIndexed> =
std::collections::HashSet::new();
block.instructions.retain(|ins| {
if matches!(ins.instr.real(), Some(Instruction::Nop)) {
let line = ins.location.line;
// Remove if another instruction covers this line,
// or if we already kept a NOP for this line
if non_nop_lines.contains(&line) || kept_nop_lines.contains(&line) {
return false;
}
kept_nop_lines.insert(line);
}
true
});
}
let mut block_to_offset = vec![Label(0); blocks.len()];
@@ -501,9 +519,14 @@ impl CodeInfo {
let tuple_const = ConstantData::Tuple { elements };
let (const_idx, _) = self.metadata.consts.insert_full(tuple_const);
// Replace preceding LOAD instructions with NOP
// Replace preceding LOAD instructions with NOP, using the
// BUILD_TUPLE location so remove_nops() treats them as
// same-line and removes them (multi-line tuple literals
// would otherwise leave line-introducing NOPs behind).
let folded_loc = block.instructions[i].location;
for j in start_idx..i {
block.instructions[j].instr = Instruction::Nop.into();
block.instructions[j].location = folded_loc;
}
// Replace BUILD_TUPLE with LOAD_CONST
@@ -673,12 +696,23 @@ impl CodeInfo {
}
}
/// Remove NOP instructions from all blocks
/// Remove NOP instructions from all blocks, but keep NOPs that introduce
/// a new source line (they serve as line markers for monitoring LINE events).
fn remove_nops(&mut self) {
for block in &mut self.blocks {
block
.instructions
.retain(|ins| !matches!(ins.instr.real(), Some(Instruction::Nop)));
let mut prev_line = None;
block.instructions.retain(|ins| {
if matches!(ins.instr.real(), Some(Instruction::Nop)) {
let line = ins.location.line;
if prev_line == Some(line) {
// Same line as previous instruction — safe to remove
return false;
}
// This NOP introduces a new line — keep it
}
prev_line = Some(ins.location.line);
true
});
}
}
+271 -19
View File
@@ -369,6 +369,7 @@ impl Py<Frame> {
lasti: &self.lasti,
object: self,
state: &mut state,
monitoring_mask: 0,
};
f(exec)
}
@@ -414,6 +415,7 @@ impl Py<Frame> {
lasti: &self.lasti,
object: self,
state: &mut state,
monitoring_mask: 0,
};
exec.yield_from_target().map(PyObject::to_owned)
}
@@ -448,6 +450,8 @@ struct ExecutingFrame<'a> {
object: &'a Py<Frame>,
lasti: &'a Lasti,
state: &'a mut FrameState,
/// Cached monitoring events mask. Reloaded at Resume instruction only,
monitoring_mask: u32,
}
impl fmt::Debug for ExecutingFrame<'_> {
@@ -498,6 +502,7 @@ impl ExecutingFrame<'_> {
let instructions = &self.code.instructions;
let mut arg_state = bytecode::OpArgState::default();
let mut prev_line: u32 = 0;
self.monitoring_mask = vm.state.monitoring_events.load();
loop {
let idx = self.lasti() as usize;
// Fire 'line' trace event when line number changes.
@@ -515,6 +520,25 @@ impl ExecutingFrame<'_> {
let bytecode::CodeUnit { op, arg } = instructions[idx];
let arg = arg_state.extend(arg);
let mut do_extend_arg = false;
// Always track current line for LINE monitoring, even when
// monitoring is off. This prevents spurious LINE events when
// monitoring is enabled mid-function.
if !matches!(op, Instruction::Resume { .. } | Instruction::ExtendedArg) {
let line = self.code.locations[idx].0.line.get() as u32;
if self.monitoring_mask != 0 {
use crate::stdlib::sys::monitoring;
let offset = idx as u32 * 2;
if self.monitoring_mask & monitoring::EVENT_LINE != 0 && line != prev_line {
monitoring::fire_line(vm, self.code, offset, line)?;
}
if self.monitoring_mask & monitoring::EVENT_INSTRUCTION != 0 {
monitoring::fire_instruction(vm, self.code, offset)?;
}
}
prev_line = line;
}
let result = self.execute_instruction(op, arg, &mut do_extend_arg, vm);
match result {
Ok(None) => {}
@@ -601,10 +625,57 @@ impl ExecutingFrame<'_> {
)
);
// Fire RAISE or RERAISE monitoring event.
// fire_reraise internally deduplicates: only the first
// re-raise after each EXCEPTION_HANDLED fires the event.
// If the callback raises (e.g. ValueError for illegal DISABLE),
// replace the original exception.
let exception = {
use crate::stdlib::sys::monitoring;
if is_reraise {
if self.monitoring_mask & monitoring::EVENT_RERAISE != 0 {
let offset = idx as u32 * 2;
let exc_obj: PyObjectRef = exception.clone().into();
match monitoring::fire_reraise(vm, self.code, offset, &exc_obj) {
Ok(()) => exception,
Err(monitor_exc) => monitor_exc,
}
} else {
exception
}
} else if self.monitoring_mask & monitoring::EVENT_RAISE != 0 {
let offset = idx as u32 * 2;
let exc_obj: PyObjectRef = exception.clone().into();
match monitoring::fire_raise(vm, self.code, offset, &exc_obj) {
Ok(()) => exception,
Err(monitor_exc) => monitor_exc,
}
} else {
exception
}
};
match handle_exception(self, exception, idx, is_reraise, is_new_raise, vm) {
Ok(None) => {}
Ok(Some(result)) => break Ok(result),
Err(exception) => {
// Fire PY_UNWIND: exception escapes this frame
let exception = if self.monitoring_mask
& crate::stdlib::sys::monitoring::EVENT_PY_UNWIND
!= 0
{
let offset = idx as u32 * 2;
let exc_obj: PyObjectRef = exception.clone().into();
match crate::stdlib::sys::monitoring::fire_py_unwind(
vm, self.code, offset, &exc_obj,
) {
Ok(()) => exception,
Err(monitor_exc) => monitor_exc,
}
} else {
exception
};
// Restore lasti from traceback so frame.f_lineno matches tb_lineno
// The traceback was created with the correct lasti when exception
// was first raised, but frame.lasti may have changed during cleanup
@@ -671,6 +742,7 @@ impl ExecutingFrame<'_> {
exc_val: PyObjectRef,
exc_tb: PyObjectRef,
) -> PyResult<ExecutionResult> {
self.monitoring_mask = vm.state.monitoring_events.load();
if let Some(jen) = self.yield_from_target() {
// Check if the exception is GeneratorExit (type or instance).
// For GeneratorExit, close the sub-iterator instead of throwing.
@@ -781,6 +853,33 @@ impl ExecutingFrame<'_> {
exception.set_traceback_typed(Some(new_traceback.into_ref(&vm.ctx)));
}
// Fire PY_THROW and RAISE events before raising the exception in the
// generator. do_monitor_exc in CPython replaces the active exception
// when a callback fails, so we mirror that here.
let exception = {
use crate::stdlib::sys::monitoring;
let exception = if self.monitoring_mask & monitoring::EVENT_PY_THROW != 0 {
let offset = idx as u32 * 2;
let exc_obj: PyObjectRef = exception.clone().into();
match monitoring::fire_py_throw(vm, self.code, offset, &exc_obj) {
Ok(()) => exception,
Err(monitor_exc) => monitor_exc,
}
} else {
exception
};
if self.monitoring_mask & monitoring::EVENT_RAISE != 0 {
let offset = idx as u32 * 2;
let exc_obj: PyObjectRef = exception.clone().into();
match monitoring::fire_raise(vm, self.code, offset, &exc_obj) {
Ok(()) => exception,
Err(monitor_exc) => monitor_exc,
}
} else {
exception
}
};
// when raising an exception, set __context__ to the current exception
// This is done in _PyErr_SetObject
vm.contextualize_exception(&exception);
@@ -792,7 +891,26 @@ impl ExecutingFrame<'_> {
match self.unwind_blocks(vm, UnwindReason::Raising { exception }) {
Ok(None) => self.run(vm),
Ok(Some(result)) => Ok(result),
Err(exception) => Err(exception),
Err(exception) => {
// Fire PY_UNWIND: exception escapes the generator frame.
// do_monitor_exc replaces the exception on callback failure.
let exception = if self.monitoring_mask
& crate::stdlib::sys::monitoring::EVENT_PY_UNWIND
!= 0
{
let offset = idx as u32 * 2;
let exc_obj: PyObjectRef = exception.clone().into();
match crate::stdlib::sys::monitoring::fire_py_unwind(
vm, self.code, offset, &exc_obj,
) {
Ok(()) => exception,
Err(monitor_exc) => monitor_exc,
}
} else {
exception
};
Err(exception)
}
}
}
@@ -1371,7 +1489,18 @@ impl ExecutingFrame<'_> {
Ok(None)
}
Instruction::JumpBackward { target } => {
self.jump(target.get(arg));
let src_offset = (self.lasti() - 1) * 2;
let dest = target.get(arg);
self.jump(dest);
// JUMP events fire only for backward jumps (loop iterations)
if self.monitoring_mask & crate::stdlib::sys::monitoring::EVENT_JUMP != 0 {
crate::stdlib::sys::monitoring::fire_jump(
vm,
self.code,
src_offset,
dest.0 * 2,
)?;
}
Ok(None)
}
Instruction::JumpBackwardNoInterrupt { target } => {
@@ -1976,16 +2105,24 @@ impl ExecutingFrame<'_> {
Instruction::PopJumpIfTrue { target } => self.pop_jump_if(vm, target.get(arg), true),
Instruction::PopJumpIfNone { target } => {
let value = self.pop_value();
if vm.is_none(&value) {
self.jump(target.get(arg));
let branch_taken = vm.is_none(&value);
let target = target.get(arg);
let src_offset = (self.lasti() - 1) * 2;
if branch_taken {
self.jump(target);
}
self.fire_branch_event(vm, src_offset, branch_taken, target)?;
Ok(None)
}
Instruction::PopJumpIfNotNone { target } => {
let value = self.pop_value();
if !vm.is_none(&value) {
self.jump(target.get(arg));
let branch_taken = !vm.is_none(&value);
let target = target.get(arg);
let src_offset = (self.lasti() - 1) * 2;
if branch_taken {
self.jump(target);
}
self.fire_branch_event(vm, src_offset, branch_taken, target)?;
Ok(None)
}
Instruction::PopTop => {
@@ -2010,19 +2147,25 @@ impl ExecutingFrame<'_> {
}
Instruction::RaiseVarargs { kind } => self.execute_raise(vm, kind.get(arg)),
Instruction::Resume { arg: resume_arg } => {
// Resume execution after yield, await, or at function start
// In CPython, this checks instrumentation and eval breaker
// For now, we just check for signals/interrupts
let _resume_type = resume_arg.get(arg);
// Check for interrupts if not resuming from yield_from
// if resume_type < bytecode::ResumeType::AfterYieldFrom as u32 {
// vm.check_signals()?;
// }
use crate::stdlib::sys::monitoring;
let resume_type = resume_arg.get(arg);
self.monitoring_mask = vm.state.monitoring_events.load();
let offset = (self.lasti() - 1) * 2;
if resume_type == 0 {
if self.monitoring_mask & monitoring::EVENT_PY_START != 0 {
monitoring::fire_py_start(vm, self.code, offset)?;
}
} else if self.monitoring_mask & monitoring::EVENT_PY_RESUME != 0 {
monitoring::fire_py_resume(vm, self.code, offset)?;
}
Ok(None)
}
Instruction::ReturnValue => {
let value = self.pop_value();
if self.monitoring_mask & crate::stdlib::sys::monitoring::EVENT_PY_RETURN != 0 {
let offset = (self.lasti() - 1) * 2;
crate::stdlib::sys::monitoring::fire_py_return(vm, self.code, offset, &value)?;
}
self.unwind_blocks(vm, UnwindReason::Returning { value })
}
Instruction::SetAdd { i } => {
@@ -2061,8 +2204,10 @@ impl ExecutingFrame<'_> {
vm.set_exception(Some(exc_ref.to_owned()));
}
// Complete stack operations
self.push_value(prev_exc);
self.push_value(exc);
Ok(None)
}
Instruction::CheckExcMatch => {
@@ -2243,6 +2388,10 @@ impl ExecutingFrame<'_> {
}
Instruction::YieldValue { arg: oparg } => {
let value = self.pop_value();
if self.monitoring_mask & crate::stdlib::sys::monitoring::EVENT_PY_YIELD != 0 {
let offset = (self.lasti() - 1) * 2;
crate::stdlib::sys::monitoring::fire_py_yield(vm, self.code, offset, &value)?;
}
// arg=0: direct yield (wrapped for async generators)
// arg=1: yield from await/yield-from (NOT wrapped)
let wrap = oparg.get(arg) == 0;
@@ -2571,6 +2720,23 @@ impl ExecutingFrame<'_> {
if let Some(entry) =
bytecode::find_exception_handler(&self.code.exceptiontable, offset)
{
// Fire EXCEPTION_HANDLED before setting up handler.
// If the callback raises, the handler is NOT set up and the
// new exception propagates instead.
if self.monitoring_mask
& crate::stdlib::sys::monitoring::EVENT_EXCEPTION_HANDLED
!= 0
{
let byte_offset = offset * 2;
let exc_obj: PyObjectRef = exception.clone().into();
crate::stdlib::sys::monitoring::fire_exception_handled(
vm,
self.code,
byte_offset,
&exc_obj,
)?;
}
// 1. Pop stack to entry.depth
while self.state.stack.len() > entry.depth as usize {
self.state.stack.pop();
@@ -2774,6 +2940,8 @@ impl ExecutingFrame<'_> {
#[inline]
fn execute_call(&mut self, args: FuncArgs, vm: &VirtualMachine) -> FrameResult {
use crate::stdlib::sys::monitoring;
// Stack: [callable, self_or_null, ...]
let self_or_null = self.pop_value_opt(); // Option<PyObjectRef>
let callable = self.pop_value();
@@ -2792,9 +2960,50 @@ impl ExecutingFrame<'_> {
args
};
let value = callable.call(final_args, vm)?;
self.push_value(value);
Ok(None)
let is_python_call = callable.downcast_ref::<PyFunction>().is_some();
// Compute arg0 once for CALL, C_RETURN, and C_RAISE events
let call_arg0 = if self.monitoring_mask & monitoring::EVENT_CALL != 0 {
let arg0 = final_args
.args
.first()
.cloned()
.unwrap_or_else(|| monitoring::get_missing(vm));
let offset = (self.lasti() - 1) * 2;
monitoring::fire_call(vm, self.code, offset, &callable, arg0.clone())?;
Some(arg0)
} else {
None
};
match callable.call(final_args, vm) {
Ok(value) => {
if let Some(arg0) = call_arg0
&& !is_python_call
{
let offset = (self.lasti() - 1) * 2;
monitoring::fire_c_return(vm, self.code, offset, &callable, arg0)?;
}
self.push_value(value);
Ok(None)
}
Err(exc) => {
// call_instrumentation_vector_protected replaces the active
// exception when the callback itself raises.
let exc = if let Some(arg0) = call_arg0
&& !is_python_call
{
let offset = (self.lasti() - 1) * 2;
match monitoring::fire_c_raise(vm, self.code, offset, &callable, arg0) {
Ok(()) => exc,
Err(monitor_exc) => monitor_exc,
}
} else {
exc
};
Err(exc)
}
}
}
fn execute_raise(&mut self, vm: &VirtualMachine, kind: bytecode::RaiseKind) -> FrameResult {
@@ -2923,6 +3132,43 @@ impl ExecutingFrame<'_> {
self.update_lasti(|i| *i = target_pc);
}
#[inline]
fn fire_branch_event(
&self,
vm: &VirtualMachine,
src_offset: u32,
branch_taken: bool,
target: bytecode::Label,
) -> PyResult<()> {
if self.monitoring_mask
& (crate::stdlib::sys::monitoring::EVENT_BRANCH_LEFT
| crate::stdlib::sys::monitoring::EVENT_BRANCH_RIGHT)
!= 0
{
let dest_offset = if branch_taken {
target.0 * 2
} else {
self.lasti() * 2
};
if branch_taken {
crate::stdlib::sys::monitoring::fire_branch_right(
vm,
self.code,
src_offset,
dest_offset,
)?;
} else {
crate::stdlib::sys::monitoring::fire_branch_left(
vm,
self.code,
src_offset,
dest_offset,
)?;
}
}
Ok(())
}
#[inline]
fn pop_jump_if(
&mut self,
@@ -2932,9 +3178,12 @@ impl ExecutingFrame<'_> {
) -> FrameResult {
let obj = self.pop_value();
let value = obj.try_to_bool(vm)?;
if value == flag {
let branch_taken = value == flag;
let src_offset = (self.lasti() - 1) * 2;
if branch_taken {
self.jump(target);
}
self.fire_branch_event(vm, src_offset, branch_taken, target)?;
Ok(None)
}
@@ -2942,11 +3191,13 @@ impl ExecutingFrame<'_> {
fn execute_for_iter(&mut self, vm: &VirtualMachine, target: bytecode::Label) -> FrameResult {
let top_of_stack = PyIter::new(self.top_value());
let next_obj = top_of_stack.next(vm);
let src_offset = (self.lasti() - 1) * 2;
// Check the next object:
match next_obj {
Ok(PyIterReturn::Return(value)) => {
self.push_value(value);
self.fire_branch_event(vm, src_offset, false, target)?;
Ok(None)
}
Ok(PyIterReturn::StopIteration(_)) => {
@@ -2970,6 +3221,7 @@ impl ExecutingFrame<'_> {
target
};
self.jump(jump_target);
self.fire_branch_event(vm, src_offset, true, jump_target)?;
Ok(None)
}
Err(next_error) => {
+5
View File
@@ -1,3 +1,5 @@
pub(crate) mod monitoring;
use crate::{Py, PyPayload, PyResult, VirtualMachine, builtins::PyModule, convert::ToPyObject};
#[cfg(all(not(feature = "host_env"), feature = "stdio"))]
@@ -73,6 +75,9 @@ mod sys {
RUST_MULTIARCH.replace("-unknown", "")
}
#[pymodule(name = "monitoring", with(super::monitoring::sys_monitoring))]
pub(super) mod monitoring {}
#[pyclass(no_attr, name = "_BootstrapStderr")]
#[derive(Debug, PyPayload)]
pub(super) struct BootstrapStderr;
+872
View File
@@ -0,0 +1,872 @@
use crate::{
AsObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
builtins::{PyCode, PyDictRef, PyNamespace, PyUtf8StrRef},
function::FuncArgs,
};
use crossbeam_utils::atomic::AtomicCell;
use std::collections::{HashMap, HashSet};
pub const TOOL_LIMIT: usize = 6;
const EVENTS_COUNT: usize = 19;
const LOCAL_EVENTS_COUNT: usize = 11;
const UNGROUPED_EVENTS_COUNT: usize = 18;
// Event bit positions
bitflags::bitflags! {
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct MonitoringEvents: u32 {
const PY_START = 1 << 0;
const PY_RESUME = 1 << 1;
const PY_RETURN = 1 << 2;
const PY_YIELD = 1 << 3;
const CALL = 1 << 4;
const LINE = 1 << 5;
const INSTRUCTION = 1 << 6;
const JUMP = 1 << 7;
const BRANCH_LEFT = 1 << 8;
const BRANCH_RIGHT = 1 << 9;
const STOP_ITERATION = 1 << 10;
const RAISE = 1 << 11;
const EXCEPTION_HANDLED = 1 << 12;
const PY_UNWIND = 1 << 13;
const PY_THROW = 1 << 14;
const RERAISE = 1 << 15;
const C_RETURN = 1 << 16;
const C_RAISE = 1 << 17;
const BRANCH = 1 << 18;
}
}
// Re-export as plain u32 constants for use in frame.rs hot-path checks
pub const EVENT_PY_START: u32 = MonitoringEvents::PY_START.bits();
pub const EVENT_PY_RESUME: u32 = MonitoringEvents::PY_RESUME.bits();
pub const EVENT_PY_RETURN: u32 = MonitoringEvents::PY_RETURN.bits();
pub const EVENT_PY_YIELD: u32 = MonitoringEvents::PY_YIELD.bits();
pub const EVENT_CALL: u32 = MonitoringEvents::CALL.bits();
pub const EVENT_LINE: u32 = MonitoringEvents::LINE.bits();
pub const EVENT_INSTRUCTION: u32 = MonitoringEvents::INSTRUCTION.bits();
pub const EVENT_JUMP: u32 = MonitoringEvents::JUMP.bits();
pub const EVENT_BRANCH_LEFT: u32 = MonitoringEvents::BRANCH_LEFT.bits();
pub const EVENT_BRANCH_RIGHT: u32 = MonitoringEvents::BRANCH_RIGHT.bits();
pub const EVENT_RAISE: u32 = MonitoringEvents::RAISE.bits();
pub const EVENT_EXCEPTION_HANDLED: u32 = MonitoringEvents::EXCEPTION_HANDLED.bits();
pub const EVENT_PY_UNWIND: u32 = MonitoringEvents::PY_UNWIND.bits();
pub const EVENT_C_RETURN: u32 = MonitoringEvents::C_RETURN.bits();
const EVENT_C_RAISE: u32 = MonitoringEvents::C_RAISE.bits();
const EVENT_STOP_ITERATION: u32 = MonitoringEvents::STOP_ITERATION.bits();
pub const EVENT_PY_THROW: u32 = MonitoringEvents::PY_THROW.bits();
const EVENT_BRANCH: u32 = MonitoringEvents::BRANCH.bits();
pub const EVENT_RERAISE: u32 = MonitoringEvents::RERAISE.bits();
const EVENT_C_RETURN_MASK: u32 = EVENT_C_RETURN | EVENT_C_RAISE;
const EVENT_NAMES: [&str; EVENTS_COUNT] = [
"PY_START",
"PY_RESUME",
"PY_RETURN",
"PY_YIELD",
"CALL",
"LINE",
"INSTRUCTION",
"JUMP",
"BRANCH_LEFT",
"BRANCH_RIGHT",
"STOP_ITERATION",
"RAISE",
"EXCEPTION_HANDLED",
"PY_UNWIND",
"PY_THROW",
"RERAISE",
"C_RETURN",
"C_RAISE",
"BRANCH",
];
/// Interpreter-level monitoring state, shared by all threads.
pub struct MonitoringState {
pub tool_names: [Option<String>; TOOL_LIMIT],
pub global_events: [u32; TOOL_LIMIT],
pub local_events: HashMap<(usize, usize), u32>,
pub callbacks: HashMap<(usize, usize), PyObjectRef>,
/// Per-instruction disabled tools: (code_id, offset, tool)
pub disabled: HashSet<(usize, usize, usize)>,
/// Cached MISSING sentinel singleton
pub missing: Option<PyObjectRef>,
/// Cached DISABLE sentinel singleton
pub disable: Option<PyObjectRef>,
}
impl Default for MonitoringState {
fn default() -> Self {
Self {
tool_names: Default::default(),
global_events: [0; TOOL_LIMIT],
local_events: HashMap::new(),
callbacks: HashMap::new(),
disabled: HashSet::new(),
missing: None,
disable: None,
}
}
}
impl MonitoringState {
/// Compute the OR of all tools' global_events + local_events.
/// This is used for the fast-path atomic mask to skip monitoring
/// when no events are registered at all.
pub fn combined_events(&self) -> u32 {
let global = self.global_events.iter().fold(0, |acc, &e| acc | e);
let local = self.local_events.values().fold(0, |acc, &e| acc | e);
global | local
}
}
/// Global atomic mask: OR of all tools' events. Checked in the hot path
/// to skip monitoring overhead when no events are registered.
/// Lives in PyGlobalState alongside the PyMutex<MonitoringState>.
pub type MonitoringEventsMask = AtomicCell<u32>;
/// Get the MISSING sentinel, creating it if necessary.
pub fn get_missing(vm: &VirtualMachine) -> PyObjectRef {
let mut state = vm.state.monitoring.lock();
if let Some(ref m) = state.missing {
m.clone()
} else {
let m: PyObjectRef = sys_monitoring::MonitoringSentinel.into_ref(&vm.ctx).into();
state.missing = Some(m.clone());
m
}
}
/// Get the DISABLE sentinel, creating it if necessary.
pub fn get_disable(vm: &VirtualMachine) -> PyObjectRef {
let mut state = vm.state.monitoring.lock();
if let Some(ref d) = state.disable {
d.clone()
} else {
let d: PyObjectRef = sys_monitoring::MonitoringSentinel.into_ref(&vm.ctx).into();
state.disable = Some(d.clone());
d
}
}
fn check_valid_tool(tool_id: i32, vm: &VirtualMachine) -> PyResult<usize> {
if !(0..TOOL_LIMIT as i32).contains(&tool_id) {
return Err(vm.new_value_error(format!("invalid tool {tool_id} (must be between 0 and 5)")));
}
Ok(tool_id as usize)
}
fn check_tool_in_use(tool: usize, vm: &VirtualMachine) -> PyResult<()> {
let state = vm.state.monitoring.lock();
if state.tool_names[tool].is_some() {
Ok(())
} else {
Err(vm.new_value_error(format!("tool {tool} is not in use")))
}
}
fn parse_single_event(event: i32, vm: &VirtualMachine) -> PyResult<usize> {
let event = u32::try_from(event)
.map_err(|_| vm.new_value_error("The callback can only be set for one event at a time"))?;
if event.count_ones() != 1 {
return Err(vm.new_value_error("The callback can only be set for one event at a time"));
}
let event_id = event.trailing_zeros() as usize;
if event_id >= EVENTS_COUNT {
return Err(vm.new_value_error(format!("invalid event {event}")));
}
Ok(event_id)
}
fn normalize_event_set(event_set: i32, local: bool, vm: &VirtualMachine) -> PyResult<u32> {
if event_set < 0 {
let kind = if local {
"local event set"
} else {
"event set"
};
return Err(vm.new_value_error(format!("invalid {kind} 0x{event_set:x}")));
}
let mut event_set = event_set as u32;
if event_set >= (1 << EVENTS_COUNT) {
let kind = if local {
"local event set"
} else {
"event set"
};
return Err(vm.new_value_error(format!("invalid {kind} 0x{event_set:x}")));
}
if (event_set & EVENT_C_RETURN_MASK) != 0 && (event_set & EVENT_CALL) != EVENT_CALL {
return Err(vm.new_value_error("cannot set C_RETURN or C_RAISE events independently"));
}
event_set &= !EVENT_C_RETURN_MASK;
if (event_set & EVENT_BRANCH) != 0 {
event_set &= !EVENT_BRANCH;
event_set |= EVENT_BRANCH_LEFT | EVENT_BRANCH_RIGHT;
}
if local && event_set >= (1 << LOCAL_EVENTS_COUNT) {
return Err(vm.new_value_error(format!("invalid local event set 0x{event_set:x}")));
}
Ok(event_set)
}
/// Update the global monitoring_events atomic mask from current state.
fn update_events_mask(vm: &VirtualMachine, state: &MonitoringState) {
vm.state.monitoring_events.store(state.combined_events());
}
fn use_tool_id(tool_id: i32, name: &str, vm: &VirtualMachine) -> PyResult<()> {
let tool = check_valid_tool(tool_id, vm)?;
let mut state = vm.state.monitoring.lock();
if state.tool_names[tool].is_some() {
return Err(vm.new_value_error(format!("tool {tool_id} is already in use")));
}
state.tool_names[tool] = Some(name.to_owned());
Ok(())
}
fn clear_tool_id(tool_id: i32, vm: &VirtualMachine) -> PyResult<()> {
let tool = check_valid_tool(tool_id, vm)?;
let mut state = vm.state.monitoring.lock();
if state.tool_names[tool].is_some() {
state.global_events[tool] = 0;
state
.local_events
.retain(|(local_tool, _), _| *local_tool != tool);
state.callbacks.retain(|(cb_tool, _), _| *cb_tool != tool);
state.disabled.retain(|&(_, _, t)| t != tool);
}
update_events_mask(vm, &state);
Ok(())
}
fn free_tool_id(tool_id: i32, vm: &VirtualMachine) -> PyResult<()> {
let tool = check_valid_tool(tool_id, vm)?;
let mut state = vm.state.monitoring.lock();
if state.tool_names[tool].is_some() {
state.global_events[tool] = 0;
state
.local_events
.retain(|(local_tool, _), _| *local_tool != tool);
state.callbacks.retain(|(cb_tool, _), _| *cb_tool != tool);
state.disabled.retain(|&(_, _, t)| t != tool);
state.tool_names[tool] = None;
}
update_events_mask(vm, &state);
Ok(())
}
fn get_tool(tool_id: i32, vm: &VirtualMachine) -> PyResult<Option<String>> {
let tool = check_valid_tool(tool_id, vm)?;
let state = vm.state.monitoring.lock();
Ok(state.tool_names[tool].clone())
}
fn register_callback(
tool_id: i32,
event: i32,
func: PyObjectRef,
vm: &VirtualMachine,
) -> PyResult<PyObjectRef> {
let tool = check_valid_tool(tool_id, vm)?;
let event_id = parse_single_event(event, vm)?;
let mut state = vm.state.monitoring.lock();
let prev = state
.callbacks
.remove(&(tool, event_id))
.unwrap_or_else(|| vm.ctx.none());
let branch_id = EVENT_BRANCH.trailing_zeros() as usize;
let branch_left_id = EVENT_BRANCH_LEFT.trailing_zeros() as usize;
let branch_right_id = EVENT_BRANCH_RIGHT.trailing_zeros() as usize;
if !vm.is_none(&func) {
state.callbacks.insert((tool, event_id), func.clone());
// BRANCH is a composite event: also register for BRANCH_LEFT/RIGHT
if event_id == branch_id {
state.callbacks.insert((tool, branch_left_id), func.clone());
state.callbacks.insert((tool, branch_right_id), func);
}
} else {
// Also clear BRANCH_LEFT/RIGHT when clearing BRANCH
if event_id == branch_id {
state.callbacks.remove(&(tool, branch_left_id));
state.callbacks.remove(&(tool, branch_right_id));
}
}
Ok(prev)
}
fn get_events(tool_id: i32, vm: &VirtualMachine) -> PyResult<u32> {
let tool = check_valid_tool(tool_id, vm)?;
let state = vm.state.monitoring.lock();
Ok(state.global_events[tool])
}
fn set_events(tool_id: i32, event_set: i32, vm: &VirtualMachine) -> PyResult<()> {
let tool = check_valid_tool(tool_id, vm)?;
check_tool_in_use(tool, vm)?;
let normalized = normalize_event_set(event_set, false, vm)?;
let mut state = vm.state.monitoring.lock();
state.global_events[tool] = normalized;
update_events_mask(vm, &state);
Ok(())
}
fn get_local_events(tool_id: i32, code: PyObjectRef, vm: &VirtualMachine) -> PyResult<u32> {
if code.downcast_ref::<PyCode>().is_none() {
return Err(vm.new_type_error("code must be a code object"));
}
let tool = check_valid_tool(tool_id, vm)?;
let code_id = code.get_id();
let state = vm.state.monitoring.lock();
Ok(state
.local_events
.get(&(tool, code_id))
.copied()
.unwrap_or(0))
}
fn set_local_events(
tool_id: i32,
code: PyObjectRef,
event_set: i32,
vm: &VirtualMachine,
) -> PyResult<()> {
if code.downcast_ref::<PyCode>().is_none() {
return Err(vm.new_type_error("code must be a code object"));
}
let tool = check_valid_tool(tool_id, vm)?;
check_tool_in_use(tool, vm)?;
let normalized = normalize_event_set(event_set, true, vm)?;
let code_id = code.get_id();
let mut state = vm.state.monitoring.lock();
if normalized == 0 {
state.local_events.remove(&(tool, code_id));
} else {
state.local_events.insert((tool, code_id), normalized);
}
update_events_mask(vm, &state);
Ok(())
}
fn restart_events(vm: &VirtualMachine) {
let mut state = vm.state.monitoring.lock();
state.disabled.clear();
}
fn all_events(vm: &VirtualMachine) -> PyResult<PyDictRef> {
// Collect data under the lock, then release before calling into Python VM.
let masks: Vec<(&str, u8)> = {
let state = vm.state.monitoring.lock();
EVENT_NAMES
.iter()
.take(UNGROUPED_EVENTS_COUNT)
.enumerate()
.filter_map(|(event_id, event_name)| {
let event_bit = 1u32 << event_id;
let mut tools_mask = 0u8;
for tool in 0..TOOL_LIMIT {
if (state.global_events[tool] & event_bit) != 0 {
tools_mask |= 1 << tool;
}
}
if tools_mask != 0 {
Some((*event_name, tools_mask))
} else {
None
}
})
.collect()
};
let all_events = vm.ctx.new_dict();
for (name, mask) in masks {
all_events.set_item(name, vm.ctx.new_int(mask).into(), vm)?;
}
Ok(all_events)
}
// Event dispatch
use core::cell::Cell;
thread_local! {
/// Re-entrancy guard: prevents monitoring callbacks from triggering
/// additional monitoring events (which would cause infinite recursion).
static FIRING: Cell<bool> = const { Cell::new(false) };
/// Tracks whether a RERAISE event has been fired since the last
/// EXCEPTION_HANDLED. Used to suppress duplicate RERAISE from
/// cleanup handlers that chain through multiple exception table entries.
static RERAISE_PENDING: Cell<bool> = const { Cell::new(false) };
}
/// Fire an event for all tools that have the event bit set.
/// `cb_extra` contains the callback arguments after the code object.
fn fire(
vm: &VirtualMachine,
event: u32,
code: &PyRef<PyCode>,
offset: u32,
cb_extra: &[PyObjectRef],
) -> PyResult<()> {
// Prevent recursive event firing
if FIRING.with(|f| f.get()) {
return Ok(());
}
let event_id = event.trailing_zeros() as usize;
let code_id = code.get_id();
// C_RETURN and C_RAISE are implicitly enabled when CALL is set.
let check_bit = if event & EVENT_C_RETURN_MASK != 0 {
event | EVENT_CALL
} else {
event
};
// Collect callbacks and snapshot the DISABLE sentinel under a single lock.
let (callbacks, disable_sentinel): (Vec<(usize, PyObjectRef)>, Option<PyObjectRef>) = {
let state = vm.state.monitoring.lock();
let mut cbs = Vec::new();
for tool in 0..TOOL_LIMIT {
let global = state.global_events[tool];
let local = state
.local_events
.get(&(tool, code_id))
.copied()
.unwrap_or(0);
if ((global | local) & check_bit) == 0 {
continue;
}
if state.disabled.contains(&(code_id, offset as usize, tool)) {
continue;
}
if let Some(cb) = state.callbacks.get(&(tool, event_id)) {
cbs.push((tool, cb.clone()));
}
}
(cbs, state.disable.clone())
};
if callbacks.is_empty() {
return Ok(());
}
let mut args_vec = Vec::with_capacity(1 + cb_extra.len());
args_vec.push(code.clone().into());
args_vec.extend_from_slice(cb_extra);
let args = FuncArgs::from(args_vec);
FIRING.with(|f| f.set(true));
let result = (|| {
for (tool, cb) in callbacks {
let result = cb.call(args.clone(), vm)?;
if disable_sentinel.as_ref().is_some_and(|d| result.is(d)) {
// Only local events (event_id < LOCAL_EVENTS_COUNT) can be disabled.
// Non-local events (RAISE, EXCEPTION_HANDLED, PY_UNWIND, etc.)
// cannot be disabled per code object.
if event_id >= LOCAL_EVENTS_COUNT {
return Err(vm.new_value_error(format!(
"cannot disable {} events",
EVENT_NAMES[event_id]
)));
}
let mut state = vm.state.monitoring.lock();
state.disabled.insert((code_id, offset as usize, tool));
}
}
Ok(())
})();
FIRING.with(|f| f.set(false));
result
}
// Public dispatch functions (called from frame.rs)
pub fn fire_py_start(vm: &VirtualMachine, code: &PyRef<PyCode>, offset: u32) -> PyResult<()> {
fire(
vm,
EVENT_PY_START,
code,
offset,
&[vm.ctx.new_int(offset).into()],
)
}
pub fn fire_py_resume(vm: &VirtualMachine, code: &PyRef<PyCode>, offset: u32) -> PyResult<()> {
fire(
vm,
EVENT_PY_RESUME,
code,
offset,
&[vm.ctx.new_int(offset).into()],
)
}
pub fn fire_py_return(
vm: &VirtualMachine,
code: &PyRef<PyCode>,
offset: u32,
retval: &PyObjectRef,
) -> PyResult<()> {
fire(
vm,
EVENT_PY_RETURN,
code,
offset,
&[vm.ctx.new_int(offset).into(), retval.clone()],
)
}
pub fn fire_py_yield(
vm: &VirtualMachine,
code: &PyRef<PyCode>,
offset: u32,
retval: &PyObjectRef,
) -> PyResult<()> {
fire(
vm,
EVENT_PY_YIELD,
code,
offset,
&[vm.ctx.new_int(offset).into(), retval.clone()],
)
}
pub fn fire_call(
vm: &VirtualMachine,
code: &PyRef<PyCode>,
offset: u32,
callable: &PyObjectRef,
arg0: PyObjectRef,
) -> PyResult<()> {
fire(
vm,
EVENT_CALL,
code,
offset,
&[vm.ctx.new_int(offset).into(), callable.clone(), arg0],
)
}
pub fn fire_c_return(
vm: &VirtualMachine,
code: &PyRef<PyCode>,
offset: u32,
callable: &PyObjectRef,
arg0: PyObjectRef,
) -> PyResult<()> {
fire(
vm,
EVENT_C_RETURN,
code,
offset,
&[vm.ctx.new_int(offset).into(), callable.clone(), arg0],
)
}
pub fn fire_c_raise(
vm: &VirtualMachine,
code: &PyRef<PyCode>,
offset: u32,
callable: &PyObjectRef,
arg0: PyObjectRef,
) -> PyResult<()> {
fire(
vm,
EVENT_C_RAISE,
code,
offset,
&[vm.ctx.new_int(offset).into(), callable.clone(), arg0],
)
}
pub fn fire_line(
vm: &VirtualMachine,
code: &PyRef<PyCode>,
offset: u32,
line: u32,
) -> PyResult<()> {
fire(vm, EVENT_LINE, code, offset, &[vm.ctx.new_int(line).into()])
}
pub fn fire_instruction(vm: &VirtualMachine, code: &PyRef<PyCode>, offset: u32) -> PyResult<()> {
fire(
vm,
EVENT_INSTRUCTION,
code,
offset,
&[vm.ctx.new_int(offset).into()],
)
}
pub fn fire_raise(
vm: &VirtualMachine,
code: &PyRef<PyCode>,
offset: u32,
exception: &PyObjectRef,
) -> PyResult<()> {
fire(
vm,
EVENT_RAISE,
code,
offset,
&[vm.ctx.new_int(offset).into(), exception.clone()],
)
}
/// Only fires if no RERAISE has been fired since the last EXCEPTION_HANDLED,
/// preventing duplicate events from chained cleanup handlers.
pub fn fire_reraise(
vm: &VirtualMachine,
code: &PyRef<PyCode>,
offset: u32,
exception: &PyObjectRef,
) -> PyResult<()> {
if RERAISE_PENDING.with(|f| f.get()) {
return Ok(());
}
RERAISE_PENDING.with(|f| f.set(true));
fire(
vm,
EVENT_RERAISE,
code,
offset,
&[vm.ctx.new_int(offset).into(), exception.clone()],
)
}
pub fn fire_exception_handled(
vm: &VirtualMachine,
code: &PyRef<PyCode>,
offset: u32,
exception: &PyObjectRef,
) -> PyResult<()> {
RERAISE_PENDING.with(|f| f.set(false));
fire(
vm,
EVENT_EXCEPTION_HANDLED,
code,
offset,
&[vm.ctx.new_int(offset).into(), exception.clone()],
)
}
pub fn fire_py_unwind(
vm: &VirtualMachine,
code: &PyRef<PyCode>,
offset: u32,
exception: &PyObjectRef,
) -> PyResult<()> {
RERAISE_PENDING.with(|f| f.set(false));
fire(
vm,
EVENT_PY_UNWIND,
code,
offset,
&[vm.ctx.new_int(offset).into(), exception.clone()],
)
}
pub fn fire_py_throw(
vm: &VirtualMachine,
code: &PyRef<PyCode>,
offset: u32,
exception: &PyObjectRef,
) -> PyResult<()> {
fire(
vm,
EVENT_PY_THROW,
code,
offset,
&[vm.ctx.new_int(offset).into(), exception.clone()],
)
}
#[allow(dead_code)]
pub fn fire_stop_iteration(
vm: &VirtualMachine,
code: &PyRef<PyCode>,
offset: u32,
exception: &PyObjectRef,
) -> PyResult<()> {
fire(
vm,
EVENT_STOP_ITERATION,
code,
offset,
&[vm.ctx.new_int(offset).into(), exception.clone()],
)
}
pub fn fire_jump(
vm: &VirtualMachine,
code: &PyRef<PyCode>,
offset: u32,
destination: u32,
) -> PyResult<()> {
fire(
vm,
EVENT_JUMP,
code,
offset,
&[
vm.ctx.new_int(offset).into(),
vm.ctx.new_int(destination).into(),
],
)
}
pub fn fire_branch_left(
vm: &VirtualMachine,
code: &PyRef<PyCode>,
offset: u32,
destination: u32,
) -> PyResult<()> {
fire(
vm,
EVENT_BRANCH_LEFT,
code,
offset,
&[
vm.ctx.new_int(offset).into(),
vm.ctx.new_int(destination).into(),
],
)
}
pub fn fire_branch_right(
vm: &VirtualMachine,
code: &PyRef<PyCode>,
offset: u32,
destination: u32,
) -> PyResult<()> {
fire(
vm,
EVENT_BRANCH_RIGHT,
code,
offset,
&[
vm.ctx.new_int(offset).into(),
vm.ctx.new_int(destination).into(),
],
)
}
#[pymodule(sub)]
pub(super) mod sys_monitoring {
use super::*;
#[pyclass(no_attr, module = "sys.monitoring", name = "_Sentinel")]
#[derive(Debug, PyPayload)]
pub(super) struct MonitoringSentinel;
#[pyclass]
impl MonitoringSentinel {}
#[pyattr(name = "DEBUGGER_ID")]
const DEBUGGER_ID: u8 = 0;
#[pyattr(name = "COVERAGE_ID")]
const COVERAGE_ID: u8 = 1;
#[pyattr(name = "PROFILER_ID")]
const PROFILER_ID: u8 = 2;
#[pyattr(name = "OPTIMIZER_ID")]
const OPTIMIZER_ID: u8 = 5;
#[pyattr(once, name = "DISABLE")]
fn disable(vm: &VirtualMachine) -> PyObjectRef {
super::get_disable(vm)
}
#[pyattr(once, name = "MISSING")]
fn missing(vm: &VirtualMachine) -> PyObjectRef {
super::get_missing(vm)
}
#[pyattr(once)]
fn events(vm: &VirtualMachine) -> PyRef<PyNamespace> {
let events = PyNamespace::default().into_ref(&vm.ctx);
for (event_id, event_name) in EVENT_NAMES.iter().enumerate() {
events
.as_object()
.set_attr(*event_name, vm.ctx.new_int(1u32 << event_id), vm)
.expect("setting sys.monitoring.events attribute should not fail");
}
events
.as_object()
.set_attr("NO_EVENTS", vm.ctx.new_int(0), vm)
.expect("setting sys.monitoring.events.NO_EVENTS should not fail");
events
}
#[pyfunction]
fn use_tool_id(tool_id: i32, name: PyUtf8StrRef, vm: &VirtualMachine) -> PyResult<()> {
super::use_tool_id(tool_id, name.as_str(), vm)
}
#[pyfunction]
fn clear_tool_id(tool_id: i32, vm: &VirtualMachine) -> PyResult<()> {
super::clear_tool_id(tool_id, vm)
}
#[pyfunction]
fn free_tool_id(tool_id: i32, vm: &VirtualMachine) -> PyResult<()> {
super::free_tool_id(tool_id, vm)
}
#[pyfunction]
fn get_tool(tool_id: i32, vm: &VirtualMachine) -> PyResult<Option<String>> {
super::get_tool(tool_id, vm)
}
#[pyfunction]
fn register_callback(
tool_id: i32,
event: i32,
func: PyObjectRef,
vm: &VirtualMachine,
) -> PyResult<PyObjectRef> {
super::register_callback(tool_id, event, func, vm)
}
#[pyfunction]
fn get_events(tool_id: i32, vm: &VirtualMachine) -> PyResult<u32> {
super::get_events(tool_id, vm)
}
#[pyfunction]
fn set_events(tool_id: i32, event_set: i32, vm: &VirtualMachine) -> PyResult<()> {
super::set_events(tool_id, event_set, vm)
}
#[pyfunction]
fn get_local_events(tool_id: i32, code: PyObjectRef, vm: &VirtualMachine) -> PyResult<u32> {
super::get_local_events(tool_id, code, vm)
}
#[pyfunction]
fn set_local_events(
tool_id: i32,
code: PyObjectRef,
event_set: i32,
vm: &VirtualMachine,
) -> PyResult<()> {
super::set_local_events(tool_id, code, event_set, vm)
}
#[pyfunction]
fn restart_events(vm: &VirtualMachine) {
super::restart_events(vm)
}
#[pyfunction]
fn _all_events(vm: &VirtualMachine) -> PyResult<PyDictRef> {
super::all_events(vm)
}
}
+2
View File
@@ -124,6 +124,8 @@ where
thread_handles: parking_lot::Mutex::new(Vec::new()),
#[cfg(feature = "threading")]
shutdown_handles: parking_lot::Mutex::new(Vec::new()),
monitoring: PyMutex::default(),
monitoring_events: AtomicCell::new(0),
});
// Create VM with the global state
+4
View File
@@ -155,6 +155,10 @@ pub struct PyGlobalState {
/// Registry for non-daemon threads that need to be joined at shutdown
#[cfg(feature = "threading")]
pub shutdown_handles: parking_lot::Mutex<Vec<stdlib::thread::ShutdownEntry>>,
/// sys.monitoring state (tool names, events, callbacks)
pub monitoring: PyMutex<stdlib::sys::monitoring::MonitoringState>,
/// Fast-path mask: OR of all tools' events. 0 means no monitoring overhead.
pub monitoring_events: stdlib::sys::monitoring::MonitoringEventsMask,
}
pub fn process_hash_secret_seed() -> u32 {