Skip to content

Commit 573fb78

Browse files
committed
Stage 2
1 parent aa3f613 commit 573fb78

File tree

5 files changed

+90
-101
lines changed

5 files changed

+90
-101
lines changed

codetracer-python-recorder/benches/trace_filter.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ fn run_workload(engine: &TraceFilterEngine, dataset: &WorkloadDataset) {
5252
for &index in &dataset.event_indices {
5353
let code = dataset.codes[index].as_ref();
5454
let resolution = engine
55-
.resolve(py, code)
55+
.resolve(py, code, None)
5656
.expect("trace filter resolution should succeed during benchmarking");
5757
let policy = resolution.value_policy();
5858
for name in dataset.locals.iter() {
@@ -66,7 +66,7 @@ fn prewarm_engine(engine: &TraceFilterEngine, dataset: &WorkloadDataset) {
6666
Python::with_gil(|py| {
6767
for code in &dataset.codes {
6868
let _ = engine
69-
.resolve(py, code.as_ref())
69+
.resolve(py, code.as_ref(), None)
7070
.expect("prewarm resolution failed");
7171
}
7272
});

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

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -176,28 +176,20 @@ impl Tracer for RuntimeTracer {
176176
code: &CodeObjectWrapper,
177177
_offset: i32,
178178
) -> CallbackResult {
179-
if self.module_name_from_globals {
180-
if let Ok(qualname) = code.qualname(py) {
181-
if qualname == "<module>" {
182-
let globals_name = match capture_frame(py, code) {
183-
Ok(snapshot) => {
184-
let mapping = snapshot.globals().unwrap_or_else(|| snapshot.locals());
185-
mapping
186-
.get_item("__name__")
187-
.ok()
188-
.flatten()
189-
.and_then(|value| value.extract::<String>().ok())
190-
.map(|name| name.trim().to_string())
191-
.filter(|name| !name.is_empty())
192-
}
193-
Err(_) => None,
194-
};
195-
196-
self.filter
197-
.set_module_name_hint(code.id(), globals_name.clone());
198-
}
179+
let globals_name = match capture_frame(py, code) {
180+
Ok(snapshot) => {
181+
let mapping = snapshot.globals().unwrap_or_else(|| snapshot.locals());
182+
mapping
183+
.get_item("__name__")
184+
.ok()
185+
.flatten()
186+
.and_then(|value| value.extract::<String>().ok())
187+
.map(|name| name.trim().to_string())
188+
.filter(|name| !name.is_empty())
199189
}
200-
}
190+
Err(_) => None,
191+
};
192+
self.filter.set_module_name_hint(code.id(), globals_name);
201193

202194
if let Some(outcome) = self.evaluate_gate(py, code, true) {
203195
return Ok(outcome);

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

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -128,19 +128,9 @@ impl FilterCoordinator {
128128
return Some(existing.clone());
129129
}
130130

131-
match engine.resolve(py, code) {
131+
let hint = self.module_name_hints.get(&code_id).map(|s| s.as_str());
132+
match engine.resolve(py, code, hint) {
132133
Ok(resolution) => {
133-
let hint = self.module_name_hints.get(&code_id).cloned();
134-
let resolution = if let Some(hint) = hint {
135-
if resolution.module_name() == Some(hint.as_str()) {
136-
resolution
137-
} else {
138-
Arc::new(resolution.clone_with_module_name(Some(hint)))
139-
}
140-
} else {
141-
resolution
142-
};
143-
144134
if resolution.exec() == ExecDecision::Trace {
145135
self.scope_cache.insert(code_id, Arc::clone(&resolution));
146136
} else {

codetracer-python-recorder/src/trace_filter/engine.rs

Lines changed: 69 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
//! and caches per-code-object resolutions so the hot tracing callbacks only pay a fast lookup.
55
66
use crate::code_object::CodeObjectWrapper;
7-
use crate::module_identity::{is_valid_module_name, normalise_to_posix, ModuleIdentityResolver};
7+
use crate::module_identity::{is_valid_module_name, module_from_relative, normalise_to_posix};
88
use crate::trace_filter::config::{
99
ExecDirective, FilterSource, FilterSummary, ScopeRule, TraceFilterConfig, ValueAction,
1010
ValuePattern,
@@ -178,20 +178,6 @@ impl ScopeResolution {
178178
pub fn matched_rule_reason(&self) -> Option<&str> {
179179
self.matched_rule_reason.as_deref()
180180
}
181-
182-
pub(crate) fn clone_with_module_name(&self, module_name: Option<String>) -> ScopeResolution {
183-
ScopeResolution {
184-
exec: self.exec,
185-
value_policy: Arc::clone(&self.value_policy),
186-
module_name,
187-
object_name: self.object_name.clone(),
188-
relative_path: self.relative_path.clone(),
189-
absolute_path: self.absolute_path.clone(),
190-
matched_rule_index: self.matched_rule_index,
191-
matched_rule_source: self.matched_rule_source,
192-
matched_rule_reason: self.matched_rule_reason.clone(),
193-
}
194-
}
195181
}
196182

197183
/// Runtime engine wrapping a compiled filter configuration.
@@ -202,7 +188,6 @@ pub struct TraceFilterEngine {
202188
default_value_source: usize,
203189
rules: Arc<[CompiledScopeRule]>,
204190
cache: DashMap<usize, Arc<ScopeResolution>>,
205-
module_resolver: ModuleIdentityResolver,
206191
}
207192

208193
impl TraceFilterEngine {
@@ -212,7 +197,6 @@ impl TraceFilterEngine {
212197
let default_value_action = config.default_value_action();
213198
let default_value_source = config.default_value_source();
214199
let rules = compile_rules(config.rules());
215-
let module_resolver = ModuleIdentityResolver::new();
216200

217201
TraceFilterEngine {
218202
config: Arc::new(config),
@@ -221,7 +205,6 @@ impl TraceFilterEngine {
221205
default_value_source,
222206
rules,
223207
cache: DashMap::new(),
224-
module_resolver,
225208
}
226209
}
227210

@@ -230,12 +213,13 @@ impl TraceFilterEngine {
230213
&self,
231214
py: Python<'py>,
232215
code: &CodeObjectWrapper,
216+
module_hint: Option<&str>,
233217
) -> RecorderResult<Arc<ScopeResolution>> {
234218
if let Some(entry) = self.cache.get(&code.id()) {
235219
return Ok(entry.clone());
236220
}
237221

238-
let resolution = Arc::new(self.resolve_uncached(py, code)?);
222+
let resolution = Arc::new(self.resolve_uncached(py, code, module_hint)?);
239223
let entry = self
240224
.cache
241225
.entry(code.id())
@@ -247,6 +231,7 @@ impl TraceFilterEngine {
247231
&self,
248232
py: Python<'_>,
249233
code: &CodeObjectWrapper,
234+
module_hint: Option<&str>,
250235
) -> RecorderResult<ScopeResolution> {
251236
let filename = code
252237
.filename(py)
@@ -257,23 +242,31 @@ impl TraceFilterEngine {
257242

258243
let mut context = ScopeContext::derive(filename, self.config.sources());
259244
context.refresh_object_name(qualname);
260-
if let Some(absolute) = context.absolute_path.clone() {
261-
if let Some(module) = self.resolve_module_name(py, &absolute) {
262-
let replace = match context.module_name.as_deref() {
263-
None => true,
264-
Some(existing) => {
265-
!is_valid_module_name(existing)
266-
|| (module == "__main__" && !existing.contains('.'))
267-
}
268-
};
269-
if replace {
270-
context.update_module_name(module, qualname);
245+
246+
if let Some(hint) = module_hint {
247+
if is_valid_module_name(hint) {
248+
context.module_name = Some(hint.to_string());
249+
context.refresh_object_name(qualname);
250+
}
251+
}
252+
253+
if context
254+
.module_name
255+
.as_ref()
256+
.map(|name| !is_valid_module_name(name))
257+
.unwrap_or(true)
258+
&& context.module_name.is_none()
259+
{
260+
if let Some(absolute) = context.absolute_path.as_deref() {
261+
if let Some(module) = module_name_from_packages(Path::new(absolute)) {
262+
context.module_name = Some(module);
263+
context.refresh_object_name(qualname);
264+
} else {
265+
log::debug!(
266+
"[TraceFilter] unable to derive module name for '{}'; package selectors may not match",
267+
absolute
268+
);
271269
}
272-
} else if context.module_name.is_none() {
273-
log::debug!(
274-
"[TraceFilter] unable to derive module name for '{}'; package selectors may not match",
275-
absolute
276-
);
277270
}
278271
}
279272

@@ -338,10 +331,6 @@ impl TraceFilterEngine {
338331
pub fn summary(&self) -> FilterSummary {
339332
self.config.summary()
340333
}
341-
342-
fn resolve_module_name(&self, py: Python<'_>, absolute: &str) -> Option<String> {
343-
self.module_resolver.resolve_absolute(py, absolute)
344-
}
345334
}
346335

347336
#[derive(Debug, Clone)]
@@ -463,7 +452,8 @@ impl ScopeContext {
463452

464453
let module_name = relative_path
465454
.as_deref()
466-
.and_then(|rel| crate::module_identity::module_from_relative(rel));
455+
.and_then(|rel| module_from_relative(rel))
456+
.filter(|name| is_valid_module_name(name));
467457

468458
ScopeContext {
469459
module_name,
@@ -482,11 +472,6 @@ impl ScopeContext {
482472
(None, true) => None,
483473
};
484474
}
485-
486-
fn update_module_name(&mut self, module: String, qualname: &str) {
487-
self.module_name = Some(module);
488-
self.refresh_object_name(qualname);
489-
}
490475
}
491476

492477
fn normalise_relative(relative: PathBuf) -> String {
@@ -506,6 +491,39 @@ fn normalise_relative(relative: PathBuf) -> String {
506491
components.join("/")
507492
}
508493

494+
fn module_name_from_packages(path: &Path) -> Option<String> {
495+
let mut segments: Vec<String> = Vec::new();
496+
let mut current = path.parent();
497+
498+
while let Some(dir) = current {
499+
let init = dir.join("__init__.py");
500+
if init.exists() {
501+
if let Some(name) = dir.file_name().and_then(|s| s.to_str()) {
502+
if is_valid_module_name(name) {
503+
segments.push(name.to_string());
504+
current = dir.parent();
505+
continue;
506+
}
507+
}
508+
}
509+
break;
510+
}
511+
512+
segments.reverse();
513+
514+
if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
515+
if stem != "__init__" && is_valid_module_name(stem) {
516+
segments.push(stem.to_string());
517+
}
518+
}
519+
520+
if segments.is_empty() {
521+
return None;
522+
}
523+
524+
Some(segments.join("."))
525+
}
526+
509527
fn py_attr_error(attr: &str, err: PyErr) -> recorder_errors::RecorderError {
510528
target!(
511529
ErrorCode::FrameIntrospectionFailed,
@@ -518,9 +536,8 @@ fn py_attr_error(attr: &str, err: PyErr) -> recorder_errors::RecorderError {
518536
#[cfg(test)]
519537
mod tests {
520538
use super::*;
521-
use crate::module_identity::module_name_from_roots;
522539
use crate::trace_filter::config::TraceFilterConfig;
523-
use pyo3::types::{PyAny, PyCode, PyDict, PyList, PyModule};
540+
use pyo3::types::{PyAny, PyCode, PyList, PyModule};
524541
use std::ffi::CString;
525542
use std::fs;
526543
use std::io::Write;
@@ -564,7 +581,7 @@ mod tests {
564581
let wrapper = CodeObjectWrapper::new(py, &code_obj);
565582
let engine = TraceFilterEngine::new(config.clone());
566583

567-
let first = engine.resolve(py, &wrapper)?;
584+
let first = engine.resolve(py, &wrapper, None)?;
568585
assert_eq!(first.exec(), ExecDecision::Trace);
569586
assert_eq!(first.matched_rule_index(), Some(0));
570587
assert_eq!(first.module_name(), Some("app.foo"));
@@ -583,7 +600,7 @@ mod tests {
583600
ValueAction::Allow
584601
);
585602

586-
let second = engine.resolve(py, &wrapper)?;
603+
let second = engine.resolve(py, &wrapper, None)?;
587604
assert!(Arc::ptr_eq(&first, &second));
588605
Ok(())
589606
})
@@ -619,7 +636,7 @@ mod tests {
619636
let wrapper = CodeObjectWrapper::new(py, &code_obj);
620637

621638
let engine = TraceFilterEngine::new(config);
622-
let resolution = engine.resolve(py, &wrapper)?;
639+
let resolution = engine.resolve(py, &wrapper, None)?;
623640

624641
assert_eq!(resolution.exec(), ExecDecision::Trace);
625642
assert_eq!(resolution.matched_rule_index(), Some(1));
@@ -672,13 +689,6 @@ mod tests {
672689
normalise_to_posix(Path::new(file_path.to_string_lossy().as_ref())).unwrap();
673690

674691
let module = py.import("app.foo").expect("import app.foo");
675-
let modules_any = sys.getattr("modules").expect("sys.modules");
676-
let modules: Bound<'_, PyDict> =
677-
modules_any.downcast_into::<PyDict>().expect("modules dict");
678-
modules
679-
.set_item("app.foo", module.as_any())
680-
.expect("register module");
681-
682692
let func: Bound<'_, PyAny> = module.getattr("foo").expect("get foo");
683693
let code_obj = func
684694
.getattr("__code__")
@@ -690,18 +700,11 @@ mod tests {
690700
assert_eq!(recorded_filename, file_path.to_string_lossy());
691701

692702
let engine = TraceFilterEngine::new(config.clone());
693-
let expected_root =
694-
normalise_to_posix(Path::new(project_root.to_string_lossy().as_ref())).unwrap();
695-
let resolver_roots = engine.module_resolver.module_roots();
696-
assert!(resolver_roots.iter().any(|root| root == &expected_root));
697-
let derived_from_roots = module_name_from_roots(resolver_roots, absolute_path.as_str());
698-
assert_eq!(derived_from_roots, Some("app.foo".to_string()));
699-
let resolution = engine.resolve(py, &wrapper)?;
703+
let resolution = engine.resolve(py, &wrapper, None)?;
700704
assert_eq!(resolution.absolute_path(), Some(absolute_path.as_str()));
701705
assert_eq!(resolution.module_name(), Some("app.foo"));
702706
assert_eq!(resolution.exec(), ExecDecision::Skip);
703707

704-
modules.del_item("app.foo").expect("remove module");
705708
sys_path.del_item(0).expect("restore sys.path");
706709
Ok(())
707710
})
@@ -727,7 +730,7 @@ mod tests {
727730
let wrapper = CodeObjectWrapper::new(py, &code_obj);
728731

729732
let engine = TraceFilterEngine::new(config);
730-
let resolution = engine.resolve(py, &wrapper)?;
733+
let resolution = engine.resolve(py, &wrapper, None)?;
731734

732735
assert_eq!(resolution.exec(), ExecDecision::Skip);
733736
assert_eq!(resolution.relative_path(), Some("app/foo.py"));

design-docs/module-name-resolution-globals-implementation-plan.status.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,7 @@
2323
### Stage 1 – Capture `__name__` at `py_start`
2424
- **Status:** Completed
2525
`RuntimeTracer` now captures `frame.f_globals['__name__']` for `<module>` code when the feature flag is on, threads the hint through `FilterCoordinator`, and prefers it during both filter decisions and runtime naming. Added integration coverage ensuring opt-in sessions record `<__main__>` for scripts, plus unit updates for the new plumbing.
26+
27+
### Stage 2 – Simplify Filter Engine
28+
- **Status:** Completed
29+
Removed the resolver dependency from `TraceFilterEngine`, teach it to accept globals-based hints, and added a package-structure fallback when relative paths are unavailable. Filtering decisions now rely on the new hint flow while keeping legacy behaviour intact for `module_name_from_globals` opt-out scenarios.

0 commit comments

Comments
 (0)