Skip to content

Commit 963246c

Browse files
committed
WS2
codetracer-python-recorder/src/runtime/tracer/events.rs: codetracer-python-recorder/src/runtime/tracer/runtime_tracer.rs: design-docs/module-call-event-naming-implementation-plan.status.md: Signed-off-by: Tzanko Matev <[email protected]>
1 parent 414e44f commit 963246c

File tree

3 files changed

+103
-19
lines changed

3 files changed

+103
-19
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,7 @@ impl Tracer for RuntimeTracer {
508508
.map_err(ffi::map_recorder_error)?;
509509
}
510510
self.function_ids.clear();
511+
self.module_names.clear();
511512
self.io.clear_snapshots();
512513
self.filter.reset();
513514
self.lifecycle.reset_event_state();
@@ -521,6 +522,7 @@ impl Tracer for RuntimeTracer {
521522
.finalise(&mut self.writer, &self.filter)
522523
.map_err(ffi::map_recorder_error)?;
523524
self.function_ids.clear();
525+
self.module_names.clear();
524526
self.filter.reset();
525527
self.io.clear_snapshots();
526528
self.lifecycle.reset_event_state();

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

Lines changed: 97 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use super::io::IoCoordinator;
44
use super::lifecycle::LifecycleController;
55
use crate::code_object::CodeObjectWrapper;
66
use crate::ffi;
7+
use crate::module_identity::{ModuleIdentityCache, ModuleNameHints};
78
use crate::policy::RecorderPolicy;
89
use crate::runtime::io_capture::{IoCaptureSettings, ScopedMuteIoCapture};
910
use crate::runtime::line_snapshots::LineSnapshotStore;
@@ -12,7 +13,7 @@ use crate::trace_filter::engine::TraceFilterEngine;
1213
use pyo3::prelude::*;
1314
use runtime_tracing::NonStreamingTraceWriter;
1415
use runtime_tracing::{Line, TraceEventsFileFormat, TraceWriter};
15-
use std::collections::{hash_map::Entry, HashMap};
16+
use std::collections::HashMap;
1617
use std::path::Path;
1718
use std::sync::Arc;
1819
use std::thread::ThreadId;
@@ -26,6 +27,7 @@ pub struct RuntimeTracer {
2627
pub(super) function_ids: HashMap<usize, runtime_tracing::FunctionId>,
2728
pub(super) io: IoCoordinator,
2829
pub(super) filter: FilterCoordinator,
30+
pub(super) module_names: ModuleIdentityCache,
2931
}
3032

3133
impl RuntimeTracer {
@@ -46,6 +48,7 @@ impl RuntimeTracer {
4648
function_ids: HashMap::new(),
4749
io: IoCoordinator::new(),
4850
filter: FilterCoordinator::new(trace_filter),
51+
module_names: ModuleIdentityCache::new(),
4952
}
5053
}
5154

@@ -101,21 +104,20 @@ impl RuntimeTracer {
101104
py: Python<'_>,
102105
code: &CodeObjectWrapper,
103106
) -> PyResult<runtime_tracing::FunctionId> {
104-
match self.function_ids.entry(code.id()) {
105-
Entry::Occupied(entry) => Ok(*entry.get()),
106-
Entry::Vacant(slot) => {
107-
let name = code.qualname(py)?;
108-
let filename = code.filename(py)?;
109-
let first_line = code.first_line(py)?;
110-
let function_id = TraceWriter::ensure_function_id(
111-
&mut self.writer,
112-
name,
113-
Path::new(filename),
114-
Line(first_line as i64),
115-
);
116-
Ok(*slot.insert(function_id))
117-
}
107+
if let Some(fid) = self.function_ids.get(&code.id()) {
108+
return Ok(*fid);
118109
}
110+
let name = self.function_name(py, code)?;
111+
let filename = code.filename(py)?;
112+
let first_line = code.first_line(py)?;
113+
let function_id = TraceWriter::ensure_function_id(
114+
&mut self.writer,
115+
name.as_str(),
116+
Path::new(filename),
117+
Line(first_line as i64),
118+
);
119+
self.function_ids.insert(code.id(), function_id);
120+
Ok(function_id)
119121
}
120122

121123
pub(super) fn should_trace_code(
@@ -125,6 +127,34 @@ impl RuntimeTracer {
125127
) -> TraceDecision {
126128
self.filter.decide(py, code)
127129
}
130+
131+
fn function_name(&self, py: Python<'_>, code: &CodeObjectWrapper) -> PyResult<String> {
132+
let qualname = code.qualname(py)?;
133+
if qualname == "<module>" {
134+
Ok(self
135+
.derive_module_name(py, code)
136+
.map(|module| format!("<{module}>"))
137+
.unwrap_or_else(|| qualname.to_string()))
138+
} else {
139+
Ok(qualname.to_string())
140+
}
141+
}
142+
143+
fn derive_module_name(&self, py: Python<'_>, code: &CodeObjectWrapper) -> Option<String> {
144+
let resolution = self.filter.cached_resolution(code.id());
145+
if let Some(resolution) = resolution.as_ref() {
146+
let hints = ModuleNameHints {
147+
preferred: resolution.module_name(),
148+
relative_path: resolution.relative_path(),
149+
absolute_path: resolution.absolute_path(),
150+
globals_name: None,
151+
};
152+
self.module_names.resolve_for_code(py, code, hints)
153+
} else {
154+
self.module_names
155+
.resolve_for_code(py, code, ModuleNameHints::default())
156+
}
157+
}
128158
}
129159

130160
#[cfg(test)]
@@ -1039,6 +1069,58 @@ sensitive("s3cr3t")
10391069
});
10401070
}
10411071

1072+
#[test]
1073+
fn module_import_records_module_name() {
1074+
Python::with_gil(|py| {
1075+
ensure_test_module(py);
1076+
1077+
let mut tracer =
1078+
RuntimeTracer::new("runner.py", &[], TraceEventsFileFormat::Json, None, None);
1079+
1080+
let project = tempfile::tempdir().expect("project dir");
1081+
let pkg_root = project.path().join("lib");
1082+
let pkg_dir = pkg_root.join("my_pkg");
1083+
fs::create_dir_all(&pkg_dir).expect("create package dir");
1084+
fs::write(pkg_dir.join("__init__.py"), "\n").expect("write __init__");
1085+
fs::write(
1086+
pkg_dir.join("mod.py"),
1087+
"value = 1\nprint('imported module value', value)\n",
1088+
)
1089+
.expect("write module file");
1090+
1091+
let script = format!(
1092+
"import sys\nsys.path.insert(0, r\"{root}\")\nimport my_pkg.mod\n",
1093+
root = pkg_root.display()
1094+
);
1095+
1096+
{
1097+
let _guard = ScopedTracer::new(&mut tracer);
1098+
LAST_OUTCOME.with(|cell| cell.set(None));
1099+
let script_c = CString::new(script).expect("script contains nul byte");
1100+
py.run(script_c.as_c_str(), None, None)
1101+
.expect("execute module import");
1102+
}
1103+
1104+
let function_records: Vec<String> = tracer
1105+
.writer
1106+
.events
1107+
.iter()
1108+
.filter_map(|event| {
1109+
if let TraceLowLevelEvent::Function(record) = event {
1110+
Some(record.name.clone())
1111+
} else {
1112+
None
1113+
}
1114+
})
1115+
.collect();
1116+
1117+
assert!(
1118+
function_records.iter().any(|name| name == "<my_pkg.mod>"),
1119+
"expected module function name to be rewritten: {function_records:?}"
1120+
);
1121+
});
1122+
}
1123+
10421124
#[test]
10431125
fn user_drop_default_overrides_builtin_allowance() {
10441126
Python::with_gil(|py| {

design-docs/module-call-event-naming-implementation-plan.status.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@
1818

1919
### WS1 – Shared Module Identity Helper
2020
- **Scope recap:** Extract and centralise module-name derivation (relative path stripping + `sys.modules` lookup) so both filters and the runtime tracer can reuse it with caching.
21-
- **Status:** _In Progress_
22-
- **Notes:** Created `src/module_identity.rs` housing `ModuleIdentityResolver`, a per-code `ModuleIdentityCache`, sanitisation helpers, and unit tests (covering `.py`/`.pyc`, package roots, and hint precedence). `TraceFilterEngine` now consumes the resolver instead of bespoke logic, and shared helpers (`module_from_relative`, `normalise_to_posix`, etc.) moved into the new module. `just dev` + `just test` succeed after the changeset; the cache will be hooked into `RuntimeTracer` during WS2.
21+
- **Status:** _Completed_
22+
- **Notes:** `src/module_identity.rs` now owns `ModuleIdentityResolver`, `ModuleIdentityCache`, sanitisation helpers, and unit tests covering `.py` vs `.pyc`, package roots, and hint precedence. `TraceFilterEngine` uses the shared resolver for all module lookups, keeping behaviour aligned between filtering and runtime components.
2323

2424
### WS2 – Runtime Tracer Integration
2525
- **Scope recap:** Detect module-level code (`co_qualname == "<module>"`) and rename call events to `<module-name>` using the shared resolver; plumb filter-derived names to avoid duplicate work.
26-
- **Status:** _Not Started_
27-
- **Notes:** Will require updates to `RuntimeTracer::ensure_function_id`, cache management, and potentially `capture_call_arguments` to surface `globals["__name__"]` when needed.
26+
- **Status:** _In Progress_
27+
- **Notes:** `RuntimeTracer` now owns a `ModuleIdentityCache`, rewrites module-level function names via shared hints, clears the cache between runs, and has a regression test (`module_import_records_module_name`) confirming `<my_pkg.mod>` call records. Remaining work: thread globals-based hints (if needed) and add higher-level integration tests / documentation (see WS3).
2828

2929
### WS3 – Testing, Tooling, and Docs
3030
- **Scope recap:** Add regression tests (Python + Rust) validating the new naming, update documentation/changelog, and refresh any snapshot expectations.

0 commit comments

Comments
 (0)