Skip to content

Commit 8da5d18

Browse files
committed
Milestone 5 - Step 3
codetracer-python-recorder/src/runtime/tracer/filtering.rs: codetracer-python-recorder/src/runtime/tracer/runtime_tracer.rs: design-docs/codetracer-architecture-refactor-implementation-plan.status.md: Signed-off-by: Tzanko Matev <[email protected]>
1 parent 471d594 commit 8da5d18

File tree

3 files changed

+216
-166
lines changed

3 files changed

+216
-166
lines changed
Lines changed: 189 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,191 @@
11
//! Trace filter cache management for `RuntimeTracer`.
22
3-
// Placeholder module; implementations will arrive during Milestone 5.
3+
use crate::code_object::CodeObjectWrapper;
4+
use crate::logging::{record_dropped_event, with_error_code};
5+
use crate::runtime::io_capture::ScopedMuteIoCapture;
6+
use crate::runtime::value_capture::ValueFilterStats;
7+
use crate::trace_filter::engine::{ExecDecision, ScopeResolution, TraceFilterEngine, ValueKind};
8+
use pyo3::prelude::*;
9+
use recorder_errors::ErrorCode;
10+
use serde_json::{self, json};
11+
use std::collections::{HashMap, HashSet};
12+
use std::sync::Arc;
13+
14+
/// Filtering outcome for a code object.
15+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16+
pub(crate) enum TraceDecision {
17+
Trace,
18+
SkipAndDisable,
19+
}
20+
21+
/// Coordinates trace filter execution, caching, and telemetry.
22+
pub(crate) struct FilterCoordinator {
23+
engine: Option<Arc<TraceFilterEngine>>,
24+
ignored_code_ids: HashSet<usize>,
25+
scope_cache: HashMap<usize, Arc<ScopeResolution>>,
26+
stats: FilterStats,
27+
}
28+
29+
impl FilterCoordinator {
30+
pub(crate) fn new(engine: Option<Arc<TraceFilterEngine>>) -> Self {
31+
Self {
32+
engine,
33+
ignored_code_ids: HashSet::new(),
34+
scope_cache: HashMap::new(),
35+
stats: FilterStats::default(),
36+
}
37+
}
38+
39+
pub(crate) fn engine(&self) -> Option<&Arc<TraceFilterEngine>> {
40+
self.engine.as_ref()
41+
}
42+
43+
pub(crate) fn cached_resolution(&self, code_id: usize) -> Option<Arc<ScopeResolution>> {
44+
self.scope_cache.get(&code_id).cloned()
45+
}
46+
47+
pub(crate) fn summary_json(&self) -> serde_json::Value {
48+
self.stats.summary_json()
49+
}
50+
51+
pub(crate) fn values_mut(&mut self) -> &mut ValueFilterStats {
52+
self.stats.values_mut()
53+
}
54+
55+
pub(crate) fn clear_caches(&mut self) {
56+
self.ignored_code_ids.clear();
57+
self.scope_cache.clear();
58+
}
59+
60+
pub(crate) fn reset(&mut self) {
61+
self.clear_caches();
62+
self.stats.reset();
63+
}
64+
65+
pub(crate) fn decide(&mut self, py: Python<'_>, code: &CodeObjectWrapper) -> TraceDecision {
66+
let code_id = code.id();
67+
if self.ignored_code_ids.contains(&code_id) {
68+
return TraceDecision::SkipAndDisable;
69+
}
70+
71+
if let Some(resolution) = self.resolve(py, code) {
72+
if resolution.exec() == ExecDecision::Skip {
73+
self.mark_ignored(code_id);
74+
self.stats.record_skip();
75+
record_dropped_event("filter_scope_skip");
76+
return TraceDecision::SkipAndDisable;
77+
}
78+
}
79+
80+
let filename = match code.filename(py) {
81+
Ok(name) => name,
82+
Err(err) => {
83+
with_error_code(ErrorCode::Io, || {
84+
let _mute = ScopedMuteIoCapture::new();
85+
log::error!("failed to resolve code filename: {err}");
86+
});
87+
record_dropped_event("filename_lookup_failed");
88+
self.mark_ignored(code_id);
89+
return TraceDecision::SkipAndDisable;
90+
}
91+
};
92+
93+
if is_real_filename(filename) {
94+
TraceDecision::Trace
95+
} else {
96+
record_dropped_event("synthetic_filename");
97+
self.mark_ignored(code_id);
98+
TraceDecision::SkipAndDisable
99+
}
100+
}
101+
102+
fn resolve(
103+
&mut self,
104+
py: Python<'_>,
105+
code: &CodeObjectWrapper,
106+
) -> Option<Arc<ScopeResolution>> {
107+
let engine = self.engine.as_ref()?;
108+
let code_id = code.id();
109+
110+
if let Some(existing) = self.scope_cache.get(&code_id) {
111+
return Some(existing.clone());
112+
}
113+
114+
match engine.resolve(py, code) {
115+
Ok(resolution) => {
116+
if resolution.exec() == ExecDecision::Trace {
117+
self.scope_cache.insert(code_id, Arc::clone(&resolution));
118+
} else {
119+
self.scope_cache.remove(&code_id);
120+
}
121+
Some(resolution)
122+
}
123+
Err(err) => {
124+
let message = err.to_string();
125+
let error_code = err.code;
126+
with_error_code(error_code, || {
127+
let _mute = ScopedMuteIoCapture::new();
128+
log::error!(
129+
"[RuntimeTracer] trace filter resolution failed for code id {}: {}",
130+
code_id,
131+
message
132+
);
133+
});
134+
record_dropped_event("filter_resolution_error");
135+
None
136+
}
137+
}
138+
}
139+
140+
fn mark_ignored(&mut self, code_id: usize) {
141+
self.scope_cache.remove(&code_id);
142+
self.ignored_code_ids.insert(code_id);
143+
}
144+
}
145+
146+
/// Return true when the filename refers to a concrete source file.
147+
pub(crate) fn is_real_filename(filename: &str) -> bool {
148+
let trimmed = filename.trim();
149+
!(trimmed.starts_with('<') && trimmed.ends_with('>'))
150+
}
151+
152+
#[derive(Debug, Default)]
153+
struct FilterStats {
154+
skipped_scopes: u64,
155+
values: ValueFilterStats,
156+
}
157+
158+
impl FilterStats {
159+
fn record_skip(&mut self) {
160+
self.skipped_scopes += 1;
161+
}
162+
163+
fn values_mut(&mut self) -> &mut ValueFilterStats {
164+
&mut self.values
165+
}
166+
167+
fn reset(&mut self) {
168+
self.skipped_scopes = 0;
169+
self.values = ValueFilterStats::default();
170+
}
171+
172+
fn summary_json(&self) -> serde_json::Value {
173+
let mut redactions = serde_json::Map::new();
174+
let mut drops = serde_json::Map::new();
175+
for kind in ValueKind::ALL {
176+
redactions.insert(
177+
kind.label().to_string(),
178+
json!(self.values.redacted_count(kind)),
179+
);
180+
drops.insert(
181+
kind.label().to_string(),
182+
json!(self.values.dropped_count(kind)),
183+
);
184+
}
185+
json!({
186+
"scopes_skipped": self.skipped_scopes,
187+
"value_redactions": serde_json::Value::Object(redactions),
188+
"value_drops": serde_json::Value::Object(drops),
189+
})
190+
}
191+
}

0 commit comments

Comments
 (0)