Skip to content

Commit 50c3299

Browse files
committed
WS3
codetracer-python-recorder/src/runtime/activation.rs: codetracer-python-recorder/src/runtime/tracer/events.rs: design-docs/balanced-call-stack-events-implementation-plan.status.md: Signed-off-by: Tzanko Matev <[email protected]>
1 parent e9829e7 commit 50c3299

File tree

3 files changed

+81
-24
lines changed

3 files changed

+81
-24
lines changed

codetracer-python-recorder/src/runtime/activation.rs

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ pub struct ActivationController {
1616
activation_code_id: Option<usize>,
1717
activation_done: bool,
1818
started: bool,
19+
suspended: bool,
20+
}
21+
22+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
23+
pub enum ActivationExitKind {
24+
Suspended,
25+
Completed,
1926
}
2027

2128
impl ActivationController {
@@ -28,6 +35,7 @@ impl ActivationController {
2835
activation_code_id: None,
2936
activation_done: false,
3037
started,
38+
suspended: false,
3139
}
3240
}
3341

@@ -38,6 +46,7 @@ impl ActivationController {
3846
/// Ensure activation state reflects the current event and report whether
3947
/// tracing should continue processing it.
4048
pub fn should_process_event(&mut self, py: Python<'_>, code: &CodeObjectWrapper) -> bool {
49+
self.resume_if_needed(code);
4150
self.ensure_started(py, code);
4251
self.is_active()
4352
}
@@ -73,15 +82,33 @@ impl ActivationController {
7382
}
7483
}
7584

76-
/// Handle return events and turn off tracing when the activation function
77-
/// exits. Returns `true` when tracing was deactivated by this call.
78-
pub fn handle_return_event(&mut self, code_id: usize) -> bool {
79-
if self.activation_code_id == Some(code_id) {
80-
self.started = false;
81-
self.activation_done = true;
82-
return true;
85+
/// Handle activation exits, marking suspension or completion as appropriate.
86+
/// Returns `true` when tracing was deactivated by this call.
87+
pub fn handle_exit(&mut self, code_id: usize, exit: ActivationExitKind) -> bool {
88+
if self.activation_code_id != Some(code_id) {
89+
return false;
90+
}
91+
match exit {
92+
ActivationExitKind::Suspended => {
93+
self.suspended = true;
94+
false
95+
}
96+
ActivationExitKind::Completed => {
97+
self.started = false;
98+
self.activation_done = true;
99+
self.suspended = false;
100+
true
101+
}
102+
}
103+
}
104+
105+
fn resume_if_needed(&mut self, code: &CodeObjectWrapper) {
106+
if self.started
107+
&& self.suspended
108+
&& self.activation_code_id == Some(code.id())
109+
{
110+
self.suspended = false;
83111
}
84-
false
85112
}
86113
}
87114

@@ -147,12 +174,28 @@ mod tests {
147174
let mut controller = ActivationController::new(Some(Path::new("/tmp/target.py")));
148175
assert!(controller.should_process_event(py, &code));
149176
assert!(controller.is_active());
150-
assert!(controller.handle_return_event(code.id()));
177+
assert!(controller.handle_exit(code.id(), ActivationExitKind::Completed));
151178
assert!(!controller.is_active());
152179
assert!(!controller.should_process_event(py, &code));
153180
});
154181
}
155182

183+
#[test]
184+
fn suspension_keeps_tracing_active() {
185+
Python::with_gil(|py| {
186+
let code = build_code(py, "target", "/tmp/target.py");
187+
let mut controller = ActivationController::new(Some(Path::new("/tmp/target.py")));
188+
assert!(controller.should_process_event(py, &code));
189+
assert!(controller.is_active());
190+
assert!(!controller.handle_exit(code.id(), ActivationExitKind::Suspended));
191+
// While suspended, subsequent events should keep tracing alive and clear suspension
192+
assert!(controller.should_process_event(py, &code));
193+
assert!(controller.is_active());
194+
assert!(controller.handle_exit(code.id(), ActivationExitKind::Completed));
195+
assert!(!controller.is_active());
196+
});
197+
}
198+
156199
#[test]
157200
fn start_path_prefers_activation_path() {
158201
let controller = ActivationController::new(Some(Path::new("/tmp/target.py")));
@@ -164,6 +207,6 @@ mod tests {
164207
impl ActivationController {
165208
#[allow(dead_code)]
166209
pub fn handle_return(&mut self, code_id: usize) -> bool {
167-
self.handle_return_event(code_id)
210+
self.handle_exit(code_id, ActivationExitKind::Completed)
168211
}
169212
}

codetracer-python-recorder/src/runtime/tracer/events.rs

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use crate::monitoring::{
99
events_union, CallbackOutcome, CallbackResult, EventSet, MonitoringEvents, Tracer,
1010
};
1111
use crate::policy::policy_snapshot;
12+
use crate::runtime::activation::ActivationExitKind;
1213
use crate::runtime::frame_inspector::capture_frame;
1314
use crate::runtime::io_capture::ScopedMuteIoCapture;
1415
use crate::runtime::line_snapshots::FrameId;
@@ -338,7 +339,15 @@ impl Tracer for RuntimeTracer {
338339
_offset: i32,
339340
retval: &Bound<'_, PyAny>,
340341
) -> CallbackResult {
341-
self.handle_return_edge(py, code, "on_py_return", retval, None, true, true)
342+
self.handle_return_edge(
343+
py,
344+
code,
345+
"on_py_return",
346+
retval,
347+
None,
348+
Some(ActivationExitKind::Completed),
349+
true,
350+
)
342351
}
343352

344353
fn on_py_yield(
@@ -354,7 +363,7 @@ impl Tracer for RuntimeTracer {
354363
"on_py_yield",
355364
retval,
356365
Some("<yield>"),
357-
false,
366+
Some(ActivationExitKind::Suspended),
358367
false,
359368
)
360369
}
@@ -422,7 +431,7 @@ impl Tracer for RuntimeTracer {
422431
"on_py_unwind",
423432
exception,
424433
Some("<unwind>"),
425-
true,
434+
Some(ActivationExitKind::Completed),
426435
false,
427436
)
428437
}
@@ -536,7 +545,7 @@ impl RuntimeTracer {
536545
label: &'static str,
537546
retval: &Bound<'_, PyAny>,
538547
capture_label: Option<&'static str>,
539-
deactivate_on_exit: bool,
548+
exit_kind: Option<ActivationExitKind>,
540549
allow_disable: bool,
541550
) -> CallbackResult {
542551
let is_active = self
@@ -587,14 +596,15 @@ impl RuntimeTracer {
587596
);
588597
self.mark_event();
589598

590-
if deactivate_on_exit
591-
&& self
599+
if let Some(kind) = exit_kind {
600+
if self
592601
.lifecycle
593602
.activation_mut()
594-
.handle_return_event(code.id())
595-
{
596-
let _mute = ScopedMuteIoCapture::new();
597-
log::debug!("[RuntimeTracer] deactivated on activation return");
603+
.handle_exit(code.id(), kind)
604+
{
605+
let _mute = ScopedMuteIoCapture::new();
606+
log::debug!("[RuntimeTracer] deactivated on activation return");
607+
}
598608
}
599609

600610
Ok(CallbackOutcome::Continue)

design-docs/balanced-call-stack-events-implementation-plan.status.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,16 @@
3131
- Verification: `just dev test` (maturin develop + cargo nextest + pytest) passes.
3232

3333
### WS3 – Activation & Lifecycle Behaviour
34-
- **Status:** Not started.
34+
- **Status:** _Completed_
35+
- `ActivationController` now tracks a suspended state and exposes `handle_exit(code_id, ActivationExitKind)`, so `PY_YIELD` transitions into suspension without disabling the activation while `PY_RETURN`/`PY_UNWIND` mark completion.
36+
- Resume events clear suspension via `should_process_event`, ensuring activation gating stays engaged until the generator/coroutine finishes.
37+
- Added Rust unit tests covering the suspension/resume flow, and the runtime now routes return-edge handling through the new enum to keep lifecycle state consistent.
38+
- Verification: `just dev test` passes end-to-end.
3539

3640
### WS4 – Testing & Validation
3741
- **Status:** Not started.
3842

3943
## Next Checkpoints
40-
1. Begin WS3 by teaching `ActivationController` about suspension/resume semantics.
41-
2. Plan and implement lifecycle tests ensuring activation gating stays consistent across yields/unwinds.
42-
3. Evaluate whether additional telemetry/logging is needed before landing WS3/WS4.
44+
1. Expand WS4 coverage per plan (async awaits, throw/resume, unwind) and update rust/python integration tests accordingly.
45+
2. Add rust-side assertions (e.g., `print_tracer`) to validate the expanded event mask.
46+
3. Document any telemetry updates or metadata changes before shipping the feature.

0 commit comments

Comments
 (0)