#[derive(Debug, Clone)]
pub struct RttEstimator {
smoothed: Option<f32>,
min: Option<f32>,
var: f32,
alpha: f32,
}
impl RttEstimator {
pub fn new(alpha: f32) -> Self {
Self {
smoothed: None,
min: None,
var: 0.0,
alpha: alpha.clamp(f32::EPSILON, 1.0),
}
}
pub fn observe(&mut self, sample: u64) {
let sample = sample as f32;
match self.smoothed {
Some(prev) => {
let beta = (self.alpha * 2.0).min(1.0);
self.var += ((prev - sample).abs() - self.var) * beta;
self.smoothed = Some(prev + (sample - prev) * self.alpha);
}
None => {
self.smoothed = Some(sample);
self.var = sample / 2.0;
}
}
self.min = Some(self.min.map_or(sample, |m| m.min(sample)));
}
pub fn observe_pong(&mut self, origin: u64, now: u64) {
self.observe(now.saturating_sub(origin));
}
pub fn rtt(&self) -> Option<f32> {
self.smoothed
}
pub fn one_way(&self) -> Option<f32> {
self.smoothed.map(|r| r / 2.0)
}
pub fn min_rtt(&self) -> Option<f32> {
self.min
}
pub fn jitter(&self) -> Option<f32> {
self.smoothed.map(|_| self.var)
}
pub fn clear(&mut self) {
self.smoothed = None;
self.min = None;
self.var = 0.0;
}
}
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(), None);
assert_eq!(e.one_way(), None);
}
#[test]
fn the_first_sample_sets_the_estimate() {
let mut e = RttEstimator::new(0.1);
e.observe(100);
assert_eq!(e.rtt(), Some(100.0));
assert_eq!(e.one_way(), 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(), 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(), 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(), 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().unwrap() < 1.0, "constant RTT means near-zero jitter, got {:?}", e.jitter());
}
#[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().unwrap();
assert!(jitter > 20.0, "swinging RTT should show real jitter, got {jitter}");
}
#[test]
fn any_unit_works_because_none_is_assumed() {
let mut micros = RttEstimator::new(0.5);
let mut millis = RttEstimator::new(0.5);
micros.observe(120_000);
millis.observe(120);
assert_eq!(micros.rtt(), Some(120_000.0));
assert_eq!(millis.rtt(), Some(120.0));
}
#[test]
fn clearing_forgets_what_a_suspend_would_have_poisoned() {
let mut e = RttEstimator::new(0.5);
e.observe(100);
e.observe(600_000);
e.clear();
assert_eq!(e.rtt(), None);
assert_eq!(e.min_rtt(), None);
e.observe(100);
assert_eq!(e.rtt(), Some(100.0), "and starts over from the next sample");
}
}