#[derive(Debug, Clone)]
pub struct ThreadDef {
pub name: String,
pub open_pattern_idx: usize,
pub close_pattern_idx: usize,
}
#[derive(Debug, Clone)]
pub struct ThreadStatus {
pub name: String,
pub open_count: usize,
pub close_count: u64,
pub unresolved: bool,
}
#[derive(Debug, Clone)]
pub struct FiloViolation {
pub closed_thread: String,
pub blocking_thread: String,
}
#[derive(Debug, Clone, Default)]
pub struct ThreadTracker {
threads: Vec<ThreadDef>,
open_order: Vec<String>,
close_order: Vec<String>,
}
impl ThreadTracker {
pub fn new() -> Self {
Self::default()
}
pub fn register(
&mut self,
name: impl Into<String>,
open_pattern_idx: usize,
close_pattern_idx: usize,
) {
self.threads.push(ThreadDef {
name: name.into(),
open_pattern_idx,
close_pattern_idx,
});
}
pub fn record_open(&mut self, thread_name: &str) {
if !self.open_order.contains(&thread_name.to_string()) {
self.open_order.push(thread_name.to_string());
}
}
pub fn record_close(&mut self, thread_name: &str) {
self.close_order.push(thread_name.to_string());
}
pub fn observe_delta(&mut self, delta: &fabula::engine::TickDelta) {
let opens: Vec<String> = self
.threads
.iter()
.filter(|t| delta.advanced.contains(&format!("{}_open", t.name)))
.map(|t| t.name.clone())
.collect();
let closes: Vec<String> = self
.threads
.iter()
.filter(|t| delta.completed.contains(&format!("{}_close", t.name)))
.map(|t| t.name.clone())
.collect();
for name in opens {
self.record_open(&name);
}
for name in closes {
self.record_close(&name);
}
}
pub fn status(
&self,
metrics_fn: impl Fn(usize) -> Option<fabula::engine::PatternMetrics>,
) -> Vec<ThreadStatus> {
self.threads
.iter()
.map(|thread| {
let metrics_open = metrics_fn(thread.open_pattern_idx);
let metrics_close = metrics_fn(thread.close_pattern_idx);
let open_count = metrics_open
.as_ref()
.map(|m| m.active_pm_count)
.unwrap_or(0);
let close_count = metrics_close
.as_ref()
.map(|m| m.completion_count)
.unwrap_or(0);
let open_completions = metrics_open
.as_ref()
.map(|m| m.completion_count)
.unwrap_or(0);
ThreadStatus {
name: thread.name.clone(),
open_count,
close_count,
unresolved: open_completions > close_count,
}
})
.collect()
}
pub fn unresolved_thread_count(
&self,
metrics_fn: impl Fn(usize) -> Option<fabula::engine::PatternMetrics>,
) -> usize {
self.status(metrics_fn)
.iter()
.filter(|s| s.unresolved)
.count()
}
pub fn check_filo(&self) -> Vec<FiloViolation> {
let mut violations = Vec::new();
let mut still_open: Vec<&str> = self.open_order.iter().map(|s| s.as_str()).collect();
for closed in &self.close_order {
if let Some(pos) = still_open.iter().position(|&s| s == closed.as_str()) {
for &later_open in &still_open[pos + 1..] {
let later_closed_count = self
.close_order
.iter()
.filter(|c| c.as_str() == later_open)
.count();
let later_open_count = self
.open_order
.iter()
.filter(|o| o.as_str() == later_open)
.count();
if later_closed_count < later_open_count {
violations.push(FiloViolation {
closed_thread: closed.clone(),
blocking_thread: later_open.to_string(),
});
}
}
still_open.remove(pos);
}
}
violations
}
pub fn reset(&mut self) {
self.open_order.clear();
self.close_order.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn filo_correct_nesting() {
let mut tracker = ThreadTracker::new();
tracker.register("milieu", 0, 1);
tracker.register("inquiry", 2, 3);
tracker.record_open("milieu");
tracker.record_open("inquiry");
tracker.record_close("inquiry");
tracker.record_close("milieu");
assert!(
tracker.check_filo().is_empty(),
"correct nesting should have no violations"
);
}
#[test]
fn filo_violation_detected() {
let mut tracker = ThreadTracker::new();
tracker.register("milieu", 0, 1);
tracker.register("inquiry", 2, 3);
tracker.record_open("milieu");
tracker.record_open("inquiry");
tracker.record_close("milieu");
let violations = tracker.check_filo();
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].closed_thread, "milieu");
assert_eq!(violations[0].blocking_thread, "inquiry");
}
#[test]
fn reset_clears_tracking() {
let mut tracker = ThreadTracker::new();
tracker.register("test", 0, 1);
tracker.record_open("test");
tracker.record_close("test");
tracker.reset();
assert!(tracker.check_filo().is_empty());
}
}