1use crate::{SimTrace, TraceEventKind};
6
7#[derive(Debug, Clone, Default)]
9pub struct SimStats {
10 pub messages_sent: u64,
11 pub messages_delivered: u64,
12 pub messages_dropped: u64,
13 pub state_transitions: u64,
14 pub faults_injected: u64,
15 pub faults_healed: u64,
16 pub storage_ops: u64,
17 pub timers_fired: u64,
18 pub custom_events: u64,
19 latency_samples: Vec<u64>,
21}
22
23impl SimStats {
24 pub fn from_trace(trace: &SimTrace) -> Self {
26 let mut stats = Self::default();
27
28 for event in trace.events() {
29 match &event.kind {
30 TraceEventKind::MessageSent { .. } => stats.messages_sent += 1,
31 TraceEventKind::MessageDelivered { .. } => stats.messages_delivered += 1,
32 TraceEventKind::MessageDropped { .. } => stats.messages_dropped += 1,
33 TraceEventKind::StateTransition { .. } => stats.state_transitions += 1,
34 TraceEventKind::FaultInjected { .. } => stats.faults_injected += 1,
35 TraceEventKind::FaultHealed { .. } => stats.faults_healed += 1,
36 TraceEventKind::StorageOp { .. } => stats.storage_ops += 1,
37 TraceEventKind::TimerFired { .. } => stats.timers_fired += 1,
38 TraceEventKind::Custom { .. } => stats.custom_events += 1,
39 }
40 }
41
42 let events = trace.events();
46 for (i, event) in events.iter().enumerate() {
47 if let TraceEventKind::MessageDelivered { from, msg_type, .. } = &event.kind {
48 for j in (0..i).rev() {
49 if let TraceEventKind::MessageSent {
50 to, msg_type: st, ..
51 } = &events[j].kind
52 && *to == event.node_id
53 && st == msg_type
54 && events[j].node_id == *from
55 {
56 let latency = event.tick.saturating_sub(events[j].tick);
57 stats.latency_samples.push(latency);
58 break;
59 }
60 }
61 }
62 }
63
64 stats.latency_samples.sort();
65 stats
66 }
67
68 pub fn latency_percentile(&self, p: f64) -> u64 {
70 if self.latency_samples.is_empty() {
71 return 0;
72 }
73 let idx = ((p / 100.0) * (self.latency_samples.len() - 1) as f64).round() as usize;
74 let idx = idx.min(self.latency_samples.len() - 1);
75 self.latency_samples[idx]
76 }
77
78 pub fn p50(&self) -> u64 {
80 self.latency_percentile(50.0)
81 }
82 pub fn p95(&self) -> u64 {
84 self.latency_percentile(95.0)
85 }
86 pub fn p99(&self) -> u64 {
88 self.latency_percentile(99.0)
89 }
90
91 pub fn avg_latency(&self) -> f64 {
93 if self.latency_samples.is_empty() {
94 return 0.0;
95 }
96 self.latency_samples.iter().sum::<u64>() as f64 / self.latency_samples.len() as f64
97 }
98
99 pub fn latency_sample_count(&self) -> usize {
101 self.latency_samples.len()
102 }
103
104 pub fn summary(&self) -> String {
106 let mut lines = Vec::new();
107 lines.push("=== Simulation Statistics ===".to_string());
108 lines.push(format!(
109 "Messages: sent={}, delivered={}, dropped={}",
110 self.messages_sent, self.messages_delivered, self.messages_dropped
111 ));
112 lines.push(format!("State transitions: {}", self.state_transitions));
113 lines.push(format!("Storage ops: {}", self.storage_ops));
114 lines.push(format!("Timers fired: {}", self.timers_fired));
115 lines.push(format!(
116 "Faults: injected={}, healed={}",
117 self.faults_injected, self.faults_healed
118 ));
119 if !self.latency_samples.is_empty() {
120 lines.push(format!(
121 "Latency (ticks): avg={:.1}, p50={}, p95={}, p99={}",
122 self.avg_latency(),
123 self.p50(),
124 self.p95(),
125 self.p99()
126 ));
127 }
128 lines.join("\n")
129 }
130
131 pub fn to_json(&self) -> String {
133 serde_json::json!({
134 "messages_sent": self.messages_sent,
135 "messages_delivered": self.messages_delivered,
136 "messages_dropped": self.messages_dropped,
137 "state_transitions": self.state_transitions,
138 "faults_injected": self.faults_injected,
139 "faults_healed": self.faults_healed,
140 "storage_ops": self.storage_ops,
141 "timers_fired": self.timers_fired,
142 "custom_events": self.custom_events,
143 "latency_avg": self.avg_latency(),
144 "latency_p50": self.p50(),
145 "latency_p95": self.p95(),
146 "latency_p99": self.p99(),
147 "latency_samples": self.latency_samples.len(),
148 })
149 .to_string()
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156 use crate::SimTrace;
157
158 fn build_test_trace() -> SimTrace {
159 let mut trace = SimTrace::new();
160 trace.record(
161 1,
162 1,
163 TraceEventKind::MessageSent {
164 to: 2,
165 msg_type: "AppendEntries".into(),
166 size_bytes: 100,
167 },
168 );
169 trace.record(
170 2,
171 2,
172 TraceEventKind::MessageDelivered {
173 from: 1,
174 msg_type: "AppendEntries".into(),
175 size_bytes: 100,
176 },
177 );
178 trace.record(
179 3,
180 1,
181 TraceEventKind::MessageSent {
182 to: 2,
183 msg_type: "Heartbeat".into(),
184 size_bytes: 10,
185 },
186 );
187 trace.record(
188 5,
189 2,
190 TraceEventKind::MessageDelivered {
191 from: 1,
192 msg_type: "Heartbeat".into(),
193 size_bytes: 10,
194 },
195 );
196 trace.record(
197 10,
198 1,
199 TraceEventKind::StateTransition {
200 from_state: "Follower".into(),
201 to_state: "Leader".into(),
202 metadata: "term=1".into(),
203 },
204 );
205 trace.record(
206 15,
207 0,
208 TraceEventKind::FaultInjected {
209 fault_type: "partition".into(),
210 details: "test".into(),
211 },
212 );
213 trace.record(
214 20,
215 0,
216 TraceEventKind::FaultHealed {
217 fault_type: "partition".into(),
218 details: "test".into(),
219 },
220 );
221 trace.record(
222 25,
223 1,
224 TraceEventKind::StorageOp {
225 op_type: "put".into(),
226 key_count: 5,
227 },
228 );
229 trace.record(
230 30,
231 1,
232 TraceEventKind::TimerFired {
233 timer_type: "election".into(),
234 },
235 );
236 trace.record(
237 35,
238 0,
239 TraceEventKind::MessageDropped {
240 from: 1,
241 to: 3,
242 reason: "partition".into(),
243 },
244 );
245 trace
246 }
247
248 #[test]
249 fn test_stats_message_counts() {
250 let trace = build_test_trace();
251 let stats = SimStats::from_trace(&trace);
252 assert_eq!(stats.messages_sent, 2);
253 assert_eq!(stats.messages_delivered, 2);
254 assert_eq!(stats.messages_dropped, 1);
255 }
256
257 #[test]
258 fn test_stats_event_counts() {
259 let trace = build_test_trace();
260 let stats = SimStats::from_trace(&trace);
261 assert_eq!(stats.state_transitions, 1);
262 assert_eq!(stats.faults_injected, 1);
263 assert_eq!(stats.faults_healed, 1);
264 assert_eq!(stats.storage_ops, 1);
265 assert_eq!(stats.timers_fired, 1);
266 }
267
268 #[test]
269 fn test_stats_latency_percentiles() {
270 let trace = build_test_trace();
271 let stats = SimStats::from_trace(&trace);
272 assert_eq!(stats.latency_sample_count(), 2);
274 assert!(stats.p50() >= 1 && stats.p50() <= 2);
275 assert_eq!(stats.p99(), 2);
276 assert!((stats.avg_latency() - 1.5).abs() < 0.01);
277 }
278
279 #[test]
280 fn test_stats_summary_contains_metrics() {
281 let trace = build_test_trace();
282 let stats = SimStats::from_trace(&trace);
283 let summary = stats.summary();
284 assert!(summary.contains("sent=2"));
285 assert!(summary.contains("delivered=2"));
286 assert!(summary.contains("dropped=1"));
287 assert!(summary.contains("State transitions: 1"));
288 }
289
290 #[test]
291 fn test_stats_json_valid() {
292 let trace = build_test_trace();
293 let stats = SimStats::from_trace(&trace);
294 let json = stats.to_json();
295 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
296 assert_eq!(parsed["messages_sent"], 2);
297 assert_eq!(parsed["messages_delivered"], 2);
298 assert_eq!(parsed["timers_fired"], 1);
299 }
300
301 #[test]
302 fn test_stats_empty_trace() {
303 let trace = SimTrace::new();
304 let stats = SimStats::from_trace(&trace);
305 assert_eq!(stats.messages_sent, 0);
306 assert_eq!(stats.p50(), 0);
307 assert_eq!(stats.avg_latency(), 0.0);
308 }
309}