use std::sync::LazyLock;
use super::types::tc::qdisc::TcRateSpec;
const TIME_UNITS_PER_SEC: f64 = 1_000_000.0;
pub const TC_RTAB_SIZE: usize = 1024;
const TC_RTAB_ENTRIES: usize = TC_RTAB_SIZE / 4;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[repr(u8)]
#[non_exhaustive]
pub enum LinkLayer {
Unaware = 0,
#[default]
Ethernet = 1,
Atm = 2,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Psched {
pub tick_in_usec: f64,
pub clock_factor: f64,
pub hz: u64,
}
impl Psched {
pub const MODERN: Psched = Psched {
tick_in_usec: 15.625,
clock_factor: 1.0,
hz: 1_000_000_000,
};
pub fn from_fields(t2us: u32, us2t: u32, clock_res: u32, nsec_per_sec: u32) -> Option<Self> {
if us2t == 0 || clock_res == 0 {
return None;
}
let t2us = if clock_res == 1_000_000_000 {
us2t
} else {
t2us
};
let clock_factor = f64::from(clock_res) / TIME_UNITS_PER_SEC;
let tick_in_usec = (f64::from(t2us) / f64::from(us2t)) * clock_factor;
let hz = if clock_res == 1_000_000 {
u64::from(nsec_per_sec)
} else {
Self::MODERN.hz
};
if !tick_in_usec.is_finite() || tick_in_usec <= 0.0 || hz == 0 {
return None;
}
Some(Self {
tick_in_usec,
clock_factor,
hz,
})
}
pub fn parse(contents: &str) -> Option<Self> {
let mut fields = contents.split_whitespace();
let mut next = || u32::from_str_radix(fields.next()?, 16).ok();
let (t2us, us2t, clock_res, nsec_per_sec) = (next()?, next()?, next()?, next()?);
Self::from_fields(t2us, us2t, clock_res, nsec_per_sec)
}
pub fn time2tick(&self, time_us: f64) -> u32 {
saturate_u32(time_us * self.tick_in_usec)
}
pub fn tick2time(&self, ticks: u32) -> f64 {
f64::from(ticks) / self.tick_in_usec
}
pub fn calc_xmittime(&self, rate_bytes_per_sec: u64, size_bytes: u32) -> u32 {
if rate_bytes_per_sec == 0 {
return 0;
}
let secs = f64::from(size_bytes) / (rate_bytes_per_sec as f64);
self.time2tick(TIME_UNITS_PER_SEC * secs)
}
pub fn calc_xmitsize(&self, rate_bytes_per_sec: u64, ticks: u32) -> u32 {
saturate_u32((rate_bytes_per_sec as f64) * self.tick2time(ticks) / TIME_UNITS_PER_SEC)
}
pub fn calc_rtable(
&self,
spec: &mut TcRateSpec,
rate_bytes_per_sec: u64,
mtu: u32,
linklayer: LinkLayer,
) -> [u8; TC_RTAB_SIZE] {
let mtu = if mtu == 0 { 2047 } else { mtu };
let mut cell_log: u8 = 0;
while (mtu >> cell_log) > (TC_RTAB_ENTRIES as u32 - 1) {
cell_log += 1;
}
let mut table = [0u8; TC_RTAB_SIZE];
for (i, entry) in table.chunks_exact_mut(4).enumerate() {
let size = adjust_size((i as u32 + 1) << cell_log, spec.mpu, linklayer);
let ticks = self.calc_xmittime(rate_bytes_per_sec, size);
entry.copy_from_slice(&ticks.to_ne_bytes());
}
spec.cell_log = cell_log;
spec.cell_align = -1;
spec.linklayer = linklayer as u8 & 0x0F;
table
}
}
fn adjust_size(size: u32, mpu: u16, linklayer: LinkLayer) -> u32 {
let size = size.max(u32::from(mpu));
match linklayer {
LinkLayer::Atm => {
size.div_ceil(48) * 53
}
LinkLayer::Ethernet | LinkLayer::Unaware => size,
}
}
fn saturate_u32(v: f64) -> u32 {
if !v.is_finite() || v <= 0.0 {
0
} else if v >= f64::from(u32::MAX) {
u32::MAX
} else {
v as u32
}
}
static PSCHED: LazyLock<Psched> = LazyLock::new(|| {
std::fs::read_to_string("/proc/net/psched")
.ok()
.and_then(|s| Psched::parse(&s))
.unwrap_or(Psched::MODERN)
});
pub fn psched() -> Psched {
*PSCHED
}
pub fn tick_in_usec() -> f64 {
psched().tick_in_usec
}
pub fn hz() -> u64 {
psched().hz
}
pub fn tc_calc_xmittime(rate_bytes_per_sec: u64, size_bytes: u32) -> u32 {
psched().calc_xmittime(rate_bytes_per_sec, size_bytes)
}
pub fn tc_calc_xmitsize(rate_bytes_per_sec: u64, ticks: u32) -> u32 {
psched().calc_xmitsize(rate_bytes_per_sec, ticks)
}
pub fn tc_calc_rtable(
spec: &mut TcRateSpec,
rate_bytes_per_sec: u64,
mtu: u32,
linklayer: LinkLayer,
) -> [u8; TC_RTAB_SIZE] {
psched().calc_rtable(spec, rate_bytes_per_sec, mtu, linklayer)
}
#[cfg(test)]
mod tests {
use super::*;
fn modern() -> Psched {
Psched::from_fields(1000, 64, 1_000_000, 1_000_000_000).unwrap()
}
#[test]
fn from_fields_matches_the_modern_constants() {
let p = modern();
assert_eq!(p.tick_in_usec, 15.625);
assert_eq!(p.clock_factor, 1.0);
assert_eq!(p.hz, 1_000_000_000);
assert_eq!(p, Psched::MODERN);
}
#[test]
fn parse_reads_the_proc_format() {
let p = Psched::parse("000003e8 00000040 000f4240 3b9aca00\n").unwrap();
assert_eq!(p, Psched::MODERN);
}
#[test]
fn parse_rejects_malformed_input() {
assert!(Psched::parse("").is_none());
assert!(Psched::parse("000003e8 00000040").is_none());
assert!(Psched::parse("zzz 00000040 000f4240 3b9aca00").is_none());
assert!(Psched::parse("000003e8 00000000 000f4240 3b9aca00").is_none());
}
#[test]
fn from_fields_handles_a_nanosecond_clock() {
let p = Psched::from_fields(1, 64, 1_000_000_000, 1_000_000_000).unwrap();
assert_eq!(p.tick_in_usec, 1000.0);
assert_eq!(p.hz, Psched::MODERN.hz);
}
#[test]
fn calc_xmittime_tbf_anchor() {
let p = modern();
assert_eq!(p.calc_xmittime(125_000, 32_768), 4_096_000);
assert_eq!(262_144_000u32 >> 6, 4_096_000);
}
#[test]
fn calc_xmittime_and_xmitsize_round_trip() {
let p = modern();
for &(rate, size) in &[
(125_000u64, 32_768u32),
(12_500_000, 1_600),
(1_250_000_000, 1_514),
(1_250, 64),
] {
let ticks = p.calc_xmittime(rate, size);
let back = p.calc_xmitsize(rate, ticks);
let slack = (rate as f64 / p.tick_in_usec / TIME_UNITS_PER_SEC).ceil() as u32 + 1;
assert!(
back.abs_diff(size) <= slack,
"rate={rate} size={size} ticks={ticks} back={back} slack={slack}",
);
}
}
#[test]
fn calc_xmittime_zero_rate_does_not_divide_by_zero() {
assert_eq!(modern().calc_xmittime(0, 1_500), 0);
assert_eq!(modern().calc_xmitsize(0, 1_500), 0);
}
#[test]
fn calc_rtable_picks_cell_log_and_stamps_the_spec() {
let p = modern();
let mut spec = TcRateSpec::new(125_000);
let table = p.calc_rtable(&mut spec, 125_000, 1514, LinkLayer::Ethernet);
assert_eq!(table.len(), TC_RTAB_SIZE);
assert_eq!(spec.cell_log, 3);
assert_eq!(spec.cell_align, -1);
assert_eq!(spec.linklayer, LinkLayer::Ethernet as u8);
assert_eq!(u32::from_ne_bytes(table[0..4].try_into().unwrap()), 1_000);
assert_eq!(
u32::from_ne_bytes(table[1020..1024].try_into().unwrap()),
256_000
);
}
#[test]
fn calc_rtable_cell_log_grows_with_mtu() {
let p = modern();
for &(mtu, expected) in &[(255u32, 0u8), (256, 1), (511, 1), (512, 2), (9000, 6)] {
let mut spec = TcRateSpec::new(125_000);
p.calc_rtable(&mut spec, 125_000, mtu, LinkLayer::Ethernet);
assert_eq!(spec.cell_log, expected, "mtu={mtu}");
assert!(
(mtu >> spec.cell_log) <= 255,
"mtu={mtu} does not fit 256 entries at cell_log={}",
spec.cell_log
);
}
}
#[test]
fn calc_rtable_atm_rounds_up_to_cells() {
assert_eq!(adjust_size(48, 0, LinkLayer::Atm), 53);
assert_eq!(adjust_size(49, 0, LinkLayer::Atm), 106);
assert_eq!(adjust_size(96, 0, LinkLayer::Atm), 106);
assert_eq!(adjust_size(96, 0, LinkLayer::Ethernet), 96);
}
#[test]
fn adjust_size_honors_mpu() {
assert_eq!(adjust_size(8, 64, LinkLayer::Ethernet), 64);
assert_eq!(adjust_size(128, 64, LinkLayer::Ethernet), 128);
}
#[test]
fn saturate_u32_clamps_instead_of_wrapping() {
assert_eq!(saturate_u32(-1.0), 0);
assert_eq!(saturate_u32(f64::NAN), 0);
assert_eq!(saturate_u32(1e30), u32::MAX);
assert_eq!(saturate_u32(42.9), 42);
}
}