use crate::{SimTrace, TraceEventKind};
#[derive(Debug, Clone, Default)]
pub struct SimStats {
pub messages_sent: u64,
pub messages_delivered: u64,
pub messages_dropped: u64,
pub state_transitions: u64,
pub faults_injected: u64,
pub faults_healed: u64,
pub storage_ops: u64,
pub timers_fired: u64,
pub custom_events: u64,
latency_samples: Vec<u64>,
}
impl SimStats {
pub fn from_trace(trace: &SimTrace) -> Self {
let mut stats = Self::default();
for event in trace.events() {
match &event.kind {
TraceEventKind::MessageSent { .. } => stats.messages_sent += 1,
TraceEventKind::MessageDelivered { .. } => stats.messages_delivered += 1,
TraceEventKind::MessageDropped { .. } => stats.messages_dropped += 1,
TraceEventKind::StateTransition { .. } => stats.state_transitions += 1,
TraceEventKind::FaultInjected { .. } => stats.faults_injected += 1,
TraceEventKind::FaultHealed { .. } => stats.faults_healed += 1,
TraceEventKind::StorageOp { .. } => stats.storage_ops += 1,
TraceEventKind::TimerFired { .. } => stats.timers_fired += 1,
TraceEventKind::Custom { .. } => stats.custom_events += 1,
}
}
let events = trace.events();
for (i, event) in events.iter().enumerate() {
if let TraceEventKind::MessageDelivered { from, msg_type, .. } = &event.kind {
for j in (0..i).rev() {
if let TraceEventKind::MessageSent {
to, msg_type: st, ..
} = &events[j].kind
&& *to == event.node_id
&& st == msg_type
&& events[j].node_id == *from
{
let latency = event.tick.saturating_sub(events[j].tick);
stats.latency_samples.push(latency);
break;
}
}
}
}
stats.latency_samples.sort();
stats
}
pub fn latency_percentile(&self, p: f64) -> u64 {
if self.latency_samples.is_empty() {
return 0;
}
let idx = ((p / 100.0) * (self.latency_samples.len() - 1) as f64).round() as usize;
let idx = idx.min(self.latency_samples.len() - 1);
self.latency_samples[idx]
}
pub fn p50(&self) -> u64 {
self.latency_percentile(50.0)
}
pub fn p95(&self) -> u64 {
self.latency_percentile(95.0)
}
pub fn p99(&self) -> u64 {
self.latency_percentile(99.0)
}
pub fn avg_latency(&self) -> f64 {
if self.latency_samples.is_empty() {
return 0.0;
}
self.latency_samples.iter().sum::<u64>() as f64 / self.latency_samples.len() as f64
}
pub fn latency_sample_count(&self) -> usize {
self.latency_samples.len()
}
pub fn summary(&self) -> String {
let mut lines = Vec::new();
lines.push("=== Simulation Statistics ===".to_string());
lines.push(format!(
"Messages: sent={}, delivered={}, dropped={}",
self.messages_sent, self.messages_delivered, self.messages_dropped
));
lines.push(format!("State transitions: {}", self.state_transitions));
lines.push(format!("Storage ops: {}", self.storage_ops));
lines.push(format!("Timers fired: {}", self.timers_fired));
lines.push(format!(
"Faults: injected={}, healed={}",
self.faults_injected, self.faults_healed
));
if !self.latency_samples.is_empty() {
lines.push(format!(
"Latency (ticks): avg={:.1}, p50={}, p95={}, p99={}",
self.avg_latency(),
self.p50(),
self.p95(),
self.p99()
));
}
lines.join("\n")
}
pub fn to_json(&self) -> String {
serde_json::json!({
"messages_sent": self.messages_sent,
"messages_delivered": self.messages_delivered,
"messages_dropped": self.messages_dropped,
"state_transitions": self.state_transitions,
"faults_injected": self.faults_injected,
"faults_healed": self.faults_healed,
"storage_ops": self.storage_ops,
"timers_fired": self.timers_fired,
"custom_events": self.custom_events,
"latency_avg": self.avg_latency(),
"latency_p50": self.p50(),
"latency_p95": self.p95(),
"latency_p99": self.p99(),
"latency_samples": self.latency_samples.len(),
})
.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::SimTrace;
fn build_test_trace() -> SimTrace {
let mut trace = SimTrace::new();
trace.record(
1,
1,
TraceEventKind::MessageSent {
to: 2,
msg_type: "AppendEntries".into(),
size_bytes: 100,
},
);
trace.record(
2,
2,
TraceEventKind::MessageDelivered {
from: 1,
msg_type: "AppendEntries".into(),
size_bytes: 100,
},
);
trace.record(
3,
1,
TraceEventKind::MessageSent {
to: 2,
msg_type: "Heartbeat".into(),
size_bytes: 10,
},
);
trace.record(
5,
2,
TraceEventKind::MessageDelivered {
from: 1,
msg_type: "Heartbeat".into(),
size_bytes: 10,
},
);
trace.record(
10,
1,
TraceEventKind::StateTransition {
from_state: "Follower".into(),
to_state: "Leader".into(),
metadata: "term=1".into(),
},
);
trace.record(
15,
0,
TraceEventKind::FaultInjected {
fault_type: "partition".into(),
details: "test".into(),
},
);
trace.record(
20,
0,
TraceEventKind::FaultHealed {
fault_type: "partition".into(),
details: "test".into(),
},
);
trace.record(
25,
1,
TraceEventKind::StorageOp {
op_type: "put".into(),
key_count: 5,
},
);
trace.record(
30,
1,
TraceEventKind::TimerFired {
timer_type: "election".into(),
},
);
trace.record(
35,
0,
TraceEventKind::MessageDropped {
from: 1,
to: 3,
reason: "partition".into(),
},
);
trace
}
#[test]
fn test_stats_message_counts() {
let trace = build_test_trace();
let stats = SimStats::from_trace(&trace);
assert_eq!(stats.messages_sent, 2);
assert_eq!(stats.messages_delivered, 2);
assert_eq!(stats.messages_dropped, 1);
}
#[test]
fn test_stats_event_counts() {
let trace = build_test_trace();
let stats = SimStats::from_trace(&trace);
assert_eq!(stats.state_transitions, 1);
assert_eq!(stats.faults_injected, 1);
assert_eq!(stats.faults_healed, 1);
assert_eq!(stats.storage_ops, 1);
assert_eq!(stats.timers_fired, 1);
}
#[test]
fn test_stats_latency_percentiles() {
let trace = build_test_trace();
let stats = SimStats::from_trace(&trace);
assert_eq!(stats.latency_sample_count(), 2);
assert!(stats.p50() >= 1 && stats.p50() <= 2);
assert_eq!(stats.p99(), 2);
assert!((stats.avg_latency() - 1.5).abs() < 0.01);
}
#[test]
fn test_stats_summary_contains_metrics() {
let trace = build_test_trace();
let stats = SimStats::from_trace(&trace);
let summary = stats.summary();
assert!(summary.contains("sent=2"));
assert!(summary.contains("delivered=2"));
assert!(summary.contains("dropped=1"));
assert!(summary.contains("State transitions: 1"));
}
#[test]
fn test_stats_json_valid() {
let trace = build_test_trace();
let stats = SimStats::from_trace(&trace);
let json = stats.to_json();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["messages_sent"], 2);
assert_eq!(parsed["messages_delivered"], 2);
assert_eq!(parsed["timers_fired"], 1);
}
#[test]
fn test_stats_empty_trace() {
let trace = SimTrace::new();
let stats = SimStats::from_trace(&trace);
assert_eq!(stats.messages_sent, 0);
assert_eq!(stats.p50(), 0);
assert_eq!(stats.avg_latency(), 0.0);
}
}