#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PathQuality {
pub rtt_ewma_ms: f64,
pub loss_ewma: f64,
pub reorder_depth: u32,
}
impl PathQuality {
#[must_use]
pub fn new(rtt_ewma_ms: f64, loss_ewma: f64, reorder_depth: u32) -> Self {
Self {
rtt_ewma_ms: rtt_ewma_ms.max(0.0),
loss_ewma: loss_ewma.clamp(0.0, 1.0),
reorder_depth,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct AdaptiveLayoutConfig {
pub min_source_symbols: u16,
pub max_source_symbols: u16,
pub min_overhead: u16,
pub max_overhead: u16,
pub rtt_saturation_ms: f64,
pub static_source_symbols: u16,
pub static_overhead: u16,
}
impl Default for AdaptiveLayoutConfig {
fn default() -> Self {
Self {
min_source_symbols: 4,
max_source_symbols: 64,
min_overhead: 2,
max_overhead: 32,
rtt_saturation_ms: 200.0,
static_source_symbols: 16,
static_overhead: 4,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BlockLayoutChoice {
pub source_symbols: u16,
pub overhead: u16,
}
impl AdaptiveLayoutConfig {
#[must_use]
pub fn fallback(&self) -> BlockLayoutChoice {
BlockLayoutChoice {
source_symbols: self.static_source_symbols.clamp(
self.min_source_symbols.min(self.max_source_symbols),
self.max_source_symbols.max(self.min_source_symbols),
),
overhead: self.static_overhead.clamp(
self.min_overhead.min(self.max_overhead),
self.max_overhead.max(self.min_overhead),
),
}
}
#[must_use]
pub fn derive(&self, quality: Option<PathQuality>) -> BlockLayoutChoice {
let Some(q) = quality else {
return self.fallback();
};
let overhead = lerp_u16(self.min_overhead, self.max_overhead, q.loss_ewma);
let rtt_term = if self.rtt_saturation_ms > 0.0 {
(q.rtt_ewma_ms / self.rtt_saturation_ms).clamp(0.0, 1.0)
} else {
0.0
};
let block_t = (0.6 * q.loss_ewma + 0.4 * rtt_term).clamp(0.0, 1.0);
let source_symbols = lerp_u16(self.min_source_symbols, self.max_source_symbols, block_t);
BlockLayoutChoice {
source_symbols,
overhead,
}
}
}
fn lerp_u16(lo: u16, hi: u16, t: f64) -> u16 {
if hi <= lo {
return lo;
}
let t = t.clamp(0.0, 1.0);
let span = f64::from(hi - lo);
lo + (span * t).round() as u16
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unknown_quality_uses_static_fallback() {
let cfg = AdaptiveLayoutConfig::default();
let choice = cfg.derive(None);
assert_eq!(choice.source_symbols, 16);
assert_eq!(choice.overhead, 4);
assert_eq!(choice, cfg.fallback());
}
#[test]
fn outputs_respect_floors_and_ceilings() {
let cfg = AdaptiveLayoutConfig::default();
for loss_pct in 0..=100 {
let q = PathQuality::new(50.0, f64::from(loss_pct) / 100.0, 0);
let choice = cfg.derive(Some(q));
assert!(choice.source_symbols >= cfg.min_source_symbols);
assert!(choice.source_symbols <= cfg.max_source_symbols);
assert!(choice.overhead >= cfg.min_overhead);
assert!(choice.overhead <= cfg.max_overhead);
}
}
#[test]
fn overhead_and_block_size_are_monotone_in_loss() {
let cfg = AdaptiveLayoutConfig::default();
let mut prev = cfg.derive(Some(PathQuality::new(50.0, 0.0, 0)));
let mut loss = 0u32;
while loss <= 100 {
let q = PathQuality::new(50.0, f64::from(loss) / 100.0, 0);
let choice = cfg.derive(Some(q));
assert!(
choice.overhead >= prev.overhead,
"overhead regressed at loss {loss}: {} < {}",
choice.overhead,
prev.overhead
);
assert!(
choice.source_symbols >= prev.source_symbols,
"block size regressed at loss {loss}"
);
prev = choice;
loss += 5;
}
}
#[test]
fn block_size_is_monotone_in_rtt() {
let cfg = AdaptiveLayoutConfig::default();
let mut prev = cfg.derive(Some(PathQuality::new(0.0, 0.1, 0)));
let mut rtt = 0u32;
while rtt <= 400 {
let q = PathQuality::new(f64::from(rtt), 0.1, 0);
let choice = cfg.derive(Some(q));
assert!(choice.source_symbols >= prev.source_symbols);
prev = choice;
rtt += 25;
}
}
#[test]
fn clean_path_is_smaller_than_lossy_path() {
let cfg = AdaptiveLayoutConfig::default();
let clean = cfg.derive(Some(PathQuality::new(10.0, 0.0, 0)));
let lossy = cfg.derive(Some(PathQuality::new(10.0, 0.5, 0)));
assert!(lossy.overhead > clean.overhead);
assert!(lossy.source_symbols > clean.source_symbols);
}
#[test]
fn loss_fraction_is_clamped() {
let q = PathQuality::new(-5.0, 2.0, 3);
assert_eq!(q.loss_ewma, 1.0);
assert_eq!(q.rtt_ewma_ms, 0.0);
}
#[test]
fn determinism() {
let cfg = AdaptiveLayoutConfig::default();
let q = PathQuality::new(120.0, 0.12, 2);
assert_eq!(cfg.derive(Some(q)), cfg.derive(Some(q)));
}
}