Skip to content

Commit 1498d7f

Browse files
committed
Implement opcode tracing and fix per-code instrumentation
- Add TraceEvent::Opcode for sys.settrace f_trace_opcodes support - Fire 'opcode' event per instruction when f_trace_opcodes is set, gated by trace_opcodes lock in _trace_event_inner - Add events_for_code() to compute per-code-object event mask (global events | code-local events) instead of broadcasting all local events to every code object, which caused stack corruption when INSTRUCTION was applied to unrelated running frames - Use events_for_code() in update_events_mask and Resume handlers - Remove test_stepinstr expectedFailure marker in test_bdb
1 parent 38719f7 commit 1498d7f

File tree

5 files changed

+57
-9
lines changed

5 files changed

+57
-9
lines changed

Lib/test/test_bdb.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -614,7 +614,6 @@ def test_step_next_on_last_statement(self):
614614
with TracerRun(self) as tracer:
615615
tracer.runcall(tfunc_main)
616616

617-
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: All paired tuples have not been processed, the last one was number 1 [('next',), ('quit',)]
618617
def test_stepinstr(self):
619618
self.expect_set = [
620619
('line', 2, 'tfunc_main'), ('stepinstr', ),

crates/vm/src/frame.rs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,21 @@ impl ExecutingFrame<'_> {
596596
self.state.prev_line = loc.line.get() as u32;
597597
}
598598

599+
// Fire 'opcode' trace event for sys.settrace when f_trace_opcodes
600+
// is set. Skip RESUME and ExtendedArg (matching CPython's exclusion
601+
// of these in _Py_call_instrumentation_instruction).
602+
if vm.use_tracing.get()
603+
&& !vm.is_none(&self.object.trace.lock())
604+
&& !matches!(
605+
op,
606+
Instruction::Resume { .. }
607+
| Instruction::InstrumentedResume
608+
| Instruction::ExtendedArg
609+
)
610+
{
611+
vm.trace_event(crate::protocol::TraceEvent::Opcode, None)?;
612+
}
613+
599614
let result = self.execute_instruction(op, arg, &mut do_extend_arg, vm);
600615
match result {
601616
Ok(None) => {}
@@ -2192,7 +2207,10 @@ impl ExecutingFrame<'_> {
21922207
.instrumentation_version
21932208
.load(atomic::Ordering::Acquire);
21942209
if code_ver != global_ver {
2195-
let events = vm.state.monitoring_events.load();
2210+
let events = {
2211+
let state = vm.state.monitoring.lock();
2212+
state.events_for_code(self.code.get_id())
2213+
};
21962214
monitoring::instrument_code(self.code, events);
21972215
self.code
21982216
.instrumentation_version
@@ -2569,7 +2587,10 @@ impl ExecutingFrame<'_> {
25692587
.instrumentation_version
25702588
.load(atomic::Ordering::Acquire);
25712589
if code_ver != global_ver {
2572-
let events = vm.state.monitoring_events.load();
2590+
let events = {
2591+
let state = vm.state.monitoring.lock();
2592+
state.events_for_code(self.code.get_id())
2593+
};
25732594
monitoring::instrument_code(self.code, events);
25742595
self.code
25752596
.instrumentation_version

crates/vm/src/protocol/callable.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ pub(crate) enum TraceEvent {
7676
Return,
7777
Exception,
7878
Line,
79+
Opcode,
7980
CCall,
8081
CReturn,
8182
CException,
@@ -91,6 +92,11 @@ impl TraceEvent {
9192
Self::Call | Self::Return | Self::CCall | Self::CReturn | Self::CException
9293
)
9394
}
95+
96+
/// Whether this event is dispatched only when f_trace_opcodes is set.
97+
pub(crate) fn is_opcode_event(&self) -> bool {
98+
matches!(self, Self::Opcode)
99+
}
94100
}
95101

96102
impl core::fmt::Display for TraceEvent {
@@ -101,6 +107,7 @@ impl core::fmt::Display for TraceEvent {
101107
Return => write!(f, "return"),
102108
Exception => write!(f, "exception"),
103109
Line => write!(f, "line"),
110+
Opcode => write!(f, "opcode"),
104111
CCall => write!(f, "c_call"),
105112
CReturn => write!(f, "c_return"),
106113
CException => write!(f, "c_exception"),
@@ -142,11 +149,17 @@ impl VirtualMachine {
142149
}
143150

144151
let is_profile_event = event.is_profile_event();
152+
let is_opcode_event = event.is_opcode_event();
145153

146154
let Some(frame_ref) = self.current_frame() else {
147155
return Ok(None);
148156
};
149157

158+
// Opcode events are only dispatched when f_trace_opcodes is set.
159+
if is_opcode_event && !*frame_ref.trace_opcodes.lock() {
160+
return Ok(None);
161+
}
162+
150163
let frame: PyObjectRef = frame_ref.into();
151164
let event = self.ctx.new_str(event.to_string()).into();
152165
let args = vec![frame, event, arg.unwrap_or_else(|| self.ctx.none())];

crates/vm/src/stdlib/sys/monitoring.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,20 @@ impl MonitoringState {
119119
let local = self.local_events.values().fold(0, |acc, &e| acc | e);
120120
global | local
121121
}
122+
123+
/// Compute the events that apply to a specific code object:
124+
/// global events OR'd with local events registered for that code.
125+
/// This prevents events like INSTRUCTION that are local to one code
126+
/// from being applied to unrelated code objects.
127+
pub fn events_for_code(&self, code_id: usize) -> u32 {
128+
let global = self.global_events.iter().fold(0, |acc, &e| acc | e);
129+
let local = self
130+
.local_events
131+
.iter()
132+
.filter(|((_, cid), _)| *cid == code_id)
133+
.fold(0, |acc, (_, &e)| acc | e);
134+
global | local
135+
}
122136
}
123137

124138
/// Global atomic mask: OR of all tools' events. Checked in the hot path
@@ -464,13 +478,17 @@ fn update_events_mask(vm: &VirtualMachine, state: &MonitoringState) {
464478
+ 1;
465479
// Eagerly re-instrument all frames on the current thread's stack so that
466480
// code objects already past their RESUME pick up the new event set.
481+
// Each code object gets only the events that apply to it (global + its
482+
// own local events), preventing e.g. INSTRUCTION from being applied to
483+
// unrelated code objects.
467484
for fp in vm.frames.borrow().iter() {
468485
// SAFETY: frames in the Vec are alive while their FrameRef is on the call stack.
469486
let frame = unsafe { fp.as_ref() };
470487
let code = &frame.code;
471488
let code_ver = code.instrumentation_version.load(Ordering::Acquire);
472489
if code_ver != new_ver {
473-
instrument_code(code, events);
490+
let code_events = state.events_for_code(code.get_id());
491+
instrument_code(code, code_events);
474492
code.instrumentation_version
475493
.store(new_ver, Ordering::Release);
476494
}

crates/vm/src/vm/mod.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1164,15 +1164,12 @@ impl VirtualMachine {
11641164
// PY_UNWIND fires PyTrace_RETURN with arg=None — so we fire for
11651165
// both Ok and Err, matching `call_trace_protected` behavior.
11661166
if self.use_tracing.get()
1167-
&& (!self.is_none(&frame.trace.lock())
1168-
|| !self.is_none(&self.profile_func.borrow()))
1167+
&& (!self.is_none(&frame.trace.lock()) || !self.is_none(&self.profile_func.borrow()))
11691168
{
11701169
let ret_result = self.trace_event(TraceEvent::Return, None);
11711170
// call_trace_protected: if trace function raises, its error
11721171
// replaces the original exception.
1173-
if let Err(e) = ret_result {
1174-
return Err(e);
1175-
}
1172+
ret_result?;
11761173
}
11771174

11781175
result

0 commit comments

Comments
 (0)