use std::hash::Hash;
use std::time::Duration;
use crate::Timestamp;
use crate::correlate::RollingRate;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum ByteSemantics {
#[default]
Wire,
Goodput,
}
impl ByteSemantics {
pub fn as_str(&self) -> &'static str {
match self {
ByteSemantics::Wire => "wire",
ByteSemantics::Goodput => "goodput",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Attribution(pub u64);
pub struct BandwidthByKey<K>
where
K: Hash + Eq + Clone,
{
tx: RollingRate<K, u64>,
rx: RollingRate<K, u64>,
semantics: ByteSemantics,
}
impl<K> BandwidthByKey<K>
where
K: Hash + Eq + Clone,
{
pub fn new_unbounded(window: Duration, bucket_width: Duration) -> Self {
Self::with_semantics(window, bucket_width, ByteSemantics::Wire)
}
pub fn with_semantics(
window: Duration,
bucket_width: Duration,
semantics: ByteSemantics,
) -> Self {
Self {
tx: RollingRate::new_unbounded(window, bucket_width),
rx: RollingRate::new_unbounded(window, bucket_width),
semantics,
}
}
pub fn record_tx(&mut self, key: K, bytes: u64, now: Timestamp) {
self.tx.record(key, bytes, now);
}
pub fn record_rx(&mut self, key: K, bytes: u64, now: Timestamp) {
self.rx.record(key, bytes, now);
}
pub fn tx_bps(&self, key: &K, now: Timestamp) -> f64 {
self.tx.rate(key, now)
}
pub fn rx_bps(&self, key: &K, now: Timestamp) -> f64 {
self.rx.rate(key, now)
}
pub fn total_bps(&self, key: &K, now: Timestamp) -> f64 {
self.tx.rate(key, now) + self.rx.rate(key, now)
}
pub fn top_k(&self, n: usize, now: Timestamp) -> Vec<(K, f64)> {
use std::collections::HashMap;
let mut totals: HashMap<K, f64> = HashMap::new();
for (k, r) in self.tx.snapshot(now) {
*totals.entry(k).or_default() += r;
}
for (k, r) in self.rx.snapshot(now) {
*totals.entry(k).or_default() += r;
}
let mut v: Vec<(K, f64)> = totals.into_iter().collect();
v.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
v.truncate(n);
v
}
pub fn evict_expired(&mut self, now: Timestamp) {
self.tx.evict_expired(now);
self.rx.evict_expired(now);
}
pub fn semantics(&self) -> ByteSemantics {
self.semantics
}
pub fn is_empty(&self) -> bool {
self.tx.is_empty() && self.rx.is_empty()
}
pub fn clear(&mut self) {
self.tx.clear();
self.rx.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
fn t(s: u32) -> Timestamp {
Timestamp::new(s, 0)
}
#[test]
fn tracks_tx_rx_per_key() {
let mut bw =
BandwidthByKey::<u32>::new_unbounded(Duration::from_secs(10), Duration::from_secs(1));
for s in 0..10 {
bw.record_tx(42, 1000, t(s));
bw.record_rx(7, 500, t(s));
}
let now = t(9);
assert!((bw.tx_bps(&42, now) - 1000.0).abs() < 50.0);
assert!((bw.rx_bps(&7, now) - 500.0).abs() < 50.0);
assert_eq!(bw.rx_bps(&42, now), 0.0);
}
#[test]
fn total_and_top_k_rank_by_combined_rate() {
let mut bw = BandwidthByKey::<Attribution>::new_unbounded(
Duration::from_secs(10),
Duration::from_secs(1),
);
for s in 0..10 {
bw.record_tx(Attribution(1), 100, t(s));
bw.record_rx(Attribution(1), 100, t(s)); bw.record_tx(Attribution(2), 50, t(s)); }
let now = t(9);
assert!(bw.total_bps(&Attribution(1), now) > bw.total_bps(&Attribution(2), now));
let top = bw.top_k(2, now);
assert_eq!(top[0].0, Attribution(1));
assert_eq!(top[1].0, Attribution(2));
}
#[test]
fn semantics_tag_defaults_to_wire() {
let bw =
BandwidthByKey::<u32>::new_unbounded(Duration::from_secs(10), Duration::from_secs(1));
assert_eq!(bw.semantics(), ByteSemantics::Wire);
let gp = BandwidthByKey::<u32>::with_semantics(
Duration::from_secs(10),
Duration::from_secs(1),
ByteSemantics::Goodput,
);
assert_eq!(gp.semantics(), ByteSemantics::Goodput);
assert_eq!(gp.semantics().as_str(), "goodput");
}
#[test]
fn evict_and_clear() {
let mut bw =
BandwidthByKey::<u32>::new_unbounded(Duration::from_secs(5), Duration::from_secs(1));
bw.record_tx(1, 1000, t(0));
assert!(!bw.is_empty());
bw.evict_expired(t(100));
assert_eq!(bw.tx_bps(&1, t(100)), 0.0);
bw.record_tx(1, 500, t(100));
bw.clear();
assert!(bw.is_empty());
}
}