use super::types::{TraceEntry, TraceEntryType};
use std::collections::HashMap;
use std::time::Instant;
pub struct EventChainTracer {
traces: Vec<TraceEntry>,
enabled: bool,
start_time: Instant,
current_frame: u64,
}
impl EventChainTracer {
pub fn new() -> Self {
Self {
traces: Vec::new(),
enabled: false,
start_time: Instant::now(),
current_frame: 0,
}
}
pub fn enable(&mut self) {
self.enabled = true;
self.start_time = Instant::now();
}
pub fn disable(&mut self) {
self.enabled = false;
}
pub fn is_enabled(&self) -> bool {
self.enabled
}
pub fn set_frame(&mut self, frame: u64) {
self.current_frame = frame;
}
pub fn current_frame(&self) -> u64 {
self.current_frame
}
pub fn elapsed_ms(&self) -> f64 {
self.start_time.elapsed().as_secs_f64() * 1000.0
}
pub fn record(&mut self, entry: TraceEntry) {
if !self.enabled {
return;
}
self.traces.push(entry);
}
pub fn record_simple(&mut self, entry_type: TraceEntryType, source: impl Into<String>) {
if !self.enabled {
return;
}
self.traces.push(TraceEntry {
frame: self.current_frame,
timestamp_ms: self.elapsed_ms(),
entry_type,
source: source.into(),
target: None,
});
}
pub fn clear(&mut self) {
self.traces.clear();
self.start_time = Instant::now();
self.current_frame = 0;
}
pub fn traces(&self) -> &[TraceEntry] {
&self.traces
}
pub fn traces_for_frame(&self, frame: u64) -> Vec<&TraceEntry> {
self.traces.iter().filter(|e| e.frame == frame).collect()
}
pub fn traces_for_range(&self, start: u64, end: u64) -> Vec<&TraceEntry> {
self.traces
.iter()
.filter(|e| e.frame >= start && e.frame <= end)
.collect()
}
pub fn stats(&self) -> TracerStats {
let mut event_types: HashMap<String, usize> = HashMap::new();
let mut hook_calls: HashMap<String, usize> = HashMap::new();
for entry in &self.traces {
match &entry.entry_type {
TraceEntryType::EventPublished { event_type, .. } => {
*event_types.entry(event_type.clone()).or_insert(0) += 1;
}
TraceEntryType::HookCalled { hook_name, .. } => {
*hook_calls.entry(hook_name.clone()).or_insert(0) += 1;
}
_ => {}
}
}
TracerStats {
total_entries: self.traces.len(),
total_frames: self.current_frame + 1,
event_types,
hook_calls,
}
}
pub fn export_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(&self.traces)
}
pub fn save(&self, path: &str) -> Result<(), Box<dyn std::error::Error>> {
let json = self.export_json()?;
std::fs::write(path, json)?;
Ok(())
}
pub fn load(path: &str) -> Result<Self, Box<dyn std::error::Error>> {
let json = std::fs::read_to_string(path)?;
let traces: Vec<TraceEntry> = serde_json::from_str(&json)?;
Ok(Self {
traces,
enabled: false,
start_time: Instant::now(),
current_frame: 0,
})
}
}
impl Default for EventChainTracer {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Debug)]
pub struct TracerStats {
pub total_entries: usize,
pub total_frames: u64,
pub event_types: HashMap<String, usize>,
pub hook_calls: HashMap<String, usize>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::trace::types::TraceEntryType;
#[test]
fn test_tracer_creation() {
let tracer = EventChainTracer::new();
assert!(!tracer.is_enabled());
assert_eq!(tracer.traces().len(), 0);
}
#[test]
fn test_enable_disable() {
let mut tracer = EventChainTracer::new();
tracer.enable();
assert!(tracer.is_enabled());
tracer.disable();
assert!(!tracer.is_enabled());
}
#[test]
fn test_record_when_disabled() {
let mut tracer = EventChainTracer::new();
tracer.record_simple(
TraceEntryType::EventPublished {
event_type: "TestEvent".to_string(),
event_id: "test_1".to_string(),
},
"EventBus",
);
assert_eq!(tracer.traces().len(), 0);
}
#[test]
fn test_record_when_enabled() {
let mut tracer = EventChainTracer::new();
tracer.enable();
tracer.record_simple(
TraceEntryType::EventPublished {
event_type: "TestEvent".to_string(),
event_id: "test_1".to_string(),
},
"EventBus",
);
assert_eq!(tracer.traces().len(), 1);
assert_eq!(tracer.traces()[0].source, "EventBus");
}
#[test]
fn test_frame_tracking() {
let mut tracer = EventChainTracer::new();
tracer.enable();
tracer.set_frame(0);
tracer.record_simple(
TraceEntryType::EventPublished {
event_type: "Event1".to_string(),
event_id: "1".to_string(),
},
"EventBus",
);
tracer.set_frame(1);
tracer.record_simple(
TraceEntryType::EventPublished {
event_type: "Event2".to_string(),
event_id: "2".to_string(),
},
"EventBus",
);
let frame_0 = tracer.traces_for_frame(0);
let frame_1 = tracer.traces_for_frame(1);
assert_eq!(frame_0.len(), 1);
assert_eq!(frame_1.len(), 1);
}
#[test]
fn test_clear() {
let mut tracer = EventChainTracer::new();
tracer.enable();
tracer.record_simple(
TraceEntryType::EventPublished {
event_type: "TestEvent".to_string(),
event_id: "test_1".to_string(),
},
"EventBus",
);
assert_eq!(tracer.traces().len(), 1);
tracer.clear();
assert_eq!(tracer.traces().len(), 0);
}
#[test]
fn test_stats() {
let mut tracer = EventChainTracer::new();
tracer.enable();
tracer.record_simple(
TraceEntryType::EventPublished {
event_type: "Event1".to_string(),
event_id: "1".to_string(),
},
"EventBus",
);
tracer.record_simple(
TraceEntryType::EventPublished {
event_type: "Event1".to_string(),
event_id: "2".to_string(),
},
"EventBus",
);
tracer.record_simple(
TraceEntryType::HookCalled {
hook_name: "on_test".to_string(),
plugin: "TestPlugin".to_string(),
args: "()".to_string(),
},
"TestSystem",
);
let stats = tracer.stats();
assert_eq!(stats.total_entries, 3);
assert_eq!(*stats.event_types.get("Event1").unwrap(), 2);
assert_eq!(*stats.hook_calls.get("on_test").unwrap(), 1);
}
#[test]
fn test_save_and_load() {
let mut tracer = EventChainTracer::new();
tracer.enable();
tracer.record_simple(
TraceEntryType::EventPublished {
event_type: "TestEvent".to_string(),
event_id: "test_1".to_string(),
},
"EventBus",
);
let temp_file = "/tmp/test_tracer.json";
tracer.save(temp_file).unwrap();
let loaded = EventChainTracer::load(temp_file).unwrap();
assert_eq!(loaded.traces().len(), 1);
assert_eq!(loaded.traces()[0].source, "EventBus");
std::fs::remove_file(temp_file).ok();
}
}