#[derive(Debug, Clone)]
pub struct RttEstimator {
smoothed_ms: Option<f32>,
min_ms: Option<f32>,
var_ms: f32,
alpha: f32,
}
impl RttEstimator {
pub fn new(alpha: f32) -> Self {
Self {
smoothed_ms: None,
min_ms: None,
var_ms: 0.0,
alpha: alpha.clamp(f32::EPSILON, 1.0),
}
}
pub fn observe(&mut self, rtt_sample_ms: u64) {
let sample = rtt_sample_ms as f32;
match self.smoothed_ms {
Some(prev) => {
let beta = (self.alpha * 2.0).min(1.0);
self.var_ms += ((prev - sample).abs() - self.var_ms) * beta;
self.smoothed_ms = Some(prev + (sample - prev) * self.alpha);
}
None => {
self.smoothed_ms = Some(sample);
self.var_ms = sample / 2.0;
}
}
self.min_ms = Some(self.min_ms.map_or(sample, |m| m.min(sample)));
}
pub fn observe_pong(&mut self, origin_time_ms: u64, now_ms: u64) {
self.observe(now_ms.saturating_sub(origin_time_ms));
}
pub fn rtt_ms(&self) -> Option<f32> {
self.smoothed_ms
}
pub fn one_way_ms(&self) -> Option<f32> {
self.smoothed_ms.map(|r| r / 2.0)
}
pub fn min_rtt_ms(&self) -> Option<f32> {
self.min_ms
}
pub fn jitter_ms(&self) -> Option<f32> {
self.smoothed_ms.map(|_| self.var_ms)
}
}
impl Default for RttEstimator {
fn default() -> Self {
Self::new(0.1)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_samples_means_no_estimate() {
let e = RttEstimator::default();
assert_eq!(e.rtt_ms(), None);
assert_eq!(e.one_way_ms(), None);
}
#[test]
fn the_first_sample_sets_the_estimate() {
let mut e = RttEstimator::new(0.1);
e.observe(100);
assert_eq!(e.rtt_ms(), Some(100.0));
assert_eq!(e.one_way_ms(), Some(50.0));
}
#[test]
fn the_estimate_moves_toward_new_samples() {
let mut e = RttEstimator::new(0.5);
e.observe(100);
e.observe(200); assert_eq!(e.rtt_ms(), Some(150.0));
}
#[test]
fn the_minimum_ignores_later_jitter_spikes() {
let mut e = RttEstimator::new(0.5);
e.observe(120);
e.observe(300); e.observe(140);
assert_eq!(e.min_rtt_ms(), Some(120.0), "min stays at the lowest sample");
}
#[test]
fn observe_pong_computes_the_round_trip() {
let mut e = RttEstimator::default();
e.observe_pong(1000, 1180); assert_eq!(e.rtt_ms(), Some(180.0));
}
#[test]
fn a_steady_connection_has_low_jitter() {
let mut e = RttEstimator::new(0.3);
for _ in 0..40 {
e.observe(100); }
assert!(e.jitter_ms().unwrap() < 1.0, "constant RTT means near-zero jitter, got {:?}", e.jitter_ms());
}
#[test]
fn a_variable_connection_has_higher_jitter() {
let mut e = RttEstimator::new(0.3);
for (i, _) in (0..40).enumerate() {
e.observe(if i % 2 == 0 { 80 } else { 160 }); }
let jitter = e.jitter_ms().unwrap();
assert!(jitter > 20.0, "swinging RTT should show real jitter, got {jitter}");
}
}