diff --git a/.agents/tasks/2025/08/18-1208-tracer-trait b/.agents/tasks/2025/08/18-1208-tracer-trait new file mode 100644 index 0000000..c1717fa --- /dev/null +++ b/.agents/tasks/2025/08/18-1208-tracer-trait @@ -0,0 +1,80 @@ +1. Create a trait for a tracer implemented using the Python monitoring API. The methods of the trait should correspond to the events that one can subscribe to via the API. + +Here's a sketch of the design of the trait. We want to support tracers which implement only some of the methods. + +``rs +use bitflags::bitflags; + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct EventMask: u32 { + const CALL = 1 << 0; + //CODEX: write this + ... + } +} + +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Event { + Call, + //CODEX: write this + ... + // Non-exhaustive for forward compatibility. +} + +pub trait Tracer: Send { + /// Tell the dispatcher what you want to receive. Default: nothing (fast path). + fn interest(&self) -> EventMask { EventMask::empty() } + + // Default no-ops let implementers pick only what they need. + fn on_call(&mut self, ...) {} + //CODEX: write this +} + +// Example tracer: only cares about CALL. +struct CallsOnly; +impl Tracer for CallsOnly { + fn interest(&self) -> EventMask { EventMask::CALL | ... } + fn on_call(&mut self, ...) { println!("call: {:?}", ...); } + ... +} + +// Dispatcher checks the mask to avoid vtable calls it doesn’t need. +pub struct Dispatcher { + tracer: Box, + mask: EventMask, +} + +impl Dispatcher { + pub fn new(tracer: Box) -> Self { + let mask = tracer.interest(); + Self { tracer, mask } + } + + pub fn dispatch_call(&mut self, ...) { + if self.mask.contains(EventMask::CALL) { + self.tracer.on_call(...); + } + + } + // CODEX: ... same for other events +} +`` + +2. Create code which takes a trait implementation and hooks it to the global tracing. Follow the design-docs for the specific API that needs to be implemented. + +3. Create a test implementation of the trait which prints text to stdout. Run the test implementation. Note that the test implementation should not be included in the final build +artefact, it will be used only for testing. + +4. Update the testing framework to be able to use `just test` also for Rust tests. Specifically we want to run our test implementation from point 2. using `just test` + +Refer to the design-docs folder for the current planned design. Add/update files in the folder to match what was implemented in this task. + + +--- FOLLOW UP TASK --- +1. In codetracer-python-recorder/Cargo.toml, move pyo3’s extension-module feature to an optional crate feature and enable it only for release builds. +2. Update Justfile (and CI scripts) to run cargo test --no-default-features so the test binary links with the Python C library. +3. Add pyo3 with auto-initialize under [dev-dependencies] if tests require the interpreter to be initialized automatically. +--- FOLLOW UP TASK --- +thread 'tracer_prints_on_call' panicked at tests/print_tracer.rs:21:51:\ncalled `Result::unwrap()` on an `Err` value: PyErr { type: , value: AttributeError("module 'sys' has no attribute 'monitoring'"), traceback: None }\nnote: run with `RUST_BACKTRACE=1` environment variable to display a backtrace\n\n\nfailures:\n tracer_prints_on_call\n\ntest result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8ca839c..8fdf573 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,8 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.10","3.11","3.12","3.13"] + #python-version: ["3.10","3.11","3.12","3.13"] + python-version: ["3.12","3.13"] steps: - uses: actions/checkout@v4 - uses: cachix/install-nix-action@v27 diff --git a/Justfile b/Justfile index ce4a73b..2e67f20 100644 --- a/Justfile +++ b/Justfile @@ -32,9 +32,15 @@ dev: uv run --directory codetracer-python-recorder maturin develop --uv # Run unit tests of dev build -test: - uv run --group dev --group test pytest +test: cargo-test py-test + +# Run Rust unit tests without default features to link Python C library +cargo-test: + uv run cargo test --manifest-path codetracer-python-recorder/Cargo.toml --no-default-features +py-test: + uv run --group dev --group test pytest + # Run tests only on the pure recorder test-pure: uv run --group dev --group test pytest codetracer-pure-python-recorder diff --git a/codetracer-python-recorder/Cargo.lock b/codetracer-python-recorder/Cargo.lock index d688a23..e0c1fa0 100644 --- a/codetracer-python-recorder/Cargo.lock +++ b/codetracer-python-recorder/Cargo.lock @@ -8,11 +8,96 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "capnp" +version = "0.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "def25bdbbc2758b363d79129c7f277520e3347e8b647c404d4823591f837c4ad" +dependencies = [ + "embedded-io", +] + +[[package]] +name = "capnpc" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93a18ec8176d4a87f1852b6a560b4196729365c01ba3cad03b73a376a23c56e" +dependencies = [ + "capnp", +] + +[[package]] +name = "cbor4ii" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "189a2a2e5eec2f203b2bb8bc4c2db55c7253770d2c6bf3ae5f79ace5a15c305f" +dependencies = [ + "serde", +] + +[[package]] +name = "cc" +version = "1.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2352e5597e9c544d5e6d9c95190d5d27738ade584fa8db0a16e130e5c2b5296e" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + [[package]] name = "codetracer-python-recorder" version = "0.1.0" dependencies = [ + "bitflags", "pyo3", + "runtime_tracing", +] + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "fscommon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "315ce685aca5ddcc5a3e7e436ef47d4a5d0064462849b6f0f628c28140103531" +dependencies = [ + "log", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi", ] [[package]] @@ -27,12 +112,40 @@ version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jobserver" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +dependencies = [ + "getrandom", + "libc", +] + [[package]] name = "libc" version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + [[package]] name = "memoffset" version = "0.9.1" @@ -42,12 +155,38 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + [[package]] name = "portable-atomic" version = "1.11.1" @@ -134,6 +273,86 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "runtime_tracing" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb39bbb7e2fe3f83c9020a2f871e7affd293e1ef5cc2f1c137012d9931611db6" +dependencies = [ + "base64", + "capnp", + "capnpc", + "cbor4ii", + "fscommon", + "num-derive", + "num-traits", + "serde", + "serde_json", + "serde_repr", + "zeekstd", +] + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.142" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "syn" version = "2.0.104" @@ -162,3 +381,49 @@ name = "unindent" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "zeekstd" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78be0afb4741f4d364cbc6a3151b93d4564e48c2fea7ec244e938f13465f847e" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.15+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/codetracer-python-recorder/Cargo.toml b/codetracer-python-recorder/Cargo.toml index 7150965..b25b86c 100644 --- a/codetracer-python-recorder/Cargo.toml +++ b/codetracer-python-recorder/Cargo.toml @@ -8,7 +8,16 @@ repository = "https://github.com/metacraft-labs/codetracer-python-recorder" [lib] name = "codetracer_python_recorder" -crate-type = ["cdylib"] +crate-type = ["cdylib", "rlib"] + +[features] +extension-module = ["pyo3/extension-module"] +default = ["extension-module"] [dependencies] -pyo3 = { version = "0.25.1", features = ["extension-module"] } +pyo3 = { version = "0.25.1" } +runtime_tracing = "0.14.0" +bitflags = "2.4" + +[dev-dependencies] +pyo3 = { version = "0.25.1", features = ["auto-initialize"] } diff --git a/codetracer-python-recorder/src/lib.rs b/codetracer-python-recorder/src/lib.rs index 92782b5..31d6b52 100644 --- a/codetracer-python-recorder/src/lib.rs +++ b/codetracer-python-recorder/src/lib.rs @@ -3,6 +3,9 @@ use std::sync::atomic::{AtomicBool, Ordering}; use pyo3::exceptions::PyRuntimeError; use pyo3::prelude::*; +pub mod tracer; +pub use crate::tracer::{install_tracer, uninstall_tracer, EventSet, Tracer}; + /// Global flag tracking whether tracing is active. static ACTIVE: AtomicBool = AtomicBool::new(false); diff --git a/codetracer-python-recorder/src/tracer.rs b/codetracer-python-recorder/src/tracer.rs new file mode 100644 index 0000000..2842906 --- /dev/null +++ b/codetracer-python-recorder/src/tracer.rs @@ -0,0 +1,244 @@ +use std::sync::{Mutex, OnceLock}; +use pyo3::{ + exceptions::PyRuntimeError, + prelude::*, + types::{PyAny, PyCFunction, PyModule}, +}; + +const MONITORING_TOOL_NAME: &str = "codetracer"; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(transparent)] +pub struct EventId(pub i32); + +#[allow(non_snake_case)] +#[derive(Clone, Copy, Debug)] +pub struct MonitoringEvents { + pub BRANCH: EventId, + pub CALL: EventId, + pub C_RAISE: EventId, + pub C_RETURN: EventId, + pub EXCEPTION_HANDLED: EventId, + pub INSTRUCTION: EventId, + pub JUMP: EventId, + pub LINE: EventId, + pub PY_RESUME: EventId, + pub PY_RETURN: EventId, + pub PY_START: EventId, + pub PY_THROW: EventId, + pub PY_UNWIND: EventId, + pub PY_YIELD: EventId, + pub RAISE: EventId, + pub RERAISE: EventId, + pub STOP_ITERATION: EventId, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ToolId { + pub id: u8, +} + +pub type CallbackFn<'py> = Bound<'py, PyCFunction>; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct EventSet(pub i32); + +pub const NO_EVENTS: EventSet = EventSet(0); + +impl EventSet { + pub const fn empty() -> Self { + NO_EVENTS + } + pub fn contains(&self, ev: &EventId) -> bool { + (self.0 & ev.0) != 0 + } +} + +pub fn acquire_tool_id(py: Python<'_>) -> PyResult { + let monitoring = py.import("sys")?.getattr("monitoring")?; + const FALLBACK_ID: u8 = 5; + monitoring.call_method1("use_tool_id", (FALLBACK_ID, MONITORING_TOOL_NAME))?; + Ok(ToolId { id: FALLBACK_ID }) +} + +pub fn load_monitoring_events(py: Python<'_>) -> PyResult { + let monitoring = py.import("sys")?.getattr("monitoring")?; + let events = monitoring.getattr("events")?; + Ok(MonitoringEvents { + BRANCH: EventId(events.getattr("BRANCH")?.extract()?), + CALL: EventId(events.getattr("CALL")?.extract()?), + C_RAISE: EventId(events.getattr("C_RAISE")?.extract()?), + C_RETURN: EventId(events.getattr("C_RETURN")?.extract()?), + EXCEPTION_HANDLED: EventId(events.getattr("EXCEPTION_HANDLED")?.extract()?), + INSTRUCTION: EventId(events.getattr("INSTRUCTION")?.extract()?), + JUMP: EventId(events.getattr("JUMP")?.extract()?), + LINE: EventId(events.getattr("LINE")?.extract()?), + PY_RESUME: EventId(events.getattr("PY_RESUME")?.extract()?), + PY_RETURN: EventId(events.getattr("PY_RETURN")?.extract()?), + PY_START: EventId(events.getattr("PY_START")?.extract()?), + PY_THROW: EventId(events.getattr("PY_THROW")?.extract()?), + PY_UNWIND: EventId(events.getattr("PY_UNWIND")?.extract()?), + PY_YIELD: EventId(events.getattr("PY_YIELD")?.extract()?), + RAISE: EventId(events.getattr("RAISE")?.extract()?), + RERAISE: EventId(events.getattr("RERAISE")?.extract()?), + STOP_ITERATION: EventId(events.getattr("STOP_ITERATION")?.extract()?), + }) +} + +static MONITORING_EVENTS: OnceLock = OnceLock::new(); + +pub fn monitoring_events(py: Python<'_>) -> PyResult<&'static MonitoringEvents> { + if let Some(ev) = MONITORING_EVENTS.get() { + return Ok(ev); + } + let ev = load_monitoring_events(py)?; + let _ = MONITORING_EVENTS.set(ev); + Ok(MONITORING_EVENTS.get().unwrap()) +} + +pub fn register_callback( + py: Python<'_>, + tool: &ToolId, + event: &EventId, + cb: Option<&CallbackFn<'_>>, +) -> PyResult<()> { + let monitoring = py.import("sys")?.getattr("monitoring")?; + match cb { + Some(cb) => { + monitoring.call_method("register_callback", (tool.id, event.0, cb), None)?; + } + None => { + monitoring.call_method("register_callback", (tool.id, event.0, py.None()), None)?; + } + } + Ok(()) +} + +pub fn events_union(ids: &[EventId]) -> EventSet { + let mut bits = 0i32; + for id in ids { + bits |= id.0; + } + EventSet(bits) +} + +pub fn set_events(py: Python<'_>, tool: &ToolId, set: EventSet) -> PyResult<()> { + let monitoring = py.import("sys")?.getattr("monitoring")?; + monitoring.call_method1("set_events", (tool.id, set.0))?; + Ok(()) +} + +pub fn free_tool_id(py: Python<'_>, tool: &ToolId) -> PyResult<()> { + let monitoring = py.import("sys")?.getattr("monitoring")?; + monitoring.call_method1("free_tool_id", (tool.id,))?; + Ok(()) +} + + +/// Trait implemented by tracing backends. +/// +/// Each method corresponds to an event from `sys.monitoring`. Default +/// implementations allow implementers to only handle the events they care +/// about. +pub trait Tracer: Send { + /// Return the set of events the tracer wants to receive. + fn interest(&self, _events: &MonitoringEvents) -> EventSet { + NO_EVENTS + } + + /// Called on Python function calls. + fn on_call( + &mut self, + _py: Python<'_>, + _code: &Bound<'_, PyAny>, + _offset: i32, + _callable: &Bound<'_, PyAny>, + _arg0: Option<&Bound<'_, PyAny>>, + ) { + } + + /// Called on line execution. + fn on_line(&mut self, _py: Python<'_>, _code: &Bound<'_, PyAny>, _lineno: u32) {} +} + +struct Global { + tracer: Box, + mask: EventSet, + tool: ToolId, +} + +static GLOBAL: Mutex> = Mutex::new(None); + +/// Install a tracer and hook it into Python's `sys.monitoring`. +pub fn install_tracer(py: Python<'_>, tracer: Box) -> PyResult<()> { + let mut guard = GLOBAL.lock().unwrap(); + if guard.is_some() { + return Err(PyRuntimeError::new_err("tracer already installed")); + } + + let tool = acquire_tool_id(py)?; + let events = monitoring_events(py)?; + + let module = PyModule::new(py, "_codetracer_callbacks")?; + + let mask = tracer.interest(events); + + if mask.contains(&events.CALL) { + let cb = wrap_pyfunction!(callback_call, &module)?; + + register_callback(py, &tool, &events.CALL, Some(&cb))?; + + } + if mask.contains(&events.LINE) { + let cb = wrap_pyfunction!(callback_line, &module)?; + register_callback(py, &tool, &events.LINE, Some(&cb))?; + } + set_events(py, &tool, mask)?; + + + *guard = Some(Global { + tracer, + mask, + tool, + }); + Ok(()) +} + +/// Remove the installed tracer if any. +pub fn uninstall_tracer(py: Python<'_>) -> PyResult<()> { + let mut guard = GLOBAL.lock().unwrap(); + if let Some(global) = guard.take() { + let events = monitoring_events(py)?; + if global.mask.contains(&events.CALL) { + register_callback(py, &global.tool, &events.CALL, None)?; + } + if global.mask.contains(&events.LINE) { + register_callback(py, &global.tool, &events.LINE, None)?; + } + set_events(py, &global.tool, NO_EVENTS)?; + free_tool_id(py, &global.tool)?; + } + Ok(()) +} + +#[pyfunction] +fn callback_call( + py: Python<'_>, + code: Bound<'_, PyAny>, + offset: i32, + callable: Bound<'_, PyAny>, + arg0: Option>, +) -> PyResult<()> { + if let Some(global) = GLOBAL.lock().unwrap().as_mut() { + global.tracer.on_call(py, &code, offset, &callable, arg0.as_ref()); + } + Ok(()) +} + +#[pyfunction] +fn callback_line(py: Python<'_>, code: Bound<'_, PyAny>, lineno: u32) -> PyResult<()> { + if let Some(global) = GLOBAL.lock().unwrap().as_mut() { + global.tracer.on_line(py, &code, lineno); + } + Ok(()) +} diff --git a/codetracer-python-recorder/tests/print_tracer.rs b/codetracer-python-recorder/tests/print_tracer.rs new file mode 100644 index 0000000..5972af7 --- /dev/null +++ b/codetracer-python-recorder/tests/print_tracer.rs @@ -0,0 +1,47 @@ +use codetracer_python_recorder::{install_tracer, uninstall_tracer, EventSet, Tracer}; +use codetracer_python_recorder::tracer::{MonitoringEvents, events_union}; +use pyo3::prelude::*; +use std::ffi::CString; +use std::sync::atomic::{AtomicUsize, Ordering}; + +static CALL_COUNT: AtomicUsize = AtomicUsize::new(0); + +struct PrintTracer; + +impl Tracer for PrintTracer { + fn interest(&self, events:&MonitoringEvents) -> EventSet { + events_union(&[events.CALL]) + } + + fn on_call( + &mut self, + _py: Python<'_>, + _code: &pyo3::Bound<'_, pyo3::types::PyAny>, + _offset: i32, + _callable: &pyo3::Bound<'_, pyo3::types::PyAny>, + _arg0: Option<&pyo3::Bound<'_, pyo3::types::PyAny>>, + ) { + CALL_COUNT.fetch_add(1, Ordering::SeqCst); + } +} + +#[test] +fn tracer_prints_on_call() { + Python::with_gil(|py| { + CALL_COUNT.store(0, Ordering::SeqCst); + if let Err(e) = install_tracer(py, Box::new(PrintTracer)) { + e.print(py); + panic!("Install Tracer failed"); + } + let code = CString::new("def foo():\n return 1\nfoo()").expect("CString::new failed"); + if let Err(e) = py.run(code.as_c_str(), None, None) { + e.print(py); + uninstall_tracer(py).ok(); + panic!("Python raised an exception"); + } + uninstall_tracer(py).unwrap(); + let count = CALL_COUNT.load(Ordering::SeqCst); + assert!(count >= 1, "expected at least one CALL event, got {}", count); + }); +} + diff --git a/design-docs/design-001.md b/design-docs/design-001.md index 1945608..063e668 100644 --- a/design-docs/design-001.md +++ b/design-docs/design-001.md @@ -8,6 +8,13 @@ The tracer collects `sys.monitoring` events, converts them to `runtime_tracing` ## Architecture +### Tracer Abstraction +Rust code exposes a `Tracer` trait representing callbacks for Python +`sys.monitoring` events. Implementations advertise their desired events via an +`EventMask` bit flag returned from `interest`. A `Dispatcher` wraps a trait +object and forwards events only when the mask contains the corresponding flag, +allowing tracers to implement just the methods they care about. + ### Tool Initialization - Acquire a tool identifier via `sys.monitoring.use_tool_id`; store it for the lifetime of the tracer. ```rs diff --git a/flake.nix b/flake.nix index 3938f41..3c462e8 100644 --- a/flake.nix +++ b/flake.nix @@ -36,6 +36,9 @@ maturin uv pkg-config + + # CapNProto + capnproto ]; shellHook = ''