use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct OutboundBudget {
pub bytes_per_sec: Option<f64>,
pub frames_per_sec: Option<f64>,
pub burst: Duration,
}
impl OutboundBudget {
pub const DEFAULT_BURST: Duration = Duration::from_secs(1);
pub fn bytes_per_second(bytes: f64) -> Self {
debug_assert!(bytes.is_finite() && bytes >= 0.0, "a budget is a finite count per second");
Self {
bytes_per_sec: Some(bytes),
frames_per_sec: None,
burst: Self::DEFAULT_BURST,
}
}
pub fn frames_per_second(frames: f64) -> Self {
debug_assert!(frames.is_finite() && frames >= 0.0, "a budget is a finite count per second");
Self {
bytes_per_sec: None,
frames_per_sec: Some(frames),
burst: Self::DEFAULT_BURST,
}
}
pub fn and_bytes_per_second(mut self, bytes: f64) -> Self {
debug_assert!(bytes.is_finite() && bytes >= 0.0, "a budget is a finite count per second");
self.bytes_per_sec = Some(bytes);
self
}
pub fn and_frames_per_second(mut self, frames: f64) -> Self {
debug_assert!(frames.is_finite() && frames >= 0.0, "a budget is a finite count per second");
self.frames_per_sec = Some(frames);
self
}
pub fn burst(mut self, burst: Duration) -> Self {
debug_assert!(burst > Duration::ZERO, "a zero burst is owed nothing, ever");
self.burst = burst;
self
}
fn burst_secs(&self) -> f64 {
self.burst.as_secs_f64()
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Credit {
bytes: f64,
frames: f64,
last_us: u64,
}
impl Credit {
pub(crate) fn full(budget: &OutboundBudget, now_us: u64) -> Self {
Self {
bytes: budget.bytes_per_sec.map_or(0.0, |rate| rate * budget.burst_secs()),
frames: budget.frames_per_sec.map_or(0.0, |rate| rate * budget.burst_secs()),
last_us: now_us,
}
}
fn refill(&mut self, budget: &OutboundBudget, now_us: u64) {
let elapsed = now_us.saturating_sub(self.last_us) as f64 / 1_000_000.0;
self.last_us = now_us;
let burst = budget.burst_secs();
if let Some(rate) = budget.bytes_per_sec {
self.bytes = (self.bytes + elapsed * rate).min(rate * burst);
}
if let Some(rate) = budget.frames_per_sec {
self.frames = (self.frames + elapsed * rate).min(rate * burst);
}
}
pub(crate) fn owed(&mut self, budget: &OutboundBudget, now_us: u64) -> bool {
self.refill(budget, now_us);
(budget.bytes_per_sec.is_none() || self.bytes > 0.0) && (budget.frames_per_sec.is_none() || self.frames > 0.0)
}
pub(crate) fn charge(&mut self, budget: &OutboundBudget, now_us: u64, bytes: u64) {
self.refill(budget, now_us);
self.bytes -= bytes as f64;
self.frames -= 1.0;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ms(t: u64) -> u64 {
t * 1_000
}
fn sends_between(credit: &mut Credit, budget: &OutboundBudget, from: u64, to: u64, bytes: u64) -> u32 {
let mut sent = 0;
for t in from..=to {
while credit.owed(budget, ms(t)) {
credit.charge(budget, ms(t), bytes);
sent += 1;
}
}
sent
}
#[test]
fn a_budget_in_bytes_becomes_a_cadence_of_whole_frames() {
let budget = OutboundBudget::bytes_per_second(10_000.0);
let mut credit = Credit::full(&budget, 0);
assert_eq!(sends_between(&mut credit, &budget, 0, 0, 2_000), 5, "the burst goes at once");
let steady = sends_between(&mut credit, &budget, 1, 10_000, 2_000);
assert!((49..=51).contains(&steady), "then five whole frames a second, got {steady} in ten");
}
#[test]
fn a_frame_larger_than_the_burst_still_goes_and_is_then_paid_off() {
let budget = OutboundBudget::bytes_per_second(1_000.0);
let mut credit = Credit::full(&budget, 0);
assert!(credit.owed(&budget, 0));
credit.charge(&budget, 0, 5_000);
assert!(!credit.owed(&budget, ms(3_999)), "four seconds of debt");
assert!(credit.owed(&budget, ms(4_001)));
}
#[test]
fn a_budget_in_frames_is_a_snapshot_rate() {
let budget = OutboundBudget::frames_per_second(20.0);
let mut credit = Credit::full(&budget, 0);
assert_eq!(sends_between(&mut credit, &budget, 0, 0, 1), 20);
let steady = sends_between(&mut credit, &budget, 1, 10_000, 1);
assert!((199..=201).contains(&steady), "twenty a second, got {steady} in ten");
let heavy = sends_between(&mut credit, &budget, 10_001, 20_000, 1_000_000);
assert!((199..=201).contains(&heavy), "bytes are unbounded here, got {heavy} in ten");
}
#[test]
fn an_idle_connection_banks_no_more_than_its_burst() {
let budget = OutboundBudget::frames_per_second(10.0).burst(Duration::from_millis(500));
let mut credit = Credit::full(&budget, 0);
sends_between(&mut credit, &budget, 0, 0, 1);
let hour = 3_600_000;
assert_eq!(sends_between(&mut credit, &budget, hour, hour, 1), 5, "half a second's worth, however long the silence");
}
#[test]
fn both_bounds_have_to_hold() {
let budget = OutboundBudget::bytes_per_second(1_000.0).and_frames_per_second(2.0);
let mut credit = Credit::full(&budget, 0);
credit.charge(&budget, 0, 10);
credit.charge(&budget, 0, 10);
assert!(!credit.owed(&budget, 0), "bytes remain and frames do not");
assert!(credit.owed(&budget, ms(1)));
credit.charge(&budget, ms(1), 5_000);
assert!(!credit.owed(&budget, ms(1_000)), "frames remain and bytes do not");
}
}