use std::collections::HashMap;
use elara_core::NodeId;
#[derive(Clone, Debug)]
pub struct PeerNetworkModel {
pub offset: f64,
pub skew: f64,
pub jitter_envelope: f64,
samples: Vec<f64>,
max_samples: usize,
}
impl PeerNetworkModel {
pub fn new() -> Self {
PeerNetworkModel {
offset: 0.0,
skew: 0.0,
jitter_envelope: 0.0,
samples: Vec::new(),
max_samples: 100,
}
}
pub fn update(&mut self, local_time: f64, remote_time: f64) {
let sample = local_time - remote_time;
self.samples.push(sample);
if self.samples.len() > self.max_samples {
self.samples.remove(0);
}
if self.samples.len() >= 5 {
self.offset = Self::median(&self.samples);
self.jitter_envelope = self
.samples
.iter()
.map(|s| (s - self.offset).abs())
.fold(0.0, f64::max);
}
}
fn median(values: &[f64]) -> f64 {
let mut sorted = values.to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
let mid = sorted.len() / 2;
if sorted.len().is_multiple_of(2) {
(sorted[mid - 1] + sorted[mid]) / 2.0
} else {
sorted[mid]
}
}
}
impl Default for PeerNetworkModel {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Default)]
pub struct NetworkModel {
pub peers: HashMap<NodeId, PeerNetworkModel>,
pub latency_mean: f64,
pub jitter: f64,
pub reorder_depth: u32,
pub loss_rate: f64,
pub stability_score: f64,
}
impl NetworkModel {
pub fn new() -> Self {
NetworkModel::default()
}
pub fn update_from_packet(
&mut self,
peer: NodeId,
local_time: f64,
remote_time: f64,
_seq: u16,
) {
let peer_model = self.peers.entry(peer).or_default();
peer_model.update(local_time, remote_time);
self.update_aggregates();
}
pub fn record_reorder(&mut self, depth: u32) {
self.reorder_depth = self.reorder_depth.max(depth);
}
pub fn record_loss(&mut self, lost_count: u32, total_count: u32) {
if total_count > 0 {
let new_rate = lost_count as f64 / total_count as f64;
self.loss_rate = self.loss_rate * 0.9 + new_rate * 0.1;
}
}
fn update_aggregates(&mut self) {
if self.peers.is_empty() {
return;
}
let total_jitter: f64 = self.peers.values().map(|p| p.jitter_envelope).sum();
self.jitter = total_jitter / self.peers.len() as f64;
let jitter_factor = 1.0 / (1.0 + self.jitter * 10.0);
let loss_factor = 1.0 - self.loss_rate;
let reorder_factor = 1.0 / (1.0 + self.reorder_depth as f64 * 0.1);
self.stability_score = (jitter_factor * loss_factor * reorder_factor).clamp(0.0, 1.0);
}
pub fn get_peer(&self, peer: NodeId) -> Option<&PeerNetworkModel> {
self.peers.get(&peer)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_peer_model_update() {
let mut model = PeerNetworkModel::new();
for i in 0..20 {
let jitter = (i % 5) as f64 * 0.005; model.update(1.0 + i as f64 * 0.1, 0.95 + i as f64 * 0.1 + jitter);
}
assert!((model.offset - 0.05).abs() < 0.02);
}
#[test]
fn test_network_model_stability() {
let mut model = NetworkModel::new();
for i in 0..10 {
model.update_from_packet(NodeId::new(1), i as f64, i as f64 - 0.05, i as u16);
}
assert!(model.stability_score > 0.8);
model.record_loss(50, 100);
model.update_aggregates();
assert!(model.stability_score < 1.0);
}
}