#![allow(
clippy::suboptimal_flops,
reason = "preserve mosh's exact non-FMA timer arithmetic"
)]
use tracing::trace;
use crate::ssp::{SEND_INTERVAL_MAX, SEND_INTERVAL_MIN};
#[derive(Debug, Clone)]
pub struct RttEstimator {
srtt: f64,
rttvar: f64,
hit: bool,
last: Option<f64>,
}
impl Default for RttEstimator {
fn default() -> Self {
Self {
srtt: 1000.0,
rttvar: 500.0,
hit: false,
last: None,
}
}
}
impl RttEstimator {
pub fn new() -> Self {
Self::default()
}
pub fn sample(&mut self, r_ms: f64) {
if !(r_ms.is_finite()) || r_ms >= 5000.0 {
return;
}
if self.last == Some(r_ms) {
return;
}
self.last = Some(r_ms);
if self.hit {
self.rttvar = 0.75 * self.rttvar + 0.25 * (self.srtt - r_ms).abs();
self.srtt = 0.875 * self.srtt + 0.125 * r_ms;
} else {
self.srtt = r_ms;
self.rttvar = r_ms / 2.0;
self.hit = true;
}
trace!(
sample = r_ms,
srtt = self.srtt,
rttvar = self.rttvar,
"rtt sample"
);
}
pub const fn srtt_ms(&self) -> f64 {
self.srtt
}
pub fn timeout(&self) -> u64 {
let rto = (self.srtt + 4.0 * self.rttvar).ceil() as i64;
rto.clamp(50, 1000) as u64
}
pub fn send_interval(&self) -> u64 {
let si = (self.srtt / 2.0).ceil() as i64;
si.clamp(SEND_INTERVAL_MIN as i64, SEND_INTERVAL_MAX as i64) as u64
}
}
#[cfg(test)]
#[allow(
clippy::float_cmp,
reason = "these tests assert EXACT float values on purpose (e.g. the EWMA must not drift)"
)]
mod tests {
use super::*;
#[test]
fn initial_values_match_mosh() {
let e = RttEstimator::new();
assert_eq!(e.timeout(), 1000);
assert_eq!(e.send_interval(), 250);
}
#[test]
fn send_interval_is_clamped_half_srtt() {
let mut mid = RttEstimator::new();
for _ in 0..200 {
mid.sample(100.0);
}
assert!(
(45..=55).contains(&mid.send_interval()),
"≈ srtt/2 = 50, got {}",
mid.send_interval()
);
let mut fast = RttEstimator::new();
for _ in 0..200 {
fast.sample(2.0);
}
assert_eq!(fast.send_interval(), SEND_INTERVAL_MIN);
let mut slow = RttEstimator::new();
for _ in 0..200 {
slow.sample(1000.0);
}
assert_eq!(slow.send_interval(), SEND_INTERVAL_MAX);
}
#[test]
fn converges_to_low_rtt() {
let mut e = RttEstimator::new();
for _ in 0..200 {
e.sample(10.0);
}
assert!((e.srtt_ms() - 10.0).abs() < 1.0);
assert_eq!(e.timeout(), 50); assert_eq!(e.send_interval(), 20); }
#[test]
fn ignores_outliers() {
let mut e = RttEstimator::new();
e.sample(40.0); let before = e.srtt_ms();
e.sample(9000.0); assert_eq!(e.srtt_ms(), before);
}
#[test]
fn repeated_identical_samples_do_not_drift_ewma() {
let mut e = RttEstimator::new();
e.sample(100.0); e.sample(20.0); let srtt = e.srtt_ms();
let rto = e.timeout();
for _ in 0..100 {
e.sample(20.0);
}
assert_eq!(
e.srtt_ms(),
srtt,
"srtt must not drift on a repeated identical sample"
);
assert_eq!(
e.timeout(),
rto,
"rttvar (hence the RTO) must not decay on repeats"
);
e.sample(21.0);
assert_ne!(
e.srtt_ms(),
srtt,
"a changed sample still updates the estimate"
);
}
}