use std::time::Instant;
use moq_net::bandwidth;
const HYSTERESIS: f64 = 0.05;
const RAMP: f64 = 0.25;
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct Policy {
pub max: bandwidth::Rate,
pub min: bandwidth::Rate,
}
impl Policy {
pub fn new(max: bandwidth::Rate) -> Self {
Self {
max,
min: bandwidth::Rate::from_bps(max.as_bps() / 10),
}
}
}
#[derive(Clone, Debug)]
pub struct Control {
policy: Policy,
target: bandwidth::Rate,
applied: Option<Instant>,
}
impl Control {
pub fn new(policy: Policy) -> Self {
Self {
target: policy.max,
policy,
applied: None,
}
}
pub fn target(&self) -> bandwidth::Rate {
self.target
}
pub fn update(&mut self, estimate: Option<bandwidth::Rate>, now: Instant) -> Option<bandwidth::Rate> {
let estimate = estimate?;
let min = self.policy.min.min(self.policy.max);
let desired = estimate.clamp(min, self.policy.max);
let next = if desired <= self.target {
desired
} else {
match self.applied {
Some(applied) => {
let elapsed = now.saturating_duration_since(applied).as_secs_f64();
let grown = self.target.scaled(1.0 + RAMP * elapsed);
grown.min(desired).clamp(min, self.policy.max)
}
None => desired,
}
};
let settling = next > self.target && next == self.policy.max;
if !settling && next.abs_diff(self.target) < self.target.scaled(HYSTERESIS) {
return None;
}
self.target = next;
self.applied = Some(now);
Some(next)
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
fn bps(bps: u64) -> bandwidth::Rate {
bandwidth::Rate::from_bps(bps)
}
fn control() -> Control {
Control::new(Policy::new(bps(4_000_000)))
}
#[test]
fn starts_optimistic() {
assert_eq!(control().target(), bps(4_000_000));
}
#[test]
fn drop_applies_immediately() {
let mut control = control();
assert_eq!(
control.update(Some(bps(2_000_000)), Instant::now()),
Some(bps(2_000_000))
);
assert_eq!(control.target(), bps(2_000_000));
}
#[test]
fn missing_estimate_holds_the_target() {
let mut control = control();
let now = Instant::now();
control.update(Some(bps(2_000_000)), now).unwrap();
assert_eq!(control.update(None, now + Duration::from_secs(10)), None);
assert_eq!(control.target(), bps(2_000_000));
}
#[test]
fn estimate_never_raises_above_max() {
let mut control = control();
assert_eq!(control.update(Some(bps(100_000_000)), Instant::now()), None);
assert_eq!(control.target(), bps(4_000_000));
}
#[test]
fn target_never_falls_below_min() {
let mut control = control();
assert_eq!(control.update(Some(bps(1)), Instant::now()), Some(bps(400_000)));
assert_eq!(control.target(), bps(400_000));
}
#[test]
fn raise_is_ramp_limited() {
let mut control = control();
let start = Instant::now();
control.update(Some(bps(1_000_000)), start).unwrap();
let raised = control
.update(Some(bps(4_000_000)), start + Duration::from_secs(1))
.unwrap();
assert_eq!(raised, bps(1_250_000));
}
#[test]
fn raise_eventually_reaches_the_estimate() {
let mut control = control();
let start = Instant::now();
control.update(Some(bps(1_000_000)), start).unwrap();
for tick in 1..=200 {
control.update(Some(bps(4_000_000)), start + Duration::from_millis(100 * tick));
}
assert_eq!(control.target(), bps(4_000_000));
}
#[test]
fn suppressed_raises_do_not_starve_the_ramp() {
let mut control = control();
let start = Instant::now();
control.update(Some(bps(1_000_000)), start).unwrap();
let mut raised = None;
for tick in 1..=10 {
if let Some(next) = control.update(Some(bps(4_000_000)), start + Duration::from_millis(100 * tick)) {
raised = Some((tick, next));
break;
}
}
let (tick, next) = raised.expect("a raise must eventually clear hysteresis");
assert_eq!(tick, 2);
assert_eq!(next, bps(1_050_000));
}
#[test]
fn a_raise_reaching_the_ceiling_beats_hysteresis() {
let mut control = control();
let start = Instant::now();
control.update(Some(bps(1_000_000)), start).unwrap();
let mut last = None;
for tick in 1..=200 {
if let Some(next) = control.update(Some(bps(4_000_000)), start + Duration::from_millis(100 * tick)) {
last = Some(next);
}
}
assert_eq!(last, Some(bps(4_000_000)));
assert_eq!(
control.update(Some(bps(4_000_000)), start + Duration::from_secs(60)),
None
);
}
#[test]
fn upward_jitter_stays_inside_the_deadband() {
let mut control = control();
let start = Instant::now();
control.update(Some(bps(2_000_000)), start).unwrap();
assert_eq!(
control.update(Some(bps(2_010_000)), start + Duration::from_millis(100)),
None
);
for tick in 2..=9 {
let estimate = 2_000_000 + tick * 10_000;
assert_eq!(
control.update(Some(bps(estimate)), start + Duration::from_millis(100 * tick)),
None,
"estimate {estimate} is inside the deadband"
);
}
assert_eq!(control.target(), bps(2_000_000));
assert_eq!(
control.update(Some(bps(2_200_000)), start + Duration::from_secs(2)),
Some(bps(2_200_000))
);
}
#[test]
fn small_moves_are_suppressed() {
let mut control = control();
let now = Instant::now();
control.update(Some(bps(2_000_000)), now).unwrap();
assert_eq!(control.update(Some(bps(1_960_000)), now + Duration::from_secs(1)), None);
assert_eq!(control.target(), bps(2_000_000));
assert_eq!(
control.update(Some(bps(1_600_000)), now + Duration::from_secs(2)),
Some(bps(1_600_000))
);
}
#[test]
fn inverted_bounds_use_the_ceiling() {
let mut policy = Policy::new(bps(1_000_000));
policy.min = bps(5_000_000);
let mut control = Control::new(policy);
assert_eq!(control.target(), bps(1_000_000));
assert_eq!(control.update(Some(bps(2_000_000)), Instant::now()), None);
assert_eq!(control.target(), bps(1_000_000));
}
}