ndn_sim/
tracer.rs

1//! `SimTracer` — structured event capture for simulation analysis.
2//!
3//! A lightweight event recorder that captures packet-level events during
4//! simulation runs. Events are stored in memory for post-hoc analysis,
5//! filtering, and serialization to JSON.
6//!
7//! # Usage
8//!
9//! ```rust,no_run
10//! use ndn_sim::tracer::{SimTracer, SimEvent, EventKind};
11//!
12//! let tracer = SimTracer::new();
13//!
14//! // Record events during simulation...
15//! tracer.record(SimEvent {
16//!     timestamp_us: 1000,
17//!     node: 0,
18//!     face: Some(1),
19//!     kind: EventKind::InterestIn,
20//!     name: "/ndn/test/data".into(),
21//!     detail: None,
22//! });
23//!
24//! // Analyze after simulation
25//! let events = tracer.events();
26//! let json = tracer.to_json();
27//! ```
28
29use std::sync::Mutex;
30use std::time::Instant;
31
32/// A recorded simulation event.
33#[derive(Clone, Debug)]
34pub struct SimEvent {
35    /// Microseconds since simulation start.
36    pub timestamp_us: u64,
37    /// Node index where the event occurred.
38    pub node: usize,
39    /// Face ID involved (if applicable).
40    pub face: Option<u32>,
41    /// Event classification.
42    pub kind: EventKind,
43    /// NDN name involved.
44    pub name: String,
45    /// Optional detail string (e.g. "cache-hit", "nack:NoRoute").
46    pub detail: Option<String>,
47}
48
49/// Classification of simulation events.
50#[derive(Clone, Debug, PartialEq, Eq)]
51pub enum EventKind {
52    /// Interest received on a face.
53    InterestIn,
54    /// Interest forwarded out a face.
55    InterestOut,
56    /// Data received on a face.
57    DataIn,
58    /// Data sent out a face.
59    DataOut,
60    /// Content Store cache hit.
61    CacheHit,
62    /// Content Store insert.
63    CacheInsert,
64    /// PIT entry created.
65    PitInsert,
66    /// PIT entry satisfied.
67    PitSatisfy,
68    /// PIT entry expired.
69    PitExpire,
70    /// Nack received.
71    NackIn,
72    /// Nack sent.
73    NackOut,
74    /// Face created.
75    FaceUp,
76    /// Face destroyed.
77    FaceDown,
78    /// Strategy decision.
79    StrategyDecision,
80    /// Custom event.
81    Custom(String),
82}
83
84impl std::fmt::Display for EventKind {
85    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86        match self {
87            Self::InterestIn => write!(f, "interest-in"),
88            Self::InterestOut => write!(f, "interest-out"),
89            Self::DataIn => write!(f, "data-in"),
90            Self::DataOut => write!(f, "data-out"),
91            Self::CacheHit => write!(f, "cache-hit"),
92            Self::CacheInsert => write!(f, "cache-insert"),
93            Self::PitInsert => write!(f, "pit-insert"),
94            Self::PitSatisfy => write!(f, "pit-satisfy"),
95            Self::PitExpire => write!(f, "pit-expire"),
96            Self::NackIn => write!(f, "nack-in"),
97            Self::NackOut => write!(f, "nack-out"),
98            Self::FaceUp => write!(f, "face-up"),
99            Self::FaceDown => write!(f, "face-down"),
100            Self::StrategyDecision => write!(f, "strategy-decision"),
101            Self::Custom(s) => write!(f, "{s}"),
102        }
103    }
104}
105
106/// Thread-safe event recorder for simulation runs.
107///
108/// Create one per simulation, pass references to components that need to
109/// record events, then retrieve the full event log after the run.
110pub struct SimTracer {
111    start: Instant,
112    events: Mutex<Vec<SimEvent>>,
113}
114
115impl SimTracer {
116    pub fn new() -> Self {
117        Self {
118            start: Instant::now(),
119            events: Mutex::new(Vec::new()),
120        }
121    }
122
123    /// Record a pre-built event.
124    pub fn record(&self, event: SimEvent) {
125        self.events.lock().unwrap().push(event);
126    }
127
128    /// Record an event with automatic timestamping.
129    pub fn record_now(
130        &self,
131        node: usize,
132        face: Option<u32>,
133        kind: EventKind,
134        name: impl Into<String>,
135        detail: Option<String>,
136    ) {
137        let ts = self.start.elapsed().as_micros() as u64;
138        self.record(SimEvent {
139            timestamp_us: ts,
140            node,
141            face,
142            kind,
143            name: name.into(),
144            detail,
145        });
146    }
147
148    /// Get a snapshot of all recorded events.
149    pub fn events(&self) -> Vec<SimEvent> {
150        self.events.lock().unwrap().clone()
151    }
152
153    /// Number of recorded events.
154    pub fn len(&self) -> usize {
155        self.events.lock().unwrap().len()
156    }
157
158    /// Whether any events have been recorded.
159    pub fn is_empty(&self) -> bool {
160        self.events.lock().unwrap().is_empty()
161    }
162
163    /// Clear all recorded events.
164    pub fn clear(&self) {
165        self.events.lock().unwrap().clear();
166    }
167
168    /// Filter events by node.
169    pub fn events_for_node(&self, node: usize) -> Vec<SimEvent> {
170        self.events
171            .lock()
172            .unwrap()
173            .iter()
174            .filter(|e| e.node == node)
175            .cloned()
176            .collect()
177    }
178
179    /// Filter events by kind.
180    pub fn events_of_kind(&self, kind: &EventKind) -> Vec<SimEvent> {
181        self.events
182            .lock()
183            .unwrap()
184            .iter()
185            .filter(|e| &e.kind == kind)
186            .cloned()
187            .collect()
188    }
189
190    /// Serialize all events to JSON lines format.
191    pub fn to_json(&self) -> String {
192        let events = self.events.lock().unwrap();
193        let mut output = String::new();
194        output.push('[');
195        for (i, event) in events.iter().enumerate() {
196            if i > 0 {
197                output.push(',');
198            }
199            output.push('\n');
200            output.push_str(&format!(
201                r#"  {{"t":{},"node":{},"face":{},"kind":"{}","name":"{}""#,
202                event.timestamp_us,
203                event.node,
204                event.face.map_or("null".to_string(), |f| f.to_string()),
205                event.kind,
206                event.name,
207            ));
208            if let Some(ref detail) = event.detail {
209                output.push_str(&format!(r#","detail":"{detail}""#));
210            }
211            output.push('}');
212        }
213        output.push_str("\n]");
214        output
215    }
216}
217
218impl Default for SimTracer {
219    fn default() -> Self {
220        Self::new()
221    }
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    #[test]
229    fn tracer_records_and_retrieves() {
230        let tracer = SimTracer::new();
231        tracer.record_now(0, Some(1), EventKind::InterestIn, "/test", None);
232        tracer.record_now(1, Some(2), EventKind::DataOut, "/test", Some("ok".into()));
233
234        assert_eq!(tracer.len(), 2);
235        let events = tracer.events();
236        assert_eq!(events[0].kind, EventKind::InterestIn);
237        assert_eq!(events[1].detail.as_deref(), Some("ok"));
238    }
239
240    #[test]
241    fn filter_by_node() {
242        let tracer = SimTracer::new();
243        tracer.record_now(0, None, EventKind::FaceUp, "/", None);
244        tracer.record_now(1, None, EventKind::FaceUp, "/", None);
245        tracer.record_now(0, None, EventKind::InterestIn, "/test", None);
246
247        let node0 = tracer.events_for_node(0);
248        assert_eq!(node0.len(), 2);
249    }
250
251    #[test]
252    fn json_output() {
253        let tracer = SimTracer::new();
254        tracer.record(SimEvent {
255            timestamp_us: 100,
256            node: 0,
257            face: Some(1),
258            kind: EventKind::CacheHit,
259            name: "/test".into(),
260            detail: None,
261        });
262        let json = tracer.to_json();
263        assert!(json.contains("cache-hit"));
264        assert!(json.contains("/test"));
265    }
266}