From 62cab6a9c53030cc28c316be49c25678ec010ce4 Mon Sep 17 00:00:00 2001 From: duanxy23 Date: Sat, 6 Jun 2026 14:25:53 +0800 Subject: [PATCH 01/40] add red (without unit tests) --- rattan-core/src/cells/bandwidth/queue/mod.rs | 6 + rattan-core/src/cells/bandwidth/queue/red.rs | 270 +++++++++++++++++++ 2 files changed, 276 insertions(+) create mode 100644 rattan-core/src/cells/bandwidth/queue/red.rs diff --git a/rattan-core/src/cells/bandwidth/queue/mod.rs b/rattan-core/src/cells/bandwidth/queue/mod.rs index bca02677..cb6441a6 100644 --- a/rattan-core/src/cells/bandwidth/queue/mod.rs +++ b/rattan-core/src/cells/bandwidth/queue/mod.rs @@ -15,11 +15,17 @@ mod codel; mod drophead; mod droptail; mod infinite; +mod red; +// Duan: mod ared; +// Duan: mod pie; pub use codel::*; pub use drophead::*; pub use droptail::*; pub use infinite::*; +pub use red::*; +// Duan: pub use ared::*; +// Duan: pub use pie::*; #[cfg(feature = "serde")] fn serde_default(t: &T) -> bool { diff --git a/rattan-core/src/cells/bandwidth/queue/red.rs b/rattan-core/src/cells/bandwidth/queue/red.rs new file mode 100644 index 00000000..5dc2a9e5 --- /dev/null +++ b/rattan-core/src/cells/bandwidth/queue/red.rs @@ -0,0 +1,270 @@ +// RED Queue Implementation Reference: +// https://github.com/torvalds/linux/blob/master/include/net/red.h + +use std::collections::VecDeque; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +use tokio::time::{Instant, Duration}; +use rand::random_range; +use tracing::{debug, warn}; + +#[cfg(feature = "serde")] +use super::serde_default; +use super::{BwType, PacketQueue}; +use crate::cells::Packet; + +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize), serde(default))] +#[derive(Debug, Clone)] +pub struct RedQueueConfig { + pub packet_limit: Option, + pub byte_limit: Option, + pub w_q: f64, // queue weight for calculating the average queue length + pub min_th: usize, // minimum threshold of average queue length + pub max_th: usize, // maximum threshold of average queue length + pub max_p: f64, // maximum probability of dropping a packet + #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "serde_default"))] + pub pkt_tx_time: Duration, // typical packet tx time (us) + #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "serde_default"))] + pub bw_type: BwType, +} + +impl Default for RedQueueConfig { + fn default() -> Self { + Self { + packet_limit: None, + byte_limit: None, + w_q: 0.002, + min_th: 7500, // 5 * 1500 bytes + max_th: 22500, // 15 * 1500 bytes + max_p: 0.02, + pkt_tx_time: Duration::from_micros(120), // 1500 bytes * 8 / 100Mbps = 120 us + bw_type: BwType::default(), + } + } +} + +impl RedQueueConfig { + pub fn new>, B: Into>>( + packet_limit: A, + byte_limit: B, + w_q: f64, + min_th: usize, + max_th: usize, + max_p: f64, + pkt_tx_time: Duration, + bw_type: BwType + ) -> Self { + // Warning: The caller must ensure that the parameters are valid. + // It's recommended to do validation before calling this function, + // or we may need to return a Result instead of Self in the future. + if min_th >= max_th { + warn!("RedQueueConfig: min_th ({}) >= max_th ({}), which may cause invalid behavior.", min_th, max_th); + } + if pkt_tx_time.as_micros() == 0 { + warn!("RedQueueConfig: pkt_tx_time is 0, which will cause divide-by-zero in m calculation."); + } + if !(0.0..=1.0).contains(&w_q) { + warn!("RedQueueConfig: w_q ({}) is out of expected range [0.0, 1.0]. This is an EWMA weight.", w_q); + } + if !(0.0..=1.0).contains(&max_p) { + warn!("RedQueueConfig: max_p ({}) is out of expected range [0.0, 1.0]. This is a probability.", max_p); + } + + Self { + packet_limit: packet_limit.into(), + byte_limit: byte_limit.into(), + w_q, + min_th, + max_th, + max_p, + pkt_tx_time, + bw_type + } + } +} + +impl

From for RedQueue

{ + fn from(config: RedQueueConfig) -> Self { + RedQueue::new(config) + } +} + +#[derive(Debug)] +pub struct RedQueue

{ + queue: VecDeque

, + config: RedQueueConfig, + now_bytes: usize, // for calculating average_queue_length + average_queue_length: f64, + count_packet: i32, // number of packets since last dropping + idle_start: Option, // start time of current idle period +} + +impl

RedQueue

{ + pub fn new(config: RedQueueConfig) -> Self { + debug!(?config, "New RedQueue"); + Self { + queue: VecDeque::new(), + config, + now_bytes: 0, + average_queue_length: 0.0, + count_packet: -1, + idle_start: None, + } + } +} + +impl

Default for RedQueue

+where + P: Packet +{ + fn default() -> Self { + Self::new(RedQueueConfig::default()) + } +} + +impl

RedQueue

+where + P: Packet, +{ + fn update_avg (&mut self) { + match self.is_empty() { + false => { + self.average_queue_length = (1.0 - self.config.w_q) * self.average_queue_length + self.config.w_q * (self.now_bytes as f64) + }, + true => { + if let Some(idle_start) = self.idle_start { + let now = Instant::now(); + let idle_duration = now.duration_since(idle_start); + let m = idle_duration.as_micros() as f64 / self.config.pkt_tx_time.as_micros() as f64; + self.average_queue_length *= f64::powf(1.0 - self.config.w_q, m); + self.idle_start = Some(now); + } + } + } + } + + fn should_drop (&mut self) -> bool { + let avg = self.average_queue_length; + let min_th = self.config.min_th as f64; + let max_th = self.config.max_th as f64; + if avg >= min_th && avg < max_th { + self.count_packet += 1; + let p_b = self.config.max_p * (avg - min_th) / (max_th - min_th); + let p_a = if self.count_packet as f64 * p_b >= 1.0 { + 1.0 + } else { + p_b / (1.0 - self.count_packet as f64 * p_b) + }; + + let rand_val = random_range(0.0 .. 1.0); + if rand_val < p_a { + self.count_packet = 0; + true + } else { + false + } + } else if avg >= max_th { + self.count_packet = 0; + true + } else { + self.count_packet = -1; + false + } + } +} + +impl

PacketQueue

for RedQueue

+where + P: Packet, +{ + type Config = RedQueueConfig; + + fn configure(&mut self, config: Self::Config) { + self.config = config; + } + + fn is_zero_buffer(&self) -> bool { + self.config.packet_limit.is_some_and(|limit| limit == 0) + || self.config.byte_limit.is_some_and(|limit| limit == 0) + } + + fn enqueue(&mut self, packet: P) { + if self + .config + .packet_limit + .is_none_or(|limit| self.queue.len() < limit) + && self.config.byte_limit.is_none_or(|limit| { + self.now_bytes + packet.l3_length() + self.config.bw_type.extra_length() <= limit + }) + { + let packet_size = packet.l3_length() + self.get_extra_length(); + self.update_avg(); + match self.should_drop() { + true => { + #[cfg(test)] + tracing::trace!( + avg = self.average_queue_length, + count = self.count_packet, + header = ?format!("{:X?}", &packet.as_slice()[0..std::cmp::min(56, packet.length())]), + "Drop packet(l3_len: {}, extra_len: {}) due to RED algorithm", packet.l3_length(), self.get_extra_length() + ); + return; + }, + false => { + self.now_bytes += packet_size; + self.queue.push_back(packet); + self.idle_start = None; + } + } + } else { + self.count_packet = 0; + #[cfg(test)] + tracing::trace!( + queue_len = self.queue.len(), + now_bytes = self.now_bytes, + header = ?format!("{:X?}", &packet.as_slice()[0..std::cmp::min(56, packet.length())]), + "Drop packet(l3_len: {}, extra_len: {}) due to hard limit", packet.l3_length(), self.config.bw_type.extra_length() + ); + } + } + + fn dequeue(&mut self) -> Option

{ + match self.queue.pop_front() { + Some(packet ) => { + self.now_bytes -= packet.l3_length() + self.get_extra_length(); + if self.is_empty() { + self.idle_start = Some(Instant::now()); + } + Some(packet) + }, + None => None + } + } + + fn is_empty(&self) -> bool { + self.queue.is_empty() + } + + #[inline(always)] + fn get_extra_length(&self) -> usize { + self.config.bw_type.extra_length() + } + + fn get_front_size(&self) -> Option { + self.queue + .front() + .map(|packet| self.get_packet_size(packet)) + } + + fn length(&self) -> usize { + self.queue.len() + } + + fn retain(&mut self, mut f: F) + where + F: FnMut(&P) -> bool, + { + self.queue.retain(|packet| f(packet)); + } +} \ No newline at end of file From 6e536be343a498748b70a97e9fadfe9c0f6b47b5 Mon Sep 17 00:00:00 2001 From: duanxy23 Date: Sat, 6 Jun 2026 19:20:00 +0800 Subject: [PATCH 02/40] add red unit tests --- rattan-core/src/cells/bandwidth/queue/red.rs | 139 +++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/rattan-core/src/cells/bandwidth/queue/red.rs b/rattan-core/src/cells/bandwidth/queue/red.rs index 5dc2a9e5..360dd833 100644 --- a/rattan-core/src/cells/bandwidth/queue/red.rs +++ b/rattan-core/src/cells/bandwidth/queue/red.rs @@ -267,4 +267,143 @@ where { self.queue.retain(|packet| f(packet)); } +} + + +#[cfg(test)] +mod tests { + use super::*; + use crate::cells::{Packet, StdPacket}; + use tokio::time::Instant; + + fn create_packet(size: usize) -> StdPacket { + let buf = vec![0u8; size]; + StdPacket::with_timestamp(&buf, Instant::now()) + } + + #[test_log::test] + fn test_red_queue_basic() { + let mut config = RedQueueConfig::default(); + config.min_th = 1000; + config.max_th = 2000; + let mut queue: RedQueue = RedQueue::new(config); + + assert!(queue.is_empty()); + + let pkt1 = create_packet(500); + queue.enqueue(pkt1); + assert!(!queue.is_empty()); + assert_eq!(queue.length(), 1); + + let dequeued = queue.dequeue(); + assert!(dequeued.is_some()); + assert!(queue.is_empty()); + } + + #[test_log::test] + fn test_red_queue_hard_limit_packet() { + let mut config = RedQueueConfig::default(); + config.packet_limit = Some(2); + config.min_th = 100000; // avoid red drop + config.max_th = 200000; + let mut queue: RedQueue = RedQueue::new(config); + + queue.enqueue(create_packet(100)); + queue.enqueue(create_packet(100)); + assert_eq!(queue.length(), 2); + + // This one should be dropped due to packet limit + queue.enqueue(create_packet(100)); + assert_eq!(queue.length(), 2); + } + + #[test_log::test] + fn test_red_queue_hard_limit_byte() { + let mut config = RedQueueConfig::default(); + config.byte_limit = Some(150); + config.min_th = 100000; // avoid red drop + config.max_th = 200000; + let mut queue: RedQueue = RedQueue::new(config); + + queue.enqueue(create_packet(100)); // l3 length 86. + assert_eq!(queue.length(), 1); + + // This one should be dropped due to byte limit (86 + 86 > 150) + queue.enqueue(create_packet(100)); + assert_eq!(queue.length(), 1); + } + + #[test_log::test] + fn test_red_queue_max_th_drop() { + let mut config = RedQueueConfig::default(); + config.min_th = 100; + config.max_th = 200; + config.w_q = 1.0; // max weight, avg matches instantly + let mut queue: RedQueue = RedQueue::new(config); + + // First packet + queue.enqueue(create_packet(100)); + + // Second packet + queue.enqueue(create_packet(300)); + + // At this point, queue length is 2, now_bytes is high enough. + // The next enqueue should see average_queue_length > max_th and drop the packet. + let before_len = queue.length(); + queue.enqueue(create_packet(100)); + assert_eq!(queue.length(), before_len, "Packet should be dropped by RED max_th"); + } + + #[test_log::test] + fn test_red_queue_min_th_no_drop() { + let mut config = RedQueueConfig::default(); + config.min_th = 1000; + config.max_th = 2000; + config.w_q = 1.0; // Instantly reach exact byte size + let mut queue: RedQueue = RedQueue::new(config); + + // First packet: queue empty, avg remains 0. + queue.enqueue(create_packet(514)); // L3 size = 514 - 14 (Ethernet header) = 500 + assert_eq!(queue.length(), 1); + + // Second packet: queue has 500 bytes. w_q=1.0 makes avg = 500. + // 500 < min_th(1000), so it should not drop. + queue.enqueue(create_packet(414)); // L3 size = 400 + assert_eq!(queue.length(), 2); + + // Check internal state: count_packet is -1 when avg < min_th + assert_eq!(queue.count_packet, -1); + } + + #[test_log::test] + fn test_red_queue_probabilistic_drop() { + let mut config = RedQueueConfig::default(); + config.min_th = 100; + config.max_th = 300; + config.max_p = 0.5; + config.w_q = 1.0; + let mut queue: RedQueue = RedQueue::new(config); + + // First packet: queue empty, avg = 0. L3 size = 200. + queue.enqueue(create_packet(214)); + assert_eq!(queue.length(), 1); + + let mut drop_count = 0; + let total_packets = 1000; + + for _ in 0..total_packets { + // enqueue packets with L3 size 0 (total size 14). + // now_bytes stays at 200. w_q=1.0 makes avg exactly 200. + // 100 (min_th) <= avg(200) < 300 (max_th), entering probabilistic drop zone. + let before = queue.length(); + queue.enqueue(create_packet(14)); + if queue.length() == before { + drop_count += 1; + } + } + + // It should drop some packets, but not all of them + assert!(drop_count > 0, "Should have dropped some packets probabilistically"); + assert!(drop_count < total_packets, "Should not drop all packets"); + } } \ No newline at end of file From e1bb737851b155c48437dfeabd2891451cfd2192 Mon Sep 17 00:00:00 2001 From: duanxy23 Date: Sun, 7 Jun 2026 00:10:52 +0800 Subject: [PATCH 03/40] add ared & fix red --- rattan-core/src/cells/bandwidth/queue/ared.rs | 290 ++++++++++++++++++ rattan-core/src/cells/bandwidth/queue/mod.rs | 4 +- rattan-core/src/cells/bandwidth/queue/red.rs | 10 +- 3 files changed, 298 insertions(+), 6 deletions(-) create mode 100644 rattan-core/src/cells/bandwidth/queue/ared.rs diff --git a/rattan-core/src/cells/bandwidth/queue/ared.rs b/rattan-core/src/cells/bandwidth/queue/ared.rs new file mode 100644 index 00000000..e12ed29e --- /dev/null +++ b/rattan-core/src/cells/bandwidth/queue/ared.rs @@ -0,0 +1,290 @@ +// Adaptive RED Queue Implementation Reference: +// https://www.icir.org/floyd/papers/adaptiveRed.pdf#:~:text=We%20find%20that%20this%20re-vised%20version%20of%20Adaptive,length%20in%20a%20wide%20variety%20of%20traffic%20scenarios. + +use std::collections::VecDeque; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +use tokio::time::{Instant, Duration}; +use rand::random_range; +use tracing::{debug, warn}; + +#[cfg(feature = "serde")] +use super::serde_default; +use super::{BwType, PacketQueue}; +use crate::cells::Packet; + +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize), serde(default))] +#[derive(Debug, Clone)] +pub struct AdaptiveRedQueueConfig { + pub packet_limit: Option, + pub byte_limit: Option, + pub w_q: f64, // queue weight for calculating the average queue length + pub min_th: usize, // minimum threshold of average queue length + pub max_th: usize, // maximum threshold of average queue length + pub max_p: f64, // maximum probability of dropping a packet + #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "serde_default"))] + pub pkt_tx_time: Duration, // typical packet tx time (us) + #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "serde_default"))] + pub bw_type: BwType, +} + +impl Default for AdaptiveRedQueueConfig { + fn default() -> Self { + Self { + packet_limit: None, + byte_limit: None, + w_q: 0.002, + min_th: 7500, // 5 * 1500 bytes + max_th: 22500, // 15 * 1500 bytes + max_p: 0.02, + pkt_tx_time: Duration::from_micros(120), // 1500 bytes * 8 / 100Mbps = 120 us + bw_type: BwType::default(), + } + } +} + +impl AdaptiveRedQueueConfig { + pub fn new>, B: Into>>( + packet_limit: A, + byte_limit: B, + w_q: f64, + min_th: usize, + max_th: usize, + max_p: f64, + pkt_tx_time: Duration, + bw_type: BwType + ) -> Self { + // Warning: The caller must ensure that the parameters are valid. + // It's recommended to do validation before calling this function, + // or we may need to return a Result instead of Self in the future. + if min_th >= max_th { + warn!("AdaptiveRedQueueConfig: min_th ({}) >= max_th ({}), which may cause invalid behavior.", min_th, max_th); + } + if pkt_tx_time.as_micros() == 0 { + warn!("AdaptiveRedQueueConfig: pkt_tx_time is 0, which will cause divide-by-zero in m calculation."); + } + if !(0.0..=1.0).contains(&w_q) { + warn!("AdaptiveRedQueueConfig: w_q ({}) is out of expected range [0.0, 1.0]. This is an EWMA weight.", w_q); + } + if !(0.0..=1.0).contains(&max_p) { + warn!("AdaptiveRedQueueConfig: max_p ({}) is out of expected range [0.0, 1.0]. This is a probability.", max_p); + } + + Self { + packet_limit: packet_limit.into(), + byte_limit: byte_limit.into(), + w_q, + min_th, + max_th, + max_p, + pkt_tx_time, + bw_type + } + } +} + +impl

From for AdaptiveRedQueue

{ + fn from(config: AdaptiveRedQueueConfig) -> Self { + AdaptiveRedQueue::new(config) + } +} + +#[derive(Debug)] +pub struct AdaptiveRedQueue

{ + queue: VecDeque

, + config: AdaptiveRedQueueConfig, + now_bytes: usize, // for calculating average_queue_length + average_queue_length: f64, + count_packet: i32, // number of packets since last dropping + idle_start: Option, // start time of current idle period + latest_max_p_update: Instant, // latest time when max_p is updates +} + +impl

AdaptiveRedQueue

{ + pub fn new(config: AdaptiveRedQueueConfig) -> Self { + debug!(?config, "New AdaptiveRedQueue"); + Self { + queue: VecDeque::new(), + config, + now_bytes: 0, + average_queue_length: 0.0, + count_packet: -1, + idle_start: None, + latest_max_p_update: Instant::now(), + } + } +} + +impl

Default for AdaptiveRedQueue

+where + P: Packet +{ + fn default() -> Self { + Self::new(AdaptiveRedQueueConfig::default()) + } +} + +impl

AdaptiveRedQueue

+where + P: Packet, +{ + fn update_avg (&mut self) { + match self.is_empty() { + false => { + self.average_queue_length = (1.0 - self.config.w_q) * self.average_queue_length + self.config.w_q * (self.now_bytes as f64) + }, + true => { + if let Some(idle_start) = self.idle_start { + let now = Instant::now(); + let idle_duration = now.saturating_duration_since(idle_start); + let m = idle_duration.as_micros() as f64 / self.config.pkt_tx_time.as_micros() as f64; + self.average_queue_length *= f64::powf(1.0 - self.config.w_q, m); + self.idle_start = Some(now); + } + } + } + } + + fn should_drop (&mut self) -> bool { + let avg = self.average_queue_length; + let min_th = self.config.min_th as f64; + let max_th = self.config.max_th as f64; + if avg >= min_th && avg < max_th { + self.count_packet += 1; + let p_b = self.config.max_p * (avg - min_th) / (max_th - min_th); + let p_a = if self.count_packet as f64 * p_b >= 1.0 { + 1.0 + } else { + p_b / (1.0 - self.count_packet as f64 * p_b) + }; + + let rand_val = random_range(0.0 .. 1.0); + if rand_val < p_a { + self.count_packet = 0; + true + } else { + false + } + } else if avg >= max_th { + self.count_packet = 0; + true + } else { + self.count_packet = -1; + false + } + } + + fn update_max_p (&mut self) { + let target_min = self.config.min_th as f64 + 0.4 * (self.config.max_th - self.config.min_th) as f64; + let target_max = self.config.min_th as f64 + 0.6 * (self.config.max_th - self.config.min_th) as f64; + if self.average_queue_length > target_max && self.config.max_p <= 0.5 { + self.config.max_p += (self.config.max_p / 4.0).min(0.01); + } else if self.average_queue_length < target_min && self.config.max_p >= 0.01 { + self.config.max_p *= 0.9; + } + } +} + +impl

PacketQueue

for AdaptiveRedQueue

+where + P: Packet, +{ + type Config = AdaptiveRedQueueConfig; + + fn configure(&mut self, config: Self::Config) { + self.config = config; + } + + fn is_zero_buffer(&self) -> bool { + self.config.packet_limit.is_some_and(|limit| limit == 0) + || self.config.byte_limit.is_some_and(|limit| limit == 0) + } + + fn enqueue(&mut self, packet: P) { + self.update_avg(); + + let now = Instant::now(); + if now.saturating_duration_since(self.latest_max_p_update) >= Duration::from_millis(500) { + self.update_max_p(); + self.latest_max_p_update = now; + } + + if self + .config + .packet_limit + .is_none_or(|limit| self.queue.len() < limit) + && self.config.byte_limit.is_none_or(|limit| { + self.now_bytes + packet.l3_length() + self.config.bw_type.extra_length() <= limit + }) + { + let packet_size = packet.l3_length() + self.get_extra_length(); + + match self.should_drop() { + true => { + #[cfg(test)] + tracing::trace!( + avg = self.average_queue_length, + count = self.count_packet, + header = ?format!("{:X?}", &packet.as_slice()[0..std::cmp::min(56, packet.length())]), + "Drop packet(l3_len: {}, extra_len: {}) due to ARED algorithm", packet.l3_length(), self.get_extra_length() + ); + return; + }, + false => { + self.now_bytes += packet_size; + self.queue.push_back(packet); + self.idle_start = None; + } + } + } else { + self.count_packet = 0; + #[cfg(test)] + tracing::trace!( + queue_len = self.queue.len(), + now_bytes = self.now_bytes, + header = ?format!("{:X?}", &packet.as_slice()[0..std::cmp::min(56, packet.length())]), + "Drop packet(l3_len: {}, extra_len: {}) due to hard limit", packet.l3_length(), self.config.bw_type.extra_length() + ); + } + } + + fn dequeue(&mut self) -> Option

{ + match self.queue.pop_front() { + Some(packet ) => { + self.now_bytes -= packet.l3_length() + self.get_extra_length(); + if self.is_empty() { + self.idle_start = Some(Instant::now()); + } + Some(packet) + }, + None => None + } + } + + fn is_empty(&self) -> bool { + self.queue.is_empty() + } + + #[inline(always)] + fn get_extra_length(&self) -> usize { + self.config.bw_type.extra_length() + } + + fn get_front_size(&self) -> Option { + self.queue + .front() + .map(|packet| self.get_packet_size(packet)) + } + + fn length(&self) -> usize { + self.queue.len() + } + + fn retain(&mut self, mut f: F) + where + F: FnMut(&P) -> bool, + { + self.queue.retain(|packet| f(packet)); + } +} diff --git a/rattan-core/src/cells/bandwidth/queue/mod.rs b/rattan-core/src/cells/bandwidth/queue/mod.rs index cb6441a6..7a13cbdb 100644 --- a/rattan-core/src/cells/bandwidth/queue/mod.rs +++ b/rattan-core/src/cells/bandwidth/queue/mod.rs @@ -16,7 +16,7 @@ mod drophead; mod droptail; mod infinite; mod red; -// Duan: mod ared; +mod ared; // Duan: mod pie; pub use codel::*; @@ -24,7 +24,7 @@ pub use drophead::*; pub use droptail::*; pub use infinite::*; pub use red::*; -// Duan: pub use ared::*; +pub use ared::*; // Duan: pub use pie::*; #[cfg(feature = "serde")] diff --git a/rattan-core/src/cells/bandwidth/queue/red.rs b/rattan-core/src/cells/bandwidth/queue/red.rs index 360dd833..0f9becbf 100644 --- a/rattan-core/src/cells/bandwidth/queue/red.rs +++ b/rattan-core/src/cells/bandwidth/queue/red.rs @@ -1,4 +1,5 @@ // RED Queue Implementation Reference: +// https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=251892 // https://github.com/torvalds/linux/blob/master/include/net/red.h use std::collections::VecDeque; @@ -135,7 +136,7 @@ where true => { if let Some(idle_start) = self.idle_start { let now = Instant::now(); - let idle_duration = now.duration_since(idle_start); + let idle_duration = now.saturating_duration_since(idle_start); let m = idle_duration.as_micros() as f64 / self.config.pkt_tx_time.as_micros() as f64; self.average_queue_length *= f64::powf(1.0 - self.config.w_q, m); self.idle_start = Some(now); @@ -190,6 +191,8 @@ where } fn enqueue(&mut self, packet: P) { + self.update_avg(); + if self .config .packet_limit @@ -199,7 +202,7 @@ where }) { let packet_size = packet.l3_length() + self.get_extra_length(); - self.update_avg(); + match self.should_drop() { true => { #[cfg(test)] @@ -273,8 +276,7 @@ where #[cfg(test)] mod tests { use super::*; - use crate::cells::{Packet, StdPacket}; - use tokio::time::Instant; + use crate::cells::StdPacket; fn create_packet(size: usize) -> StdPacket { let buf = vec![0u8; size]; From 876b84188249753b569ef5a1040c83a527ef8676 Mon Sep 17 00:00:00 2001 From: duanxy23 Date: Sun, 7 Jun 2026 00:25:24 +0800 Subject: [PATCH 04/40] add ared unit tests --- rattan-core/src/cells/bandwidth/queue/ared.rs | 195 ++++++++++++++++++ 1 file changed, 195 insertions(+) diff --git a/rattan-core/src/cells/bandwidth/queue/ared.rs b/rattan-core/src/cells/bandwidth/queue/ared.rs index e12ed29e..22c45d14 100644 --- a/rattan-core/src/cells/bandwidth/queue/ared.rs +++ b/rattan-core/src/cells/bandwidth/queue/ared.rs @@ -288,3 +288,198 @@ where self.queue.retain(|packet| f(packet)); } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::cells::StdPacket; + + fn create_packet(size: usize) -> StdPacket { + let buf = vec![0u8; size]; + StdPacket::with_timestamp(&buf, Instant::now()) + } + + #[test_log::test] + fn test_ared_queue_basic() { + let mut config = AdaptiveRedQueueConfig::default(); + config.min_th = 1000; + config.max_th = 2000; + let mut queue: AdaptiveRedQueue = AdaptiveRedQueue::new(config); + + assert!(queue.is_empty()); + + let pkt1 = create_packet(500); + queue.enqueue(pkt1); + assert!(!queue.is_empty()); + assert_eq!(queue.length(), 1); + + let dequeued = queue.dequeue(); + assert!(dequeued.is_some()); + assert!(queue.is_empty()); + } + + #[test_log::test] + fn test_ared_queue_hard_limit_packet() { + let mut config = AdaptiveRedQueueConfig::default(); + config.packet_limit = Some(2); + config.min_th = 100000; // avoid red drop + config.max_th = 200000; + let mut queue: AdaptiveRedQueue = AdaptiveRedQueue::new(config); + + queue.enqueue(create_packet(100)); + queue.enqueue(create_packet(100)); + assert_eq!(queue.length(), 2); + + // This one should be dropped due to packet limit + queue.enqueue(create_packet(100)); + assert_eq!(queue.length(), 2); + } + + #[test_log::test] + fn test_ared_queue_hard_limit_byte() { + let mut config = AdaptiveRedQueueConfig::default(); + config.byte_limit = Some(150); + config.min_th = 100000; // avoid red drop + config.max_th = 200000; + let mut queue: AdaptiveRedQueue = AdaptiveRedQueue::new(config); + + queue.enqueue(create_packet(100)); // l3 length 86. + assert_eq!(queue.length(), 1); + + // This one should be dropped due to byte limit (86 + 86 > 150) + queue.enqueue(create_packet(100)); + assert_eq!(queue.length(), 1); + } + + #[test_log::test] + fn test_ared_queue_max_th_drop() { + let mut config = AdaptiveRedQueueConfig::default(); + config.min_th = 100; + config.max_th = 200; + config.w_q = 1.0; // max weight, avg matches instantly + let mut queue: AdaptiveRedQueue = AdaptiveRedQueue::new(config); + + // First packet + queue.enqueue(create_packet(100)); + + // Second packet + queue.enqueue(create_packet(300)); + + // At this point, queue length is 2, now_bytes is high enough. + // The next enqueue should see average_queue_length > max_th and drop the packet. + let before_len = queue.length(); + queue.enqueue(create_packet(100)); + assert_eq!(queue.length(), before_len, "Packet should be dropped by ARED max_th"); + } + + #[test_log::test] + fn test_ared_queue_min_th_no_drop() { + let mut config = AdaptiveRedQueueConfig::default(); + config.min_th = 1000; + config.max_th = 2000; + config.w_q = 1.0; // Instantly reach exact byte size + let mut queue: AdaptiveRedQueue = AdaptiveRedQueue::new(config); + + // First packet: queue empty, avg remains 0. + queue.enqueue(create_packet(514)); // L3 size = 514 - 14 (Ethernet header) = 500 + assert_eq!(queue.length(), 1); + + // Second packet: queue has 500 bytes. w_q=1.0 makes avg = 500. + // 500 < min_th(1000), so it should not drop. + queue.enqueue(create_packet(414)); // L3 size = 400 + assert_eq!(queue.length(), 2); + + // Check internal state: count_packet is -1 when avg < min_th + assert_eq!(queue.count_packet, -1); + } + + #[test_log::test] + fn test_ared_queue_probabilistic_drop() { + let mut config = AdaptiveRedQueueConfig::default(); + config.min_th = 100; + config.max_th = 300; + config.max_p = 0.5; + config.w_q = 1.0; + let mut queue: AdaptiveRedQueue = AdaptiveRedQueue::new(config); + + // First packet: queue empty, avg = 0. L3 size = 200. + queue.enqueue(create_packet(214)); + assert_eq!(queue.length(), 1); + + let mut drop_count = 0; + let total_packets = 1000; + + for _ in 0..total_packets { + // enqueue packets with L3 size 0 (total size 14). + // now_bytes stays at 200. w_q=1.0 makes avg exactly 200. + // 100 (min_th) <= avg(200) < 300 (max_th), entering probabilistic drop zone. + let before = queue.length(); + queue.enqueue(create_packet(14)); + if queue.length() == before { + drop_count += 1; + } + } + + // It should drop some packets, but not all of them + assert!(drop_count > 0, "Should have dropped some packets probabilistically"); + assert!(drop_count < total_packets, "Should not drop all packets"); + } + + #[test_log::test] + fn test_ared_queue_max_p_increase() { + let mut config = AdaptiveRedQueueConfig::default(); + config.min_th = 100; + config.max_th = 200; + config.max_p = 0.02; + config.w_q = 1.0; // Instantly update avg + let mut queue: AdaptiveRedQueue = AdaptiveRedQueue::new(config); + + // enqueue to make average_queue_length > target_max + // target_max = min_th + 0.6 * (max_th - min_th) = 100 + 60 = 160 + // We make avg = 180 (L3 size 180, total 194) + queue.enqueue(create_packet(194)); + assert_eq!(queue.average_queue_length, 0.0); // First enqueue updates avg based on empty queue rule (avg=0). + + // Second enqueue updates avg to 180 + queue.enqueue(create_packet(14)); + assert_eq!(queue.average_queue_length, 180.0); + + // Set latest_max_p_update to 600ms ago to trigger update_max_p + queue.latest_max_p_update = Instant::now() - Duration::from_millis(600); + let before_max_p = queue.config.max_p; + + // Third enqueue triggers update_max_p + queue.enqueue(create_packet(14)); + + let after_max_p = queue.config.max_p; + assert!(after_max_p > before_max_p, "max_p should increase when avg > target_max"); + } + + #[test_log::test] + fn test_ared_queue_max_p_decrease() { + let mut config = AdaptiveRedQueueConfig::default(); + config.min_th = 100; + config.max_th = 200; + config.max_p = 0.05; // Starting with a high max_p + config.w_q = 1.0; + let mut queue: AdaptiveRedQueue = AdaptiveRedQueue::new(config); + + // enqueue to make average_queue_length < target_min + // target_min = min_th + 0.4 * (max_th - min_th) = 100 + 40 = 140 + // We make avg = 120 (L3 size 120, total 134) + queue.enqueue(create_packet(134)); + assert_eq!(queue.average_queue_length, 0.0); + + queue.enqueue(create_packet(14)); + assert_eq!(queue.average_queue_length, 120.0); + + // Set latest_max_p_update to 600ms ago + queue.latest_max_p_update = Instant::now() - Duration::from_millis(600); + let before_max_p = queue.config.max_p; + + queue.enqueue(create_packet(14)); + + let after_max_p = queue.config.max_p; + assert!(after_max_p < before_max_p, "max_p should decrease when avg < target_min"); + } +} From bc934d62e61f1f2a14a8f1e411b21e8f7f8d72b1 Mon Sep 17 00:00:00 2001 From: duanxy23 Date: Mon, 8 Jun 2026 15:46:24 +0800 Subject: [PATCH 05/40] add pie (without unit tests) --- rattan-core/src/cells/bandwidth/queue/mod.rs | 4 +- rattan-core/src/cells/bandwidth/queue/pie.rs | 330 +++++++++++++++++++ 2 files changed, 332 insertions(+), 2 deletions(-) create mode 100644 rattan-core/src/cells/bandwidth/queue/pie.rs diff --git a/rattan-core/src/cells/bandwidth/queue/mod.rs b/rattan-core/src/cells/bandwidth/queue/mod.rs index 7a13cbdb..9fc1286a 100644 --- a/rattan-core/src/cells/bandwidth/queue/mod.rs +++ b/rattan-core/src/cells/bandwidth/queue/mod.rs @@ -17,7 +17,7 @@ mod droptail; mod infinite; mod red; mod ared; -// Duan: mod pie; +mod pie; pub use codel::*; pub use drophead::*; @@ -25,7 +25,7 @@ pub use droptail::*; pub use infinite::*; pub use red::*; pub use ared::*; -// Duan: pub use pie::*; +pub use pie::*; #[cfg(feature = "serde")] fn serde_default(t: &T) -> bool { diff --git a/rattan-core/src/cells/bandwidth/queue/pie.rs b/rattan-core/src/cells/bandwidth/queue/pie.rs new file mode 100644 index 00000000..7e9e8193 --- /dev/null +++ b/rattan-core/src/cells/bandwidth/queue/pie.rs @@ -0,0 +1,330 @@ +// PIE Queue Implementation Reference: +// https://www.rfc-editor.org/info/rfc8033 +// https://ieeexplore.ieee.org/document/6602305 +// Reproduced according to RFC 8033 Appendix B, +// rather than original paper or RFC Appendix A. + +use std::collections::VecDeque; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +use tokio::time::{Instant, Duration}; +use rand::random_range; +use tracing::debug; + +#[cfg(feature = "serde")] +use super::serde_default; +use super::{BwType, PacketQueue}; +use crate::cells::Packet; + +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize), serde(default))] +#[derive(Debug, Clone)] +pub struct PieQueueConfig { + pub packet_limit: Option, + pub byte_limit: Option, + pub ref_del: f64, // target delay (sec) + pub t_update: Duration, // update interval + pub tilde_alpha: f64, // base value of alpha (Hz, 1/sec) + pub tilde_beta: f64, // base value of beta (Hz, 1/sec) + pub dq_threshold: usize, // threshold of queue length (bytes) + pub epsilon: f64, // EWMA weight + pub max_burst: f64, // MAX_BURST (ms) + #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "serde_default"))] + pub bw_type: BwType, +} + +impl Default for PieQueueConfig { + fn default() -> Self { + Self { + packet_limit: None, + byte_limit: None, + ref_del: 0.015, // RFC 8033 + t_update: Duration::from_millis(15), + tilde_alpha: 0.125, + tilde_beta: 1.25, + dq_threshold: 16384, // 16 KB + epsilon: 0.125, + max_burst: 150.0, + bw_type: BwType::default(), + } + } +} + +impl PieQueueConfig { + pub fn new>, B: Into>>( + packet_limit: A, + byte_limit: B, + ref_del: f64, + t_update: Duration, + tilde_alpha: f64, + tilde_beta: f64, + dq_threshold: usize, + epsilon: f64, + max_burst: f64, + bw_type: BwType + ) -> Self { + Self { + packet_limit: packet_limit.into(), + byte_limit: byte_limit.into(), + ref_del, + t_update, + tilde_alpha, + tilde_beta, + dq_threshold, + epsilon, + max_burst, + bw_type + } + } +} + +impl

From for PieQueue

{ + fn from(config: PieQueueConfig) -> Self { + PieQueue::new(config) + } +} + +#[derive(Debug)] +pub struct PieQueue

{ + queue: VecDeque

, + config: PieQueueConfig, + now_bytes: usize, + old_del: f64, // previous delay (sec) + p: f64, // current drop probability + dq_count: usize, // departure count (bytes) + start_update: Instant, // start time of t_update + start_measurement: Option, // Some(Instant) when in a measurement cycle, None when quit + avg_drate: f64, + burst_allowance: f64 +} + +impl

PieQueue

{ + pub fn new(config: PieQueueConfig) -> Self { + debug!(?config, "New PieQueue"); + let max_burst = config.max_burst; + Self { + queue: VecDeque::new(), + config, + now_bytes: 0, + old_del: 0.0, + p: 0.0, + dq_count: 0, + start_update: Instant::now(), + start_measurement: None, + avg_drate: 0.0, + burst_allowance: max_burst + } + } +} + +impl

Default for PieQueue

+where + P: Packet +{ + fn default() -> Self { + Self::new(PieQueueConfig::default()) + } +} + +impl

PieQueue

+where + P: Packet, +{ + fn update_drop_probability(&mut self) { + let now = Instant::now(); + let elapsed_ms = now.saturating_duration_since(self.start_update).as_secs_f64() * 1000.0; + + let cur_del = if self.avg_drate.abs() < f64::EPSILON { + 0.0 + } else { + self.now_bytes as f64 / self.avg_drate + }; + + let mut p_increment = self.config.tilde_alpha * (cur_del - self.config.ref_del) + + self.config.tilde_beta * (cur_del - self.old_del); + if self.p < 0.000001 { + p_increment /= 2048.0; + } else if self.p < 0.00001 { + p_increment /= 512.0; + } else if self.p < 0.0001 { + p_increment /= 128.0; + } else if self.p < 0.001 { + p_increment /= 32.0; + } else if self.p < 0.01 { + p_increment /= 8.0; + } else if self.p < 0.1 { + p_increment /= 2.0; + } + self.p += p_increment; + // RFC 8033 Section 4.2: Exponential decay when system is not congested + if cur_del < self.config.ref_del / 2.0 && self.old_del < self.config.ref_del / 2.0 { + self.p *= 0.98; + } + self.p = self.p.clamp(0.0, 1.0); + + // RFC 8033 Section 4.4: Burst Tolerance + if self.p < f64::EPSILON && cur_del < self.config.ref_del / 2.0 && self.old_del < self.config.ref_del / 2.0 { + self.burst_allowance = self.config.max_burst; + } else { + self.burst_allowance = (self.burst_allowance - elapsed_ms).max(0.0); + } + + self.old_del = cur_del; + self.start_update = now; + } + + fn should_drop(&self) -> bool { + // RFC 8033 Section 4.4: Enqueue packet bypassing random drop if burst_allow > 0 + if self.burst_allowance > f64::EPSILON { + return false; + } + + // RFC 8033 Section 4.1: Bypass random drop logic to be work conserving + let bypass_drop = (self.old_del < self.config.ref_del / 2.0 && self.p < 0.2) + || self.queue.len() <= 2; + if bypass_drop { + return false; + } + + let rand_val = random_range(0.0 .. 1.0); + rand_val < self.p + } + + fn update_avg_drate(&mut self, pkt_size: usize) { + let now = Instant::now(); + + // Enter a measurement cycle + if self.now_bytes > self.config.dq_threshold && self.start_measurement.is_none() { + self.start_measurement = Some(now); + self.dq_count = 0; + } + + // Update departure rate if we are in a measurement cycle + if let Some(start) = self.start_measurement { + self.dq_count += pkt_size; + if self.dq_count > self.config.dq_threshold { + let dq_int = now.saturating_duration_since(start).as_secs_f64(); + if dq_int > f64::EPSILON { + let dq_rate = self.dq_count as f64 / dq_int; + if self.avg_drate.abs() < f64::EPSILON { + self.avg_drate = dq_rate; + } else { + // Duan: epsilon 参数可能不能自己设 + self.avg_drate = (1.0 - self.config.epsilon) * self.avg_drate + self.config.epsilon * dq_rate; + } + self.start_measurement = Some(now); + self.dq_count = 0; + } + } + + // Exit measurement cycle if queue length drops below threshold + if self.now_bytes < self.config.dq_threshold { + self.start_measurement = None; + self.dq_count = 0; + } + } + } +} + +impl

PacketQueue

for PieQueue

+where + P: Packet, +{ + type Config = PieQueueConfig; + + fn configure(&mut self, config: Self::Config) { + self.config = config; + } + + fn is_zero_buffer(&self) -> bool { + self.config.packet_limit.is_some_and(|limit| limit == 0) + || self.config.byte_limit.is_some_and(|limit| limit == 0) + } + + fn enqueue(&mut self, packet: P) { + // Simulate time-driven with event-driven approach + let interval_update = Instant::now().saturating_duration_since(self.start_update); + if interval_update >= self.config.t_update { + self.update_drop_probability(); + } + + // hard limit check + if self + .config + .packet_limit + .is_none_or(|limit| self.queue.len() < limit) + && self.config.byte_limit.is_none_or(|limit| { + self.now_bytes + packet.l3_length() + self.config.bw_type.extra_length() <= limit + }) + { + match self.should_drop() { + true => { + #[cfg(test)] + tracing::trace!( + p = self.p, + old_delay = self.old_del, + header = ?format!("{:X?}", &packet.as_slice()[0..std::cmp::min(56, packet.length())]), + "Drop packet(l3_len: {}, extra_len: {}) due to PIE algorithm", packet.l3_length(), self.get_extra_length() + ); + return; + }, + false => { + self.now_bytes += packet.l3_length() + self.get_extra_length(); + self.queue.push_back(packet); + } + } + } else { + #[cfg(test)] + tracing::trace!( + queue_len = self.queue.len(), + now_bytes = self.now_bytes, + header = ?format!("{:X?}", &packet.as_slice()[0..std::cmp::min(56, packet.length())]), + "Drop packet(l3_len: {}, extra_len: {}) due to hard limit", packet.l3_length(), self.config.bw_type.extra_length() + ); + } + } + + fn dequeue(&mut self) -> Option

{ + // Simulate time-driven with event-driven approach + let interval_update = Instant::now().saturating_duration_since(self.start_update); + if interval_update >= self.config.t_update { + self.update_drop_probability(); + } + + match self.queue.pop_front() { + Some(packet) => { + let pkt_size = packet.l3_length() + self.get_extra_length(); + self.now_bytes -= pkt_size; + self.update_avg_drate(pkt_size); + Some(packet) + }, + None => None, + } + } + + fn is_empty(&self) -> bool { + self.queue.is_empty() + } + + #[inline(always)] + fn get_extra_length(&self) -> usize { + self.config.bw_type.extra_length() + } + + fn get_front_size(&self) -> Option { + self.queue + .front() + .map(|packet| self.get_packet_size(packet)) + } + + fn length(&self) -> usize { + self.queue.len() + } + + fn retain(&mut self, mut f: F) + where + F: FnMut(&P) -> bool, + { + self.queue.retain(|packet| f(packet)); + } +} \ No newline at end of file From 5f1e4e8d12e8277506066ead5a7a5330b8e727ec Mon Sep 17 00:00:00 2001 From: duanxy23 Date: Mon, 8 Jun 2026 18:13:56 +0800 Subject: [PATCH 06/40] update pie --- rattan-core/src/cells/bandwidth/queue/pie.rs | 154 ++++++++++++++++++- 1 file changed, 151 insertions(+), 3 deletions(-) diff --git a/rattan-core/src/cells/bandwidth/queue/pie.rs b/rattan-core/src/cells/bandwidth/queue/pie.rs index 7e9e8193..d3d9abb8 100644 --- a/rattan-core/src/cells/bandwidth/queue/pie.rs +++ b/rattan-core/src/cells/bandwidth/queue/pie.rs @@ -168,7 +168,6 @@ where } else { self.burst_allowance = (self.burst_allowance - elapsed_ms).max(0.0); } - self.old_del = cur_del; self.start_update = now; } @@ -202,14 +201,13 @@ where // Update departure rate if we are in a measurement cycle if let Some(start) = self.start_measurement { self.dq_count += pkt_size; - if self.dq_count > self.config.dq_threshold { + if self.dq_count >= self.config.dq_threshold { let dq_int = now.saturating_duration_since(start).as_secs_f64(); if dq_int > f64::EPSILON { let dq_rate = self.dq_count as f64 / dq_int; if self.avg_drate.abs() < f64::EPSILON { self.avg_drate = dq_rate; } else { - // Duan: epsilon 参数可能不能自己设 self.avg_drate = (1.0 - self.config.epsilon) * self.avg_drate + self.config.epsilon * dq_rate; } self.start_measurement = Some(now); @@ -327,4 +325,154 @@ where { self.queue.retain(|packet| f(packet)); } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::cells::StdPacket; + + fn create_packet(size: usize) -> StdPacket { + let buf = vec![0u8; size]; + StdPacket::with_timestamp(&buf, Instant::now()) + } + + #[test_log::test] + fn test_pie_queue_basic() { + let config = PieQueueConfig::default(); + let mut queue: PieQueue = PieQueue::new(config); + + assert!(queue.is_empty()); + + let pkt1 = create_packet(500); + queue.enqueue(pkt1); + assert!(!queue.is_empty()); + assert_eq!(queue.length(), 1); + + let dequeued = queue.dequeue(); + assert!(dequeued.is_some()); + assert!(queue.is_empty()); + } + + #[test_log::test] + fn test_pie_queue_hard_limit_packet() { + let mut config = PieQueueConfig::default(); + config.packet_limit = Some(2); + let mut queue: PieQueue = PieQueue::new(config); + + queue.enqueue(create_packet(100)); + queue.enqueue(create_packet(100)); + assert_eq!(queue.length(), 2); + + // This one should be dropped due to packet limit + queue.enqueue(create_packet(100)); + assert_eq!(queue.length(), 2); + } + + #[test_log::test] + fn test_pie_queue_hard_limit_byte() { + let mut config = PieQueueConfig::default(); + config.byte_limit = Some(150); + let mut queue: PieQueue = PieQueue::new(config); + + queue.enqueue(create_packet(100)); // l3 length 86. + assert_eq!(queue.length(), 1); + + // This one should be dropped due to byte limit (86 + 86 > 150) + queue.enqueue(create_packet(100)); + assert_eq!(queue.length(), 1); + } + + #[test_log::test] + fn test_pie_queue_burst_allowance() { + let config = PieQueueConfig::default(); + let mut queue: PieQueue = PieQueue::new(config); + + // Force a high drop probability + queue.p = 1.0; + queue.burst_allowance = 100.0; + + // burst_allowance > 0 bypasses random drop + queue.enqueue(create_packet(100)); + assert_eq!(queue.length(), 1); + + // Fill queue > 2 to bypass work conserving logic later + queue.enqueue(create_packet(100)); + queue.enqueue(create_packet(100)); + assert_eq!(queue.length(), 3); + + queue.burst_allowance = 0.0; + // With burst_allowance = 0.0, queue > 2, and p = 1.0, it should drop + queue.enqueue(create_packet(100)); + assert_eq!(queue.length(), 3); + } + + #[test_log::test] + fn test_pie_queue_work_conserving() { + let config = PieQueueConfig::default(); + let mut queue: PieQueue = PieQueue::new(config.clone()); + + queue.p = 1.0; + queue.burst_allowance = 0.0; + + // bypass_drop handles queue.len() <= 2 + queue.enqueue(create_packet(100)); + assert_eq!(queue.length(), 1); + queue.enqueue(create_packet(100)); + assert_eq!(queue.length(), 2); + queue.enqueue(create_packet(100)); + assert_eq!(queue.length(), 3); + + // For the 4th element, queue.len() is 3, so it does not bypass based on length + // Since p = 1.0, it drops + queue.enqueue(create_packet(100)); + assert_eq!(queue.length(), 3); + + // Now test bypass_drop condition: old_del < ref_del/2 and p < 0.2 + queue.p = 0.15; + queue.old_del = config.ref_del / 3.0; + queue.enqueue(create_packet(100)); + assert_eq!(queue.length(), 4); + } + + #[test_log::test] + fn test_pie_queue_avg_drate_update() { + let mut config = PieQueueConfig::default(); + config.dq_threshold = 50; // Small threshold + let mut queue: PieQueue = PieQueue::new(config); + + queue.enqueue(create_packet(114)); // l3 length 100 + queue.enqueue(create_packet(114)); // l3 length 100 + assert_eq!(queue.now_bytes, 200); + + // First dequeue triggers start of measurement cycle + assert!(queue.start_measurement.is_none()); + queue.dequeue(); // dequeues 100 bytes + assert!(queue.start_measurement.is_some()); + assert_eq!(queue.now_bytes, 100); + + std::thread::sleep(Duration::from_millis(10)); + + // Second dequeue triggers calculation of avg_drate + queue.dequeue(); // dequeues 100 bytes + assert!(queue.avg_drate > 0.0, "avg_drate should be calculated"); + assert!(queue.start_measurement.is_none(), "Should exit measurement cycle since queue is empty"); + } + + #[test_log::test] + fn test_pie_queue_update_drop_probability() { + let config = PieQueueConfig::default(); + let mut queue: PieQueue = PieQueue::new(config.clone()); + + // Fake high delay + queue.avg_drate = 1000.0; + queue.now_bytes = 100000; // delay = 100.0s > ref_del + + std::thread::sleep(config.t_update); // Wait to exceed t_update + + // This enqueue will trigger update_drop_probability() + queue.enqueue(create_packet(14)); + + assert!(queue.p > 0.0, "Probability should increase when delay is high"); + } } \ No newline at end of file From 7dbc2748b88ad67e8869d3ab066df35c7792ab90 Mon Sep 17 00:00:00 2001 From: duanxy23 Date: Mon, 8 Jun 2026 19:28:23 +0800 Subject: [PATCH 07/40] update pie unit tests --- rattan-core/src/cells/bandwidth/queue/pie.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/rattan-core/src/cells/bandwidth/queue/pie.rs b/rattan-core/src/cells/bandwidth/queue/pie.rs index d3d9abb8..0c1d01cf 100644 --- a/rattan-core/src/cells/bandwidth/queue/pie.rs +++ b/rattan-core/src/cells/bandwidth/queue/pie.rs @@ -341,7 +341,6 @@ mod tests { fn test_pie_queue_basic() { let config = PieQueueConfig::default(); let mut queue: PieQueue = PieQueue::new(config); - assert!(queue.is_empty()); let pkt1 = create_packet(500); @@ -411,7 +410,6 @@ mod tests { fn test_pie_queue_work_conserving() { let config = PieQueueConfig::default(); let mut queue: PieQueue = PieQueue::new(config.clone()); - queue.p = 1.0; queue.burst_allowance = 0.0; From 3632057aba9027c6678089bde78e39d8057c340b Mon Sep 17 00:00:00 2001 From: duanxy23 Date: Tue, 9 Jun 2026 19:00:10 +0800 Subject: [PATCH 08/40] fix un-lock-able's review on ared --- rattan-core/src/cells/bandwidth/queue/ared.rs | 97 ++++++++----------- rattan-core/src/cells/bandwidth/queue/pie.rs | 63 ++++++------ rattan-core/src/cells/bandwidth/queue/red.rs | 66 ++++++------- 3 files changed, 100 insertions(+), 126 deletions(-) diff --git a/rattan-core/src/cells/bandwidth/queue/ared.rs b/rattan-core/src/cells/bandwidth/queue/ared.rs index 22c45d14..fd3b7942 100644 --- a/rattan-core/src/cells/bandwidth/queue/ared.rs +++ b/rattan-core/src/cells/bandwidth/queue/ared.rs @@ -1,6 +1,5 @@ // Adaptive RED Queue Implementation Reference: -// https://www.icir.org/floyd/papers/adaptiveRed.pdf#:~:text=We%20find%20that%20this%20re-vised%20version%20of%20Adaptive,length%20in%20a%20wide%20variety%20of%20traffic%20scenarios. - +// https://www.icir.org/floyd/papers/adaptiveRed.pdf use std::collections::VecDeque; #[cfg(feature = "serde")] @@ -129,20 +128,18 @@ impl

AdaptiveRedQueue

where P: Packet, { - fn update_avg (&mut self) { - match self.is_empty() { - false => { - self.average_queue_length = (1.0 - self.config.w_q) * self.average_queue_length + self.config.w_q * (self.now_bytes as f64) - }, - true => { - if let Some(idle_start) = self.idle_start { - let now = Instant::now(); - let idle_duration = now.saturating_duration_since(idle_start); - let m = idle_duration.as_micros() as f64 / self.config.pkt_tx_time.as_micros() as f64; - self.average_queue_length *= f64::powf(1.0 - self.config.w_q, m); - self.idle_start = Some(now); - } - } + fn update_avg(&mut self) { + if !self.is_empty() { + self.average_queue_length = (1.0 - self.config.w_q) * self.average_queue_length + self.config.w_q * (self.now_bytes as f64); + return; + } + + if let Some(idle_start) = self.idle_start { + let now = Instant::now(); + let idle_duration = now.saturating_duration_since(idle_start); + let m = idle_duration.as_micros() as f64 / self.config.pkt_tx_time.as_micros() as f64; + self.average_queue_length *= f64::powf(1.0 - self.config.w_q, m); + self.idle_start = Some(now); } } @@ -210,55 +207,47 @@ where self.latest_max_p_update = now; } - if self - .config - .packet_limit - .is_none_or(|limit| self.queue.len() < limit) - && self.config.byte_limit.is_none_or(|limit| { - self.now_bytes + packet.l3_length() + self.config.bw_type.extra_length() <= limit - }) - { - let packet_size = packet.l3_length() + self.get_extra_length(); - - match self.should_drop() { - true => { - #[cfg(test)] - tracing::trace!( - avg = self.average_queue_length, - count = self.count_packet, - header = ?format!("{:X?}", &packet.as_slice()[0..std::cmp::min(56, packet.length())]), - "Drop packet(l3_len: {}, extra_len: {}) due to ARED algorithm", packet.l3_length(), self.get_extra_length() - ); - return; - }, - false => { - self.now_bytes += packet_size; - self.queue.push_back(packet); - self.idle_start = None; - } - } - } else { + let packet_size = packet.l3_length() + self.get_extra_length(); + let pass_hard_limit = self.config.packet_limit.is_none_or(|limit| self.queue.len() < limit) + && self.config.byte_limit.is_none_or(|limit| self.now_bytes + packet_size <= limit); + + if !pass_hard_limit { self.count_packet = 0; #[cfg(test)] tracing::trace!( queue_len = self.queue.len(), now_bytes = self.now_bytes, header = ?format!("{:X?}", &packet.as_slice()[0..std::cmp::min(56, packet.length())]), - "Drop packet(l3_len: {}, extra_len: {}) due to hard limit", packet.l3_length(), self.config.bw_type.extra_length() + "Drop packet(l3_len: {}, extra_len: {}) due to hard limit", packet.l3_length(), self.get_extra_length() + ); + return; + } + + if self.should_drop() { + #[cfg(test)] + tracing::trace!( + avg = self.average_queue_length, + count = self.count_packet, + header = ?format!("{:X?}", &packet.as_slice()[0..std::cmp::min(56, packet.length())]), + "Drop packet(l3_len: {}, extra_len: {}) due to ARED algorithm", packet.l3_length(), self.get_extra_length() ); + return; } + + self.now_bytes += packet_size; + self.queue.push_back(packet); + self.idle_start = None; } fn dequeue(&mut self) -> Option

{ - match self.queue.pop_front() { - Some(packet ) => { - self.now_bytes -= packet.l3_length() + self.get_extra_length(); - if self.is_empty() { - self.idle_start = Some(Instant::now()); - } - Some(packet) - }, - None => None + if let Some(packet) = self.queue.pop_front() { + self.now_bytes -= packet.l3_length() + self.get_extra_length(); + if self.is_empty() { + self.idle_start = Some(Instant::now()); + } + Some(packet) + } else { + None } } diff --git a/rattan-core/src/cells/bandwidth/queue/pie.rs b/rattan-core/src/cells/bandwidth/queue/pie.rs index 0c1d01cf..22d0e35b 100644 --- a/rattan-core/src/cells/bandwidth/queue/pie.rs +++ b/rattan-core/src/cells/bandwidth/queue/pie.rs @@ -246,40 +246,34 @@ where self.update_drop_probability(); } - // hard limit check - if self - .config - .packet_limit - .is_none_or(|limit| self.queue.len() < limit) - && self.config.byte_limit.is_none_or(|limit| { - self.now_bytes + packet.l3_length() + self.config.bw_type.extra_length() <= limit - }) - { - match self.should_drop() { - true => { - #[cfg(test)] - tracing::trace!( - p = self.p, - old_delay = self.old_del, - header = ?format!("{:X?}", &packet.as_slice()[0..std::cmp::min(56, packet.length())]), - "Drop packet(l3_len: {}, extra_len: {}) due to PIE algorithm", packet.l3_length(), self.get_extra_length() - ); - return; - }, - false => { - self.now_bytes += packet.l3_length() + self.get_extra_length(); - self.queue.push_back(packet); - } - } - } else { + let packet_size = packet.l3_length() + self.get_extra_length(); + let pass_hard_limit = self.config.packet_limit.is_none_or(|limit| self.queue.len() < limit) + && self.config.byte_limit.is_none_or(|limit| self.now_bytes + packet_size <= limit); + + if !pass_hard_limit { #[cfg(test)] tracing::trace!( queue_len = self.queue.len(), now_bytes = self.now_bytes, header = ?format!("{:X?}", &packet.as_slice()[0..std::cmp::min(56, packet.length())]), - "Drop packet(l3_len: {}, extra_len: {}) due to hard limit", packet.l3_length(), self.config.bw_type.extra_length() + "Drop packet(l3_len: {}, extra_len: {}) due to hard limit", packet.l3_length(), self.get_extra_length() + ); + return; + } + + if self.should_drop() { + #[cfg(test)] + tracing::trace!( + p = self.p, + old_delay = self.old_del, + header = ?format!("{:X?}", &packet.as_slice()[0..std::cmp::min(56, packet.length())]), + "Drop packet(l3_len: {}, extra_len: {}) due to PIE algorithm", packet.l3_length(), self.get_extra_length() ); + return; } + + self.now_bytes += packet_size; + self.queue.push_back(packet); } fn dequeue(&mut self) -> Option

{ @@ -289,14 +283,13 @@ where self.update_drop_probability(); } - match self.queue.pop_front() { - Some(packet) => { - let pkt_size = packet.l3_length() + self.get_extra_length(); - self.now_bytes -= pkt_size; - self.update_avg_drate(pkt_size); - Some(packet) - }, - None => None, + if let Some(packet) = self.queue.pop_front() { + let pkt_size = packet.l3_length() + self.get_extra_length(); + self.now_bytes -= pkt_size; + self.update_avg_drate(pkt_size); + Some(packet) + } else { + None } } diff --git a/rattan-core/src/cells/bandwidth/queue/red.rs b/rattan-core/src/cells/bandwidth/queue/red.rs index 0f9becbf..c5389aa8 100644 --- a/rattan-core/src/cells/bandwidth/queue/red.rs +++ b/rattan-core/src/cells/bandwidth/queue/red.rs @@ -193,34 +193,11 @@ where fn enqueue(&mut self, packet: P) { self.update_avg(); - if self - .config - .packet_limit - .is_none_or(|limit| self.queue.len() < limit) - && self.config.byte_limit.is_none_or(|limit| { - self.now_bytes + packet.l3_length() + self.config.bw_type.extra_length() <= limit - }) - { - let packet_size = packet.l3_length() + self.get_extra_length(); - - match self.should_drop() { - true => { - #[cfg(test)] - tracing::trace!( - avg = self.average_queue_length, - count = self.count_packet, - header = ?format!("{:X?}", &packet.as_slice()[0..std::cmp::min(56, packet.length())]), - "Drop packet(l3_len: {}, extra_len: {}) due to RED algorithm", packet.l3_length(), self.get_extra_length() - ); - return; - }, - false => { - self.now_bytes += packet_size; - self.queue.push_back(packet); - self.idle_start = None; - } - } - } else { + let packet_size = packet.l3_length() + self.get_extra_length(); + let pass_hard_limit = self.config.packet_limit.is_none_or(|limit| self.queue.len() < limit) + && self.config.byte_limit.is_none_or(|limit| self.now_bytes + packet_size <= limit); + + if !pass_hard_limit { self.count_packet = 0; #[cfg(test)] tracing::trace!( @@ -229,19 +206,34 @@ where header = ?format!("{:X?}", &packet.as_slice()[0..std::cmp::min(56, packet.length())]), "Drop packet(l3_len: {}, extra_len: {}) due to hard limit", packet.l3_length(), self.config.bw_type.extra_length() ); + return; } + + if self.should_drop() { + #[cfg(test)] + tracing::trace!( + avg = self.average_queue_length, + count = self.count_packet, + header = ?format!("{:X?}", &packet.as_slice()[0..std::cmp::min(56, packet.length())]), + "Drop packet(l3_len: {}, extra_len: {}) due to RED algorithm", packet.l3_length(), self.get_extra_length() + ); + return; + } + + self.now_bytes += packet_size; + self.queue.push_back(packet); + self.idle_start = None; } fn dequeue(&mut self) -> Option

{ - match self.queue.pop_front() { - Some(packet ) => { - self.now_bytes -= packet.l3_length() + self.get_extra_length(); - if self.is_empty() { - self.idle_start = Some(Instant::now()); - } - Some(packet) - }, - None => None + if let Some(packet) = self.queue.pop_front() { + self.now_bytes -= packet.l3_length() + self.get_extra_length(); + if self.is_empty() { + self.idle_start = Some(Instant::now()); + } + Some(packet) + } else { + None } } From 8732309133f44536e1531081bf0f9309a79e076f Mon Sep 17 00:00:00 2001 From: duanxy23 Date: Tue, 9 Jun 2026 19:22:17 +0800 Subject: [PATCH 09/40] fix warnings from cargo clippy (in github actions) and run cargo fmt --- rattan-core/src/cells/bandwidth/queue/ared.rs | 184 +++++++++++------- rattan-core/src/cells/bandwidth/queue/mod.rs | 8 +- rattan-core/src/cells/bandwidth/queue/pie.rs | 129 +++++++----- rattan-core/src/cells/bandwidth/queue/red.rs | 157 +++++++++------ 4 files changed, 296 insertions(+), 182 deletions(-) diff --git a/rattan-core/src/cells/bandwidth/queue/ared.rs b/rattan-core/src/cells/bandwidth/queue/ared.rs index fd3b7942..6be1a5f7 100644 --- a/rattan-core/src/cells/bandwidth/queue/ared.rs +++ b/rattan-core/src/cells/bandwidth/queue/ared.rs @@ -2,10 +2,10 @@ // https://www.icir.org/floyd/papers/adaptiveRed.pdf use std::collections::VecDeque; +use rand::random_range; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -use tokio::time::{Instant, Duration}; -use rand::random_range; +use tokio::time::{Duration, Instant}; use tracing::{debug, warn}; #[cfg(feature = "serde")] @@ -18,13 +18,19 @@ use crate::cells::Packet; pub struct AdaptiveRedQueueConfig { pub packet_limit: Option, pub byte_limit: Option, - pub w_q: f64, // queue weight for calculating the average queue length + pub w_q: f64, // queue weight for calculating the average queue length pub min_th: usize, // minimum threshold of average queue length pub max_th: usize, // maximum threshold of average queue length - pub max_p: f64, // maximum probability of dropping a packet - #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "serde_default"))] + pub max_p: f64, // maximum probability of dropping a packet + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "serde_default") + )] pub pkt_tx_time: Duration, // typical packet tx time (us) - #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "serde_default"))] + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "serde_default") + )] pub bw_type: BwType, } @@ -34,7 +40,7 @@ impl Default for AdaptiveRedQueueConfig { packet_limit: None, byte_limit: None, w_q: 0.002, - min_th: 7500, // 5 * 1500 bytes + min_th: 7500, // 5 * 1500 bytes max_th: 22500, // 15 * 1500 bytes max_p: 0.02, pkt_tx_time: Duration::from_micros(120), // 1500 bytes * 8 / 100Mbps = 120 us @@ -44,6 +50,7 @@ impl Default for AdaptiveRedQueueConfig { } impl AdaptiveRedQueueConfig { + #[allow(clippy::too_many_arguments)] pub fn new>, B: Into>>( packet_limit: A, byte_limit: B, @@ -52,7 +59,7 @@ impl AdaptiveRedQueueConfig { max_th: usize, max_p: f64, pkt_tx_time: Duration, - bw_type: BwType + bw_type: BwType, ) -> Self { // Warning: The caller must ensure that the parameters are valid. // It's recommended to do validation before calling this function, @@ -69,7 +76,7 @@ impl AdaptiveRedQueueConfig { if !(0.0..=1.0).contains(&max_p) { warn!("AdaptiveRedQueueConfig: max_p ({}) is out of expected range [0.0, 1.0]. This is a probability.", max_p); } - + Self { packet_limit: packet_limit.into(), byte_limit: byte_limit.into(), @@ -78,7 +85,7 @@ impl AdaptiveRedQueueConfig { max_th, max_p, pkt_tx_time, - bw_type + bw_type, } } } @@ -95,8 +102,8 @@ pub struct AdaptiveRedQueue

{ config: AdaptiveRedQueueConfig, now_bytes: usize, // for calculating average_queue_length average_queue_length: f64, - count_packet: i32, // number of packets since last dropping - idle_start: Option, // start time of current idle period + count_packet: i32, // number of packets since last dropping + idle_start: Option, // start time of current idle period latest_max_p_update: Instant, // latest time when max_p is updates } @@ -117,7 +124,7 @@ impl

AdaptiveRedQueue

{ impl

Default for AdaptiveRedQueue

where - P: Packet + P: Packet, { fn default() -> Self { Self::new(AdaptiveRedQueueConfig::default()) @@ -126,11 +133,12 @@ where impl

AdaptiveRedQueue

where - P: Packet, + P: Packet, { fn update_avg(&mut self) { if !self.is_empty() { - self.average_queue_length = (1.0 - self.config.w_q) * self.average_queue_length + self.config.w_q * (self.now_bytes as f64); + self.average_queue_length = (1.0 - self.config.w_q) * self.average_queue_length + + self.config.w_q * (self.now_bytes as f64); return; } @@ -143,7 +151,7 @@ where } } - fn should_drop (&mut self) -> bool { + fn should_drop(&mut self) -> bool { let avg = self.average_queue_length; let min_th = self.config.min_th as f64; let max_th = self.config.max_th as f64; @@ -156,7 +164,7 @@ where p_b / (1.0 - self.count_packet as f64 * p_b) }; - let rand_val = random_range(0.0 .. 1.0); + let rand_val = random_range(0.0..1.0); if rand_val < p_a { self.count_packet = 0; true @@ -172,9 +180,11 @@ where } } - fn update_max_p (&mut self) { - let target_min = self.config.min_th as f64 + 0.4 * (self.config.max_th - self.config.min_th) as f64; - let target_max = self.config.min_th as f64 + 0.6 * (self.config.max_th - self.config.min_th) as f64; + fn update_max_p(&mut self) { + let target_min = + self.config.min_th as f64 + 0.4 * (self.config.max_th - self.config.min_th) as f64; + let target_max = + self.config.min_th as f64 + 0.6 * (self.config.max_th - self.config.min_th) as f64; if self.average_queue_length > target_max && self.config.max_p <= 0.5 { self.config.max_p += (self.config.max_p / 4.0).min(0.01); } else if self.average_queue_length < target_min && self.config.max_p >= 0.01 { @@ -208,8 +218,14 @@ where } let packet_size = packet.l3_length() + self.get_extra_length(); - let pass_hard_limit = self.config.packet_limit.is_none_or(|limit| self.queue.len() < limit) - && self.config.byte_limit.is_none_or(|limit| self.now_bytes + packet_size <= limit); + let pass_hard_limit = self + .config + .packet_limit + .is_none_or(|limit| self.queue.len() < limit) + && self + .config + .byte_limit + .is_none_or(|limit| self.now_bytes + packet_size <= limit); if !pass_hard_limit { self.count_packet = 0; @@ -231,6 +247,7 @@ where header = ?format!("{:X?}", &packet.as_slice()[0..std::cmp::min(56, packet.length())]), "Drop packet(l3_len: {}, extra_len: {}) due to ARED algorithm", packet.l3_length(), self.get_extra_length() ); + #[allow(clippy::needless_return)] return; } @@ -290,9 +307,11 @@ mod tests { #[test_log::test] fn test_ared_queue_basic() { - let mut config = AdaptiveRedQueueConfig::default(); - config.min_th = 1000; - config.max_th = 2000; + let config = AdaptiveRedQueueConfig { + min_th: 1000, + max_th: 2000, + ..Default::default() + }; let mut queue: AdaptiveRedQueue = AdaptiveRedQueue::new(config); assert!(queue.is_empty()); @@ -309,10 +328,12 @@ mod tests { #[test_log::test] fn test_ared_queue_hard_limit_packet() { - let mut config = AdaptiveRedQueueConfig::default(); - config.packet_limit = Some(2); - config.min_th = 100000; // avoid red drop - config.max_th = 200000; + let config = AdaptiveRedQueueConfig { + packet_limit: Some(2), + min_th: 100000, // avoid red drop + max_th: 200000, + ..Default::default() + }; let mut queue: AdaptiveRedQueue = AdaptiveRedQueue::new(config); queue.enqueue(create_packet(100)); @@ -326,10 +347,12 @@ mod tests { #[test_log::test] fn test_ared_queue_hard_limit_byte() { - let mut config = AdaptiveRedQueueConfig::default(); - config.byte_limit = Some(150); - config.min_th = 100000; // avoid red drop - config.max_th = 200000; + let config = AdaptiveRedQueueConfig { + byte_limit: Some(150), + min_th: 100000, // avoid red drop + max_th: 200000, + ..Default::default() + }; let mut queue: AdaptiveRedQueue = AdaptiveRedQueue::new(config); queue.enqueue(create_packet(100)); // l3 length 86. @@ -342,34 +365,42 @@ mod tests { #[test_log::test] fn test_ared_queue_max_th_drop() { - let mut config = AdaptiveRedQueueConfig::default(); - config.min_th = 100; - config.max_th = 200; - config.w_q = 1.0; // max weight, avg matches instantly + let config = AdaptiveRedQueueConfig { + min_th: 100, + max_th: 200, + w_q: 1.0, // max weight, avg matches instantly + ..Default::default() + }; let mut queue: AdaptiveRedQueue = AdaptiveRedQueue::new(config); // First packet - queue.enqueue(create_packet(100)); - + queue.enqueue(create_packet(100)); + // Second packet queue.enqueue(create_packet(300)); - + // At this point, queue length is 2, now_bytes is high enough. // The next enqueue should see average_queue_length > max_th and drop the packet. let before_len = queue.length(); queue.enqueue(create_packet(100)); - assert_eq!(queue.length(), before_len, "Packet should be dropped by ARED max_th"); + assert_eq!( + queue.length(), + before_len, + "Packet should be dropped by ARED max_th" + ); } #[test_log::test] fn test_ared_queue_min_th_no_drop() { - let mut config = AdaptiveRedQueueConfig::default(); - config.min_th = 1000; - config.max_th = 2000; - config.w_q = 1.0; // Instantly reach exact byte size + let config = AdaptiveRedQueueConfig { + min_th: 1000, + max_th: 2000, + w_q: 1.0, // Instantly reach exact byte size + ..Default::default() + }; let mut queue: AdaptiveRedQueue = AdaptiveRedQueue::new(config); - // First packet: queue empty, avg remains 0. + // First packet: queue empty, avg remains 0. queue.enqueue(create_packet(514)); // L3 size = 514 - 14 (Ethernet header) = 500 assert_eq!(queue.length(), 1); @@ -377,50 +408,57 @@ mod tests { // 500 < min_th(1000), so it should not drop. queue.enqueue(create_packet(414)); // L3 size = 400 assert_eq!(queue.length(), 2); - + // Check internal state: count_packet is -1 when avg < min_th assert_eq!(queue.count_packet, -1); } #[test_log::test] fn test_ared_queue_probabilistic_drop() { - let mut config = AdaptiveRedQueueConfig::default(); - config.min_th = 100; - config.max_th = 300; - config.max_p = 0.5; - config.w_q = 1.0; + let config = AdaptiveRedQueueConfig { + min_th: 100, + max_th: 300, + max_p: 0.5, + w_q: 1.0, + ..Default::default() + }; let mut queue: AdaptiveRedQueue = AdaptiveRedQueue::new(config); // First packet: queue empty, avg = 0. L3 size = 200. - queue.enqueue(create_packet(214)); + queue.enqueue(create_packet(214)); assert_eq!(queue.length(), 1); let mut drop_count = 0; let total_packets = 1000; - + for _ in 0..total_packets { // enqueue packets with L3 size 0 (total size 14). // now_bytes stays at 200. w_q=1.0 makes avg exactly 200. // 100 (min_th) <= avg(200) < 300 (max_th), entering probabilistic drop zone. let before = queue.length(); - queue.enqueue(create_packet(14)); + queue.enqueue(create_packet(14)); if queue.length() == before { drop_count += 1; } } - + // It should drop some packets, but not all of them - assert!(drop_count > 0, "Should have dropped some packets probabilistically"); + assert!( + drop_count > 0, + "Should have dropped some packets probabilistically" + ); assert!(drop_count < total_packets, "Should not drop all packets"); } #[test_log::test] fn test_ared_queue_max_p_increase() { - let mut config = AdaptiveRedQueueConfig::default(); - config.min_th = 100; - config.max_th = 200; - config.max_p = 0.02; - config.w_q = 1.0; // Instantly update avg + let config = AdaptiveRedQueueConfig { + min_th: 100, + max_th: 200, + max_p: 0.02, + w_q: 1.0, // Instantly update avg + ..Default::default() + }; let mut queue: AdaptiveRedQueue = AdaptiveRedQueue::new(config); // enqueue to make average_queue_length > target_max @@ -441,16 +479,21 @@ mod tests { queue.enqueue(create_packet(14)); let after_max_p = queue.config.max_p; - assert!(after_max_p > before_max_p, "max_p should increase when avg > target_max"); + assert!( + after_max_p > before_max_p, + "max_p should increase when avg > target_max" + ); } #[test_log::test] fn test_ared_queue_max_p_decrease() { - let mut config = AdaptiveRedQueueConfig::default(); - config.min_th = 100; - config.max_th = 200; - config.max_p = 0.05; // Starting with a high max_p - config.w_q = 1.0; + let config = AdaptiveRedQueueConfig { + min_th: 100, + max_th: 200, + max_p: 0.05, // Starting with a high max_p + w_q: 1.0, + ..Default::default() + }; let mut queue: AdaptiveRedQueue = AdaptiveRedQueue::new(config); // enqueue to make average_queue_length < target_min @@ -469,6 +512,9 @@ mod tests { queue.enqueue(create_packet(14)); let after_max_p = queue.config.max_p; - assert!(after_max_p < before_max_p, "max_p should decrease when avg < target_min"); + assert!( + after_max_p < before_max_p, + "max_p should decrease when avg < target_min" + ); } } diff --git a/rattan-core/src/cells/bandwidth/queue/mod.rs b/rattan-core/src/cells/bandwidth/queue/mod.rs index 9fc1286a..03d7dcd9 100644 --- a/rattan-core/src/cells/bandwidth/queue/mod.rs +++ b/rattan-core/src/cells/bandwidth/queue/mod.rs @@ -11,21 +11,21 @@ use tokio::time::Instant; use super::BwType; use crate::cells::{Packet, LARGE_DURATION}; +mod ared; mod codel; mod drophead; mod droptail; mod infinite; -mod red; -mod ared; mod pie; +mod red; +pub use ared::*; pub use codel::*; pub use drophead::*; pub use droptail::*; pub use infinite::*; -pub use red::*; -pub use ared::*; pub use pie::*; +pub use red::*; #[cfg(feature = "serde")] fn serde_default(t: &T) -> bool { diff --git a/rattan-core/src/cells/bandwidth/queue/pie.rs b/rattan-core/src/cells/bandwidth/queue/pie.rs index 22d0e35b..50318dc5 100644 --- a/rattan-core/src/cells/bandwidth/queue/pie.rs +++ b/rattan-core/src/cells/bandwidth/queue/pie.rs @@ -1,15 +1,15 @@ // PIE Queue Implementation Reference: // https://www.rfc-editor.org/info/rfc8033 // https://ieeexplore.ieee.org/document/6602305 -// Reproduced according to RFC 8033 Appendix B, +// Reproduced according to RFC 8033 Appendix B, // rather than original paper or RFC Appendix A. use std::collections::VecDeque; +use rand::random_range; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -use tokio::time::{Instant, Duration}; -use rand::random_range; +use tokio::time::{Duration, Instant}; use tracing::debug; #[cfg(feature = "serde")] @@ -22,14 +22,17 @@ use crate::cells::Packet; pub struct PieQueueConfig { pub packet_limit: Option, pub byte_limit: Option, - pub ref_del: f64, // target delay (sec) - pub t_update: Duration, // update interval - pub tilde_alpha: f64, // base value of alpha (Hz, 1/sec) - pub tilde_beta: f64, // base value of beta (Hz, 1/sec) + pub ref_del: f64, // target delay (sec) + pub t_update: Duration, // update interval + pub tilde_alpha: f64, // base value of alpha (Hz, 1/sec) + pub tilde_beta: f64, // base value of beta (Hz, 1/sec) pub dq_threshold: usize, // threshold of queue length (bytes) - pub epsilon: f64, // EWMA weight - pub max_burst: f64, // MAX_BURST (ms) - #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "serde_default"))] + pub epsilon: f64, // EWMA weight + pub max_burst: f64, // MAX_BURST (ms) + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "serde_default") + )] pub bw_type: BwType, } @@ -51,6 +54,7 @@ impl Default for PieQueueConfig { } impl PieQueueConfig { + #[allow(clippy::too_many_arguments)] pub fn new>, B: Into>>( packet_limit: A, byte_limit: B, @@ -61,7 +65,7 @@ impl PieQueueConfig { dq_threshold: usize, epsilon: f64, max_burst: f64, - bw_type: BwType + bw_type: BwType, ) -> Self { Self { packet_limit: packet_limit.into(), @@ -73,7 +77,7 @@ impl PieQueueConfig { dq_threshold, epsilon, max_burst, - bw_type + bw_type, } } } @@ -89,13 +93,13 @@ pub struct PieQueue

{ queue: VecDeque

, config: PieQueueConfig, now_bytes: usize, - old_del: f64, // previous delay (sec) - p: f64, // current drop probability - dq_count: usize, // departure count (bytes) - start_update: Instant, // start time of t_update + old_del: f64, // previous delay (sec) + p: f64, // current drop probability + dq_count: usize, // departure count (bytes) + start_update: Instant, // start time of t_update start_measurement: Option, // Some(Instant) when in a measurement cycle, None when quit - avg_drate: f64, - burst_allowance: f64 + avg_drate: f64, + burst_allowance: f64, } impl

PieQueue

{ @@ -112,14 +116,14 @@ impl

PieQueue

{ start_update: Instant::now(), start_measurement: None, avg_drate: 0.0, - burst_allowance: max_burst + burst_allowance: max_burst, } } } impl

Default for PieQueue

-where - P: Packet +where + P: Packet, { fn default() -> Self { Self::new(PieQueueConfig::default()) @@ -132,8 +136,11 @@ where { fn update_drop_probability(&mut self) { let now = Instant::now(); - let elapsed_ms = now.saturating_duration_since(self.start_update).as_secs_f64() * 1000.0; - + let elapsed_ms = now + .saturating_duration_since(self.start_update) + .as_secs_f64() + * 1000.0; + let cur_del = if self.avg_drate.abs() < f64::EPSILON { 0.0 } else { @@ -156,14 +163,17 @@ where p_increment /= 2.0; } self.p += p_increment; - // RFC 8033 Section 4.2: Exponential decay when system is not congested + // RFC 8033 Section 4.2: Exponential decay when system is not congested if cur_del < self.config.ref_del / 2.0 && self.old_del < self.config.ref_del / 2.0 { self.p *= 0.98; - } + } self.p = self.p.clamp(0.0, 1.0); // RFC 8033 Section 4.4: Burst Tolerance - if self.p < f64::EPSILON && cur_del < self.config.ref_del / 2.0 && self.old_del < self.config.ref_del / 2.0 { + if self.p < f64::EPSILON + && cur_del < self.config.ref_del / 2.0 + && self.old_del < self.config.ref_del / 2.0 + { self.burst_allowance = self.config.max_burst; } else { self.burst_allowance = (self.burst_allowance - elapsed_ms).max(0.0); @@ -179,13 +189,13 @@ where } // RFC 8033 Section 4.1: Bypass random drop logic to be work conserving - let bypass_drop = (self.old_del < self.config.ref_del / 2.0 && self.p < 0.2) - || self.queue.len() <= 2; + let bypass_drop = + (self.old_del < self.config.ref_del / 2.0 && self.p < 0.2) || self.queue.len() <= 2; if bypass_drop { return false; } - let rand_val = random_range(0.0 .. 1.0); + let rand_val = random_range(0.0..1.0); rand_val < self.p } @@ -208,7 +218,8 @@ where if self.avg_drate.abs() < f64::EPSILON { self.avg_drate = dq_rate; } else { - self.avg_drate = (1.0 - self.config.epsilon) * self.avg_drate + self.config.epsilon * dq_rate; + self.avg_drate = (1.0 - self.config.epsilon) * self.avg_drate + + self.config.epsilon * dq_rate; } self.start_measurement = Some(now); self.dq_count = 0; @@ -244,11 +255,17 @@ where let interval_update = Instant::now().saturating_duration_since(self.start_update); if interval_update >= self.config.t_update { self.update_drop_probability(); - } - + } + let packet_size = packet.l3_length() + self.get_extra_length(); - let pass_hard_limit = self.config.packet_limit.is_none_or(|limit| self.queue.len() < limit) - && self.config.byte_limit.is_none_or(|limit| self.now_bytes + packet_size <= limit); + let pass_hard_limit = self + .config + .packet_limit + .is_none_or(|limit| self.queue.len() < limit) + && self + .config + .byte_limit + .is_none_or(|limit| self.now_bytes + packet_size <= limit); if !pass_hard_limit { #[cfg(test)] @@ -258,6 +275,7 @@ where header = ?format!("{:X?}", &packet.as_slice()[0..std::cmp::min(56, packet.length())]), "Drop packet(l3_len: {}, extra_len: {}) due to hard limit", packet.l3_length(), self.get_extra_length() ); + #[allow(clippy::needless_return)] return; } @@ -269,6 +287,7 @@ where header = ?format!("{:X?}", &packet.as_slice()[0..std::cmp::min(56, packet.length())]), "Drop packet(l3_len: {}, extra_len: {}) due to PIE algorithm", packet.l3_length(), self.get_extra_length() ); + #[allow(clippy::needless_return)] return; } @@ -313,7 +332,7 @@ where } fn retain(&mut self, mut f: F) - where + where F: FnMut(&P) -> bool, { self.queue.retain(|packet| f(packet)); @@ -348,8 +367,10 @@ mod tests { #[test_log::test] fn test_pie_queue_hard_limit_packet() { - let mut config = PieQueueConfig::default(); - config.packet_limit = Some(2); + let config = PieQueueConfig { + packet_limit: Some(2), + ..Default::default() + }; let mut queue: PieQueue = PieQueue::new(config); queue.enqueue(create_packet(100)); @@ -363,8 +384,10 @@ mod tests { #[test_log::test] fn test_pie_queue_hard_limit_byte() { - let mut config = PieQueueConfig::default(); - config.byte_limit = Some(150); + let config = PieQueueConfig { + byte_limit: Some(150), + ..Default::default() + }; let mut queue: PieQueue = PieQueue::new(config); queue.enqueue(create_packet(100)); // l3 length 86. @@ -383,7 +406,7 @@ mod tests { // Force a high drop probability queue.p = 1.0; queue.burst_allowance = 100.0; - + // burst_allowance > 0 bypasses random drop queue.enqueue(create_packet(100)); assert_eq!(queue.length(), 1); @@ -428,8 +451,10 @@ mod tests { #[test_log::test] fn test_pie_queue_avg_drate_update() { - let mut config = PieQueueConfig::default(); - config.dq_threshold = 50; // Small threshold + let config = PieQueueConfig { + dq_threshold: 50, // Small threshold + ..Default::default() + }; let mut queue: PieQueue = PieQueue::new(config); queue.enqueue(create_packet(114)); // l3 length 100 @@ -447,7 +472,10 @@ mod tests { // Second dequeue triggers calculation of avg_drate queue.dequeue(); // dequeues 100 bytes assert!(queue.avg_drate > 0.0, "avg_drate should be calculated"); - assert!(queue.start_measurement.is_none(), "Should exit measurement cycle since queue is empty"); + assert!( + queue.start_measurement.is_none(), + "Should exit measurement cycle since queue is empty" + ); } #[test_log::test] @@ -458,12 +486,15 @@ mod tests { // Fake high delay queue.avg_drate = 1000.0; queue.now_bytes = 100000; // delay = 100.0s > ref_del - + std::thread::sleep(config.t_update); // Wait to exceed t_update - + // This enqueue will trigger update_drop_probability() - queue.enqueue(create_packet(14)); - - assert!(queue.p > 0.0, "Probability should increase when delay is high"); + queue.enqueue(create_packet(14)); + + assert!( + queue.p > 0.0, + "Probability should increase when delay is high" + ); } -} \ No newline at end of file +} diff --git a/rattan-core/src/cells/bandwidth/queue/red.rs b/rattan-core/src/cells/bandwidth/queue/red.rs index c5389aa8..f5b60a6b 100644 --- a/rattan-core/src/cells/bandwidth/queue/red.rs +++ b/rattan-core/src/cells/bandwidth/queue/red.rs @@ -4,10 +4,10 @@ use std::collections::VecDeque; +use rand::random_range; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -use tokio::time::{Instant, Duration}; -use rand::random_range; +use tokio::time::{Duration, Instant}; use tracing::{debug, warn}; #[cfg(feature = "serde")] @@ -20,13 +20,19 @@ use crate::cells::Packet; pub struct RedQueueConfig { pub packet_limit: Option, pub byte_limit: Option, - pub w_q: f64, // queue weight for calculating the average queue length + pub w_q: f64, // queue weight for calculating the average queue length pub min_th: usize, // minimum threshold of average queue length pub max_th: usize, // maximum threshold of average queue length - pub max_p: f64, // maximum probability of dropping a packet - #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "serde_default"))] + pub max_p: f64, // maximum probability of dropping a packet + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "serde_default") + )] pub pkt_tx_time: Duration, // typical packet tx time (us) - #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "serde_default"))] + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "serde_default") + )] pub bw_type: BwType, } @@ -36,7 +42,7 @@ impl Default for RedQueueConfig { packet_limit: None, byte_limit: None, w_q: 0.002, - min_th: 7500, // 5 * 1500 bytes + min_th: 7500, // 5 * 1500 bytes max_th: 22500, // 15 * 1500 bytes max_p: 0.02, pkt_tx_time: Duration::from_micros(120), // 1500 bytes * 8 / 100Mbps = 120 us @@ -46,6 +52,7 @@ impl Default for RedQueueConfig { } impl RedQueueConfig { + #[allow(clippy::too_many_arguments)] pub fn new>, B: Into>>( packet_limit: A, byte_limit: B, @@ -54,13 +61,16 @@ impl RedQueueConfig { max_th: usize, max_p: f64, pkt_tx_time: Duration, - bw_type: BwType + bw_type: BwType, ) -> Self { // Warning: The caller must ensure that the parameters are valid. // It's recommended to do validation before calling this function, // or we may need to return a Result instead of Self in the future. if min_th >= max_th { - warn!("RedQueueConfig: min_th ({}) >= max_th ({}), which may cause invalid behavior.", min_th, max_th); + warn!( + "RedQueueConfig: min_th ({}) >= max_th ({}), which may cause invalid behavior.", + min_th, max_th + ); } if pkt_tx_time.as_micros() == 0 { warn!("RedQueueConfig: pkt_tx_time is 0, which will cause divide-by-zero in m calculation."); @@ -71,7 +81,7 @@ impl RedQueueConfig { if !(0.0..=1.0).contains(&max_p) { warn!("RedQueueConfig: max_p ({}) is out of expected range [0.0, 1.0]. This is a probability.", max_p); } - + Self { packet_limit: packet_limit.into(), byte_limit: byte_limit.into(), @@ -80,7 +90,7 @@ impl RedQueueConfig { max_th, max_p, pkt_tx_time, - bw_type + bw_type, } } } @@ -97,7 +107,7 @@ pub struct RedQueue

{ config: RedQueueConfig, now_bytes: usize, // for calculating average_queue_length average_queue_length: f64, - count_packet: i32, // number of packets since last dropping + count_packet: i32, // number of packets since last dropping idle_start: Option, // start time of current idle period } @@ -117,7 +127,7 @@ impl

RedQueue

{ impl

Default for RedQueue

where - P: Packet + P: Packet, { fn default() -> Self { Self::new(RedQueueConfig::default()) @@ -126,18 +136,20 @@ where impl

RedQueue

where - P: Packet, + P: Packet, { - fn update_avg (&mut self) { + fn update_avg(&mut self) { match self.is_empty() { false => { - self.average_queue_length = (1.0 - self.config.w_q) * self.average_queue_length + self.config.w_q * (self.now_bytes as f64) - }, + self.average_queue_length = (1.0 - self.config.w_q) * self.average_queue_length + + self.config.w_q * (self.now_bytes as f64) + } true => { if let Some(idle_start) = self.idle_start { let now = Instant::now(); let idle_duration = now.saturating_duration_since(idle_start); - let m = idle_duration.as_micros() as f64 / self.config.pkt_tx_time.as_micros() as f64; + let m = idle_duration.as_micros() as f64 + / self.config.pkt_tx_time.as_micros() as f64; self.average_queue_length *= f64::powf(1.0 - self.config.w_q, m); self.idle_start = Some(now); } @@ -145,7 +157,7 @@ where } } - fn should_drop (&mut self) -> bool { + fn should_drop(&mut self) -> bool { let avg = self.average_queue_length; let min_th = self.config.min_th as f64; let max_th = self.config.max_th as f64; @@ -158,7 +170,7 @@ where p_b / (1.0 - self.count_packet as f64 * p_b) }; - let rand_val = random_range(0.0 .. 1.0); + let rand_val = random_range(0.0..1.0); if rand_val < p_a { self.count_packet = 0; true @@ -192,10 +204,16 @@ where fn enqueue(&mut self, packet: P) { self.update_avg(); - + let packet_size = packet.l3_length() + self.get_extra_length(); - let pass_hard_limit = self.config.packet_limit.is_none_or(|limit| self.queue.len() < limit) - && self.config.byte_limit.is_none_or(|limit| self.now_bytes + packet_size <= limit); + let pass_hard_limit = self + .config + .packet_limit + .is_none_or(|limit| self.queue.len() < limit) + && self + .config + .byte_limit + .is_none_or(|limit| self.now_bytes + packet_size <= limit); if !pass_hard_limit { self.count_packet = 0; @@ -217,6 +235,7 @@ where header = ?format!("{:X?}", &packet.as_slice()[0..std::cmp::min(56, packet.length())]), "Drop packet(l3_len: {}, extra_len: {}) due to RED algorithm", packet.l3_length(), self.get_extra_length() ); + #[allow(clippy::needless_return)] return; } @@ -264,7 +283,6 @@ where } } - #[cfg(test)] mod tests { use super::*; @@ -277,9 +295,11 @@ mod tests { #[test_log::test] fn test_red_queue_basic() { - let mut config = RedQueueConfig::default(); - config.min_th = 1000; - config.max_th = 2000; + let config = RedQueueConfig { + min_th: 1000, + max_th: 2000, + ..Default::default() + }; let mut queue: RedQueue = RedQueue::new(config); assert!(queue.is_empty()); @@ -296,10 +316,12 @@ mod tests { #[test_log::test] fn test_red_queue_hard_limit_packet() { - let mut config = RedQueueConfig::default(); - config.packet_limit = Some(2); - config.min_th = 100000; // avoid red drop - config.max_th = 200000; + let config = RedQueueConfig { + packet_limit: Some(2), + min_th: 100000, // avoid red drop + max_th: 200000, + ..Default::default() + }; let mut queue: RedQueue = RedQueue::new(config); queue.enqueue(create_packet(100)); @@ -313,10 +335,12 @@ mod tests { #[test_log::test] fn test_red_queue_hard_limit_byte() { - let mut config = RedQueueConfig::default(); - config.byte_limit = Some(150); - config.min_th = 100000; // avoid red drop - config.max_th = 200000; + let config = RedQueueConfig { + byte_limit: Some(150), + min_th: 100000, // avoid red drop + max_th: 200000, + ..Default::default() + }; let mut queue: RedQueue = RedQueue::new(config); queue.enqueue(create_packet(100)); // l3 length 86. @@ -329,34 +353,42 @@ mod tests { #[test_log::test] fn test_red_queue_max_th_drop() { - let mut config = RedQueueConfig::default(); - config.min_th = 100; - config.max_th = 200; - config.w_q = 1.0; // max weight, avg matches instantly + let config = RedQueueConfig { + min_th: 100, + max_th: 200, + w_q: 1.0, // max weight, avg matches instantly + ..Default::default() + }; let mut queue: RedQueue = RedQueue::new(config); // First packet - queue.enqueue(create_packet(100)); - + queue.enqueue(create_packet(100)); + // Second packet queue.enqueue(create_packet(300)); - + // At this point, queue length is 2, now_bytes is high enough. // The next enqueue should see average_queue_length > max_th and drop the packet. let before_len = queue.length(); queue.enqueue(create_packet(100)); - assert_eq!(queue.length(), before_len, "Packet should be dropped by RED max_th"); + assert_eq!( + queue.length(), + before_len, + "Packet should be dropped by RED max_th" + ); } #[test_log::test] fn test_red_queue_min_th_no_drop() { - let mut config = RedQueueConfig::default(); - config.min_th = 1000; - config.max_th = 2000; - config.w_q = 1.0; // Instantly reach exact byte size + let config = RedQueueConfig { + min_th: 1000, + max_th: 2000, + w_q: 1.0, // Instantly reach exact byte size + ..Default::default() + }; let mut queue: RedQueue = RedQueue::new(config); - // First packet: queue empty, avg remains 0. + // First packet: queue empty, avg remains 0. queue.enqueue(create_packet(514)); // L3 size = 514 - 14 (Ethernet header) = 500 assert_eq!(queue.length(), 1); @@ -364,40 +396,45 @@ mod tests { // 500 < min_th(1000), so it should not drop. queue.enqueue(create_packet(414)); // L3 size = 400 assert_eq!(queue.length(), 2); - + // Check internal state: count_packet is -1 when avg < min_th assert_eq!(queue.count_packet, -1); } #[test_log::test] fn test_red_queue_probabilistic_drop() { - let mut config = RedQueueConfig::default(); - config.min_th = 100; - config.max_th = 300; - config.max_p = 0.5; - config.w_q = 1.0; + let config = RedQueueConfig { + min_th: 100, + max_th: 300, + max_p: 0.5, + w_q: 1.0, + ..Default::default() + }; let mut queue: RedQueue = RedQueue::new(config); // First packet: queue empty, avg = 0. L3 size = 200. - queue.enqueue(create_packet(214)); + queue.enqueue(create_packet(214)); assert_eq!(queue.length(), 1); let mut drop_count = 0; let total_packets = 1000; - + for _ in 0..total_packets { // enqueue packets with L3 size 0 (total size 14). // now_bytes stays at 200. w_q=1.0 makes avg exactly 200. // 100 (min_th) <= avg(200) < 300 (max_th), entering probabilistic drop zone. let before = queue.length(); - queue.enqueue(create_packet(14)); + queue.enqueue(create_packet(14)); if queue.length() == before { drop_count += 1; } } - + // It should drop some packets, but not all of them - assert!(drop_count > 0, "Should have dropped some packets probabilistically"); + assert!( + drop_count > 0, + "Should have dropped some packets probabilistically" + ); assert!(drop_count < total_packets, "Should not drop all packets"); } -} \ No newline at end of file +} From dda180fcd1edef8bbef163a84cc9ef0db7b4d173 Mon Sep 17 00:00:00 2001 From: duanxy23 Date: Tue, 9 Jun 2026 19:36:01 +0800 Subject: [PATCH 10/40] fix function update_avg of RED --- rattan-core/src/cells/bandwidth/queue/red.rs | 27 +++++++++----------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/rattan-core/src/cells/bandwidth/queue/red.rs b/rattan-core/src/cells/bandwidth/queue/red.rs index f5b60a6b..8c2d7a93 100644 --- a/rattan-core/src/cells/bandwidth/queue/red.rs +++ b/rattan-core/src/cells/bandwidth/queue/red.rs @@ -139,21 +139,18 @@ where P: Packet, { fn update_avg(&mut self) { - match self.is_empty() { - false => { - self.average_queue_length = (1.0 - self.config.w_q) * self.average_queue_length - + self.config.w_q * (self.now_bytes as f64) - } - true => { - if let Some(idle_start) = self.idle_start { - let now = Instant::now(); - let idle_duration = now.saturating_duration_since(idle_start); - let m = idle_duration.as_micros() as f64 - / self.config.pkt_tx_time.as_micros() as f64; - self.average_queue_length *= f64::powf(1.0 - self.config.w_q, m); - self.idle_start = Some(now); - } - } + if !self.is_empty() { + self.average_queue_length = (1.0 - self.config.w_q) * self.average_queue_length + + self.config.w_q * (self.now_bytes as f64); + return; + } + + if let Some(idle_start) = self.idle_start { + let now = Instant::now(); + let idle_duration = now.saturating_duration_since(idle_start); + let m = idle_duration.as_micros() as f64 / self.config.pkt_tx_time.as_micros() as f64; + self.average_queue_length *= f64::powf(1.0 - self.config.w_q, m); + self.idle_start = Some(now); } } From abc195cf68b17d024c0e88eb0bd2f5c27aac061c Mon Sep 17 00:00:00 2001 From: duanxy23 Date: Tue, 9 Jun 2026 23:43:10 +0800 Subject: [PATCH 11/40] fix clippy warning & corresponding logic --- rattan-core/src/cells/bandwidth/queue/ared.rs | 30 ++----- rattan-core/src/cells/bandwidth/queue/pie.rs | 84 +++++++------------ rattan-core/src/cells/bandwidth/queue/red.rs | 32 ++----- 3 files changed, 45 insertions(+), 101 deletions(-) diff --git a/rattan-core/src/cells/bandwidth/queue/ared.rs b/rattan-core/src/cells/bandwidth/queue/ared.rs index 6be1a5f7..657149d9 100644 --- a/rattan-core/src/cells/bandwidth/queue/ared.rs +++ b/rattan-core/src/cells/bandwidth/queue/ared.rs @@ -26,11 +26,6 @@ pub struct AdaptiveRedQueueConfig { feature = "serde", serde(default, skip_serializing_if = "serde_default") )] - pub pkt_tx_time: Duration, // typical packet tx time (us) - #[cfg_attr( - feature = "serde", - serde(default, skip_serializing_if = "serde_default") - )] pub bw_type: BwType, } @@ -43,14 +38,12 @@ impl Default for AdaptiveRedQueueConfig { min_th: 7500, // 5 * 1500 bytes max_th: 22500, // 15 * 1500 bytes max_p: 0.02, - pkt_tx_time: Duration::from_micros(120), // 1500 bytes * 8 / 100Mbps = 120 us bw_type: BwType::default(), } } } impl AdaptiveRedQueueConfig { - #[allow(clippy::too_many_arguments)] pub fn new>, B: Into>>( packet_limit: A, byte_limit: B, @@ -58,7 +51,6 @@ impl AdaptiveRedQueueConfig { min_th: usize, max_th: usize, max_p: f64, - pkt_tx_time: Duration, bw_type: BwType, ) -> Self { // Warning: The caller must ensure that the parameters are valid. @@ -67,9 +59,6 @@ impl AdaptiveRedQueueConfig { if min_th >= max_th { warn!("AdaptiveRedQueueConfig: min_th ({}) >= max_th ({}), which may cause invalid behavior.", min_th, max_th); } - if pkt_tx_time.as_micros() == 0 { - warn!("AdaptiveRedQueueConfig: pkt_tx_time is 0, which will cause divide-by-zero in m calculation."); - } if !(0.0..=1.0).contains(&w_q) { warn!("AdaptiveRedQueueConfig: w_q ({}) is out of expected range [0.0, 1.0]. This is an EWMA weight.", w_q); } @@ -84,7 +73,6 @@ impl AdaptiveRedQueueConfig { min_th, max_th, max_p, - pkt_tx_time, bw_type, } } @@ -145,7 +133,8 @@ where if let Some(idle_start) = self.idle_start { let now = Instant::now(); let idle_duration = now.saturating_duration_since(idle_start); - let m = idle_duration.as_micros() as f64 / self.config.pkt_tx_time.as_micros() as f64; + let pkt_tx_time = 120.0; // 1500 bytes * 8 / 100Mbps = 120 us + let m = idle_duration.as_micros() as f64 / pkt_tx_time; self.average_queue_length *= f64::powf(1.0 - self.config.w_q, m); self.idle_start = Some(now); } @@ -236,10 +225,7 @@ where header = ?format!("{:X?}", &packet.as_slice()[0..std::cmp::min(56, packet.length())]), "Drop packet(l3_len: {}, extra_len: {}) due to hard limit", packet.l3_length(), self.get_extra_length() ); - return; - } - - if self.should_drop() { + } else if self.should_drop() { #[cfg(test)] tracing::trace!( avg = self.average_queue_length, @@ -247,13 +233,11 @@ where header = ?format!("{:X?}", &packet.as_slice()[0..std::cmp::min(56, packet.length())]), "Drop packet(l3_len: {}, extra_len: {}) due to ARED algorithm", packet.l3_length(), self.get_extra_length() ); - #[allow(clippy::needless_return)] - return; + } else { + self.now_bytes += packet_size; + self.queue.push_back(packet); + self.idle_start = None; } - - self.now_bytes += packet_size; - self.queue.push_back(packet); - self.idle_start = None; } fn dequeue(&mut self) -> Option

{ diff --git a/rattan-core/src/cells/bandwidth/queue/pie.rs b/rattan-core/src/cells/bandwidth/queue/pie.rs index 50318dc5..d2fcf17a 100644 --- a/rattan-core/src/cells/bandwidth/queue/pie.rs +++ b/rattan-core/src/cells/bandwidth/queue/pie.rs @@ -22,13 +22,8 @@ use crate::cells::Packet; pub struct PieQueueConfig { pub packet_limit: Option, pub byte_limit: Option, - pub ref_del: f64, // target delay (sec) - pub t_update: Duration, // update interval - pub tilde_alpha: f64, // base value of alpha (Hz, 1/sec) - pub tilde_beta: f64, // base value of beta (Hz, 1/sec) - pub dq_threshold: usize, // threshold of queue length (bytes) - pub epsilon: f64, // EWMA weight - pub max_burst: f64, // MAX_BURST (ms) + pub ref_del: f64, // target delay (sec) + pub max_burst: f64, // MAX_BURST (ms) #[cfg_attr( feature = "serde", serde(default, skip_serializing_if = "serde_default") @@ -42,11 +37,6 @@ impl Default for PieQueueConfig { packet_limit: None, byte_limit: None, ref_del: 0.015, // RFC 8033 - t_update: Duration::from_millis(15), - tilde_alpha: 0.125, - tilde_beta: 1.25, - dq_threshold: 16384, // 16 KB - epsilon: 0.125, max_burst: 150.0, bw_type: BwType::default(), } @@ -54,16 +44,10 @@ impl Default for PieQueueConfig { } impl PieQueueConfig { - #[allow(clippy::too_many_arguments)] pub fn new>, B: Into>>( packet_limit: A, byte_limit: B, ref_del: f64, - t_update: Duration, - tilde_alpha: f64, - tilde_beta: f64, - dq_threshold: usize, - epsilon: f64, max_burst: f64, bw_type: BwType, ) -> Self { @@ -71,11 +55,6 @@ impl PieQueueConfig { packet_limit: packet_limit.into(), byte_limit: byte_limit.into(), ref_del, - t_update, - tilde_alpha, - tilde_beta, - dq_threshold, - epsilon, max_burst, bw_type, } @@ -147,8 +126,10 @@ where self.now_bytes as f64 / self.avg_drate }; - let mut p_increment = self.config.tilde_alpha * (cur_del - self.config.ref_del) - + self.config.tilde_beta * (cur_del - self.old_del); + let tilde_alpha = 0.125; // base value of alpha (Hz, 1/sec) + let tilde_beta = 1.25; // base value of beta (Hz, 1/sec) + let mut p_increment = + tilde_alpha * (cur_del - self.config.ref_del) + tilde_beta * (cur_del - self.old_del); if self.p < 0.000001 { p_increment /= 2048.0; } else if self.p < 0.00001 { @@ -201,9 +182,10 @@ where fn update_avg_drate(&mut self, pkt_size: usize) { let now = Instant::now(); + let dq_threshold = 16384; // 16 KB // Enter a measurement cycle - if self.now_bytes > self.config.dq_threshold && self.start_measurement.is_none() { + if self.now_bytes > dq_threshold && self.start_measurement.is_none() { self.start_measurement = Some(now); self.dq_count = 0; } @@ -211,15 +193,15 @@ where // Update departure rate if we are in a measurement cycle if let Some(start) = self.start_measurement { self.dq_count += pkt_size; - if self.dq_count >= self.config.dq_threshold { + if self.dq_count >= dq_threshold { let dq_int = now.saturating_duration_since(start).as_secs_f64(); if dq_int > f64::EPSILON { let dq_rate = self.dq_count as f64 / dq_int; if self.avg_drate.abs() < f64::EPSILON { self.avg_drate = dq_rate; } else { - self.avg_drate = (1.0 - self.config.epsilon) * self.avg_drate - + self.config.epsilon * dq_rate; + let epsilon = 0.125; + self.avg_drate = (1.0 - epsilon) * self.avg_drate + epsilon * dq_rate; } self.start_measurement = Some(now); self.dq_count = 0; @@ -227,7 +209,7 @@ where } // Exit measurement cycle if queue length drops below threshold - if self.now_bytes < self.config.dq_threshold { + if self.now_bytes < dq_threshold { self.start_measurement = None; self.dq_count = 0; } @@ -253,7 +235,8 @@ where fn enqueue(&mut self, packet: P) { // Simulate time-driven with event-driven approach let interval_update = Instant::now().saturating_duration_since(self.start_update); - if interval_update >= self.config.t_update { + let t_update = Duration::from_millis(15); + if interval_update >= t_update { self.update_drop_probability(); } @@ -275,11 +258,7 @@ where header = ?format!("{:X?}", &packet.as_slice()[0..std::cmp::min(56, packet.length())]), "Drop packet(l3_len: {}, extra_len: {}) due to hard limit", packet.l3_length(), self.get_extra_length() ); - #[allow(clippy::needless_return)] - return; - } - - if self.should_drop() { + } else if self.should_drop() { #[cfg(test)] tracing::trace!( p = self.p, @@ -287,18 +266,17 @@ where header = ?format!("{:X?}", &packet.as_slice()[0..std::cmp::min(56, packet.length())]), "Drop packet(l3_len: {}, extra_len: {}) due to PIE algorithm", packet.l3_length(), self.get_extra_length() ); - #[allow(clippy::needless_return)] - return; + } else { + self.now_bytes += packet_size; + self.queue.push_back(packet); } - - self.now_bytes += packet_size; - self.queue.push_back(packet); } fn dequeue(&mut self) -> Option

{ // Simulate time-driven with event-driven approach let interval_update = Instant::now().saturating_duration_since(self.start_update); - if interval_update >= self.config.t_update { + let t_update = Duration::from_millis(15); + if interval_update >= t_update { self.update_drop_probability(); } @@ -451,30 +429,28 @@ mod tests { #[test_log::test] fn test_pie_queue_avg_drate_update() { - let config = PieQueueConfig { - dq_threshold: 50, // Small threshold - ..Default::default() - }; + let config = PieQueueConfig::default(); let mut queue: PieQueue = PieQueue::new(config); - queue.enqueue(create_packet(114)); // l3 length 100 - queue.enqueue(create_packet(114)); // l3 length 100 - assert_eq!(queue.now_bytes, 200); + queue.enqueue(create_packet(10014)); // l3 length 10000 + queue.enqueue(create_packet(10014)); // l3 length 10000 + queue.enqueue(create_packet(10014)); // l3 length 10000 + assert_eq!(queue.now_bytes, 30000); // First dequeue triggers start of measurement cycle assert!(queue.start_measurement.is_none()); - queue.dequeue(); // dequeues 100 bytes + queue.dequeue(); // dequeues 10000 bytes assert!(queue.start_measurement.is_some()); - assert_eq!(queue.now_bytes, 100); + assert_eq!(queue.now_bytes, 20000); std::thread::sleep(Duration::from_millis(10)); // Second dequeue triggers calculation of avg_drate - queue.dequeue(); // dequeues 100 bytes + queue.dequeue(); // dequeues 10000 bytes assert!(queue.avg_drate > 0.0, "avg_drate should be calculated"); assert!( queue.start_measurement.is_none(), - "Should exit measurement cycle since queue is empty" + "Should exit measurement cycle since now_bytes drops below threshold" ); } @@ -487,7 +463,7 @@ mod tests { queue.avg_drate = 1000.0; queue.now_bytes = 100000; // delay = 100.0s > ref_del - std::thread::sleep(config.t_update); // Wait to exceed t_update + std::thread::sleep(Duration::from_millis(15)); // Wait to exceed t_update // This enqueue will trigger update_drop_probability() queue.enqueue(create_packet(14)); diff --git a/rattan-core/src/cells/bandwidth/queue/red.rs b/rattan-core/src/cells/bandwidth/queue/red.rs index 8c2d7a93..8b54d613 100644 --- a/rattan-core/src/cells/bandwidth/queue/red.rs +++ b/rattan-core/src/cells/bandwidth/queue/red.rs @@ -7,7 +7,7 @@ use std::collections::VecDeque; use rand::random_range; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -use tokio::time::{Duration, Instant}; +use tokio::time::Instant; use tracing::{debug, warn}; #[cfg(feature = "serde")] @@ -28,11 +28,6 @@ pub struct RedQueueConfig { feature = "serde", serde(default, skip_serializing_if = "serde_default") )] - pub pkt_tx_time: Duration, // typical packet tx time (us) - #[cfg_attr( - feature = "serde", - serde(default, skip_serializing_if = "serde_default") - )] pub bw_type: BwType, } @@ -45,14 +40,12 @@ impl Default for RedQueueConfig { min_th: 7500, // 5 * 1500 bytes max_th: 22500, // 15 * 1500 bytes max_p: 0.02, - pkt_tx_time: Duration::from_micros(120), // 1500 bytes * 8 / 100Mbps = 120 us bw_type: BwType::default(), } } } impl RedQueueConfig { - #[allow(clippy::too_many_arguments)] pub fn new>, B: Into>>( packet_limit: A, byte_limit: B, @@ -60,7 +53,6 @@ impl RedQueueConfig { min_th: usize, max_th: usize, max_p: f64, - pkt_tx_time: Duration, bw_type: BwType, ) -> Self { // Warning: The caller must ensure that the parameters are valid. @@ -72,9 +64,6 @@ impl RedQueueConfig { min_th, max_th ); } - if pkt_tx_time.as_micros() == 0 { - warn!("RedQueueConfig: pkt_tx_time is 0, which will cause divide-by-zero in m calculation."); - } if !(0.0..=1.0).contains(&w_q) { warn!("RedQueueConfig: w_q ({}) is out of expected range [0.0, 1.0]. This is an EWMA weight.", w_q); } @@ -89,7 +78,6 @@ impl RedQueueConfig { min_th, max_th, max_p, - pkt_tx_time, bw_type, } } @@ -148,7 +136,8 @@ where if let Some(idle_start) = self.idle_start { let now = Instant::now(); let idle_duration = now.saturating_duration_since(idle_start); - let m = idle_duration.as_micros() as f64 / self.config.pkt_tx_time.as_micros() as f64; + let pkt_tx_time = 120.0; // 1500 bytes * 8 / 100Mbps = 120 us + let m = idle_duration.as_micros() as f64 / pkt_tx_time; self.average_queue_length *= f64::powf(1.0 - self.config.w_q, m); self.idle_start = Some(now); } @@ -221,10 +210,7 @@ where header = ?format!("{:X?}", &packet.as_slice()[0..std::cmp::min(56, packet.length())]), "Drop packet(l3_len: {}, extra_len: {}) due to hard limit", packet.l3_length(), self.config.bw_type.extra_length() ); - return; - } - - if self.should_drop() { + } else if self.should_drop() { #[cfg(test)] tracing::trace!( avg = self.average_queue_length, @@ -232,13 +218,11 @@ where header = ?format!("{:X?}", &packet.as_slice()[0..std::cmp::min(56, packet.length())]), "Drop packet(l3_len: {}, extra_len: {}) due to RED algorithm", packet.l3_length(), self.get_extra_length() ); - #[allow(clippy::needless_return)] - return; + } else { + self.now_bytes += packet_size; + self.queue.push_back(packet); + self.idle_start = None; } - - self.now_bytes += packet_size; - self.queue.push_back(packet); - self.idle_start = None; } fn dequeue(&mut self) -> Option

{ From 61536ed32f7a08b0b9f2a898815b882673457970 Mon Sep 17 00:00:00 2001 From: duanxy23 Date: Wed, 10 Jun 2026 14:54:56 +0800 Subject: [PATCH 12/40] revert enqueue() by using early return --- rattan-core/src/cells/bandwidth/queue/ared.rs | 13 ++++++++----- rattan-core/src/cells/bandwidth/queue/pie.rs | 11 +++++++---- rattan-core/src/cells/bandwidth/queue/red.rs | 13 ++++++++----- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/rattan-core/src/cells/bandwidth/queue/ared.rs b/rattan-core/src/cells/bandwidth/queue/ared.rs index 657149d9..a57aa834 100644 --- a/rattan-core/src/cells/bandwidth/queue/ared.rs +++ b/rattan-core/src/cells/bandwidth/queue/ared.rs @@ -225,7 +225,10 @@ where header = ?format!("{:X?}", &packet.as_slice()[0..std::cmp::min(56, packet.length())]), "Drop packet(l3_len: {}, extra_len: {}) due to hard limit", packet.l3_length(), self.get_extra_length() ); - } else if self.should_drop() { + return; + } + + if self.should_drop() { #[cfg(test)] tracing::trace!( avg = self.average_queue_length, @@ -233,11 +236,11 @@ where header = ?format!("{:X?}", &packet.as_slice()[0..std::cmp::min(56, packet.length())]), "Drop packet(l3_len: {}, extra_len: {}) due to ARED algorithm", packet.l3_length(), self.get_extra_length() ); - } else { - self.now_bytes += packet_size; - self.queue.push_back(packet); - self.idle_start = None; + return; } + self.now_bytes += packet_size; + self.queue.push_back(packet); + self.idle_start = None; } fn dequeue(&mut self) -> Option

{ diff --git a/rattan-core/src/cells/bandwidth/queue/pie.rs b/rattan-core/src/cells/bandwidth/queue/pie.rs index d2fcf17a..0a759098 100644 --- a/rattan-core/src/cells/bandwidth/queue/pie.rs +++ b/rattan-core/src/cells/bandwidth/queue/pie.rs @@ -258,7 +258,10 @@ where header = ?format!("{:X?}", &packet.as_slice()[0..std::cmp::min(56, packet.length())]), "Drop packet(l3_len: {}, extra_len: {}) due to hard limit", packet.l3_length(), self.get_extra_length() ); - } else if self.should_drop() { + return; + } + + if self.should_drop() { #[cfg(test)] tracing::trace!( p = self.p, @@ -266,10 +269,10 @@ where header = ?format!("{:X?}", &packet.as_slice()[0..std::cmp::min(56, packet.length())]), "Drop packet(l3_len: {}, extra_len: {}) due to PIE algorithm", packet.l3_length(), self.get_extra_length() ); - } else { - self.now_bytes += packet_size; - self.queue.push_back(packet); + return; } + self.now_bytes += packet_size; + self.queue.push_back(packet); } fn dequeue(&mut self) -> Option

{ diff --git a/rattan-core/src/cells/bandwidth/queue/red.rs b/rattan-core/src/cells/bandwidth/queue/red.rs index 8b54d613..63af4ec8 100644 --- a/rattan-core/src/cells/bandwidth/queue/red.rs +++ b/rattan-core/src/cells/bandwidth/queue/red.rs @@ -210,7 +210,10 @@ where header = ?format!("{:X?}", &packet.as_slice()[0..std::cmp::min(56, packet.length())]), "Drop packet(l3_len: {}, extra_len: {}) due to hard limit", packet.l3_length(), self.config.bw_type.extra_length() ); - } else if self.should_drop() { + return; + } + + if self.should_drop() { #[cfg(test)] tracing::trace!( avg = self.average_queue_length, @@ -218,11 +221,11 @@ where header = ?format!("{:X?}", &packet.as_slice()[0..std::cmp::min(56, packet.length())]), "Drop packet(l3_len: {}, extra_len: {}) due to RED algorithm", packet.l3_length(), self.get_extra_length() ); - } else { - self.now_bytes += packet_size; - self.queue.push_back(packet); - self.idle_start = None; + return; } + self.now_bytes += packet_size; + self.queue.push_back(packet); + self.idle_start = None; } fn dequeue(&mut self) -> Option

{ From 411a3db463f14efe139aa0deb00ed0645409e816 Mon Sep 17 00:00:00 2001 From: duanxy23 Date: Thu, 18 Jun 2026 00:43:03 +0800 Subject: [PATCH 13/40] add seeded rng & t_update field --- rattan-core/src/cells/bandwidth/queue/ared.rs | 11 ++++--- rattan-core/src/cells/bandwidth/queue/pie.rs | 30 +++++++++++-------- rattan-core/src/cells/bandwidth/queue/red.rs | 6 ++-- 3 files changed, 29 insertions(+), 18 deletions(-) diff --git a/rattan-core/src/cells/bandwidth/queue/ared.rs b/rattan-core/src/cells/bandwidth/queue/ared.rs index a57aa834..75772e02 100644 --- a/rattan-core/src/cells/bandwidth/queue/ared.rs +++ b/rattan-core/src/cells/bandwidth/queue/ared.rs @@ -2,7 +2,7 @@ // https://www.icir.org/floyd/papers/adaptiveRed.pdf use std::collections::VecDeque; -use rand::random_range; +use rand::{rngs::StdRng, RngExt, SeedableRng}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use tokio::time::{Duration, Instant}; @@ -93,6 +93,7 @@ pub struct AdaptiveRedQueue

{ count_packet: i32, // number of packets since last dropping idle_start: Option, // start time of current idle period latest_max_p_update: Instant, // latest time when max_p is updates + rng: StdRng, } impl

AdaptiveRedQueue

{ @@ -106,6 +107,7 @@ impl

AdaptiveRedQueue

{ count_packet: -1, idle_start: None, latest_max_p_update: Instant::now(), + rng: StdRng::seed_from_u64(42), } } } @@ -153,7 +155,7 @@ where p_b / (1.0 - self.count_packet as f64 * p_b) }; - let rand_val = random_range(0.0..1.0); + let rand_val = self.rng.random_range(0.0..1.0); if rand_val < p_a { self.count_packet = 0; true @@ -174,11 +176,12 @@ where self.config.min_th as f64 + 0.4 * (self.config.max_th - self.config.min_th) as f64; let target_max = self.config.min_th as f64 + 0.6 * (self.config.max_th - self.config.min_th) as f64; - if self.average_queue_length > target_max && self.config.max_p <= 0.5 { + if self.average_queue_length > target_max { self.config.max_p += (self.config.max_p / 4.0).min(0.01); - } else if self.average_queue_length < target_min && self.config.max_p >= 0.01 { + } else if self.average_queue_length < target_min { self.config.max_p *= 0.9; } + self.config.max_p = self.config.max_p.clamp(0.01, 0.5); } } diff --git a/rattan-core/src/cells/bandwidth/queue/pie.rs b/rattan-core/src/cells/bandwidth/queue/pie.rs index 0a759098..970a9dd6 100644 --- a/rattan-core/src/cells/bandwidth/queue/pie.rs +++ b/rattan-core/src/cells/bandwidth/queue/pie.rs @@ -6,7 +6,7 @@ use std::collections::VecDeque; -use rand::random_range; +use rand::{rngs::StdRng, RngExt, SeedableRng}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use tokio::time::{Duration, Instant}; @@ -22,8 +22,9 @@ use crate::cells::Packet; pub struct PieQueueConfig { pub packet_limit: Option, pub byte_limit: Option, - pub ref_del: f64, // target delay (sec) - pub max_burst: f64, // MAX_BURST (ms) + pub ref_del: f64, // target delay (sec) + pub max_burst: f64, // MAX_BURST (ms) + pub t_update: Duration, // update interval #[cfg_attr( feature = "serde", serde(default, skip_serializing_if = "serde_default") @@ -38,6 +39,7 @@ impl Default for PieQueueConfig { byte_limit: None, ref_del: 0.015, // RFC 8033 max_burst: 150.0, + t_update: Duration::from_millis(15), bw_type: BwType::default(), } } @@ -49,6 +51,7 @@ impl PieQueueConfig { byte_limit: B, ref_del: f64, max_burst: f64, + t_update: Duration, bw_type: BwType, ) -> Self { Self { @@ -56,6 +59,7 @@ impl PieQueueConfig { byte_limit: byte_limit.into(), ref_del, max_burst, + t_update, bw_type, } } @@ -79,6 +83,7 @@ pub struct PieQueue

{ start_measurement: Option, // Some(Instant) when in a measurement cycle, None when quit avg_drate: f64, burst_allowance: f64, + rng: StdRng, } impl

PieQueue

{ @@ -96,6 +101,7 @@ impl

PieQueue

{ start_measurement: None, avg_drate: 0.0, burst_allowance: max_burst, + rng: StdRng::seed_from_u64(42), } } } @@ -163,7 +169,7 @@ where self.start_update = now; } - fn should_drop(&self) -> bool { + fn should_drop(&mut self) -> bool { // RFC 8033 Section 4.4: Enqueue packet bypassing random drop if burst_allow > 0 if self.burst_allowance > f64::EPSILON { return false; @@ -176,13 +182,13 @@ where return false; } - let rand_val = random_range(0.0..1.0); + let rand_val = self.rng.random_range(0.0..1.0); rand_val < self.p } fn update_avg_drate(&mut self, pkt_size: usize) { let now = Instant::now(); - let dq_threshold = 16384; // 16 KB + let dq_threshold = 16384; // 16 KiB // Enter a measurement cycle if self.now_bytes > dq_threshold && self.start_measurement.is_none() { @@ -235,8 +241,7 @@ where fn enqueue(&mut self, packet: P) { // Simulate time-driven with event-driven approach let interval_update = Instant::now().saturating_duration_since(self.start_update); - let t_update = Duration::from_millis(15); - if interval_update >= t_update { + if interval_update >= self.config.t_update { self.update_drop_probability(); } @@ -278,8 +283,7 @@ where fn dequeue(&mut self) -> Option

{ // Simulate time-driven with event-driven approach let interval_update = Instant::now().saturating_duration_since(self.start_update); - let t_update = Duration::from_millis(15); - if interval_update >= t_update { + if interval_update >= self.config.t_update { self.update_drop_probability(); } @@ -446,7 +450,8 @@ mod tests { assert!(queue.start_measurement.is_some()); assert_eq!(queue.now_bytes, 20000); - std::thread::sleep(Duration::from_millis(10)); + // Force time to advance deterministically to avoid flaky tests + queue.start_measurement = Some(Instant::now() - Duration::from_millis(10)); // Second dequeue triggers calculation of avg_drate queue.dequeue(); // dequeues 10000 bytes @@ -466,7 +471,8 @@ mod tests { queue.avg_drate = 1000.0; queue.now_bytes = 100000; // delay = 100.0s > ref_del - std::thread::sleep(Duration::from_millis(15)); // Wait to exceed t_update + // Force next enqueue to trigger update_drop_probability() deterministically + queue.start_update = Instant::now() - Duration::from_millis(16); // This enqueue will trigger update_drop_probability() queue.enqueue(create_packet(14)); diff --git a/rattan-core/src/cells/bandwidth/queue/red.rs b/rattan-core/src/cells/bandwidth/queue/red.rs index 63af4ec8..6321813a 100644 --- a/rattan-core/src/cells/bandwidth/queue/red.rs +++ b/rattan-core/src/cells/bandwidth/queue/red.rs @@ -4,7 +4,7 @@ use std::collections::VecDeque; -use rand::random_range; +use rand::{rngs::StdRng, RngExt, SeedableRng}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use tokio::time::Instant; @@ -97,6 +97,7 @@ pub struct RedQueue

{ average_queue_length: f64, count_packet: i32, // number of packets since last dropping idle_start: Option, // start time of current idle period + rng: StdRng, } impl

RedQueue

{ @@ -109,6 +110,7 @@ impl

RedQueue

{ average_queue_length: 0.0, count_packet: -1, idle_start: None, + rng: StdRng::seed_from_u64(42), } } } @@ -156,7 +158,7 @@ where p_b / (1.0 - self.count_packet as f64 * p_b) }; - let rand_val = random_range(0.0..1.0); + let rand_val = self.rng.random_range(0.0..1.0); if rand_val < p_a { self.count_packet = 0; true From 21c791a747b17942a8ff2d9320a0513d5518a483 Mon Sep 17 00:00:00 2001 From: duanxy23 Date: Tue, 23 Jun 2026 20:52:31 +0800 Subject: [PATCH 14/40] use logical timestamp of a packet --- rattan-core/src/cells/bandwidth/queue/ared.rs | 8 ++++---- rattan-core/src/cells/bandwidth/queue/pie.rs | 14 ++++---------- rattan-core/src/cells/bandwidth/queue/red.rs | 6 +++--- 3 files changed, 11 insertions(+), 17 deletions(-) diff --git a/rattan-core/src/cells/bandwidth/queue/ared.rs b/rattan-core/src/cells/bandwidth/queue/ared.rs index 75772e02..0f4958ae 100644 --- a/rattan-core/src/cells/bandwidth/queue/ared.rs +++ b/rattan-core/src/cells/bandwidth/queue/ared.rs @@ -125,7 +125,7 @@ impl

AdaptiveRedQueue

where P: Packet, { - fn update_avg(&mut self) { + fn update_avg(&mut self, packet: &P) { if !self.is_empty() { self.average_queue_length = (1.0 - self.config.w_q) * self.average_queue_length + self.config.w_q * (self.now_bytes as f64); @@ -133,7 +133,7 @@ where } if let Some(idle_start) = self.idle_start { - let now = Instant::now(); + let now = packet.get_timestamp(); let idle_duration = now.saturating_duration_since(idle_start); let pkt_tx_time = 120.0; // 1500 bytes * 8 / 100Mbps = 120 us let m = idle_duration.as_micros() as f64 / pkt_tx_time; @@ -201,9 +201,9 @@ where } fn enqueue(&mut self, packet: P) { - self.update_avg(); + self.update_avg(&packet); - let now = Instant::now(); + let now = packet.get_timestamp(); if now.saturating_duration_since(self.latest_max_p_update) >= Duration::from_millis(500) { self.update_max_p(); self.latest_max_p_update = now; diff --git a/rattan-core/src/cells/bandwidth/queue/pie.rs b/rattan-core/src/cells/bandwidth/queue/pie.rs index 970a9dd6..3efc703b 100644 --- a/rattan-core/src/cells/bandwidth/queue/pie.rs +++ b/rattan-core/src/cells/bandwidth/queue/pie.rs @@ -119,8 +119,8 @@ impl

PieQueue

where P: Packet, { - fn update_drop_probability(&mut self) { - let now = Instant::now(); + fn update_drop_probability(&mut self, packet: &P) { + let now = packet.get_timestamp(); let elapsed_ms = now .saturating_duration_since(self.start_update) .as_secs_f64() @@ -240,9 +240,9 @@ where fn enqueue(&mut self, packet: P) { // Simulate time-driven with event-driven approach - let interval_update = Instant::now().saturating_duration_since(self.start_update); + let interval_update = packet.get_timestamp().saturating_duration_since(self.start_update); if interval_update >= self.config.t_update { - self.update_drop_probability(); + self.update_drop_probability(&packet); } let packet_size = packet.l3_length() + self.get_extra_length(); @@ -281,12 +281,6 @@ where } fn dequeue(&mut self) -> Option

{ - // Simulate time-driven with event-driven approach - let interval_update = Instant::now().saturating_duration_since(self.start_update); - if interval_update >= self.config.t_update { - self.update_drop_probability(); - } - if let Some(packet) = self.queue.pop_front() { let pkt_size = packet.l3_length() + self.get_extra_length(); self.now_bytes -= pkt_size; diff --git a/rattan-core/src/cells/bandwidth/queue/red.rs b/rattan-core/src/cells/bandwidth/queue/red.rs index 6321813a..6ca79c23 100644 --- a/rattan-core/src/cells/bandwidth/queue/red.rs +++ b/rattan-core/src/cells/bandwidth/queue/red.rs @@ -128,7 +128,7 @@ impl

RedQueue

where P: Packet, { - fn update_avg(&mut self) { + fn update_avg(&mut self, packet: &P) { if !self.is_empty() { self.average_queue_length = (1.0 - self.config.w_q) * self.average_queue_length + self.config.w_q * (self.now_bytes as f64); @@ -136,7 +136,7 @@ where } if let Some(idle_start) = self.idle_start { - let now = Instant::now(); + let now = packet.get_timestamp(); let idle_duration = now.saturating_duration_since(idle_start); let pkt_tx_time = 120.0; // 1500 bytes * 8 / 100Mbps = 120 us let m = idle_duration.as_micros() as f64 / pkt_tx_time; @@ -191,7 +191,7 @@ where } fn enqueue(&mut self, packet: P) { - self.update_avg(); + self.update_avg(&packet); let packet_size = packet.l3_length() + self.get_extra_length(); let pass_hard_limit = self From aa2b66b03368ed92704bebbccdc8f6fb3e3f7669 Mon Sep 17 00:00:00 2001 From: duanxy23 Date: Tue, 23 Jun 2026 21:37:28 +0800 Subject: [PATCH 15/40] estimate pkt_tx_time of ARED based on packets' timestamps --- rattan-core/src/cells/bandwidth/queue/ared.rs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/rattan-core/src/cells/bandwidth/queue/ared.rs b/rattan-core/src/cells/bandwidth/queue/ared.rs index 0f4958ae..07e22e16 100644 --- a/rattan-core/src/cells/bandwidth/queue/ared.rs +++ b/rattan-core/src/cells/bandwidth/queue/ared.rs @@ -93,6 +93,7 @@ pub struct AdaptiveRedQueue

{ count_packet: i32, // number of packets since last dropping idle_start: Option, // start time of current idle period latest_max_p_update: Instant, // latest time when max_p is updates + estimated_pkt_tx_time: f64, // estimated packet transmission time in microseconds rng: StdRng, } @@ -107,6 +108,7 @@ impl

AdaptiveRedQueue

{ count_packet: -1, idle_start: None, latest_max_p_update: Instant::now(), + estimated_pkt_tx_time: 120.0, // initial estimate: 1500 bytes * 8 / 100Mbps = 120 us rng: StdRng::seed_from_u64(42), } } @@ -129,14 +131,26 @@ where if !self.is_empty() { self.average_queue_length = (1.0 - self.config.w_q) * self.average_queue_length + self.config.w_q * (self.now_bytes as f64); + + // Estimate pkt_tx_time based on first and last packet timestamps + if self.queue.len() >= 2 { + if let (Some(first), Some(last)) = (self.queue.front(), self.queue.back()) { + let first_time = first.get_timestamp(); + let last_time = last.get_timestamp(); + let time_diff = last_time.saturating_duration_since(first_time); + if time_diff.as_micros() > 0 { + let avg_inter_packet_time = time_diff.as_micros() as f64 / (self.queue.len() - 1) as f64; + self.estimated_pkt_tx_time = avg_inter_packet_time; + } + } + } return; } if let Some(idle_start) = self.idle_start { let now = packet.get_timestamp(); let idle_duration = now.saturating_duration_since(idle_start); - let pkt_tx_time = 120.0; // 1500 bytes * 8 / 100Mbps = 120 us - let m = idle_duration.as_micros() as f64 / pkt_tx_time; + let m = idle_duration.as_micros() as f64 / self.estimated_pkt_tx_time; self.average_queue_length *= f64::powf(1.0 - self.config.w_q, m); self.idle_start = Some(now); } From bc038dcdef819ec545b979e2411543d11ca51cf3 Mon Sep 17 00:00:00 2001 From: duanxy23 Date: Tue, 23 Jun 2026 21:58:21 +0800 Subject: [PATCH 16/40] fix red & ared --- rattan-core/src/cells/bandwidth/queue/ared.rs | 6 ++--- rattan-core/src/cells/bandwidth/queue/pie.rs | 4 ++-- rattan-core/src/cells/bandwidth/queue/red.rs | 24 +++++++++++++++---- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/rattan-core/src/cells/bandwidth/queue/ared.rs b/rattan-core/src/cells/bandwidth/queue/ared.rs index 07e22e16..aeefca80 100644 --- a/rattan-core/src/cells/bandwidth/queue/ared.rs +++ b/rattan-core/src/cells/bandwidth/queue/ared.rs @@ -177,7 +177,7 @@ where false } } else if avg >= max_th { - self.count_packet = 0; + self.count_packet = -1; true } else { self.count_packet = -1; @@ -224,7 +224,7 @@ where } let packet_size = packet.l3_length() + self.get_extra_length(); - let pass_hard_limit = self + let below_hard_limit = self .config .packet_limit .is_none_or(|limit| self.queue.len() < limit) @@ -233,7 +233,7 @@ where .byte_limit .is_none_or(|limit| self.now_bytes + packet_size <= limit); - if !pass_hard_limit { + if !below_hard_limit { self.count_packet = 0; #[cfg(test)] tracing::trace!( diff --git a/rattan-core/src/cells/bandwidth/queue/pie.rs b/rattan-core/src/cells/bandwidth/queue/pie.rs index 3efc703b..33afdf8a 100644 --- a/rattan-core/src/cells/bandwidth/queue/pie.rs +++ b/rattan-core/src/cells/bandwidth/queue/pie.rs @@ -246,7 +246,7 @@ where } let packet_size = packet.l3_length() + self.get_extra_length(); - let pass_hard_limit = self + let below_hard_limit = self .config .packet_limit .is_none_or(|limit| self.queue.len() < limit) @@ -255,7 +255,7 @@ where .byte_limit .is_none_or(|limit| self.now_bytes + packet_size <= limit); - if !pass_hard_limit { + if !below_hard_limit { #[cfg(test)] tracing::trace!( queue_len = self.queue.len(), diff --git a/rattan-core/src/cells/bandwidth/queue/red.rs b/rattan-core/src/cells/bandwidth/queue/red.rs index 6ca79c23..23f73e0b 100644 --- a/rattan-core/src/cells/bandwidth/queue/red.rs +++ b/rattan-core/src/cells/bandwidth/queue/red.rs @@ -98,6 +98,7 @@ pub struct RedQueue

{ count_packet: i32, // number of packets since last dropping idle_start: Option, // start time of current idle period rng: StdRng, + estimated_pkt_tx_time: f64, // estimated packet transmission time in microseconds } impl

RedQueue

{ @@ -111,6 +112,7 @@ impl

RedQueue

{ count_packet: -1, idle_start: None, rng: StdRng::seed_from_u64(42), + estimated_pkt_tx_time: 120.0, // initial estimate: 1500 bytes * 8 / 100Mbps = 120 us } } } @@ -132,14 +134,26 @@ where if !self.is_empty() { self.average_queue_length = (1.0 - self.config.w_q) * self.average_queue_length + self.config.w_q * (self.now_bytes as f64); + + // Estimate pkt_tx_time based on first and last packet timestamps + if self.queue.len() >= 2 { + if let (Some(first), Some(last)) = (self.queue.front(), self.queue.back()) { + let first_time = first.get_timestamp(); + let last_time = last.get_timestamp(); + let time_diff = last_time.saturating_duration_since(first_time); + if time_diff.as_micros() > 0 { + let avg_inter_packet_time = time_diff.as_micros() as f64 / (self.queue.len() - 1) as f64; + self.estimated_pkt_tx_time = avg_inter_packet_time; + } + } + } return; } if let Some(idle_start) = self.idle_start { let now = packet.get_timestamp(); let idle_duration = now.saturating_duration_since(idle_start); - let pkt_tx_time = 120.0; // 1500 bytes * 8 / 100Mbps = 120 us - let m = idle_duration.as_micros() as f64 / pkt_tx_time; + let m = idle_duration.as_micros() as f64 / self.estimated_pkt_tx_time; self.average_queue_length *= f64::powf(1.0 - self.config.w_q, m); self.idle_start = Some(now); } @@ -166,7 +180,7 @@ where false } } else if avg >= max_th { - self.count_packet = 0; + self.count_packet = -1; true } else { self.count_packet = -1; @@ -194,7 +208,7 @@ where self.update_avg(&packet); let packet_size = packet.l3_length() + self.get_extra_length(); - let pass_hard_limit = self + let below_hard_limit = self .config .packet_limit .is_none_or(|limit| self.queue.len() < limit) @@ -203,7 +217,7 @@ where .byte_limit .is_none_or(|limit| self.now_bytes + packet_size <= limit); - if !pass_hard_limit { + if !below_hard_limit { self.count_packet = 0; #[cfg(test)] tracing::trace!( From bef3825ed1d11545c5b7dbf9865b05f57f69f58a Mon Sep 17 00:00:00 2001 From: duanxy23 Date: Tue, 23 Jun 2026 22:09:35 +0800 Subject: [PATCH 17/40] align the logic of update_avg_drate with RFC --- rattan-core/src/cells/bandwidth/queue/pie.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rattan-core/src/cells/bandwidth/queue/pie.rs b/rattan-core/src/cells/bandwidth/queue/pie.rs index 33afdf8a..204d0e12 100644 --- a/rattan-core/src/cells/bandwidth/queue/pie.rs +++ b/rattan-core/src/cells/bandwidth/queue/pie.rs @@ -209,7 +209,7 @@ where let epsilon = 0.125; self.avg_drate = (1.0 - epsilon) * self.avg_drate + epsilon * dq_rate; } - self.start_measurement = Some(now); + self.start_measurement = None; self.dq_count = 0; } } From c86ce5253c622f0640ec0c6da9efc77c2ad25452 Mon Sep 17 00:00:00 2001 From: duanxy23 Date: Tue, 23 Jun 2026 23:02:17 +0800 Subject: [PATCH 18/40] modify red & ared unit test --- rattan-core/src/cells/bandwidth/queue/ared.rs | 29 +++++++++++++++++-- rattan-core/src/cells/bandwidth/queue/red.rs | 29 +++++++++++++++++-- 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/rattan-core/src/cells/bandwidth/queue/ared.rs b/rattan-core/src/cells/bandwidth/queue/ared.rs index aeefca80..3f83ddea 100644 --- a/rattan-core/src/cells/bandwidth/queue/ared.rs +++ b/rattan-core/src/cells/bandwidth/queue/ared.rs @@ -171,7 +171,7 @@ where let rand_val = self.rng.random_range(0.0..1.0); if rand_val < p_a { - self.count_packet = 0; + self.count_packet = -1; // first add, then calculate p_a true } else { false @@ -446,7 +446,32 @@ mod tests { } } - // It should drop some packets, but not all of them + // Calculate expected drop count based on RED algorithm + // When avg = 200, min_th = 100, max_th = 300, max_p = 0.5 + // p_b = max_p * (avg - min_th) / (max_th - min_th) = 0.5 * 100 / 200 = 0.25 + // Due to the uniform distribution of inter-drop times in RED, + // the expected drop rate is 2 * p_b / (1 + p_b) + // For p_b = 0.25, expected drop rate = 0.5 / 1.25 = 0.4 (40%) + let p_b = 0.25; + let expected_drop_rate = (2.0 * p_b) / (1.0 + p_b); + let expected_drop_count = (total_packets as f64 * expected_drop_rate) as usize; + + // Allow reasonable statistical variation (±3 standard deviations, 99.7% confidence) + // Standard deviation for binomial distribution: sqrt(n * p * (1-p)) + let std_dev = (total_packets as f64 * expected_drop_rate * (1.0 - expected_drop_rate)).sqrt(); + let margin = (3.0 * std_dev) as usize; + + let lower_bound = expected_drop_count.saturating_sub(margin); + let upper_bound = expected_drop_count + margin; + + // Verify drop count is within expected statistical range + assert!( + drop_count >= lower_bound && drop_count <= upper_bound, + "Drop count {} should be in range [{}, {}] (expected {} ± {} based on RED algorithm with p_b=0.25)", + drop_count, lower_bound, upper_bound, expected_drop_count, margin + ); + + // Keep original assertions as sanity checks assert!( drop_count > 0, "Should have dropped some packets probabilistically" diff --git a/rattan-core/src/cells/bandwidth/queue/red.rs b/rattan-core/src/cells/bandwidth/queue/red.rs index 23f73e0b..4dbe2f0e 100644 --- a/rattan-core/src/cells/bandwidth/queue/red.rs +++ b/rattan-core/src/cells/bandwidth/queue/red.rs @@ -174,7 +174,7 @@ where let rand_val = self.rng.random_range(0.0..1.0); if rand_val < p_a { - self.count_packet = 0; + self.count_packet = -1; // first add, then calculate p_a true } else { false @@ -430,7 +430,32 @@ mod tests { } } - // It should drop some packets, but not all of them + // Calculate expected drop count based on RED algorithm + // When avg = 200, min_th = 100, max_th = 300, max_p = 0.5 + // p_b = max_p * (avg - min_th) / (max_th - min_th) = 0.5 * 100 / 200 = 0.25 + // Due to the uniform distribution of inter-drop times in RED, + // the expected drop rate is 2 * p_b / (1 + p_b) + // For p_b = 0.25, expected drop rate = 0.5 / 1.25 = 0.4 (40%) + let p_b = 0.25; + let expected_drop_rate = (2.0 * p_b) / (1.0 + p_b); + let expected_drop_count = (total_packets as f64 * expected_drop_rate) as usize; + + // Allow reasonable statistical variation (±3 standard deviations, 99.7% confidence) + // Standard deviation for binomial distribution: sqrt(n * p * (1-p)) + let std_dev = (total_packets as f64 * expected_drop_rate * (1.0 - expected_drop_rate)).sqrt(); + let margin = (3.0 * std_dev) as usize; + + let lower_bound = expected_drop_count.saturating_sub(margin); + let upper_bound = expected_drop_count + margin; + + // Verify drop count is within expected statistical range + assert!( + drop_count >= lower_bound && drop_count <= upper_bound, + "Drop count {} should be in range [{}, {}] (expected {} ± {} based on RED algorithm with p_b=0.25)", + drop_count, lower_bound, upper_bound, expected_drop_count, margin + ); + + // Keep original assertions as sanity checks assert!( drop_count > 0, "Should have dropped some packets probabilistically" From 673dfdb939f0f6e13928e675a814c87bd7db9a12 Mon Sep 17 00:00:00 2001 From: duanxy23 Date: Tue, 23 Jun 2026 23:04:05 +0800 Subject: [PATCH 19/40] ready to fix pie --- rattan-core/src/cells/bandwidth/queue/ared.rs | 18 ++++++++++-------- rattan-core/src/cells/bandwidth/queue/pie.rs | 4 +++- rattan-core/src/cells/bandwidth/queue/red.rs | 18 ++++++++++-------- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/rattan-core/src/cells/bandwidth/queue/ared.rs b/rattan-core/src/cells/bandwidth/queue/ared.rs index 3f83ddea..876b624c 100644 --- a/rattan-core/src/cells/bandwidth/queue/ared.rs +++ b/rattan-core/src/cells/bandwidth/queue/ared.rs @@ -131,7 +131,7 @@ where if !self.is_empty() { self.average_queue_length = (1.0 - self.config.w_q) * self.average_queue_length + self.config.w_q * (self.now_bytes as f64); - + // Estimate pkt_tx_time based on first and last packet timestamps if self.queue.len() >= 2 { if let (Some(first), Some(last)) = (self.queue.front(), self.queue.back()) { @@ -139,7 +139,8 @@ where let last_time = last.get_timestamp(); let time_diff = last_time.saturating_duration_since(first_time); if time_diff.as_micros() > 0 { - let avg_inter_packet_time = time_diff.as_micros() as f64 / (self.queue.len() - 1) as f64; + let avg_inter_packet_time = + time_diff.as_micros() as f64 / (self.queue.len() - 1) as f64; self.estimated_pkt_tx_time = avg_inter_packet_time; } } @@ -449,28 +450,29 @@ mod tests { // Calculate expected drop count based on RED algorithm // When avg = 200, min_th = 100, max_th = 300, max_p = 0.5 // p_b = max_p * (avg - min_th) / (max_th - min_th) = 0.5 * 100 / 200 = 0.25 - // Due to the uniform distribution of inter-drop times in RED, + // Due to the uniform distribution of inter-drop times in RED, // the expected drop rate is 2 * p_b / (1 + p_b) // For p_b = 0.25, expected drop rate = 0.5 / 1.25 = 0.4 (40%) let p_b = 0.25; let expected_drop_rate = (2.0 * p_b) / (1.0 + p_b); let expected_drop_count = (total_packets as f64 * expected_drop_rate) as usize; - + // Allow reasonable statistical variation (±3 standard deviations, 99.7% confidence) // Standard deviation for binomial distribution: sqrt(n * p * (1-p)) - let std_dev = (total_packets as f64 * expected_drop_rate * (1.0 - expected_drop_rate)).sqrt(); + let std_dev = + (total_packets as f64 * expected_drop_rate * (1.0 - expected_drop_rate)).sqrt(); let margin = (3.0 * std_dev) as usize; - + let lower_bound = expected_drop_count.saturating_sub(margin); let upper_bound = expected_drop_count + margin; - + // Verify drop count is within expected statistical range assert!( drop_count >= lower_bound && drop_count <= upper_bound, "Drop count {} should be in range [{}, {}] (expected {} ± {} based on RED algorithm with p_b=0.25)", drop_count, lower_bound, upper_bound, expected_drop_count, margin ); - + // Keep original assertions as sanity checks assert!( drop_count > 0, diff --git a/rattan-core/src/cells/bandwidth/queue/pie.rs b/rattan-core/src/cells/bandwidth/queue/pie.rs index 204d0e12..2a3a5f74 100644 --- a/rattan-core/src/cells/bandwidth/queue/pie.rs +++ b/rattan-core/src/cells/bandwidth/queue/pie.rs @@ -240,7 +240,9 @@ where fn enqueue(&mut self, packet: P) { // Simulate time-driven with event-driven approach - let interval_update = packet.get_timestamp().saturating_duration_since(self.start_update); + let interval_update = packet + .get_timestamp() + .saturating_duration_since(self.start_update); if interval_update >= self.config.t_update { self.update_drop_probability(&packet); } diff --git a/rattan-core/src/cells/bandwidth/queue/red.rs b/rattan-core/src/cells/bandwidth/queue/red.rs index 4dbe2f0e..c8a11d80 100644 --- a/rattan-core/src/cells/bandwidth/queue/red.rs +++ b/rattan-core/src/cells/bandwidth/queue/red.rs @@ -98,7 +98,7 @@ pub struct RedQueue

{ count_packet: i32, // number of packets since last dropping idle_start: Option, // start time of current idle period rng: StdRng, - estimated_pkt_tx_time: f64, // estimated packet transmission time in microseconds + estimated_pkt_tx_time: f64, // estimated packet transmission time in microseconds } impl

RedQueue

{ @@ -142,7 +142,8 @@ where let last_time = last.get_timestamp(); let time_diff = last_time.saturating_duration_since(first_time); if time_diff.as_micros() > 0 { - let avg_inter_packet_time = time_diff.as_micros() as f64 / (self.queue.len() - 1) as f64; + let avg_inter_packet_time = + time_diff.as_micros() as f64 / (self.queue.len() - 1) as f64; self.estimated_pkt_tx_time = avg_inter_packet_time; } } @@ -433,28 +434,29 @@ mod tests { // Calculate expected drop count based on RED algorithm // When avg = 200, min_th = 100, max_th = 300, max_p = 0.5 // p_b = max_p * (avg - min_th) / (max_th - min_th) = 0.5 * 100 / 200 = 0.25 - // Due to the uniform distribution of inter-drop times in RED, + // Due to the uniform distribution of inter-drop times in RED, // the expected drop rate is 2 * p_b / (1 + p_b) // For p_b = 0.25, expected drop rate = 0.5 / 1.25 = 0.4 (40%) let p_b = 0.25; let expected_drop_rate = (2.0 * p_b) / (1.0 + p_b); let expected_drop_count = (total_packets as f64 * expected_drop_rate) as usize; - + // Allow reasonable statistical variation (±3 standard deviations, 99.7% confidence) // Standard deviation for binomial distribution: sqrt(n * p * (1-p)) - let std_dev = (total_packets as f64 * expected_drop_rate * (1.0 - expected_drop_rate)).sqrt(); + let std_dev = + (total_packets as f64 * expected_drop_rate * (1.0 - expected_drop_rate)).sqrt(); let margin = (3.0 * std_dev) as usize; - + let lower_bound = expected_drop_count.saturating_sub(margin); let upper_bound = expected_drop_count + margin; - + // Verify drop count is within expected statistical range assert!( drop_count >= lower_bound && drop_count <= upper_bound, "Drop count {} should be in range [{}, {}] (expected {} ± {} based on RED algorithm with p_b=0.25)", drop_count, lower_bound, upper_bound, expected_drop_count, margin ); - + // Keep original assertions as sanity checks assert!( drop_count > 0, From 7b995f80939950c5660fd6b8edbf935e1138ad70 Mon Sep 17 00:00:00 2001 From: duanxy23 Date: Wed, 24 Jun 2026 13:02:20 +0800 Subject: [PATCH 20/40] fix pie & update unit tests --- rattan-core/src/cells/bandwidth/queue/ared.rs | 16 ++-- rattan-core/src/cells/bandwidth/queue/pie.rs | 95 ++++++++++++------- 2 files changed, 72 insertions(+), 39 deletions(-) diff --git a/rattan-core/src/cells/bandwidth/queue/ared.rs b/rattan-core/src/cells/bandwidth/queue/ared.rs index 876b624c..44aa0bfd 100644 --- a/rattan-core/src/cells/bandwidth/queue/ared.rs +++ b/rattan-core/src/cells/bandwidth/queue/ared.rs @@ -502,12 +502,14 @@ mod tests { queue.enqueue(create_packet(14)); assert_eq!(queue.average_queue_length, 180.0); - // Set latest_max_p_update to 600ms ago to trigger update_max_p - queue.latest_max_p_update = Instant::now() - Duration::from_millis(600); + // Set latest_max_p_update artificially back, then enqueue a packet with current timestamp + let mut pkt3 = create_packet(14); + pkt3.delay_until(queue.latest_max_p_update + Duration::from_millis(600)); + let before_max_p = queue.config.max_p; // Third enqueue triggers update_max_p - queue.enqueue(create_packet(14)); + queue.enqueue(pkt3); let after_max_p = queue.config.max_p; assert!( @@ -536,11 +538,13 @@ mod tests { queue.enqueue(create_packet(14)); assert_eq!(queue.average_queue_length, 120.0); - // Set latest_max_p_update to 600ms ago - queue.latest_max_p_update = Instant::now() - Duration::from_millis(600); + // Enqueue a packet with timestamp 600ms later to trigger update_max_p + let mut pkt3 = create_packet(14); + pkt3.delay_until(queue.latest_max_p_update + Duration::from_millis(600)); + let before_max_p = queue.config.max_p; - queue.enqueue(create_packet(14)); + queue.enqueue(pkt3); let after_max_p = queue.config.max_p; assert!( diff --git a/rattan-core/src/cells/bandwidth/queue/pie.rs b/rattan-core/src/cells/bandwidth/queue/pie.rs index 2a3a5f74..e0d204e7 100644 --- a/rattan-core/src/cells/bandwidth/queue/pie.rs +++ b/rattan-core/src/cells/bandwidth/queue/pie.rs @@ -119,12 +119,8 @@ impl

PieQueue

where P: Packet, { - fn update_drop_probability(&mut self, packet: &P) { - let now = packet.get_timestamp(); - let elapsed_ms = now - .saturating_duration_since(self.start_update) - .as_secs_f64() - * 1000.0; + fn update_drop_probability(&mut self) { + let elapsed_ms = self.config.t_update.as_secs_f64() * 1000.0; let cur_del = if self.avg_drate.abs() < f64::EPSILON { 0.0 @@ -149,7 +145,14 @@ where } else if self.p < 0.1 { p_increment /= 2.0; } + + // RFC 8033 Section 5.5: Cap Drop Adjustment + if self.p >= 0.1 { + p_increment = p_increment.min(0.02); + } + self.p += p_increment; + // RFC 8033 Section 4.2: Exponential decay when system is not congested if cur_del < self.config.ref_del / 2.0 && self.old_del < self.config.ref_del / 2.0 { self.p *= 0.98; @@ -166,7 +169,7 @@ where self.burst_allowance = (self.burst_allowance - elapsed_ms).max(0.0); } self.old_del = cur_del; - self.start_update = now; + self.start_update += self.config.t_update; } fn should_drop(&mut self) -> bool { @@ -176,8 +179,11 @@ where } // RFC 8033 Section 4.1: Bypass random drop logic to be work conserving - let bypass_drop = - (self.old_del < self.config.ref_del / 2.0 && self.p < 0.2) || self.queue.len() <= 2; + // MEAN_PKTSIZE is generally considered to be 1500 bytes (standard MTU) in RFCs. + // We add extra_length to align with how now_bytes is calculated (L2 vs L3). + let mean_pktsize = 1500 + self.get_extra_length(); + let bypass_drop = (self.old_del < self.config.ref_del / 2.0 && self.p < 0.2) + || self.now_bytes <= 2 * mean_pktsize; if bypass_drop { return false; } @@ -239,12 +245,15 @@ where } fn enqueue(&mut self, packet: P) { - // Simulate time-driven with event-driven approach - let interval_update = packet + // Simulate time-driven with event-driven approach by using circular update logic + let mut interval_update = packet .get_timestamp() .saturating_duration_since(self.start_update); - if interval_update >= self.config.t_update { - self.update_drop_probability(&packet); + while interval_update >= self.config.t_update { + self.update_drop_probability(); + interval_update = packet + .get_timestamp() + .saturating_duration_since(self.start_update); } let packet_size = packet.l3_length() + self.get_extra_length(); @@ -389,17 +398,17 @@ mod tests { queue.burst_allowance = 100.0; // burst_allowance > 0 bypasses random drop - queue.enqueue(create_packet(100)); + queue.enqueue(create_packet(1500)); assert_eq!(queue.length(), 1); - // Fill queue > 2 to bypass work conserving logic later - queue.enqueue(create_packet(100)); - queue.enqueue(create_packet(100)); + // Fill queue > 2 * 1500 bytes to bypass work conserving logic later + queue.enqueue(create_packet(1500)); + queue.enqueue(create_packet(1500)); assert_eq!(queue.length(), 3); queue.burst_allowance = 0.0; - // With burst_allowance = 0.0, queue > 2, and p = 1.0, it should drop - queue.enqueue(create_packet(100)); + // With burst_allowance = 0.0, queue > 2 * 1500 bytes, and p = 1.0, it should drop + queue.enqueue(create_packet(1500)); assert_eq!(queue.length(), 3); } @@ -410,23 +419,25 @@ mod tests { queue.p = 1.0; queue.burst_allowance = 0.0; - // bypass_drop handles queue.len() <= 2 - queue.enqueue(create_packet(100)); + // bypass_drop handles queue.now_bytes <= 3000 + queue.enqueue(create_packet(1500)); assert_eq!(queue.length(), 1); - queue.enqueue(create_packet(100)); + queue.enqueue(create_packet(1500)); assert_eq!(queue.length(), 2); - queue.enqueue(create_packet(100)); + + // For the 3rd element, now_bytes is 3000, so it bypasses based on byte length (<= 3000) + queue.enqueue(create_packet(1500)); assert_eq!(queue.length(), 3); - // For the 4th element, queue.len() is 3, so it does not bypass based on length + // For the 4th element, now_bytes is 4500, so it does not bypass based on byte length // Since p = 1.0, it drops - queue.enqueue(create_packet(100)); + queue.enqueue(create_packet(1500)); assert_eq!(queue.length(), 3); // Now test bypass_drop condition: old_del < ref_del/2 and p < 0.2 queue.p = 0.15; queue.old_del = config.ref_del / 3.0; - queue.enqueue(create_packet(100)); + queue.enqueue(create_packet(1500)); assert_eq!(queue.length(), 4); } @@ -446,8 +457,12 @@ mod tests { assert!(queue.start_measurement.is_some()); assert_eq!(queue.now_bytes, 20000); - // Force time to advance deterministically to avoid flaky tests - queue.start_measurement = Some(Instant::now() - Duration::from_millis(10)); + // Simulate time advancing for the next measurement + let mut pkt2 = create_packet(10014); + pkt2.delay_until(queue.start_measurement.unwrap() + Duration::from_millis(10)); + + // Enqueue the packet with advanced timestamp to update queue state + queue.enqueue(pkt2); // Second dequeue triggers calculation of avg_drate queue.dequeue(); // dequeues 10000 bytes @@ -465,17 +480,31 @@ mod tests { // Fake high delay queue.avg_drate = 1000.0; - queue.now_bytes = 100000; // delay = 100.0s > ref_del + queue.now_bytes = 100000; // cur_del = 100000 / 1000.0 = 100.0s > ref_del (0.015s) + queue.old_del = 50.0; // old_del = 50.0s + queue.p = 0.0; // start with p = 0 + + // Expected p_increment calculation: + // tilde_alpha = 0.125, tilde_beta = 1.25 + // p_increment = 0.125 * (100.0 - 0.015) + 1.25 * (100.0 - 50.0) + // p_increment = 12.498125 + 62.5 = 74.998125 + // Since initial p = 0.0 < 0.000001, p_increment /= 2048.0 + // p_increment = 74.998125 / 2048.0 ≈ 0.036620178 + // Final p should be exactly this value + let expected_p = (0.125 * (100.0 - config.ref_del) + 1.25 * (100.0 - 50.0)) / 2048.0; // Force next enqueue to trigger update_drop_probability() deterministically - queue.start_update = Instant::now() - Duration::from_millis(16); + let mut pkt = create_packet(14); + pkt.delay_until(queue.start_update + Duration::from_millis(16)); // This enqueue will trigger update_drop_probability() - queue.enqueue(create_packet(14)); + queue.enqueue(pkt); assert!( - queue.p > 0.0, - "Probability should increase when delay is high" + (queue.p - expected_p).abs() < f64::EPSILON, + "Probability should be exactly calculated based on PIE formula. Expected: {}, Got: {}", + expected_p, + queue.p ); } } From 068aa4d1e02988f0299fd781544e6caa55e95b9b Mon Sep 17 00:00:00 2001 From: duanxy23 Date: Wed, 24 Jun 2026 20:14:47 +0800 Subject: [PATCH 21/40] combine ared and red --- rattan-core/src/cells/bandwidth/queue/ared.rs | 555 ------------------ rattan-core/src/cells/bandwidth/queue/mod.rs | 2 - rattan-core/src/cells/bandwidth/queue/red.rs | 230 ++++++-- 3 files changed, 178 insertions(+), 609 deletions(-) delete mode 100644 rattan-core/src/cells/bandwidth/queue/ared.rs diff --git a/rattan-core/src/cells/bandwidth/queue/ared.rs b/rattan-core/src/cells/bandwidth/queue/ared.rs deleted file mode 100644 index 44aa0bfd..00000000 --- a/rattan-core/src/cells/bandwidth/queue/ared.rs +++ /dev/null @@ -1,555 +0,0 @@ -// Adaptive RED Queue Implementation Reference: -// https://www.icir.org/floyd/papers/adaptiveRed.pdf -use std::collections::VecDeque; - -use rand::{rngs::StdRng, RngExt, SeedableRng}; -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; -use tokio::time::{Duration, Instant}; -use tracing::{debug, warn}; - -#[cfg(feature = "serde")] -use super::serde_default; -use super::{BwType, PacketQueue}; -use crate::cells::Packet; - -#[cfg_attr(feature = "serde", derive(Deserialize, Serialize), serde(default))] -#[derive(Debug, Clone)] -pub struct AdaptiveRedQueueConfig { - pub packet_limit: Option, - pub byte_limit: Option, - pub w_q: f64, // queue weight for calculating the average queue length - pub min_th: usize, // minimum threshold of average queue length - pub max_th: usize, // maximum threshold of average queue length - pub max_p: f64, // maximum probability of dropping a packet - #[cfg_attr( - feature = "serde", - serde(default, skip_serializing_if = "serde_default") - )] - pub bw_type: BwType, -} - -impl Default for AdaptiveRedQueueConfig { - fn default() -> Self { - Self { - packet_limit: None, - byte_limit: None, - w_q: 0.002, - min_th: 7500, // 5 * 1500 bytes - max_th: 22500, // 15 * 1500 bytes - max_p: 0.02, - bw_type: BwType::default(), - } - } -} - -impl AdaptiveRedQueueConfig { - pub fn new>, B: Into>>( - packet_limit: A, - byte_limit: B, - w_q: f64, - min_th: usize, - max_th: usize, - max_p: f64, - bw_type: BwType, - ) -> Self { - // Warning: The caller must ensure that the parameters are valid. - // It's recommended to do validation before calling this function, - // or we may need to return a Result instead of Self in the future. - if min_th >= max_th { - warn!("AdaptiveRedQueueConfig: min_th ({}) >= max_th ({}), which may cause invalid behavior.", min_th, max_th); - } - if !(0.0..=1.0).contains(&w_q) { - warn!("AdaptiveRedQueueConfig: w_q ({}) is out of expected range [0.0, 1.0]. This is an EWMA weight.", w_q); - } - if !(0.0..=1.0).contains(&max_p) { - warn!("AdaptiveRedQueueConfig: max_p ({}) is out of expected range [0.0, 1.0]. This is a probability.", max_p); - } - - Self { - packet_limit: packet_limit.into(), - byte_limit: byte_limit.into(), - w_q, - min_th, - max_th, - max_p, - bw_type, - } - } -} - -impl

From for AdaptiveRedQueue

{ - fn from(config: AdaptiveRedQueueConfig) -> Self { - AdaptiveRedQueue::new(config) - } -} - -#[derive(Debug)] -pub struct AdaptiveRedQueue

{ - queue: VecDeque

, - config: AdaptiveRedQueueConfig, - now_bytes: usize, // for calculating average_queue_length - average_queue_length: f64, - count_packet: i32, // number of packets since last dropping - idle_start: Option, // start time of current idle period - latest_max_p_update: Instant, // latest time when max_p is updates - estimated_pkt_tx_time: f64, // estimated packet transmission time in microseconds - rng: StdRng, -} - -impl

AdaptiveRedQueue

{ - pub fn new(config: AdaptiveRedQueueConfig) -> Self { - debug!(?config, "New AdaptiveRedQueue"); - Self { - queue: VecDeque::new(), - config, - now_bytes: 0, - average_queue_length: 0.0, - count_packet: -1, - idle_start: None, - latest_max_p_update: Instant::now(), - estimated_pkt_tx_time: 120.0, // initial estimate: 1500 bytes * 8 / 100Mbps = 120 us - rng: StdRng::seed_from_u64(42), - } - } -} - -impl

Default for AdaptiveRedQueue

-where - P: Packet, -{ - fn default() -> Self { - Self::new(AdaptiveRedQueueConfig::default()) - } -} - -impl

AdaptiveRedQueue

-where - P: Packet, -{ - fn update_avg(&mut self, packet: &P) { - if !self.is_empty() { - self.average_queue_length = (1.0 - self.config.w_q) * self.average_queue_length - + self.config.w_q * (self.now_bytes as f64); - - // Estimate pkt_tx_time based on first and last packet timestamps - if self.queue.len() >= 2 { - if let (Some(first), Some(last)) = (self.queue.front(), self.queue.back()) { - let first_time = first.get_timestamp(); - let last_time = last.get_timestamp(); - let time_diff = last_time.saturating_duration_since(first_time); - if time_diff.as_micros() > 0 { - let avg_inter_packet_time = - time_diff.as_micros() as f64 / (self.queue.len() - 1) as f64; - self.estimated_pkt_tx_time = avg_inter_packet_time; - } - } - } - return; - } - - if let Some(idle_start) = self.idle_start { - let now = packet.get_timestamp(); - let idle_duration = now.saturating_duration_since(idle_start); - let m = idle_duration.as_micros() as f64 / self.estimated_pkt_tx_time; - self.average_queue_length *= f64::powf(1.0 - self.config.w_q, m); - self.idle_start = Some(now); - } - } - - fn should_drop(&mut self) -> bool { - let avg = self.average_queue_length; - let min_th = self.config.min_th as f64; - let max_th = self.config.max_th as f64; - if avg >= min_th && avg < max_th { - self.count_packet += 1; - let p_b = self.config.max_p * (avg - min_th) / (max_th - min_th); - let p_a = if self.count_packet as f64 * p_b >= 1.0 { - 1.0 - } else { - p_b / (1.0 - self.count_packet as f64 * p_b) - }; - - let rand_val = self.rng.random_range(0.0..1.0); - if rand_val < p_a { - self.count_packet = -1; // first add, then calculate p_a - true - } else { - false - } - } else if avg >= max_th { - self.count_packet = -1; - true - } else { - self.count_packet = -1; - false - } - } - - fn update_max_p(&mut self) { - let target_min = - self.config.min_th as f64 + 0.4 * (self.config.max_th - self.config.min_th) as f64; - let target_max = - self.config.min_th as f64 + 0.6 * (self.config.max_th - self.config.min_th) as f64; - if self.average_queue_length > target_max { - self.config.max_p += (self.config.max_p / 4.0).min(0.01); - } else if self.average_queue_length < target_min { - self.config.max_p *= 0.9; - } - self.config.max_p = self.config.max_p.clamp(0.01, 0.5); - } -} - -impl

PacketQueue

for AdaptiveRedQueue

-where - P: Packet, -{ - type Config = AdaptiveRedQueueConfig; - - fn configure(&mut self, config: Self::Config) { - self.config = config; - } - - fn is_zero_buffer(&self) -> bool { - self.config.packet_limit.is_some_and(|limit| limit == 0) - || self.config.byte_limit.is_some_and(|limit| limit == 0) - } - - fn enqueue(&mut self, packet: P) { - self.update_avg(&packet); - - let now = packet.get_timestamp(); - if now.saturating_duration_since(self.latest_max_p_update) >= Duration::from_millis(500) { - self.update_max_p(); - self.latest_max_p_update = now; - } - - let packet_size = packet.l3_length() + self.get_extra_length(); - let below_hard_limit = self - .config - .packet_limit - .is_none_or(|limit| self.queue.len() < limit) - && self - .config - .byte_limit - .is_none_or(|limit| self.now_bytes + packet_size <= limit); - - if !below_hard_limit { - self.count_packet = 0; - #[cfg(test)] - tracing::trace!( - queue_len = self.queue.len(), - now_bytes = self.now_bytes, - header = ?format!("{:X?}", &packet.as_slice()[0..std::cmp::min(56, packet.length())]), - "Drop packet(l3_len: {}, extra_len: {}) due to hard limit", packet.l3_length(), self.get_extra_length() - ); - return; - } - - if self.should_drop() { - #[cfg(test)] - tracing::trace!( - avg = self.average_queue_length, - count = self.count_packet, - header = ?format!("{:X?}", &packet.as_slice()[0..std::cmp::min(56, packet.length())]), - "Drop packet(l3_len: {}, extra_len: {}) due to ARED algorithm", packet.l3_length(), self.get_extra_length() - ); - return; - } - self.now_bytes += packet_size; - self.queue.push_back(packet); - self.idle_start = None; - } - - fn dequeue(&mut self) -> Option

{ - if let Some(packet) = self.queue.pop_front() { - self.now_bytes -= packet.l3_length() + self.get_extra_length(); - if self.is_empty() { - self.idle_start = Some(Instant::now()); - } - Some(packet) - } else { - None - } - } - - fn is_empty(&self) -> bool { - self.queue.is_empty() - } - - #[inline(always)] - fn get_extra_length(&self) -> usize { - self.config.bw_type.extra_length() - } - - fn get_front_size(&self) -> Option { - self.queue - .front() - .map(|packet| self.get_packet_size(packet)) - } - - fn length(&self) -> usize { - self.queue.len() - } - - fn retain(&mut self, mut f: F) - where - F: FnMut(&P) -> bool, - { - self.queue.retain(|packet| f(packet)); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::cells::StdPacket; - - fn create_packet(size: usize) -> StdPacket { - let buf = vec![0u8; size]; - StdPacket::with_timestamp(&buf, Instant::now()) - } - - #[test_log::test] - fn test_ared_queue_basic() { - let config = AdaptiveRedQueueConfig { - min_th: 1000, - max_th: 2000, - ..Default::default() - }; - let mut queue: AdaptiveRedQueue = AdaptiveRedQueue::new(config); - - assert!(queue.is_empty()); - - let pkt1 = create_packet(500); - queue.enqueue(pkt1); - assert!(!queue.is_empty()); - assert_eq!(queue.length(), 1); - - let dequeued = queue.dequeue(); - assert!(dequeued.is_some()); - assert!(queue.is_empty()); - } - - #[test_log::test] - fn test_ared_queue_hard_limit_packet() { - let config = AdaptiveRedQueueConfig { - packet_limit: Some(2), - min_th: 100000, // avoid red drop - max_th: 200000, - ..Default::default() - }; - let mut queue: AdaptiveRedQueue = AdaptiveRedQueue::new(config); - - queue.enqueue(create_packet(100)); - queue.enqueue(create_packet(100)); - assert_eq!(queue.length(), 2); - - // This one should be dropped due to packet limit - queue.enqueue(create_packet(100)); - assert_eq!(queue.length(), 2); - } - - #[test_log::test] - fn test_ared_queue_hard_limit_byte() { - let config = AdaptiveRedQueueConfig { - byte_limit: Some(150), - min_th: 100000, // avoid red drop - max_th: 200000, - ..Default::default() - }; - let mut queue: AdaptiveRedQueue = AdaptiveRedQueue::new(config); - - queue.enqueue(create_packet(100)); // l3 length 86. - assert_eq!(queue.length(), 1); - - // This one should be dropped due to byte limit (86 + 86 > 150) - queue.enqueue(create_packet(100)); - assert_eq!(queue.length(), 1); - } - - #[test_log::test] - fn test_ared_queue_max_th_drop() { - let config = AdaptiveRedQueueConfig { - min_th: 100, - max_th: 200, - w_q: 1.0, // max weight, avg matches instantly - ..Default::default() - }; - let mut queue: AdaptiveRedQueue = AdaptiveRedQueue::new(config); - - // First packet - queue.enqueue(create_packet(100)); - - // Second packet - queue.enqueue(create_packet(300)); - - // At this point, queue length is 2, now_bytes is high enough. - // The next enqueue should see average_queue_length > max_th and drop the packet. - let before_len = queue.length(); - queue.enqueue(create_packet(100)); - assert_eq!( - queue.length(), - before_len, - "Packet should be dropped by ARED max_th" - ); - } - - #[test_log::test] - fn test_ared_queue_min_th_no_drop() { - let config = AdaptiveRedQueueConfig { - min_th: 1000, - max_th: 2000, - w_q: 1.0, // Instantly reach exact byte size - ..Default::default() - }; - let mut queue: AdaptiveRedQueue = AdaptiveRedQueue::new(config); - - // First packet: queue empty, avg remains 0. - queue.enqueue(create_packet(514)); // L3 size = 514 - 14 (Ethernet header) = 500 - assert_eq!(queue.length(), 1); - - // Second packet: queue has 500 bytes. w_q=1.0 makes avg = 500. - // 500 < min_th(1000), so it should not drop. - queue.enqueue(create_packet(414)); // L3 size = 400 - assert_eq!(queue.length(), 2); - - // Check internal state: count_packet is -1 when avg < min_th - assert_eq!(queue.count_packet, -1); - } - - #[test_log::test] - fn test_ared_queue_probabilistic_drop() { - let config = AdaptiveRedQueueConfig { - min_th: 100, - max_th: 300, - max_p: 0.5, - w_q: 1.0, - ..Default::default() - }; - let mut queue: AdaptiveRedQueue = AdaptiveRedQueue::new(config); - - // First packet: queue empty, avg = 0. L3 size = 200. - queue.enqueue(create_packet(214)); - assert_eq!(queue.length(), 1); - - let mut drop_count = 0; - let total_packets = 1000; - - for _ in 0..total_packets { - // enqueue packets with L3 size 0 (total size 14). - // now_bytes stays at 200. w_q=1.0 makes avg exactly 200. - // 100 (min_th) <= avg(200) < 300 (max_th), entering probabilistic drop zone. - let before = queue.length(); - queue.enqueue(create_packet(14)); - if queue.length() == before { - drop_count += 1; - } - } - - // Calculate expected drop count based on RED algorithm - // When avg = 200, min_th = 100, max_th = 300, max_p = 0.5 - // p_b = max_p * (avg - min_th) / (max_th - min_th) = 0.5 * 100 / 200 = 0.25 - // Due to the uniform distribution of inter-drop times in RED, - // the expected drop rate is 2 * p_b / (1 + p_b) - // For p_b = 0.25, expected drop rate = 0.5 / 1.25 = 0.4 (40%) - let p_b = 0.25; - let expected_drop_rate = (2.0 * p_b) / (1.0 + p_b); - let expected_drop_count = (total_packets as f64 * expected_drop_rate) as usize; - - // Allow reasonable statistical variation (±3 standard deviations, 99.7% confidence) - // Standard deviation for binomial distribution: sqrt(n * p * (1-p)) - let std_dev = - (total_packets as f64 * expected_drop_rate * (1.0 - expected_drop_rate)).sqrt(); - let margin = (3.0 * std_dev) as usize; - - let lower_bound = expected_drop_count.saturating_sub(margin); - let upper_bound = expected_drop_count + margin; - - // Verify drop count is within expected statistical range - assert!( - drop_count >= lower_bound && drop_count <= upper_bound, - "Drop count {} should be in range [{}, {}] (expected {} ± {} based on RED algorithm with p_b=0.25)", - drop_count, lower_bound, upper_bound, expected_drop_count, margin - ); - - // Keep original assertions as sanity checks - assert!( - drop_count > 0, - "Should have dropped some packets probabilistically" - ); - assert!(drop_count < total_packets, "Should not drop all packets"); - } - - #[test_log::test] - fn test_ared_queue_max_p_increase() { - let config = AdaptiveRedQueueConfig { - min_th: 100, - max_th: 200, - max_p: 0.02, - w_q: 1.0, // Instantly update avg - ..Default::default() - }; - let mut queue: AdaptiveRedQueue = AdaptiveRedQueue::new(config); - - // enqueue to make average_queue_length > target_max - // target_max = min_th + 0.6 * (max_th - min_th) = 100 + 60 = 160 - // We make avg = 180 (L3 size 180, total 194) - queue.enqueue(create_packet(194)); - assert_eq!(queue.average_queue_length, 0.0); // First enqueue updates avg based on empty queue rule (avg=0). - - // Second enqueue updates avg to 180 - queue.enqueue(create_packet(14)); - assert_eq!(queue.average_queue_length, 180.0); - - // Set latest_max_p_update artificially back, then enqueue a packet with current timestamp - let mut pkt3 = create_packet(14); - pkt3.delay_until(queue.latest_max_p_update + Duration::from_millis(600)); - - let before_max_p = queue.config.max_p; - - // Third enqueue triggers update_max_p - queue.enqueue(pkt3); - - let after_max_p = queue.config.max_p; - assert!( - after_max_p > before_max_p, - "max_p should increase when avg > target_max" - ); - } - - #[test_log::test] - fn test_ared_queue_max_p_decrease() { - let config = AdaptiveRedQueueConfig { - min_th: 100, - max_th: 200, - max_p: 0.05, // Starting with a high max_p - w_q: 1.0, - ..Default::default() - }; - let mut queue: AdaptiveRedQueue = AdaptiveRedQueue::new(config); - - // enqueue to make average_queue_length < target_min - // target_min = min_th + 0.4 * (max_th - min_th) = 100 + 40 = 140 - // We make avg = 120 (L3 size 120, total 134) - queue.enqueue(create_packet(134)); - assert_eq!(queue.average_queue_length, 0.0); - - queue.enqueue(create_packet(14)); - assert_eq!(queue.average_queue_length, 120.0); - - // Enqueue a packet with timestamp 600ms later to trigger update_max_p - let mut pkt3 = create_packet(14); - pkt3.delay_until(queue.latest_max_p_update + Duration::from_millis(600)); - - let before_max_p = queue.config.max_p; - - queue.enqueue(pkt3); - - let after_max_p = queue.config.max_p; - assert!( - after_max_p < before_max_p, - "max_p should decrease when avg < target_min" - ); - } -} diff --git a/rattan-core/src/cells/bandwidth/queue/mod.rs b/rattan-core/src/cells/bandwidth/queue/mod.rs index 03d7dcd9..2e2f6e8e 100644 --- a/rattan-core/src/cells/bandwidth/queue/mod.rs +++ b/rattan-core/src/cells/bandwidth/queue/mod.rs @@ -11,7 +11,6 @@ use tokio::time::Instant; use super::BwType; use crate::cells::{Packet, LARGE_DURATION}; -mod ared; mod codel; mod drophead; mod droptail; @@ -19,7 +18,6 @@ mod infinite; mod pie; mod red; -pub use ared::*; pub use codel::*; pub use drophead::*; pub use droptail::*; diff --git a/rattan-core/src/cells/bandwidth/queue/red.rs b/rattan-core/src/cells/bandwidth/queue/red.rs index c8a11d80..d78f3dd5 100644 --- a/rattan-core/src/cells/bandwidth/queue/red.rs +++ b/rattan-core/src/cells/bandwidth/queue/red.rs @@ -1,13 +1,15 @@ -// RED Queue Implementation Reference: -// https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=251892 -// https://github.com/torvalds/linux/blob/master/include/net/red.h +// Combined RED Queue with Adaptive mode +// Reference: +// RED: https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=251892 +// ARED: https://www.icir.org/floyd/papers/adaptiveRed.pdf +// Kernel RED/ARED: https://github.com/torvalds/linux/blob/master/include/net/red.h use std::collections::VecDeque; use rand::{rngs::StdRng, RngExt, SeedableRng}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -use tokio::time::Instant; +use tokio::time::{Duration, Instant}; use tracing::{debug, warn}; #[cfg(feature = "serde")] @@ -24,6 +26,14 @@ pub struct RedQueueConfig { pub min_th: usize, // minimum threshold of average queue length pub max_th: usize, // maximum threshold of average queue length pub max_p: f64, // maximum probability of dropping a packet + // Packet transmission time in microseconds. + // Used to compute the number of "virtual packet departures" during an idle period + // for average queue length decay: `m = idle_time_us / pkt_tx_time`. + // The upper layer computes this from link bandwidth `C` and average packet size + // `avpkt` as `pkt_tx_time = avpkt * 8 / C`, then passes it down as a fixed config. + pub pkt_tx_time: f64, + pub adaptive: bool, // enable adaptive mode (ARED): max_p is adjusted dynamically + #[cfg_attr( feature = "serde", serde(default, skip_serializing_if = "serde_default") @@ -40,46 +50,79 @@ impl Default for RedQueueConfig { min_th: 7500, // 5 * 1500 bytes max_th: 22500, // 15 * 1500 bytes max_p: 0.02, + pkt_tx_time: 120.0, // 1500 bytes * 8 / 100Mbps = 120 us + adaptive: false, bw_type: BwType::default(), } } } impl RedQueueConfig { - pub fn new>, B: Into>>( - packet_limit: A, - byte_limit: B, - w_q: f64, - min_th: usize, - max_th: usize, - max_p: f64, - bw_type: BwType, - ) -> Self { - // Warning: The caller must ensure that the parameters are valid. - // It's recommended to do validation before calling this function, - // or we may need to return a Result instead of Self in the future. - if min_th >= max_th { + fn validate(&self) { + if self.min_th >= self.max_th { warn!( "RedQueueConfig: min_th ({}) >= max_th ({}), which may cause invalid behavior.", - min_th, max_th + self.min_th, self.max_th ); } - if !(0.0..=1.0).contains(&w_q) { - warn!("RedQueueConfig: w_q ({}) is out of expected range [0.0, 1.0]. This is an EWMA weight.", w_q); + if !(0.0..=1.0).contains(&self.w_q) { + warn!("RedQueueConfig: w_q ({}) is out of expected range [0.0, 1.0]. This is an EWMA weight.", self.w_q); } - if !(0.0..=1.0).contains(&max_p) { - warn!("RedQueueConfig: max_p ({}) is out of expected range [0.0, 1.0]. This is a probability.", max_p); + if !(0.0..=1.0).contains(&self.max_p) { + warn!("RedQueueConfig: max_p ({}) is out of expected range [0.0, 1.0]. This is a probability.", self.max_p); } + } - Self { - packet_limit: packet_limit.into(), - byte_limit: byte_limit.into(), - w_q, - min_th, - max_th, - max_p, - bw_type, - } + // A `RedQueueConfig` can be constructed in any of these ways: + // 1. Struct literal with defaults: + // RedQueueConfig { min_th: 100, ..Default::default() } + // 2. Setter chain (avoids the old giant `new()` with 9+ args): + // RedQueueConfig::default().with_min_th(100).with_adaptive(true) + // 3. Plain default (all fields take their `Default` values): + // RedQueueConfig::default() + pub fn with_packet_limit(mut self, limit: usize) -> Self { + self.packet_limit = Some(limit); + self + } + + pub fn with_byte_limit(mut self, limit: usize) -> Self { + self.byte_limit = Some(limit); + self + } + + pub fn with_w_q(mut self, w_q: f64) -> Self { + self.w_q = w_q; + self + } + + pub fn with_min_th(mut self, min_th: usize) -> Self { + self.min_th = min_th; + self + } + + pub fn with_max_th(mut self, max_th: usize) -> Self { + self.max_th = max_th; + self + } + + pub fn with_max_p(mut self, max_p: f64) -> Self { + self.max_p = max_p; + self + } + + pub fn with_pkt_tx_time(mut self, pkt_tx_time: f64) -> Self { + self.pkt_tx_time = pkt_tx_time; + self + } + + pub fn with_adaptive(mut self, adaptive: bool) -> Self { + self.adaptive = adaptive; + self + } + + pub fn with_bw_type(mut self, bw_type: BwType) -> Self { + self.bw_type = bw_type; + self } } @@ -93,16 +136,17 @@ impl

From for RedQueue

{ pub struct RedQueue

{ queue: VecDeque

, config: RedQueueConfig, - now_bytes: usize, // for calculating average_queue_length + now_bytes: usize, average_queue_length: f64, - count_packet: i32, // number of packets since last dropping - idle_start: Option, // start time of current idle period + count_packet: i32, // number of packets since last dropping + idle_start: Option, // start time of current idle period + latest_max_p_update: Instant, // latest time when max_p was updated (used in adaptive mode) rng: StdRng, - estimated_pkt_tx_time: f64, // estimated packet transmission time in microseconds } impl

RedQueue

{ pub fn new(config: RedQueueConfig) -> Self { + config.validate(); debug!(?config, "New RedQueue"); Self { queue: VecDeque::new(), @@ -111,8 +155,8 @@ impl

RedQueue

{ average_queue_length: 0.0, count_packet: -1, idle_start: None, + latest_max_p_update: Instant::now(), rng: StdRng::seed_from_u64(42), - estimated_pkt_tx_time: 120.0, // initial estimate: 1500 bytes * 8 / 100Mbps = 120 us } } } @@ -134,27 +178,13 @@ where if !self.is_empty() { self.average_queue_length = (1.0 - self.config.w_q) * self.average_queue_length + self.config.w_q * (self.now_bytes as f64); - - // Estimate pkt_tx_time based on first and last packet timestamps - if self.queue.len() >= 2 { - if let (Some(first), Some(last)) = (self.queue.front(), self.queue.back()) { - let first_time = first.get_timestamp(); - let last_time = last.get_timestamp(); - let time_diff = last_time.saturating_duration_since(first_time); - if time_diff.as_micros() > 0 { - let avg_inter_packet_time = - time_diff.as_micros() as f64 / (self.queue.len() - 1) as f64; - self.estimated_pkt_tx_time = avg_inter_packet_time; - } - } - } return; } if let Some(idle_start) = self.idle_start { let now = packet.get_timestamp(); let idle_duration = now.saturating_duration_since(idle_start); - let m = idle_duration.as_micros() as f64 / self.estimated_pkt_tx_time; + let m = idle_duration.as_micros() as f64 / self.config.pkt_tx_time; self.average_queue_length *= f64::powf(1.0 - self.config.w_q, m); self.idle_start = Some(now); } @@ -188,6 +218,19 @@ where false } } + + fn update_max_p(&mut self) { + let target_min = + self.config.min_th as f64 + 0.4 * (self.config.max_th - self.config.min_th) as f64; + let target_max = + self.config.min_th as f64 + 0.6 * (self.config.max_th - self.config.min_th) as f64; + if self.average_queue_length > target_max { + self.config.max_p += (self.config.max_p / 4.0).min(0.01); + } else if self.average_queue_length < target_min { + self.config.max_p *= 0.9; + } + self.config.max_p = self.config.max_p.clamp(0.01, 0.5); + } } impl

PacketQueue

for RedQueue

@@ -208,6 +251,15 @@ where fn enqueue(&mut self, packet: P) { self.update_avg(&packet); + if self.config.adaptive { + let now = packet.get_timestamp(); + if now.saturating_duration_since(self.latest_max_p_update) >= Duration::from_millis(500) + { + self.update_max_p(); + self.latest_max_p_update = now; + } + } + let packet_size = packet.l3_length() + self.get_extra_length(); let below_hard_limit = self .config @@ -225,7 +277,7 @@ where queue_len = self.queue.len(), now_bytes = self.now_bytes, header = ?format!("{:X?}", &packet.as_slice()[0..std::cmp::min(56, packet.length())]), - "Drop packet(l3_len: {}, extra_len: {}) due to hard limit", packet.l3_length(), self.config.bw_type.extra_length() + "Drop packet(l3_len: {}, extra_len: {}) due to hard limit", packet.l3_length(), self.get_extra_length() ); return; } @@ -464,4 +516,78 @@ mod tests { ); assert!(drop_count < total_packets, "Should not drop all packets"); } + + #[test_log::test] + fn test_red_queue_adaptive_max_p_increase() { + let config = RedQueueConfig { + min_th: 100, + max_th: 200, + max_p: 0.02, + w_q: 1.0, // Instantly update avg + adaptive: true, + ..Default::default() + }; + let mut queue: RedQueue = RedQueue::new(config); + + // enqueue to make average_queue_length > target_max + // target_max = min_th + 0.6 * (max_th - min_th) = 100 + 60 = 160 + // We make avg = 180 (L3 size 180, total 194) + queue.enqueue(create_packet(194)); + assert_eq!(queue.average_queue_length, 0.0); // First enqueue updates avg based on empty queue rule (avg=0). + + // Second enqueue updates avg to 180 + queue.enqueue(create_packet(14)); + assert_eq!(queue.average_queue_length, 180.0); + + // Set latest_max_p_update artificially back, then enqueue a packet with current timestamp + let mut pkt3 = create_packet(14); + pkt3.delay_until(queue.latest_max_p_update + Duration::from_millis(600)); + + let before_max_p = queue.config.max_p; + + // Third enqueue triggers update_max_p + queue.enqueue(pkt3); + + let after_max_p = queue.config.max_p; + assert!( + after_max_p > before_max_p, + "max_p should increase when avg > target_max" + ); + } + + #[test_log::test] + fn test_red_queue_adaptive_max_p_decrease() { + let config = RedQueueConfig { + min_th: 100, + max_th: 200, + max_p: 0.05, // Starting with a high max_p + w_q: 1.0, + adaptive: true, + ..Default::default() + }; + let mut queue: RedQueue = RedQueue::new(config); + + // enqueue to make average_queue_length < target_min + // target_min = min_th + 0.4 * (max_th - min_th) = 100 + 40 = 140 + // We make avg = 120 (L3 size 120, total 134) + queue.enqueue(create_packet(134)); + assert_eq!(queue.average_queue_length, 0.0); + + queue.enqueue(create_packet(14)); + assert_eq!(queue.average_queue_length, 120.0); + + // Enqueue a packet with timestamp 600ms later to trigger update_max_p + let mut pkt3 = create_packet(14); + pkt3.delay_until(queue.latest_max_p_update + Duration::from_millis(600)); + + let before_max_p = queue.config.max_p; + + queue.enqueue(pkt3); + + let after_max_p = queue.config.max_p; + assert!( + after_max_p < before_max_p, + "max_p should decrease when avg < target_min" + ); + } } From 60be851d6aa52d44da454f35b410ca6a8f2d9310 Mon Sep 17 00:00:00 2001 From: duanxy23 Date: Thu, 25 Jun 2026 22:18:40 +0800 Subject: [PATCH 22/40] initialize start_update with logical timestamp --- rattan-core/src/cells/bandwidth/queue/pie.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/rattan-core/src/cells/bandwidth/queue/pie.rs b/rattan-core/src/cells/bandwidth/queue/pie.rs index e0d204e7..56956824 100644 --- a/rattan-core/src/cells/bandwidth/queue/pie.rs +++ b/rattan-core/src/cells/bandwidth/queue/pie.rs @@ -79,7 +79,7 @@ pub struct PieQueue

{ old_del: f64, // previous delay (sec) p: f64, // current drop probability dq_count: usize, // departure count (bytes) - start_update: Instant, // start time of t_update + start_update: Option, // start time of t_update, set by first enqueue start_measurement: Option, // Some(Instant) when in a measurement cycle, None when quit avg_drate: f64, burst_allowance: f64, @@ -97,7 +97,7 @@ impl

PieQueue

{ old_del: 0.0, p: 0.0, dq_count: 0, - start_update: Instant::now(), + start_update: None, start_measurement: None, avg_drate: 0.0, burst_allowance: max_burst, @@ -169,7 +169,7 @@ where self.burst_allowance = (self.burst_allowance - elapsed_ms).max(0.0); } self.old_del = cur_del; - self.start_update += self.config.t_update; + self.start_update = Some(self.start_update.unwrap() + self.config.t_update); } fn should_drop(&mut self) -> bool { @@ -246,14 +246,17 @@ where fn enqueue(&mut self, packet: P) { // Simulate time-driven with event-driven approach by using circular update logic + if self.start_update.is_none() { + self.start_update = Some(packet.get_timestamp()); + } let mut interval_update = packet .get_timestamp() - .saturating_duration_since(self.start_update); + .saturating_duration_since(self.start_update.unwrap()); while interval_update >= self.config.t_update { self.update_drop_probability(); interval_update = packet .get_timestamp() - .saturating_duration_since(self.start_update); + .saturating_duration_since(self.start_update.unwrap()); } let packet_size = packet.l3_length() + self.get_extra_length(); @@ -495,7 +498,8 @@ mod tests { // Force next enqueue to trigger update_drop_probability() deterministically let mut pkt = create_packet(14); - pkt.delay_until(queue.start_update + Duration::from_millis(16)); + queue.start_update = Some(pkt.get_timestamp()); + pkt.delay_until(queue.start_update.unwrap() + Duration::from_millis(16)); // This enqueue will trigger update_drop_probability() queue.enqueue(pkt); From 3c772927f5f326fe586135516e8c338b5a142c0b Mon Sep 17 00:00:00 2001 From: duanxy23 Date: Thu, 25 Jun 2026 23:18:37 +0800 Subject: [PATCH 23/40] initialize latest_max_p_update with logical timestamp --- rattan-core/src/cells/bandwidth/queue/red.rs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/rattan-core/src/cells/bandwidth/queue/red.rs b/rattan-core/src/cells/bandwidth/queue/red.rs index d78f3dd5..3fcf77ca 100644 --- a/rattan-core/src/cells/bandwidth/queue/red.rs +++ b/rattan-core/src/cells/bandwidth/queue/red.rs @@ -138,9 +138,9 @@ pub struct RedQueue

{ config: RedQueueConfig, now_bytes: usize, average_queue_length: f64, - count_packet: i32, // number of packets since last dropping - idle_start: Option, // start time of current idle period - latest_max_p_update: Instant, // latest time when max_p was updated (used in adaptive mode) + count_packet: i32, // number of packets since last dropping + idle_start: Option, // start time of current idle period + latest_max_p_update: Option, // latest time when max_p was updated (used in adaptive mode), set by first enqueue rng: StdRng, } @@ -155,7 +155,7 @@ impl

RedQueue

{ average_queue_length: 0.0, count_packet: -1, idle_start: None, - latest_max_p_update: Instant::now(), + latest_max_p_update: None, rng: StdRng::seed_from_u64(42), } } @@ -253,10 +253,14 @@ where if self.config.adaptive { let now = packet.get_timestamp(); - if now.saturating_duration_since(self.latest_max_p_update) >= Duration::from_millis(500) + if self.latest_max_p_update.is_none() { + self.latest_max_p_update = Some(now); + } + if now.saturating_duration_since(self.latest_max_p_update.unwrap()) + >= Duration::from_millis(500) { self.update_max_p(); - self.latest_max_p_update = now; + self.latest_max_p_update = Some(now); } } @@ -541,7 +545,7 @@ mod tests { // Set latest_max_p_update artificially back, then enqueue a packet with current timestamp let mut pkt3 = create_packet(14); - pkt3.delay_until(queue.latest_max_p_update + Duration::from_millis(600)); + pkt3.delay_until(queue.latest_max_p_update.unwrap() + Duration::from_millis(600)); let before_max_p = queue.config.max_p; @@ -578,7 +582,7 @@ mod tests { // Enqueue a packet with timestamp 600ms later to trigger update_max_p let mut pkt3 = create_packet(14); - pkt3.delay_until(queue.latest_max_p_update + Duration::from_millis(600)); + pkt3.delay_until(queue.latest_max_p_update.unwrap() + Duration::from_millis(600)); let before_max_p = queue.config.max_p; From fc6ab4f816a5b18e439a1fa468f369857218e46b Mon Sep 17 00:00:00 2001 From: duanxy23 Date: Fri, 26 Jun 2026 10:06:53 +0800 Subject: [PATCH 24/40] fix(queue): replace Instant::now() with logical timestamp in dequeue path --- rattan-core/src/cells/bandwidth/queue/pie.rs | 16 ++++++++-------- rattan-core/src/cells/bandwidth/queue/red.rs | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/rattan-core/src/cells/bandwidth/queue/pie.rs b/rattan-core/src/cells/bandwidth/queue/pie.rs index 56956824..9e1350a1 100644 --- a/rattan-core/src/cells/bandwidth/queue/pie.rs +++ b/rattan-core/src/cells/bandwidth/queue/pie.rs @@ -192,8 +192,7 @@ where rand_val < self.p } - fn update_avg_drate(&mut self, pkt_size: usize) { - let now = Instant::now(); + fn update_avg_drate(&mut self, pkt_size: usize, now: Instant) { let dq_threshold = 16384; // 16 KiB // Enter a measurement cycle @@ -294,11 +293,11 @@ where self.queue.push_back(packet); } - fn dequeue(&mut self) -> Option

{ + fn dequeue_at(&mut self, timestamp: Instant) -> Option

{ if let Some(packet) = self.queue.pop_front() { let pkt_size = packet.l3_length() + self.get_extra_length(); self.now_bytes -= pkt_size; - self.update_avg_drate(pkt_size); + self.update_avg_drate(pkt_size, timestamp); Some(packet) } else { None @@ -353,7 +352,7 @@ mod tests { assert!(!queue.is_empty()); assert_eq!(queue.length(), 1); - let dequeued = queue.dequeue(); + let dequeued = queue.dequeue_at(Instant::now()); assert!(dequeued.is_some()); assert!(queue.is_empty()); } @@ -456,19 +455,20 @@ mod tests { // First dequeue triggers start of measurement cycle assert!(queue.start_measurement.is_none()); - queue.dequeue(); // dequeues 10000 bytes + let deq_time1 = Instant::now(); + queue.dequeue_at(deq_time1); // dequeues 10000 bytes assert!(queue.start_measurement.is_some()); assert_eq!(queue.now_bytes, 20000); // Simulate time advancing for the next measurement let mut pkt2 = create_packet(10014); - pkt2.delay_until(queue.start_measurement.unwrap() + Duration::from_millis(10)); + pkt2.delay_until(deq_time1 + Duration::from_millis(10)); // Enqueue the packet with advanced timestamp to update queue state queue.enqueue(pkt2); // Second dequeue triggers calculation of avg_drate - queue.dequeue(); // dequeues 10000 bytes + queue.dequeue_at(deq_time1 + Duration::from_millis(10)); // dequeues 10000 bytes assert!(queue.avg_drate > 0.0, "avg_drate should be calculated"); assert!( queue.start_measurement.is_none(), diff --git a/rattan-core/src/cells/bandwidth/queue/red.rs b/rattan-core/src/cells/bandwidth/queue/red.rs index 3fcf77ca..94af447b 100644 --- a/rattan-core/src/cells/bandwidth/queue/red.rs +++ b/rattan-core/src/cells/bandwidth/queue/red.rs @@ -301,11 +301,11 @@ where self.idle_start = None; } - fn dequeue(&mut self) -> Option

{ + fn dequeue_at(&mut self, timestamp: Instant) -> Option

{ if let Some(packet) = self.queue.pop_front() { self.now_bytes -= packet.l3_length() + self.get_extra_length(); if self.is_empty() { - self.idle_start = Some(Instant::now()); + self.idle_start = Some(timestamp); } Some(packet) } else { @@ -366,7 +366,7 @@ mod tests { assert!(!queue.is_empty()); assert_eq!(queue.length(), 1); - let dequeued = queue.dequeue(); + let dequeued = queue.dequeue_at(Instant::now()); assert!(dequeued.is_some()); assert!(queue.is_empty()); } From 3f4784c3d652078cc809fc36df09833bf3abff8d Mon Sep 17 00:00:00 2001 From: duanxy23 Date: Wed, 15 Jul 2026 19:13:57 +0800 Subject: [PATCH 25/40] refactor(queue): remove retain from red and pie --- rattan-core/src/cells/bandwidth/queue/pie.rs | 7 ------- rattan-core/src/cells/bandwidth/queue/red.rs | 7 ------- 2 files changed, 14 deletions(-) diff --git a/rattan-core/src/cells/bandwidth/queue/pie.rs b/rattan-core/src/cells/bandwidth/queue/pie.rs index 9e1350a1..7e753a4b 100644 --- a/rattan-core/src/cells/bandwidth/queue/pie.rs +++ b/rattan-core/src/cells/bandwidth/queue/pie.rs @@ -322,13 +322,6 @@ where fn length(&self) -> usize { self.queue.len() } - - fn retain(&mut self, mut f: F) - where - F: FnMut(&P) -> bool, - { - self.queue.retain(|packet| f(packet)); - } } #[cfg(test)] diff --git a/rattan-core/src/cells/bandwidth/queue/red.rs b/rattan-core/src/cells/bandwidth/queue/red.rs index 94af447b..a5e47eb7 100644 --- a/rattan-core/src/cells/bandwidth/queue/red.rs +++ b/rattan-core/src/cells/bandwidth/queue/red.rs @@ -331,13 +331,6 @@ where fn length(&self) -> usize { self.queue.len() } - - fn retain(&mut self, mut f: F) - where - F: FnMut(&P) -> bool, - { - self.queue.retain(|packet| f(packet)); - } } #[cfg(test)] From f9975358db40ce876fe0f7d860207e7f68711423 Mon Sep 17 00:00:00 2001 From: duanxy23 Date: Wed, 15 Jul 2026 19:43:30 +0800 Subject: [PATCH 26/40] fix(queue): validate RED/PIE config in new() and configure() --- rattan-core/src/cells/bandwidth/queue/pie.rs | 44 ++++++++++++----- rattan-core/src/cells/bandwidth/queue/red.rs | 51 +++++++++++--------- 2 files changed, 59 insertions(+), 36 deletions(-) diff --git a/rattan-core/src/cells/bandwidth/queue/pie.rs b/rattan-core/src/cells/bandwidth/queue/pie.rs index 7e753a4b..1c8ff0ba 100644 --- a/rattan-core/src/cells/bandwidth/queue/pie.rs +++ b/rattan-core/src/cells/bandwidth/queue/pie.rs @@ -10,7 +10,7 @@ use rand::{rngs::StdRng, RngExt, SeedableRng}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use tokio::time::{Duration, Instant}; -use tracing::debug; +use tracing::{debug, warn}; #[cfg(feature = "serde")] use super::serde_default; @@ -46,6 +46,19 @@ impl Default for PieQueueConfig { } impl PieQueueConfig { + fn validate(&self) -> Result<(), &'static str> { + if self.ref_del <= 0.0 || !self.ref_del.is_finite() { + return Err("PieQueueConfig: ref_del must be > 0 and finite"); + } + if self.max_burst < 0.0 || !self.max_burst.is_finite() { + return Err("PieQueueConfig: max_burst must be >= 0 and finite"); + } + if self.t_update <= Duration::ZERO { + return Err("PieQueueConfig: t_update must be > 0"); + } + Ok(()) + } + pub fn new>, B: Into>>( packet_limit: A, byte_limit: B, @@ -67,7 +80,7 @@ impl PieQueueConfig { impl

From for PieQueue

{ fn from(config: PieQueueConfig) -> Self { - PieQueue::new(config) + PieQueue::new(config).expect("PieQueueConfig validation failed") } } @@ -87,10 +100,11 @@ pub struct PieQueue

{ } impl

PieQueue

{ - pub fn new(config: PieQueueConfig) -> Self { + pub fn new(config: PieQueueConfig) -> Result { + config.validate()?; debug!(?config, "New PieQueue"); let max_burst = config.max_burst; - Self { + Ok(Self { queue: VecDeque::new(), config, now_bytes: 0, @@ -102,7 +116,7 @@ impl

PieQueue

{ avg_drate: 0.0, burst_allowance: max_burst, rng: StdRng::seed_from_u64(42), - } + }) } } @@ -111,7 +125,7 @@ where P: Packet, { fn default() -> Self { - Self::new(PieQueueConfig::default()) + Self::new(PieQueueConfig::default()).expect("default PieQueueConfig is valid") } } @@ -235,6 +249,10 @@ where type Config = PieQueueConfig; fn configure(&mut self, config: Self::Config) { + if let Err(e) = config.validate() { + warn!("PieQueue: discard invalid configure: {}", e); + return; + } self.config = config; } @@ -337,7 +355,7 @@ mod tests { #[test_log::test] fn test_pie_queue_basic() { let config = PieQueueConfig::default(); - let mut queue: PieQueue = PieQueue::new(config); + let mut queue: PieQueue = PieQueue::new(config).unwrap(); assert!(queue.is_empty()); let pkt1 = create_packet(500); @@ -356,7 +374,7 @@ mod tests { packet_limit: Some(2), ..Default::default() }; - let mut queue: PieQueue = PieQueue::new(config); + let mut queue: PieQueue = PieQueue::new(config).unwrap(); queue.enqueue(create_packet(100)); queue.enqueue(create_packet(100)); @@ -373,7 +391,7 @@ mod tests { byte_limit: Some(150), ..Default::default() }; - let mut queue: PieQueue = PieQueue::new(config); + let mut queue: PieQueue = PieQueue::new(config).unwrap(); queue.enqueue(create_packet(100)); // l3 length 86. assert_eq!(queue.length(), 1); @@ -386,7 +404,7 @@ mod tests { #[test_log::test] fn test_pie_queue_burst_allowance() { let config = PieQueueConfig::default(); - let mut queue: PieQueue = PieQueue::new(config); + let mut queue: PieQueue = PieQueue::new(config).unwrap(); // Force a high drop probability queue.p = 1.0; @@ -410,7 +428,7 @@ mod tests { #[test_log::test] fn test_pie_queue_work_conserving() { let config = PieQueueConfig::default(); - let mut queue: PieQueue = PieQueue::new(config.clone()); + let mut queue: PieQueue = PieQueue::new(config.clone()).unwrap(); queue.p = 1.0; queue.burst_allowance = 0.0; @@ -439,7 +457,7 @@ mod tests { #[test_log::test] fn test_pie_queue_avg_drate_update() { let config = PieQueueConfig::default(); - let mut queue: PieQueue = PieQueue::new(config); + let mut queue: PieQueue = PieQueue::new(config).unwrap(); queue.enqueue(create_packet(10014)); // l3 length 10000 queue.enqueue(create_packet(10014)); // l3 length 10000 @@ -472,7 +490,7 @@ mod tests { #[test_log::test] fn test_pie_queue_update_drop_probability() { let config = PieQueueConfig::default(); - let mut queue: PieQueue = PieQueue::new(config.clone()); + let mut queue: PieQueue = PieQueue::new(config.clone()).unwrap(); // Fake high delay queue.avg_drate = 1000.0; diff --git a/rattan-core/src/cells/bandwidth/queue/red.rs b/rattan-core/src/cells/bandwidth/queue/red.rs index a5e47eb7..e0474791 100644 --- a/rattan-core/src/cells/bandwidth/queue/red.rs +++ b/rattan-core/src/cells/bandwidth/queue/red.rs @@ -58,19 +58,20 @@ impl Default for RedQueueConfig { } impl RedQueueConfig { - fn validate(&self) { + fn validate(&self) -> Result<(), &'static str> { if self.min_th >= self.max_th { - warn!( - "RedQueueConfig: min_th ({}) >= max_th ({}), which may cause invalid behavior.", - self.min_th, self.max_th - ); + return Err("RedQueueConfig: min_th >= max_th"); + } + if self.w_q <= 0.0 || self.w_q > 1.0 || !self.w_q.is_finite() { + return Err("RedQueueConfig: w_q must be in (0.0, 1.0] and finite"); } - if !(0.0..=1.0).contains(&self.w_q) { - warn!("RedQueueConfig: w_q ({}) is out of expected range [0.0, 1.0]. This is an EWMA weight.", self.w_q); + if self.max_p < 0.0 || self.max_p > 1.0 || !self.max_p.is_finite() { + return Err("RedQueueConfig: max_p must be in [0.0, 1.0] and finite"); } - if !(0.0..=1.0).contains(&self.max_p) { - warn!("RedQueueConfig: max_p ({}) is out of expected range [0.0, 1.0]. This is a probability.", self.max_p); + if self.pkt_tx_time <= 0.0 || !self.pkt_tx_time.is_finite() { + return Err("RedQueueConfig: pkt_tx_time must be > 0 and finite"); } + Ok(()) } // A `RedQueueConfig` can be constructed in any of these ways: @@ -128,7 +129,7 @@ impl RedQueueConfig { impl

From for RedQueue

{ fn from(config: RedQueueConfig) -> Self { - RedQueue::new(config) + RedQueue::new(config).expect("RedQueueConfig validation failed") } } @@ -145,10 +146,10 @@ pub struct RedQueue

{ } impl

RedQueue

{ - pub fn new(config: RedQueueConfig) -> Self { - config.validate(); + pub fn new(config: RedQueueConfig) -> Result { + config.validate()?; debug!(?config, "New RedQueue"); - Self { + Ok(Self { queue: VecDeque::new(), config, now_bytes: 0, @@ -157,7 +158,7 @@ impl

RedQueue

{ idle_start: None, latest_max_p_update: None, rng: StdRng::seed_from_u64(42), - } + }) } } @@ -166,7 +167,7 @@ where P: Packet, { fn default() -> Self { - Self::new(RedQueueConfig::default()) + Self::new(RedQueueConfig::default()).expect("default RedQueueConfig is valid") } } @@ -240,6 +241,10 @@ where type Config = RedQueueConfig; fn configure(&mut self, config: Self::Config) { + if let Err(e) = config.validate() { + warn!("RedQueue: discard invalid configure: {}", e); + return; + } self.config = config; } @@ -350,7 +355,7 @@ mod tests { max_th: 2000, ..Default::default() }; - let mut queue: RedQueue = RedQueue::new(config); + let mut queue: RedQueue = RedQueue::new(config).unwrap(); assert!(queue.is_empty()); @@ -372,7 +377,7 @@ mod tests { max_th: 200000, ..Default::default() }; - let mut queue: RedQueue = RedQueue::new(config); + let mut queue: RedQueue = RedQueue::new(config).unwrap(); queue.enqueue(create_packet(100)); queue.enqueue(create_packet(100)); @@ -391,7 +396,7 @@ mod tests { max_th: 200000, ..Default::default() }; - let mut queue: RedQueue = RedQueue::new(config); + let mut queue: RedQueue = RedQueue::new(config).unwrap(); queue.enqueue(create_packet(100)); // l3 length 86. assert_eq!(queue.length(), 1); @@ -409,7 +414,7 @@ mod tests { w_q: 1.0, // max weight, avg matches instantly ..Default::default() }; - let mut queue: RedQueue = RedQueue::new(config); + let mut queue: RedQueue = RedQueue::new(config).unwrap(); // First packet queue.enqueue(create_packet(100)); @@ -436,7 +441,7 @@ mod tests { w_q: 1.0, // Instantly reach exact byte size ..Default::default() }; - let mut queue: RedQueue = RedQueue::new(config); + let mut queue: RedQueue = RedQueue::new(config).unwrap(); // First packet: queue empty, avg remains 0. queue.enqueue(create_packet(514)); // L3 size = 514 - 14 (Ethernet header) = 500 @@ -460,7 +465,7 @@ mod tests { w_q: 1.0, ..Default::default() }; - let mut queue: RedQueue = RedQueue::new(config); + let mut queue: RedQueue = RedQueue::new(config).unwrap(); // First packet: queue empty, avg = 0. L3 size = 200. queue.enqueue(create_packet(214)); @@ -524,7 +529,7 @@ mod tests { adaptive: true, ..Default::default() }; - let mut queue: RedQueue = RedQueue::new(config); + let mut queue: RedQueue = RedQueue::new(config).unwrap(); // enqueue to make average_queue_length > target_max // target_max = min_th + 0.6 * (max_th - min_th) = 100 + 60 = 160 @@ -562,7 +567,7 @@ mod tests { adaptive: true, ..Default::default() }; - let mut queue: RedQueue = RedQueue::new(config); + let mut queue: RedQueue = RedQueue::new(config).unwrap(); // enqueue to make average_queue_length < target_min // target_min = min_th + 0.4 * (max_th - min_th) = 100 + 40 = 140 From a7bfb957d2f7f88bd046728a83e6cb6af569db0e Mon Sep 17 00:00:00 2001 From: duanxy23 Date: Wed, 15 Jul 2026 19:44:29 +0800 Subject: [PATCH 27/40] fix(queue): reset RED's count_packet to -1 on hard-limit drop --- rattan-core/src/cells/bandwidth/queue/red.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rattan-core/src/cells/bandwidth/queue/red.rs b/rattan-core/src/cells/bandwidth/queue/red.rs index e0474791..a93f3c26 100644 --- a/rattan-core/src/cells/bandwidth/queue/red.rs +++ b/rattan-core/src/cells/bandwidth/queue/red.rs @@ -280,7 +280,7 @@ where .is_none_or(|limit| self.now_bytes + packet_size <= limit); if !below_hard_limit { - self.count_packet = 0; + self.count_packet = -1; #[cfg(test)] tracing::trace!( queue_len = self.queue.len(), From 9dc5a5636bbaed33c35ef7fee78f8ed1cc1a25f4 Mon Sep 17 00:00:00 2001 From: duanxy23 Date: Wed, 15 Jul 2026 23:43:00 +0800 Subject: [PATCH 28/40] fix(pie): add serde duration adapter and clamp burst_allowance on reconfig --- rattan-core/src/cells/bandwidth/queue/pie.rs | 6 ++++-- rattan-core/src/cells/bandwidth/queue/red.rs | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/rattan-core/src/cells/bandwidth/queue/pie.rs b/rattan-core/src/cells/bandwidth/queue/pie.rs index 1c8ff0ba..e097824a 100644 --- a/rattan-core/src/cells/bandwidth/queue/pie.rs +++ b/rattan-core/src/cells/bandwidth/queue/pie.rs @@ -22,8 +22,9 @@ use crate::cells::Packet; pub struct PieQueueConfig { pub packet_limit: Option, pub byte_limit: Option, - pub ref_del: f64, // target delay (sec) - pub max_burst: f64, // MAX_BURST (ms) + pub ref_del: f64, // target delay (sec) + pub max_burst: f64, // MAX_BURST (ms) + #[cfg_attr(feature = "serde", serde(with = "crate::utils::serde::duration"))] pub t_update: Duration, // update interval #[cfg_attr( feature = "serde", @@ -254,6 +255,7 @@ where return; } self.config = config; + self.burst_allowance = self.burst_allowance.min(self.config.max_burst); } fn is_zero_buffer(&self) -> bool { diff --git a/rattan-core/src/cells/bandwidth/queue/red.rs b/rattan-core/src/cells/bandwidth/queue/red.rs index a93f3c26..ed3a3bf2 100644 --- a/rattan-core/src/cells/bandwidth/queue/red.rs +++ b/rattan-core/src/cells/bandwidth/queue/red.rs @@ -29,8 +29,8 @@ pub struct RedQueueConfig { // Packet transmission time in microseconds. // Used to compute the number of "virtual packet departures" during an idle period // for average queue length decay: `m = idle_time_us / pkt_tx_time`. - // The upper layer computes this from link bandwidth `C` and average packet size - // `avpkt` as `pkt_tx_time = avpkt * 8 / C`, then passes it down as a fixed config. + // The upper layer computes this from link bandwidth `C` and average packet size `avpkt` + // as `pkt_tx_time = avpkt * 8 / C` (C in Mbps), then passes it down as a fixed config. pub pkt_tx_time: f64, pub adaptive: bool, // enable adaptive mode (ARED): max_p is adjusted dynamically From aea9ce3d96fce92f55608cef4642d499c9106508 Mon Sep 17 00:00:00 2001 From: duanxy23 Date: Thu, 16 Jul 2026 12:27:14 +0800 Subject: [PATCH 29/40] refactor(queue): change RedQueue/PieQueue::new() to return Self, panic on invalid config --- rattan-core/src/cells/bandwidth/queue/pie.rs | 28 +++++++++--------- rattan-core/src/cells/bandwidth/queue/red.rs | 30 +++++++++++--------- 2 files changed, 31 insertions(+), 27 deletions(-) diff --git a/rattan-core/src/cells/bandwidth/queue/pie.rs b/rattan-core/src/cells/bandwidth/queue/pie.rs index e097824a..6b5faced 100644 --- a/rattan-core/src/cells/bandwidth/queue/pie.rs +++ b/rattan-core/src/cells/bandwidth/queue/pie.rs @@ -81,7 +81,7 @@ impl PieQueueConfig { impl

From for PieQueue

{ fn from(config: PieQueueConfig) -> Self { - PieQueue::new(config).expect("PieQueueConfig validation failed") + PieQueue::new(config) } } @@ -101,11 +101,13 @@ pub struct PieQueue

{ } impl

PieQueue

{ - pub fn new(config: PieQueueConfig) -> Result { - config.validate()?; + pub fn new(config: PieQueueConfig) -> Self { + if let Err(e) = config.validate() { + panic!("PieQueue::new: {}", e); + } debug!(?config, "New PieQueue"); let max_burst = config.max_burst; - Ok(Self { + Self { queue: VecDeque::new(), config, now_bytes: 0, @@ -117,7 +119,7 @@ impl

PieQueue

{ avg_drate: 0.0, burst_allowance: max_burst, rng: StdRng::seed_from_u64(42), - }) + } } } @@ -126,7 +128,7 @@ where P: Packet, { fn default() -> Self { - Self::new(PieQueueConfig::default()).expect("default PieQueueConfig is valid") + Self::new(PieQueueConfig::default()) } } @@ -357,7 +359,7 @@ mod tests { #[test_log::test] fn test_pie_queue_basic() { let config = PieQueueConfig::default(); - let mut queue: PieQueue = PieQueue::new(config).unwrap(); + let mut queue: PieQueue = PieQueue::new(config); assert!(queue.is_empty()); let pkt1 = create_packet(500); @@ -376,7 +378,7 @@ mod tests { packet_limit: Some(2), ..Default::default() }; - let mut queue: PieQueue = PieQueue::new(config).unwrap(); + let mut queue: PieQueue = PieQueue::new(config); queue.enqueue(create_packet(100)); queue.enqueue(create_packet(100)); @@ -393,7 +395,7 @@ mod tests { byte_limit: Some(150), ..Default::default() }; - let mut queue: PieQueue = PieQueue::new(config).unwrap(); + let mut queue: PieQueue = PieQueue::new(config); queue.enqueue(create_packet(100)); // l3 length 86. assert_eq!(queue.length(), 1); @@ -406,7 +408,7 @@ mod tests { #[test_log::test] fn test_pie_queue_burst_allowance() { let config = PieQueueConfig::default(); - let mut queue: PieQueue = PieQueue::new(config).unwrap(); + let mut queue: PieQueue = PieQueue::new(config); // Force a high drop probability queue.p = 1.0; @@ -430,7 +432,7 @@ mod tests { #[test_log::test] fn test_pie_queue_work_conserving() { let config = PieQueueConfig::default(); - let mut queue: PieQueue = PieQueue::new(config.clone()).unwrap(); + let mut queue: PieQueue = PieQueue::new(config.clone()); queue.p = 1.0; queue.burst_allowance = 0.0; @@ -459,7 +461,7 @@ mod tests { #[test_log::test] fn test_pie_queue_avg_drate_update() { let config = PieQueueConfig::default(); - let mut queue: PieQueue = PieQueue::new(config).unwrap(); + let mut queue: PieQueue = PieQueue::new(config); queue.enqueue(create_packet(10014)); // l3 length 10000 queue.enqueue(create_packet(10014)); // l3 length 10000 @@ -492,7 +494,7 @@ mod tests { #[test_log::test] fn test_pie_queue_update_drop_probability() { let config = PieQueueConfig::default(); - let mut queue: PieQueue = PieQueue::new(config.clone()).unwrap(); + let mut queue: PieQueue = PieQueue::new(config.clone()); // Fake high delay queue.avg_drate = 1000.0; diff --git a/rattan-core/src/cells/bandwidth/queue/red.rs b/rattan-core/src/cells/bandwidth/queue/red.rs index ed3a3bf2..cd700976 100644 --- a/rattan-core/src/cells/bandwidth/queue/red.rs +++ b/rattan-core/src/cells/bandwidth/queue/red.rs @@ -129,7 +129,7 @@ impl RedQueueConfig { impl

From for RedQueue

{ fn from(config: RedQueueConfig) -> Self { - RedQueue::new(config).expect("RedQueueConfig validation failed") + RedQueue::new(config) } } @@ -146,10 +146,12 @@ pub struct RedQueue

{ } impl

RedQueue

{ - pub fn new(config: RedQueueConfig) -> Result { - config.validate()?; + pub fn new(config: RedQueueConfig) -> Self { + if let Err(e) = config.validate() { + panic!("RedQueue::new: {}", e); + } debug!(?config, "New RedQueue"); - Ok(Self { + Self { queue: VecDeque::new(), config, now_bytes: 0, @@ -158,7 +160,7 @@ impl

RedQueue

{ idle_start: None, latest_max_p_update: None, rng: StdRng::seed_from_u64(42), - }) + } } } @@ -167,7 +169,7 @@ where P: Packet, { fn default() -> Self { - Self::new(RedQueueConfig::default()).expect("default RedQueueConfig is valid") + Self::new(RedQueueConfig::default()) } } @@ -355,7 +357,7 @@ mod tests { max_th: 2000, ..Default::default() }; - let mut queue: RedQueue = RedQueue::new(config).unwrap(); + let mut queue: RedQueue = RedQueue::new(config); assert!(queue.is_empty()); @@ -377,7 +379,7 @@ mod tests { max_th: 200000, ..Default::default() }; - let mut queue: RedQueue = RedQueue::new(config).unwrap(); + let mut queue: RedQueue = RedQueue::new(config); queue.enqueue(create_packet(100)); queue.enqueue(create_packet(100)); @@ -396,7 +398,7 @@ mod tests { max_th: 200000, ..Default::default() }; - let mut queue: RedQueue = RedQueue::new(config).unwrap(); + let mut queue: RedQueue = RedQueue::new(config); queue.enqueue(create_packet(100)); // l3 length 86. assert_eq!(queue.length(), 1); @@ -414,7 +416,7 @@ mod tests { w_q: 1.0, // max weight, avg matches instantly ..Default::default() }; - let mut queue: RedQueue = RedQueue::new(config).unwrap(); + let mut queue: RedQueue = RedQueue::new(config); // First packet queue.enqueue(create_packet(100)); @@ -441,7 +443,7 @@ mod tests { w_q: 1.0, // Instantly reach exact byte size ..Default::default() }; - let mut queue: RedQueue = RedQueue::new(config).unwrap(); + let mut queue: RedQueue = RedQueue::new(config); // First packet: queue empty, avg remains 0. queue.enqueue(create_packet(514)); // L3 size = 514 - 14 (Ethernet header) = 500 @@ -465,7 +467,7 @@ mod tests { w_q: 1.0, ..Default::default() }; - let mut queue: RedQueue = RedQueue::new(config).unwrap(); + let mut queue: RedQueue = RedQueue::new(config); // First packet: queue empty, avg = 0. L3 size = 200. queue.enqueue(create_packet(214)); @@ -529,7 +531,7 @@ mod tests { adaptive: true, ..Default::default() }; - let mut queue: RedQueue = RedQueue::new(config).unwrap(); + let mut queue: RedQueue = RedQueue::new(config); // enqueue to make average_queue_length > target_max // target_max = min_th + 0.6 * (max_th - min_th) = 100 + 60 = 160 @@ -567,7 +569,7 @@ mod tests { adaptive: true, ..Default::default() }; - let mut queue: RedQueue = RedQueue::new(config).unwrap(); + let mut queue: RedQueue = RedQueue::new(config); // enqueue to make average_queue_length < target_min // target_min = min_th + 0.4 * (max_th - min_th) = 100 + 40 = 140 From 2ca8d6cd5282183461c61a00876ff87d89dc8ab1 Mon Sep 17 00:00:00 2001 From: duanxy23 Date: Thu, 16 Jul 2026 13:35:27 +0800 Subject: [PATCH 30/40] refactor(queue): unify all Queue::new() to return Result and register RED/PIE in factory enums --- rattan-core/src/cells/bandwidth/queue/pie.rs | 27 ++++++++--------- rattan-core/src/cells/bandwidth/queue/red.rs | 29 +++++++++--------- rattan-core/src/config/bandwidth.rs | 22 ++++++++++++-- rattan-core/src/radix/mod.rs | 12 ++++++++ src/channel.rs | 31 +++++++++++++++++++- src/visualize_trace.rs | 4 +++ 6 files changed, 93 insertions(+), 32 deletions(-) diff --git a/rattan-core/src/cells/bandwidth/queue/pie.rs b/rattan-core/src/cells/bandwidth/queue/pie.rs index 6b5faced..387a375c 100644 --- a/rattan-core/src/cells/bandwidth/queue/pie.rs +++ b/rattan-core/src/cells/bandwidth/queue/pie.rs @@ -81,7 +81,7 @@ impl PieQueueConfig { impl

From for PieQueue

{ fn from(config: PieQueueConfig) -> Self { - PieQueue::new(config) + PieQueue::new(config).expect("PieQueueConfig validation failed") } } @@ -101,13 +101,11 @@ pub struct PieQueue

{ } impl

PieQueue

{ - pub fn new(config: PieQueueConfig) -> Self { - if let Err(e) = config.validate() { - panic!("PieQueue::new: {}", e); - } + pub fn new(config: PieQueueConfig) -> Result { + config.validate()?; debug!(?config, "New PieQueue"); let max_burst = config.max_burst; - Self { + Ok(Self { queue: VecDeque::new(), config, now_bytes: 0, @@ -119,7 +117,7 @@ impl

PieQueue

{ avg_drate: 0.0, burst_allowance: max_burst, rng: StdRng::seed_from_u64(42), - } + }) } } @@ -129,6 +127,7 @@ where { fn default() -> Self { Self::new(PieQueueConfig::default()) + .expect("PieQueueConfig::default() should never fail validation") } } @@ -359,7 +358,7 @@ mod tests { #[test_log::test] fn test_pie_queue_basic() { let config = PieQueueConfig::default(); - let mut queue: PieQueue = PieQueue::new(config); + let mut queue: PieQueue = PieQueue::new(config).unwrap(); assert!(queue.is_empty()); let pkt1 = create_packet(500); @@ -378,7 +377,7 @@ mod tests { packet_limit: Some(2), ..Default::default() }; - let mut queue: PieQueue = PieQueue::new(config); + let mut queue: PieQueue = PieQueue::new(config).unwrap(); queue.enqueue(create_packet(100)); queue.enqueue(create_packet(100)); @@ -395,7 +394,7 @@ mod tests { byte_limit: Some(150), ..Default::default() }; - let mut queue: PieQueue = PieQueue::new(config); + let mut queue: PieQueue = PieQueue::new(config).unwrap(); queue.enqueue(create_packet(100)); // l3 length 86. assert_eq!(queue.length(), 1); @@ -408,7 +407,7 @@ mod tests { #[test_log::test] fn test_pie_queue_burst_allowance() { let config = PieQueueConfig::default(); - let mut queue: PieQueue = PieQueue::new(config); + let mut queue: PieQueue = PieQueue::new(config).unwrap(); // Force a high drop probability queue.p = 1.0; @@ -432,7 +431,7 @@ mod tests { #[test_log::test] fn test_pie_queue_work_conserving() { let config = PieQueueConfig::default(); - let mut queue: PieQueue = PieQueue::new(config.clone()); + let mut queue: PieQueue = PieQueue::new(config.clone()).unwrap(); queue.p = 1.0; queue.burst_allowance = 0.0; @@ -461,7 +460,7 @@ mod tests { #[test_log::test] fn test_pie_queue_avg_drate_update() { let config = PieQueueConfig::default(); - let mut queue: PieQueue = PieQueue::new(config); + let mut queue: PieQueue = PieQueue::new(config).unwrap(); queue.enqueue(create_packet(10014)); // l3 length 10000 queue.enqueue(create_packet(10014)); // l3 length 10000 @@ -494,7 +493,7 @@ mod tests { #[test_log::test] fn test_pie_queue_update_drop_probability() { let config = PieQueueConfig::default(); - let mut queue: PieQueue = PieQueue::new(config.clone()); + let mut queue: PieQueue = PieQueue::new(config.clone()).unwrap(); // Fake high delay queue.avg_drate = 1000.0; diff --git a/rattan-core/src/cells/bandwidth/queue/red.rs b/rattan-core/src/cells/bandwidth/queue/red.rs index cd700976..38527d03 100644 --- a/rattan-core/src/cells/bandwidth/queue/red.rs +++ b/rattan-core/src/cells/bandwidth/queue/red.rs @@ -129,7 +129,7 @@ impl RedQueueConfig { impl

From for RedQueue

{ fn from(config: RedQueueConfig) -> Self { - RedQueue::new(config) + RedQueue::new(config).expect("RedQueueConfig validation failed") } } @@ -146,12 +146,10 @@ pub struct RedQueue

{ } impl

RedQueue

{ - pub fn new(config: RedQueueConfig) -> Self { - if let Err(e) = config.validate() { - panic!("RedQueue::new: {}", e); - } + pub fn new(config: RedQueueConfig) -> Result { + config.validate()?; debug!(?config, "New RedQueue"); - Self { + Ok(Self { queue: VecDeque::new(), config, now_bytes: 0, @@ -160,7 +158,7 @@ impl

RedQueue

{ idle_start: None, latest_max_p_update: None, rng: StdRng::seed_from_u64(42), - } + }) } } @@ -170,6 +168,7 @@ where { fn default() -> Self { Self::new(RedQueueConfig::default()) + .expect("RedQueueConfig::default() should never fail validation") } } @@ -357,7 +356,7 @@ mod tests { max_th: 2000, ..Default::default() }; - let mut queue: RedQueue = RedQueue::new(config); + let mut queue: RedQueue = RedQueue::new(config).unwrap(); assert!(queue.is_empty()); @@ -379,7 +378,7 @@ mod tests { max_th: 200000, ..Default::default() }; - let mut queue: RedQueue = RedQueue::new(config); + let mut queue: RedQueue = RedQueue::new(config).unwrap(); queue.enqueue(create_packet(100)); queue.enqueue(create_packet(100)); @@ -398,7 +397,7 @@ mod tests { max_th: 200000, ..Default::default() }; - let mut queue: RedQueue = RedQueue::new(config); + let mut queue: RedQueue = RedQueue::new(config).unwrap(); queue.enqueue(create_packet(100)); // l3 length 86. assert_eq!(queue.length(), 1); @@ -416,7 +415,7 @@ mod tests { w_q: 1.0, // max weight, avg matches instantly ..Default::default() }; - let mut queue: RedQueue = RedQueue::new(config); + let mut queue: RedQueue = RedQueue::new(config).unwrap(); // First packet queue.enqueue(create_packet(100)); @@ -443,7 +442,7 @@ mod tests { w_q: 1.0, // Instantly reach exact byte size ..Default::default() }; - let mut queue: RedQueue = RedQueue::new(config); + let mut queue: RedQueue = RedQueue::new(config).unwrap(); // First packet: queue empty, avg remains 0. queue.enqueue(create_packet(514)); // L3 size = 514 - 14 (Ethernet header) = 500 @@ -467,7 +466,7 @@ mod tests { w_q: 1.0, ..Default::default() }; - let mut queue: RedQueue = RedQueue::new(config); + let mut queue: RedQueue = RedQueue::new(config).unwrap(); // First packet: queue empty, avg = 0. L3 size = 200. queue.enqueue(create_packet(214)); @@ -531,7 +530,7 @@ mod tests { adaptive: true, ..Default::default() }; - let mut queue: RedQueue = RedQueue::new(config); + let mut queue: RedQueue = RedQueue::new(config).unwrap(); // enqueue to make average_queue_length > target_max // target_max = min_th + 0.6 * (max_th - min_th) = 100 + 60 = 160 @@ -569,7 +568,7 @@ mod tests { adaptive: true, ..Default::default() }; - let mut queue: RedQueue = RedQueue::new(config); + let mut queue: RedQueue = RedQueue::new(config).unwrap(); // enqueue to make average_queue_length < target_min // target_min = min_th + 0.4 * (max_th - min_th) = 100 + 40 = 140 diff --git a/rattan-core/src/config/bandwidth.rs b/rattan-core/src/config/bandwidth.rs index 80e8642f..5cf2a11c 100644 --- a/rattan-core/src/config/bandwidth.rs +++ b/rattan-core/src/config/bandwidth.rs @@ -35,6 +35,8 @@ pub enum BwCellBuildConfig { DropTail(bandwidth::BwCellConfig>), DropHead(bandwidth::BwCellConfig>), CoDel(bandwidth::BwCellConfig>), + Red(bandwidth::BwCellConfig>), + Pie(bandwidth::BwCellConfig>), } macro_rules! impl_bw_cell_into_factory { @@ -56,7 +58,14 @@ macro_rules! impl_bw_cell_into_factory { }; } -impl_bw_cell_into_factory!(InfiniteQueue, DropTailQueue, DropHeadQueue, CoDelQueue); +impl_bw_cell_into_factory!( + InfiniteQueue, + DropTailQueue, + DropHeadQueue, + CoDelQueue, + RedQueue, + PieQueue +); #[cfg_attr( feature = "serde", @@ -69,6 +78,8 @@ pub enum BwReplayCellBuildConfig { DropTail(BwReplayQueueConfig>), DropHead(BwReplayQueueConfig>), CoDel(BwReplayQueueConfig>), + Red(BwReplayQueueConfig>), + Pie(BwReplayQueueConfig>), } #[cfg_attr( @@ -218,4 +229,11 @@ macro_rules! impl_bw_replay_cell_into_factory { }; } -impl_bw_replay_cell_into_factory!(InfiniteQueue, DropTailQueue, DropHeadQueue, CoDelQueue); +impl_bw_replay_cell_into_factory!( + InfiniteQueue, + DropTailQueue, + DropHeadQueue, + CoDelQueue, + RedQueue, + PieQueue +); diff --git a/rattan-core/src/radix/mod.rs b/rattan-core/src/radix/mod.rs index d01b6f53..34eb1344 100644 --- a/rattan-core/src/radix/mod.rs +++ b/rattan-core/src/radix/mod.rs @@ -348,6 +348,12 @@ where crate::config::BwCellBuildConfig::CoDel(config) => { self.build_cell(id, config.into_factory())?; } + crate::config::BwCellBuildConfig::Red(config) => { + self.build_cell(id, config.into_factory())?; + } + crate::config::BwCellBuildConfig::Pie(config) => { + self.build_cell(id, config.into_factory())?; + } }, CellBuildConfig::BwReplay(bw_replay_config) => match bw_replay_config { crate::config::BwReplayCellBuildConfig::Infinite(config) => { @@ -362,6 +368,12 @@ where crate::config::BwReplayCellBuildConfig::CoDel(config) => { self.build_cell(id, config.into_factory())?; } + crate::config::BwReplayCellBuildConfig::Red(config) => { + self.build_cell(id, config.into_factory())?; + } + crate::config::BwReplayCellBuildConfig::Pie(config) => { + self.build_cell(id, config.into_factory())?; + } }, CellBuildConfig::Delay(config) => { self.build_cell(id, config.into_factory())?; diff --git a/src/channel.rs b/src/channel.rs index c91b89bf..ec091e10 100644 --- a/src/channel.rs +++ b/src/channel.rs @@ -5,7 +5,10 @@ use netem_trace::{Bandwidth, Delay}; use paste::paste; use rattan_core::{ cells::bandwidth::{ - queue::{CoDelQueueConfig, DropHeadQueueConfig, DropTailQueueConfig, InfiniteQueueConfig}, + queue::{ + CoDelQueueConfig, DropHeadQueueConfig, DropTailQueueConfig, InfiniteQueueConfig, + PieQueueConfig, RedQueueConfig, + }, BwCellConfig, }, config::{ @@ -92,6 +95,8 @@ enum QueueType { DropTail, DropHead, CoDel, + Red, + Pie, } // Deserialize queue args and create BwCellBuildConfig @@ -170,6 +175,12 @@ impl ChannelArgs { Some(QueueType::CoDel) => { bw_q_args_into_config!(CoDel, self.uplink_queue_args.clone(), bandwidth) } + Some(QueueType::Red) => { + bw_q_args_into_config!(Red, self.uplink_queue_args.clone(), bandwidth) + } + Some(QueueType::Pie) => { + bw_q_args_into_config!(Pie, self.uplink_queue_args.clone(), bandwidth) + } }; uplink_count += 1; cells_config.insert(format!("up_{uplink_count}"), cell_config); @@ -197,6 +208,12 @@ impl ChannelArgs { Some(QueueType::CoDel) => { bwreplay_q_args_into_config!(CoDel, self.uplink_queue_args.clone(), trace_file) } + Some(QueueType::Red) => { + bwreplay_q_args_into_config!(Red, self.uplink_queue_args.clone(), trace_file) + } + Some(QueueType::Pie) => { + bwreplay_q_args_into_config!(Pie, self.uplink_queue_args.clone(), trace_file) + } }; uplink_count += 1; cells_config.insert(format!("up_{uplink_count}"), cell_config); @@ -220,6 +237,12 @@ impl ChannelArgs { Some(QueueType::CoDel) => { bw_q_args_into_config!(CoDel, self.downlink_queue_args.clone(), bandwidth) } + Some(QueueType::Red) => { + bw_q_args_into_config!(Red, self.downlink_queue_args.clone(), bandwidth) + } + Some(QueueType::Pie) => { + bw_q_args_into_config!(Pie, self.downlink_queue_args.clone(), bandwidth) + } }; downlink_count += 1; cells_config.insert(format!("down_{downlink_count}"), cell_config); @@ -251,6 +274,12 @@ impl ChannelArgs { trace_file ) } + Some(QueueType::Red) => { + bwreplay_q_args_into_config!(Red, self.downlink_queue_args.clone(), trace_file) + } + Some(QueueType::Pie) => { + bwreplay_q_args_into_config!(Pie, self.downlink_queue_args.clone(), trace_file) + } }; downlink_count += 1; cells_config.insert(format!("down_{downlink_count}"), cell_config); diff --git a/src/visualize_trace.rs b/src/visualize_trace.rs index ebd3a198..a41c771f 100644 --- a/src/visualize_trace.rs +++ b/src/visualize_trace.rs @@ -261,6 +261,8 @@ pub fn write_visualize_trace( BwCellBuildConfig::Infinite(config) => config.bandwidth, BwCellBuildConfig::DropHead(config) => config.bandwidth, BwCellBuildConfig::DropTail(config) => config.bandwidth, + BwCellBuildConfig::Red(config) => config.bandwidth, + BwCellBuildConfig::Pie(config) => config.bandwidth, } .unwrap_or(Bandwidth::from_bps(u64::MAX)); trace_record.add_cell( @@ -292,6 +294,8 @@ pub fn write_visualize_trace( BwReplayCellBuildConfig::DropHead(config) => config.get_trace(), BwReplayCellBuildConfig::DropTail(config) => config.get_trace(), BwReplayCellBuildConfig::Infinite(config) => config.get_trace(), + BwReplayCellBuildConfig::Red(config) => config.get_trace(), + BwReplayCellBuildConfig::Pie(config) => config.get_trace(), }?; let trace_points = expand_bw_trace(trace.as_mut(), start, end_time) From 221747c1a53a8ae3c449498623e5124059b7af98 Mon Sep 17 00:00:00 2001 From: duanxy23 Date: Thu, 16 Jul 2026 13:53:53 +0800 Subject: [PATCH 31/40] feat(queue): add configurable seed to RED's and PIE's config --- rattan-core/src/cells/bandwidth/queue/pie.rs | 15 ++++++++++++++- rattan-core/src/cells/bandwidth/queue/red.rs | 18 +++++++++++++++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/rattan-core/src/cells/bandwidth/queue/pie.rs b/rattan-core/src/cells/bandwidth/queue/pie.rs index 387a375c..163edd36 100644 --- a/rattan-core/src/cells/bandwidth/queue/pie.rs +++ b/rattan-core/src/cells/bandwidth/queue/pie.rs @@ -31,6 +31,11 @@ pub struct PieQueueConfig { serde(default, skip_serializing_if = "serde_default") )] pub bw_type: BwType, + #[cfg_attr( + feature = "serde", + serde(default = "default_pie_seed", skip_serializing_if = "serde_default") + )] + pub seed: u64, } impl Default for PieQueueConfig { @@ -42,10 +47,15 @@ impl Default for PieQueueConfig { max_burst: 150.0, t_update: Duration::from_millis(15), bw_type: BwType::default(), + seed: 42, } } } +const fn default_pie_seed() -> u64 { + 42 +} + impl PieQueueConfig { fn validate(&self) -> Result<(), &'static str> { if self.ref_del <= 0.0 || !self.ref_del.is_finite() { @@ -67,6 +77,7 @@ impl PieQueueConfig { max_burst: f64, t_update: Duration, bw_type: BwType, + seed: u64, ) -> Self { Self { packet_limit: packet_limit.into(), @@ -75,6 +86,7 @@ impl PieQueueConfig { max_burst, t_update, bw_type, + seed, } } } @@ -105,6 +117,7 @@ impl

PieQueue

{ config.validate()?; debug!(?config, "New PieQueue"); let max_burst = config.max_burst; + let seed = config.seed; Ok(Self { queue: VecDeque::new(), config, @@ -116,7 +129,7 @@ impl

PieQueue

{ start_measurement: None, avg_drate: 0.0, burst_allowance: max_burst, - rng: StdRng::seed_from_u64(42), + rng: StdRng::seed_from_u64(seed), }) } } diff --git a/rattan-core/src/cells/bandwidth/queue/red.rs b/rattan-core/src/cells/bandwidth/queue/red.rs index 38527d03..acfb1ec1 100644 --- a/rattan-core/src/cells/bandwidth/queue/red.rs +++ b/rattan-core/src/cells/bandwidth/queue/red.rs @@ -39,6 +39,11 @@ pub struct RedQueueConfig { serde(default, skip_serializing_if = "serde_default") )] pub bw_type: BwType, + #[cfg_attr( + feature = "serde", + serde(default = "default_red_seed", skip_serializing_if = "serde_default") + )] + pub seed: u64, } impl Default for RedQueueConfig { @@ -53,10 +58,15 @@ impl Default for RedQueueConfig { pkt_tx_time: 120.0, // 1500 bytes * 8 / 100Mbps = 120 us adaptive: false, bw_type: BwType::default(), + seed: 42, } } } +const fn default_red_seed() -> u64 { + 42 +} + impl RedQueueConfig { fn validate(&self) -> Result<(), &'static str> { if self.min_th >= self.max_th { @@ -125,6 +135,11 @@ impl RedQueueConfig { self.bw_type = bw_type; self } + + pub fn with_seed(mut self, seed: u64) -> Self { + self.seed = seed; + self + } } impl

From for RedQueue

{ @@ -149,6 +164,7 @@ impl

RedQueue

{ pub fn new(config: RedQueueConfig) -> Result { config.validate()?; debug!(?config, "New RedQueue"); + let seed = config.seed; Ok(Self { queue: VecDeque::new(), config, @@ -157,7 +173,7 @@ impl

RedQueue

{ count_packet: -1, idle_start: None, latest_max_p_update: None, - rng: StdRng::seed_from_u64(42), + rng: StdRng::seed_from_u64(seed), }) } } From b32e3c6411c82d027b41d597a47fa17e0f378c06 Mon Sep 17 00:00:00 2001 From: duanxy23 Date: Tue, 21 Jul 2026 14:41:54 +0800 Subject: [PATCH 32/40] refactor(red pie): replace From with TryFrom and unify new() --- rattan-core/src/cells/bandwidth/queue/pie.rs | 51 ++++++++++---------- rattan-core/src/cells/bandwidth/queue/red.rs | 43 +++++++++-------- 2 files changed, 48 insertions(+), 46 deletions(-) diff --git a/rattan-core/src/cells/bandwidth/queue/pie.rs b/rattan-core/src/cells/bandwidth/queue/pie.rs index 163edd36..1f4a3dae 100644 --- a/rattan-core/src/cells/bandwidth/queue/pie.rs +++ b/rattan-core/src/cells/bandwidth/queue/pie.rs @@ -52,6 +52,7 @@ impl Default for PieQueueConfig { } } +#[cfg(feature = "serde")] const fn default_pie_seed() -> u64 { 42 } @@ -91,9 +92,11 @@ impl PieQueueConfig { } } -impl

From for PieQueue

{ - fn from(config: PieQueueConfig) -> Self { - PieQueue::new(config).expect("PieQueueConfig validation failed") +impl TryFrom for PieQueue

{ + type Error = &'static str; + + fn try_from(config: PieQueueConfig) -> Result { + PieQueue::new(config) } } @@ -112,28 +115,6 @@ pub struct PieQueue

{ rng: StdRng, } -impl

PieQueue

{ - pub fn new(config: PieQueueConfig) -> Result { - config.validate()?; - debug!(?config, "New PieQueue"); - let max_burst = config.max_burst; - let seed = config.seed; - Ok(Self { - queue: VecDeque::new(), - config, - now_bytes: 0, - old_del: 0.0, - p: 0.0, - dq_count: 0, - start_update: None, - start_measurement: None, - avg_drate: 0.0, - burst_allowance: max_burst, - rng: StdRng::seed_from_u64(seed), - }) - } -} - impl

Default for PieQueue

where P: Packet, @@ -263,6 +244,26 @@ where { type Config = PieQueueConfig; + fn new(config: PieQueueConfig) -> Result { + config.validate()?; + debug!(?config, "New PieQueue"); + let max_burst = config.max_burst; + let seed = config.seed; + Ok(Self { + queue: VecDeque::new(), + config, + now_bytes: 0, + old_del: 0.0, + p: 0.0, + dq_count: 0, + start_update: None, + start_measurement: None, + avg_drate: 0.0, + burst_allowance: max_burst, + rng: StdRng::seed_from_u64(seed), + }) + } + fn configure(&mut self, config: Self::Config) { if let Err(e) = config.validate() { warn!("PieQueue: discard invalid configure: {}", e); diff --git a/rattan-core/src/cells/bandwidth/queue/red.rs b/rattan-core/src/cells/bandwidth/queue/red.rs index acfb1ec1..e42f08a2 100644 --- a/rattan-core/src/cells/bandwidth/queue/red.rs +++ b/rattan-core/src/cells/bandwidth/queue/red.rs @@ -63,6 +63,7 @@ impl Default for RedQueueConfig { } } +#[cfg(feature = "serde")] const fn default_red_seed() -> u64 { 42 } @@ -142,9 +143,11 @@ impl RedQueueConfig { } } -impl

From for RedQueue

{ - fn from(config: RedQueueConfig) -> Self { - RedQueue::new(config).expect("RedQueueConfig validation failed") +impl TryFrom for RedQueue

{ + type Error = &'static str; + + fn try_from(config: RedQueueConfig) -> Result { + RedQueue::new(config) } } @@ -160,24 +163,6 @@ pub struct RedQueue

{ rng: StdRng, } -impl

RedQueue

{ - pub fn new(config: RedQueueConfig) -> Result { - config.validate()?; - debug!(?config, "New RedQueue"); - let seed = config.seed; - Ok(Self { - queue: VecDeque::new(), - config, - now_bytes: 0, - average_queue_length: 0.0, - count_packet: -1, - idle_start: None, - latest_max_p_update: None, - rng: StdRng::seed_from_u64(seed), - }) - } -} - impl

Default for RedQueue

where P: Packet, @@ -257,6 +242,22 @@ where { type Config = RedQueueConfig; + fn new(config: RedQueueConfig) -> Result { + config.validate()?; + debug!(?config, "New RedQueue"); + let seed = config.seed; + Ok(Self { + queue: VecDeque::new(), + config, + now_bytes: 0, + average_queue_length: 0.0, + count_packet: -1, + idle_start: None, + latest_max_p_update: None, + rng: StdRng::seed_from_u64(seed), + }) + } + fn configure(&mut self, config: Self::Config) { if let Err(e) = config.validate() { warn!("RedQueue: discard invalid configure: {}", e); From a119703bbd0b991d240eaa293b46108bc0b5e3ed Mon Sep 17 00:00:00 2001 From: duanxy23 Date: Wed, 22 Jul 2026 00:06:11 +0800 Subject: [PATCH 33/40] fix(red pie): re-seed RNG on reconfigure, fix seed serde and correct ARED timer grid --- rattan-core/src/cells/bandwidth/queue/pie.rs | 8 ++++---- rattan-core/src/cells/bandwidth/queue/red.rs | 14 +++++++++----- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/rattan-core/src/cells/bandwidth/queue/pie.rs b/rattan-core/src/cells/bandwidth/queue/pie.rs index 1f4a3dae..a73e7b40 100644 --- a/rattan-core/src/cells/bandwidth/queue/pie.rs +++ b/rattan-core/src/cells/bandwidth/queue/pie.rs @@ -31,10 +31,7 @@ pub struct PieQueueConfig { serde(default, skip_serializing_if = "serde_default") )] pub bw_type: BwType, - #[cfg_attr( - feature = "serde", - serde(default = "default_pie_seed", skip_serializing_if = "serde_default") - )] + #[cfg_attr(feature = "serde", serde(default = "default_pie_seed"))] pub seed: u64, } @@ -269,6 +266,9 @@ where warn!("PieQueue: discard invalid configure: {}", e); return; } + if config.seed != self.config.seed { + self.rng = StdRng::seed_from_u64(config.seed); + } self.config = config; self.burst_allowance = self.burst_allowance.min(self.config.max_burst); } diff --git a/rattan-core/src/cells/bandwidth/queue/red.rs b/rattan-core/src/cells/bandwidth/queue/red.rs index e42f08a2..a3d226b5 100644 --- a/rattan-core/src/cells/bandwidth/queue/red.rs +++ b/rattan-core/src/cells/bandwidth/queue/red.rs @@ -39,10 +39,7 @@ pub struct RedQueueConfig { serde(default, skip_serializing_if = "serde_default") )] pub bw_type: BwType, - #[cfg_attr( - feature = "serde", - serde(default = "default_red_seed", skip_serializing_if = "serde_default") - )] + #[cfg_attr(feature = "serde", serde(default = "default_red_seed"))] pub seed: u64, } @@ -263,6 +260,9 @@ where warn!("RedQueue: discard invalid configure: {}", e); return; } + if config.seed != self.config.seed { + self.rng = StdRng::seed_from_u64(config.seed); + } self.config = config; } @@ -283,7 +283,11 @@ where >= Duration::from_millis(500) { self.update_max_p(); - self.latest_max_p_update = Some(now); + // Advance on a fixed 500 ms grid rather than anchoring to the current packet arrival time. + // Anchoring to now would reset the timer on every trigger and silently skip adjustment intervals + // when arrivals are sparse (e.g. one packet after a 2 s idle would fire only once instead of catching up over multiple enqueues). + self.latest_max_p_update = + Some(self.latest_max_p_update.unwrap() + Duration::from_millis(500)); } } From 37a954f09724e1cb771fbbd44b8fde19f8568872 Mon Sep 17 00:00:00 2001 From: CepheusC <996390090@qq.com> Date: Wed, 22 Jul 2026 07:19:34 +0000 Subject: [PATCH 34/40] docs(red): add module-level documentation comparing RED/ARED implementation with kernel --- rattan-core/src/cells/bandwidth/queue/mod.rs | 4 +- rattan-core/src/cells/bandwidth/queue/red.rs | 242 ++++++++++++++++++- 2 files changed, 239 insertions(+), 7 deletions(-) diff --git a/rattan-core/src/cells/bandwidth/queue/mod.rs b/rattan-core/src/cells/bandwidth/queue/mod.rs index 2e2f6e8e..9c6f8e8a 100644 --- a/rattan-core/src/cells/bandwidth/queue/mod.rs +++ b/rattan-core/src/cells/bandwidth/queue/mod.rs @@ -15,8 +15,8 @@ mod codel; mod drophead; mod droptail; mod infinite; -mod pie; -mod red; +pub mod pie; +pub mod red; pub use codel::*; pub use drophead::*; diff --git a/rattan-core/src/cells/bandwidth/queue/red.rs b/rattan-core/src/cells/bandwidth/queue/red.rs index a3d226b5..35daf358 100644 --- a/rattan-core/src/cells/bandwidth/queue/red.rs +++ b/rattan-core/src/cells/bandwidth/queue/red.rs @@ -1,8 +1,240 @@ -// Combined RED Queue with Adaptive mode -// Reference: -// RED: https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=251892 -// ARED: https://www.icir.org/floyd/papers/adaptiveRed.pdf -// Kernel RED/ARED: https://github.com/torvalds/linux/blob/master/include/net/red.h +//! RED (Random Early Detection) queue with optional Adaptive mode (ARED). +//! +//! # References +//! +//! - RED paper: +//! - ARED paper: +//! - Linux kernel RED/ARED: +//! [`include/net/red.h`](https://github.com/torvalds/linux/blob/master/include/net/red.h) +//! and [`net/sched/sch_red.c`](https://github.com/torvalds/linux/blob/master/net/sched/sch_red.c) +//! (referenced as "kernel" throughout this documentation) +//! +//! # Differences from the Linux kernel implementation +//! +//! ## 1. Fixed-point integers (kernel) vs. floating-point (here) +//! +//! | Aspect | Kernel | This implementation | +//! |--------|--------|---------------------| +//! | `qavg` | Wlog-scaled integer (`u32`) | `f64` in bytes | +//! | `max_P` | Q0.32 fixed-point (`u32`) | `f64` in `[0.0, 1.0]` | +//! | weight | `Wlog` — weight `W = 1 / (1 << Wlog)` | `w_q` — direct `f64` weight | +//! | division | replaced by `reciprocal_divide()` | native `f64` division | +//! +//! The EWMA update is mathematically equivalent: +//! +//! - Kernel: `qavg += backlog - (qavg >> Wlog)` +//! - Here: `qavg = (1.0 - w_q) * qavg + w_q * backlog` +//! +//! when `w_q = 1.0 / (1 << Wlog)`. +//! +//! ## 2. Idle-period average-queue decay +//! +//! When the queue is empty, both implementations decay the average towards +//! zero by simulating virtual packet departures. The modelling differs: +//! +//! | Aspect | Kernel | This implementation | +//! |--------|--------|---------------------| +//! | time unit | cell time via `Scell_log` | `pkt_tx_time` (µs per packet) | +//! | decay computation | precomputed `Stab[256]` lookup table | direct `powf(1.0 - w_q, m)` | +//! | idle-time cap | `Scell_max` (max `255 << Scell_log`) | none (exponent may grow arbitrarily) | +//! +//! Both approaches implement the same formula from the original RED paper +//! (Floyd & Jacobson, 1993, §5 "Calculating the average queue length"): +//! +//! > When a packet arrives and the queue is empty, we compute *m*, the number +//! > of packets that could have been transmitted by the gateway during the +//! > time that the line was free. We then imagine that *m* packets have +//! > arrived to an empty queue, and calculate the average queue size: +//! > +//! > **avg ← (1 − w_q)^m × avg** +//! +//! This implementation follows the paper literally: `m` is computed as +//! `idle_us / pkt_tx_time` (where `pkt_tx_time` is the transmission time of +//! one average packet), and decay is `avg *= powf(1.0 - w_q, m)`. The kernel +//! precomputes a logarithmic lookup table (`Stab[]`) indexed by idle duration +//! scaled by `Scell_log` as a fixed-point approximation of `(1 − W)^m`, +//! avoiding both floating-point and the `pow()` call at enqueue time. The +//! two are mathematically equivalent; the kernel trades some precision for +//! integer-only computation. +//! +//! The `pkt_tx_time` parameter has no direct kernel equivalent. The kernel +//! separates idle modelling into `Wlog` (EWMA weight) and `Scell_log` (cell +//! granularity), which are independently configurable. Here, `w_q` controls +//! the decay *rate* and `pkt_tx_time` controls how many virtual departures +//! (`m`) an idle interval represents. +//! +//! ## 3. Random-number generation +//! +//! | Aspect | Kernel | This implementation | +//! |--------|--------|---------------------| +//! | source | `get_random_u32()` (kernel CSPRNG) | `rand::rngs::StdRng` (ChaCha12) | +//! | seeding | not seedable by userspace | user-configurable `seed` field | +//! | cached? | one `qR` per cycle, reused across packets | fresh `random_range(0.0..1.0)` each check | +//! +//! The kernel draws one random value per "cycle" (from the first packet in the +//! between-threshold region until a mark/drop occurs) and caches it in `qR`. +//! This implementation draws a fresh uniform random value on every +//! `should_drop()` call. Both produce the same statistical behaviour +//! (geometric inter-drop spacing); the difference is purely an implementation +//! choice. +//! +//! The seedable RNG enables deterministic, reproducible simulation runs. +//! +//! ## 4. Drop-probability computation +//! +//! Both implementations realise the same RED probability curve, but through +//! different arithmetic paths: +//! +//! - **Kernel**: `red_mark_probability()` checks +//! `((qavg - qth_min) >> Wlog) * qcount >= qR`, where `qR` is uniform in +//! `[0, qth_delta)`. This is the "uniform random numbers" (URN) method +//! with a cached threshold. +//! +//! - **Here**: Classical formula +//! `p_b = max_p * (avg - min_th) / (max_th - min_th)`, +//! then `p_a = p_b / (1.0 - count * p_b)`, +//! and compare `rand_val < p_a`. +//! +//! Both converge to the same geometric inter-drop distribution. The kernel's +//! approach saves a division per packet via `reciprocal_divide()`; this +//! implementation's approach maps more directly onto the textbook RED +//! description. +//! +//! ## 5. Adaptive RED (ARED) update cadence +//! +//! This is the most behaviourally significant difference. +//! +//! | Aspect | Kernel | This implementation | +//! |--------|--------|---------------------| +//! | trigger | kernel timer, fires every `HZ/2` (500 ms) | event-driven, on `enqueue()` | +//! | fires when idle? | yes — timer always runs | no — requires a packet arrival | +//! | grid | timer re-arms for `jiffies + HZ/2` | forward-shifted fixed 500 ms grid | +//! +//! **Kernel**: A dedicated timer (`adapt_timer`) fires every 500 ms regardless +//! of whether packets are arriving. Each firing acquires the qdisc lock, +//! calls `red_adaptative_algo()`, and re-arms. During prolonged idle periods +//! the kernel therefore decays `max_P` towards 0.01 every 500 ms. +//! +//! **Here**: ARED adjustment happens inside `enqueue()`. When a packet +//! arrives and at least 500 ms have elapsed since the last adjustment, one +//! call to `update_max_p()` is made and the timer is advanced by exactly 500 +//! ms (not reset to current time). This "fixed grid" approach +//! allows multiple intervals to be caught up over successive packet arrivals +//! after a long idle. However, if no packets arrive, no adjustments occur. +//! +//! The practical impact: after a long idle followed by a sparse trickle of +//! packets, this implementation may converge `max_P` to its resting value more +//! slowly than the kernel, because each enqueue accounts for at most one 500 +//! ms interval. +//! +//! **Why event-driven instead of a dedicated timer?** This simulator operates +//! on *logical* (simulation) time, not wall-clock time. A wall-clock timer +//! cannot accurately target a logical-time instant, especially under +//! variable-speed simulation, pause/resume, or replay scenarios. Worse, +//! spawning an OS timer per queue instance would be prohibitively expensive +//! when simulating hundreds or thousands of flows. +//! +//! ## 6. ARED `max_P` bound enforcement +//! +//! Both clamp `max_P` to `[0.01, 0.50]` (the ARED paper's range), but +//! *when* the bounds are applied differs: +//! +//! - **Kernel**: checks `<= MAX_P_MAX` *before* increasing and `>= MAX_P_MIN` +//! *before* decreasing. No explicit post-adjustment clamp — the value can +//! drift slightly past the nominal bounds in edge cases. +//! - **Here**: always calls `clamp(0.01, 0.5)` unconditionally after every +//! adjustment, enforcing strict hard bounds. +//! +//! The ARED formulae themselves are identical: +//! +//! - Increase: `max_p += min(0.01, max_p / 4.0)` (when `qavg > target_max`) +//! - Decrease: `max_p *= 0.9` (when `qavg < target_min`) +//! - Targets: `target_min = min_th + 0.4 * (max_th - min_th)`, +//! `target_max = min_th + 0.6 * (max_th - min_th)` +//! +//! Note: kernel integer arithmetic truncates in `(max_P / 10) * 9` (beta +//! decay), while here `max_p *= 0.9` is exact in `f64`. The difference is +//! negligible in practice. +//! +//! ## 7. ECN (Explicit Congestion Notification) +//! +//! | Aspect | Kernel | This implementation | +//! |--------|--------|---------------------| +//! | ECN marking | supported (`TC_RED_ECN` flag) | not supported | +//! | ECN nodrop | supported (`TC_RED_NODROP` flag) | not supported | +//! | mark vs. drop counters | separate `prob_mark`/`prob_drop` | only drops | +//! +//! The kernel can mark ECN-capable packets instead of dropping them. This +//! implementation always drops. Adding ECN support would require extending +//! the `Packet` trait with an ECN field. +//! +//! ## 8. Queue architecture +//! +//! | Aspect | Kernel | This implementation | +//! |--------|--------|---------------------| +//! | structure | classful qdisc → child bfifo | self-contained `VecDeque` | +//! | hard limit | `limit` on child qdisc (bytes) | `packet_limit` + `byte_limit` directly in config | +//! +//! The kernel's RED is a classful qdisc; it owns a child (typically `bfifo`) +//! that holds the actual packet queue. The `limit` parameter lives on the +//! child. This implementation is a flat `VecDeque`-based queue with both +//! packet-count and byte-count hard limits checked before the RED drop +//! decision. +//! +//! ## 9. Parameter validation +//! +//! The kernel validates `fls(qth) + Wlog < 32` (overflow prevention given +//! fixed-point arithmetic), `Scell_log < 32`, and all `Stab[]` entries < 32. +//! This implementation validates the semantically equivalent constraints in +//! floating-point terms: `min_th < max_th`, `w_q ∈ (0.0, 1.0]`, +//! `max_p ∈ [0.0, 1.0]`, and `pkt_tx_time > 0`. +//! +//! The kernel's overflow check (`fls(qth) + Wlog < 32`) is specific to +//! `u32` fixed-point arithmetic where `qavg` is stored Wlog-scaled. With +//! `f64`, overflow is not a concern — the exponent range comfortably covers +//! any practical queue size. The `Scell_log` and `Stab[]` checks are +//! likewise tied to the kernel's lookup-table idle model and have no +//! equivalent here. The current validation set is sufficient for a +//! floating-point RED implementation. +//! +//! ## 10. `qcount` reset value +//! +//! - **Kernel**: resets `qcount = 0` after a probabilistic mark. +//! - **Here**: resets `count_packet = -1` after a drop. +//! +//! Both produce **identical behaviour**. In the kernel, the post-mark +//! sequence is `qcount = 0 → ++qcount = 1 → probability check`, which yields +//! `p_b` for the first packet of the new cycle. Here, the sequence is +//! `count_packet = -1 → count_packet += 1 = 0 → p_a = p_b / (1.0 - 0·p_b) +//! = p_b`, giving the same first-packet probability. Subsequent packets +//! accumulate geometrically in both cases. +//! +//! The value `-1` was chosen deliberately over `0` for **internal +//! consistency**: every path that resets `count_packet` (below `min_th`, +//! above `max_th`, hard-limit drop, and probabilistic drop) uses the same +//! sentinel `-1`. Using a uniform reset value across all branches makes the +//! code easier to reason about and avoids an unnecessary special case. +//! +//! ## Summary table +//! +//! | # | Area | Kernel | This impl | +//! |---|------|--------|-----------| +//! | 1 | Arithmetic | fixed-point (`u32`/Q0.32) | `f64` floating-point | +//! | 2 | Weight | `Wlog` (log₂) | `w_q` (direct float) | +//! | 3 | Idle decay | `Stab[]` table via `Scell_log` | `powf(1-w_q, m)` via `pkt_tx_time` | +//! | 4 | `pkt_tx_time` | no direct equivalent | explicit µs-per-packet | +//! | 5 | RNG | `get_random_u32()`, cached per cycle | `StdRng`, fresh each check | +//! | 6 | Seed | not seedable | configurable `seed` | +//! | 7 | Probability | URN + `reciprocal_divide()` | classical `p_b` / `p_a` formula | +//! | 8 | ARED timing | kernel timer, always fires | event-driven on enqueue only | +//! | 9 | ARED bounds | conditional pre-check | unconditional post-clamp | +//! | 10 | ARED β precision | `(max_P/10)*9` (integer) | `*= 0.9` (float) | +//! | 11 | ECN | full support | not supported | +//! | 12 | Architecture | classful qdisc + child | self-contained `VecDeque` | +//! | 13 | Hard limit(s) | child `limit` (bytes) | `packet_limit` + `byte_limit` | +//! | 14 | Validation | `fls(qth)+Wlog<32` etc. | float range checks | +//! | 15 | `qcount` reset | `0` after mark | `-1` after drop | +//! | 16 | Idle-time cap | `Scell_max` | none | use std::collections::VecDeque; From 73a498fdd5f3075295ae4da2a905d0b71232a2c8 Mon Sep 17 00:00:00 2001 From: CepheusC <996390090@qq.com> Date: Wed, 22 Jul 2026 07:58:32 +0000 Subject: [PATCH 35/40] docs(pie): add module-level documentation comparing PIE implementation with kernel --- rattan-core/src/cells/bandwidth/queue/pie.rs | 330 ++++++++++++++++++- 1 file changed, 325 insertions(+), 5 deletions(-) diff --git a/rattan-core/src/cells/bandwidth/queue/pie.rs b/rattan-core/src/cells/bandwidth/queue/pie.rs index a73e7b40..7535488b 100644 --- a/rattan-core/src/cells/bandwidth/queue/pie.rs +++ b/rattan-core/src/cells/bandwidth/queue/pie.rs @@ -1,8 +1,328 @@ -// PIE Queue Implementation Reference: -// https://www.rfc-editor.org/info/rfc8033 -// https://ieeexplore.ieee.org/document/6602305 -// Reproduced according to RFC 8033 Appendix B, -// rather than original paper or RFC Appendix A. +//! PIE (Proportional Integral controller Enhanced) queue. +//! +//! # References +//! +//! - PIE paper: +//! +//! - RFC 8033 (PIE AQM): +//! +//! - Linux kernel PIE: +//! [`include/net/pie.h`](https://github.com/torvalds/linux/blob/master/include/net/pie.h) +//! and [`net/sched/sch_pie.c`](https://github.com/torvalds/linux/blob/master/net/sched/sch_pie.c) +//! (referenced as "kernel" throughout this documentation) +//! +//! # RFC 8033 version: Appendix A vs. Appendix B +//! +//! RFC 8033 contains two descriptions of the PIE algorithm: +//! +//! - **Appendix A** is the "original paper" algorithm: configurable α/β with +//! *dynamic* per-probability-scaling, accumulated-probability-based drop +//! decisions, and an optional timestamp-based delay estimation path. +//! +//! - **Appendix B** is a simplified pseudo-code reference implementation: +//! fixed `α = 0.125`, `β = 1.25`, *static* `p_increment` damping tables, +//! per-packet random drop (no accumulation), and always drain-rate-based +//! delay estimation. +//! +//! The kernel follows **Appendix A** (with its own extensions). This +//! implementation follows **Appendix B**. This is the most fundamental +//! design difference and explains nearly every other divergence listed below. +//! +//! # Differences from the Linux kernel implementation +//! +//! The 16 differences below fall into three categories: +//! +//! | Category | Sections | Explanation | +//! |----------|----------|-------------| +//! | **RFC 8033 Appendix A vs. B** | §3, §4, §5, §12 | The kernel follows Appendix A; this implementation follows Appendix B. These are deliberate design choices, not omissions. | +//! | **Kernel-specific extensions** | §6, §7, §8, §11, §13 | Features the Linux kernel added beyond what either RFC appendix describes. | +//! | **Implementation / architectural choices** | §1, §2, §9, §10, §14, §15, §16 | Differences arising from the simulation context (floating-point, event-driven, seedable RNG) or architectural constraints. | +//! +//! ## 1. Fixed-point integers (kernel) vs. floating-point (here) +//! +//! | Aspect | Kernel | This implementation | +//! |--------|--------|---------------------| +//! | `prob` | `u64` scaled by `MAX_PROB` (`U64_MAX >> 8`) | `f64` in `[0.0, 1.0]` | +//! | `avg_dq_rate` | `u32` scaled by `PIE_SCALE` (shift 8) | `f64` in B/s | +//! | delay | `psched_time_t` (kernel time ticks) | `f64` in seconds | +//! | `burst_time` | `psched_time_t` ticks | `f64` in milliseconds | +//! | EWMA for drain rate | `avg = (avg - (avg >> 3)) + (count >> 3)` | `avg = 0.875 * avg + 0.125 * rate` | +//! | `p_increment` damping | dynamic α/β bit-shifts (see §4) | `f64` division by `2048`/`512`/`…` | +//! +//! The EWMA update is mathematically identical (`1/8 = 0.125` weight). The +//! damping thresholds are also identical (see §4 below). +//! +//! ## 2. Drop-probability update trigger +//! +//! | Aspect | Kernel | This implementation | +//! |--------|--------|---------------------| +//! | trigger | dedicated kernel timer, fires every `tupdate` jiffies | event-driven, on `enqueue()` | +//! | fires when idle? | yes — timer always runs | no — requires a packet arrival | +//! | grid | timer re-arms for `jiffies + tupdate` | forward-shifted fixed `t_update` grid | +//! | first fire | 500 ms after init | on first enqueue | +//! +//! **Kernel**: A dedicated timer (`adapt_timer`) fires every `tupdate` +//! regardless of whether packets are arriving. During prolonged idle periods +//! the kernel therefore continues to decay `prob` and update state. +//! +//! **Here**: Probability update happens inside `enqueue()`. When a packet +//! arrives and at least `t_update` has elapsed since the last update, one +//! call to `update_drop_probability()` is made and the timer is advanced by +//! exactly `t_update` (not reset to current time). This "fixed grid" +//! approach allows multiple intervals to be caught up over successive packet +//! arrivals after a long idle. However, if no packets arrive, no updates +//! occur. +//! +//! **Why event-driven instead of a dedicated timer?** This simulator operates +//! on *logical* (simulation) time, not wall-clock time. A wall-clock timer +//! cannot accurately target a logical-time instant, especially under +//! variable-speed simulation, pause/resume, or replay scenarios. Spawning an +//! OS timer per queue instance would be prohibitively expensive when +//! simulating hundreds or thousands of flows. +//! +//! ## 3. Alpha and Beta: configurable (kernel) vs. fixed (here) +//! +//! | Aspect | Kernel | This implementation | +//! |--------|--------|---------------------| +//! | definition | `struct pie_params.alpha`, `.beta` (user-tunable 0–32) | `tilde_alpha = 0.125`, `tilde_beta = 1.25` (hard-coded) | +//! | internal scaling | `(param * MAX_PROB / PSCHED_TICKS_PER_SEC) >> 4` | direct `f64` | +//! | defaults | α = 2, β = 20 → 2/16 = 0.125, 20/16 = 1.25 | 0.125, 1.25 | +//! | tuning via | `tc qdisc ... pie alpha N beta M` | not configurable | +//! +//! The **default** values are equivalent: `2/16 = 0.125`, `20/16 = 1.25`. +//! However, the kernel exposes α and β as user-tunable knobs and dynamically +//! scales them based on current probability (described in §4). This +//! implementation hard-codes the RFC 8033 Appendix B base values and applies +//! static damping to `p_increment` instead. +//! +//! ## 4. Probability-increment damping +//! +//! Both implementations damp the probability adjustment when `prob` is small, +//! but through different mechanisms (Appendix A dynamically scales α/β; +//! Appendix B statically divides `p_increment` — thresholds and effective +//! damping factors are mathematically identical): +//! +//! **Kernel (Appendix A — dynamic α/β scaling):** +//! +//! When `prob < MAX_PROB / 10`: +//! 1. α and β are halved (`>>= 1`). +//! 2. Then repeatedly quartered (`>>= 2`) for each power-of-10 threshold: +//! `prob < MAX_PROB / 100` → quarter again, … up to `MAX_PROB / 10⁶`. +//! +//! **Here (Appendix B — static `p_increment` division):** +//! +//! `p_increment` is divided by a precomputed factor depending on `p`: +//! +//! | `p` range | Division factor | +//! |-----------|----------------:| +//! | `p ≥ 0.1` | 1 (no damping) | +//! | `0.01 ≤ p < 0.1` | 2 | +//! | `0.001 ≤ p < 0.01` | 8 | +//! | `0.0001 ≤ p < 0.001` | 32 | +//! | `0.00001 ≤ p < 0.0001` | 128 | +//! | `0.000001 ≤ p < 0.00001` | 512 | +//! | `p < 0.000001` | 2048 | +//! +//! The thresholds *and* the effective damping factors are **mathematically +//! identical** between the two approaches. The kernel modifies α/β before +//! computing the delta; this implementation computes the full delta first, +//! then divides. Because `delta = α·Δcur + β·Δold`, scaling α and β by +//! factor *k* is equivalent to scaling the delta by *k*. +//! +//! ## 5. Drop decision: probability accumulation (kernel) vs. per-packet random (here) +//! +//! | Aspect | Kernel | This implementation | +//! |--------|--------|---------------------| +//! | mechanism | `accu_prob` accumulates probability across packets | `rand < p` each packet | +//! | drop trigger | accumulated value crosses threshold | immediate comparison | +//! | burst tolerance | natural — multiple low-prob packets needed to trigger | via explicit `burst_allowance` check | +//! | RFC version | Appendix A | Appendix B | +//! +//! **Kernel**: Maintains an `accu_prob` counter in `pie_drop_early()`. Each +//! arriving packet adds `local_prob` (possibly scaled by `bytemode`) to the +//! accumulator. A packet is dropped only when `accu_prob` crosses a +//! threshold (`(MAX_PROB / 2) * 17`). This naturally spaces out drops — +//! several low-probability packets must arrive before one is dropped. +//! +//! **Here**: Each `should_drop()` call draws a fresh uniform random value and +//! compares directly against `p`. This is the simpler Appendix B approach +//! and produces the same statistical drop rate over many packets; the +//! difference is that drops are less evenly spaced (potentially clumpier) +//! without accumulation. +//! +//! ## 6. ECN (Explicit Congestion Notification) +//! +//! | Aspect | Kernel | This implementation | +//! |--------|--------|---------------------| +//! | ECN marking | supported (`TCA_PIE_ECN` flag) | not supported | +//! | mark condition | `prob ≤ MAX_PROB / 10` + packet is ECN-capable | N/A | +//! | mark counter | `ecn_mark` stat | N/A | +//! +//! The kernel can mark ECN-capable packets (setting the CE codepoint) instead +//! of dropping them when the drop probability is moderate. This +//! implementation always drops. Adding ECN support would require extending +//! the `Packet` trait with an ECN field. +//! +//! ## 7. Bytemode (packet-size-scaled probability) +//! +//! | Aspect | Kernel | This implementation | +//! |--------|--------|---------------------| +//! | feature | optional `bytemode` flag (`TCA_PIE_BYTEMODE`) | not supported | +//! | scaling | `local_prob = prob * pkt_size / mtu` (for packets ≤ MTU) | N/A | +//! +//! When enabled, the kernel scales the drop probability proportionally to the +//! packet size, making larger packets more likely to be dropped. This +//! implementation always treats all packets equally regardless of size. +//! +//! ## 8. Non-linear boost for high delay (>250 ms) +//! +//! | Aspect | Kernel | This implementation | +//! |--------|--------|---------------------| +//! | trigger | `qdelay > 250 ms` | not implemented | +//! | effect | `delta += 2%` of max probability | N/A | +//! +//! When the estimated queue delay exceeds 250 ms, the kernel adds an extra +//! 2% to the probability delta to more aggressively counteract severe +//! congestion. This boost is applied *after* the α/β damping, so it is not +//! subject to the same scaling. This implementation (following Appendix B) +//! has no such non-linear term; the sole source of `p_increment` is the +//! proportional-integral calculation, damped as described in §4. +//! +//! This is one of the more behaviourally significant differences: under +//! severe congestion (bufferbloat), the kernel will increase its drop +//! probability faster than this implementation. +//! +//! ## 9. Rapid decay condition and factor +//! +//! Both implementations reduce the drop probability when the queue is +//! consistently uncongested, but the *trigger condition* and *decay factor* +//! differ: +//! +//! | Aspect | Kernel | This implementation | +//! |--------|--------|---------------------| +//! | condition | `qdelay == 0` **and** `qdelay_old == 0` (exactly zero) | `cur_del < ref_del / 2` **and** `old_del < ref_del / 2` | +//! | guard | `update_prob == true` (no overflow/underflow this round) | none | +//! | retention factor | `63/64` ≈ 0.9844 (`prob -= prob / 64`) | `0.98` (`p *= 0.98`) | +//! +//! The kernel only decays when delay is *exactly* zero for two consecutive +//! update periods — a stricter condition. This implementation decays +//! whenever the delay is below half the target (7.5 ms at defaults), which +//! is a looser condition. Combined with the faster decay factor (0.98 vs. +//! 0.9844), this implementation may reduce `p` more aggressively during +//! lightly-loaded periods. +//! +//! ## 10. Burst allowance reduction mechanism +//! +//! | Aspect | Kernel | This implementation | +//! |--------|--------|---------------------| +//! | reduced in | `pie_process_dequeue()` — every dequeue | `update_drop_probability()` — only when update fires | +//! | reduced by | `dtime` (inter-dequeue interval, in psched ticks) | `elapsed_ms` (`t_update`, in milliseconds) | +//! | floor | 0 (via `max_t(psched_time_t, ...)`) | 0 (via `.max(0.0)`) | +//! | recharge condition | `prob == 0 && delay < target/2 && delay_old < target/2` | `p < EPSILON && cur_del < ref_del/2 && old_del < ref_del/2` | +//! | recharge value | 150 ms (`PSCHED_TICKS_PER_SEC * 150 / 1000`) | `max_burst` (default 150.0 ms) | +//! +//! The kernel reduces `burst_time` on *every* dequeue by the actual +//! inter-departure time, so burst allowance reflects real-time packet +//! departures. This implementation reduces `burst_allowance` only during +//! probability updates, by the `t_update` interval. During periods where +//! updates fire regularly (steady packet arrivals), both behave similarly. +//! During sparse arrivals where updates are caught up in bursts, the +//! reduction granularity may differ. +//! +//! ## 11. Variable reset on sustained good behaviour +//! +//! | Aspect | Kernel | This implementation | +//! |--------|--------|---------------------| +//! | full variable reset | `pie_vars_init()` when `qdelay < target/2`, `qdelay_old < target/2`, `prob == 0`, and rate estimator has a valid reading | not performed | +//! | effect of reset | clears `prob`, `avg_dq_rate`, `dq_count`, `dq_tstamp`, `accu_prob`; resets `burst_time` to 150 ms | only `burst_allowance` is recharged to `max_burst` | +//! +//! The kernel fully resets all PIE state variables when the queue has been +//! well-behaved for long enough. This implementation only recharges the +//! burst allowance (see §10) and leaves `avg_drate`, `p`, and measurement +//! state intact. After a prolonged idle or light-load period followed by +//! sudden congestion, this implementation may react with a stale (possibly +//! too-low) `avg_drate`, causing a transient over-estimation of delay. +//! +//! ## 12. Delay estimation: always drain-rate-based vs. dual-mode +//! +//! | Aspect | Kernel | This implementation | +//! |--------|--------|---------------------| +//! | timestamp mode | supported (`dq_rate_estimator = false`) | not supported | +//! | drain-rate mode | supported (`dq_rate_estimator = true`) | always active | +//! | timestamp source | `pie_skb_cb.enqueue_time` from skb control block | N/A | +//! | drain-rate threshold | 16 KiB (`QUEUE_THRESHOLD`) | 16 KiB (`dq_threshold`) | +//! | measurement entry | `backlog >= 16384` | `now_bytes > 16384` | +//! | EWMA weight | `1/8` (0.125) | `0.125` | +//! +//! The kernel can optionally measure delay directly from per-packet enqueue +//! timestamps when the drain-rate estimator is disabled. This +//! implementation always estimates delay as `now_bytes / avg_drate`, +//! equivalent to the kernel's `dq_rate_estimator = true` mode. +//! +//! The measurement cycle entry condition differs by one byte: the kernel +//! uses `>=` while this implementation uses `>`. With standard MTU packets +//! (~1514 B with L2 overhead) this off-by-one is not practically observable. +//! +//! ## 13. Update guard: zero delay with non-zero backlog +//! +//! | Aspect | Kernel | This implementation | +//! |--------|--------|---------------------| +//! | guard | when `qdelay == 0` but `backlog != 0`, skip this round's probability update | not implemented | +//! +//! The kernel refrains from updating `prob` when the drain-rate estimator +//! yields zero delay but the queue is not actually empty — this indicates +//! the estimator has not yet converged. This implementation always applies +//! the probability update; if `avg_drate ≈ 0` (estimator not yet +//! initialized), `cur_del` is forced to 0 via the `abs(avg_drate) < EPSILON` +//! check. +//! +//! ## 14. Random-number generation +//! +//! | Aspect | Kernel | This implementation | +//! |--------|--------|---------------------| +//! | source | `get_random_u64()` (kernel CSPRNG) | `rand::rngs::StdRng` (ChaCha12) | +//! | seeding | not seedable by userspace | user-configurable `seed` field | +//! +//! The kernel uses the kernel's CSPRNG for drop decisions. This +//! implementation uses a seedable ChaCha12 RNG, enabling deterministic, +//! reproducible simulation runs. +//! +//! ## 15. Queue architecture +//! +//! | Aspect | Kernel | This implementation | +//! |--------|--------|---------------------| +//! | structure | full `Qdisc` with netlink configuration | self-contained `VecDeque` | +//! | hard limit | single `limit` (packets) | dual `packet_limit` + `byte_limit` | +//! | statistics | `tc_pie_xstats` (prob, delay, packets_in, dropped, overlimit, maxq, ecn_mark) | none | +//! +//! The kernel's PIE is a full Linux qdisc with netlink-based configuration +//! (`tc qdisc ... pie`), statistics export, and lifecycle management. +//! This implementation is a queue *component* within a larger simulation +//! framework; it stores packets in an internal `VecDeque` and exposes the +//! `PacketQueue` trait. +//! +//! The kernel's single `limit` is a packet count. This implementation adds +//! an independent `byte_limit` for byte-oriented capacity management. +//! +//! ## 16. Configurable parameters +//! +//! The kernel exposes several parameters that have no counterpart here: +//! +//! | Kernel parameter | `TCA_PIE_*` attribute | Purpose | Status here | +//! |------------------|-----------------------|---------|-------------| +//! | `alpha` | `TCA_PIE_ALPHA` | PI controller α gain (0–32) | fixed at 0.125 | +//! | `beta` | `TCA_PIE_BETA` | PI controller β gain (0–32) | fixed at 1.25 | +//! | `ecn` | `TCA_PIE_ECN` | enable ECN marking | not supported | +//! | `bytemode` | `TCA_PIE_BYTEMODE` | scale drop prob by packet size | not supported | +//! | `dq_rate_estimator` | `TCA_PIE_DQ_RATE_ESTIMATOR` | enable drain-rate-based delay estimation | always on | +//! +//! Parameters present here but not in the kernel: +//! +//! | Parameter | Purpose | +//! |-----------|---------| +//! | `bw_type` | configures L2 overhead for bandwidth calculation | +//! | `seed` | deterministic RNG seed for reproducible simulation | +//! | `byte_limit` | byte-oriented hard queue limit (kernel uses single packet `limit`) | use std::collections::VecDeque; From 36d7b0127ea148ace4aa83fcace17268163bf0c6 Mon Sep 17 00:00:00 2001 From: CepheusC <996390090@qq.com> Date: Wed, 22 Jul 2026 09:02:56 +0000 Subject: [PATCH 36/40] doc(red pie): update module-level documentation --- rattan-core/src/cells/bandwidth/queue/pie.rs | 9 +-------- rattan-core/src/cells/bandwidth/queue/red.rs | 21 +------------------- 2 files changed, 2 insertions(+), 28 deletions(-) diff --git a/rattan-core/src/cells/bandwidth/queue/pie.rs b/rattan-core/src/cells/bandwidth/queue/pie.rs index 7535488b..829be128 100644 --- a/rattan-core/src/cells/bandwidth/queue/pie.rs +++ b/rattan-core/src/cells/bandwidth/queue/pie.rs @@ -315,14 +315,7 @@ //! | `ecn` | `TCA_PIE_ECN` | enable ECN marking | not supported | //! | `bytemode` | `TCA_PIE_BYTEMODE` | scale drop prob by packet size | not supported | //! | `dq_rate_estimator` | `TCA_PIE_DQ_RATE_ESTIMATOR` | enable drain-rate-based delay estimation | always on | -//! -//! Parameters present here but not in the kernel: -//! -//! | Parameter | Purpose | -//! |-----------|---------| -//! | `bw_type` | configures L2 overhead for bandwidth calculation | -//! | `seed` | deterministic RNG seed for reproducible simulation | -//! | `byte_limit` | byte-oriented hard queue limit (kernel uses single packet `limit`) | + use std::collections::VecDeque; diff --git a/rattan-core/src/cells/bandwidth/queue/red.rs b/rattan-core/src/cells/bandwidth/queue/red.rs index 35daf358..42d0f7fc 100644 --- a/rattan-core/src/cells/bandwidth/queue/red.rs +++ b/rattan-core/src/cells/bandwidth/queue/red.rs @@ -215,26 +215,7 @@ //! sentinel `-1`. Using a uniform reset value across all branches makes the //! code easier to reason about and avoids an unnecessary special case. //! -//! ## Summary table -//! -//! | # | Area | Kernel | This impl | -//! |---|------|--------|-----------| -//! | 1 | Arithmetic | fixed-point (`u32`/Q0.32) | `f64` floating-point | -//! | 2 | Weight | `Wlog` (log₂) | `w_q` (direct float) | -//! | 3 | Idle decay | `Stab[]` table via `Scell_log` | `powf(1-w_q, m)` via `pkt_tx_time` | -//! | 4 | `pkt_tx_time` | no direct equivalent | explicit µs-per-packet | -//! | 5 | RNG | `get_random_u32()`, cached per cycle | `StdRng`, fresh each check | -//! | 6 | Seed | not seedable | configurable `seed` | -//! | 7 | Probability | URN + `reciprocal_divide()` | classical `p_b` / `p_a` formula | -//! | 8 | ARED timing | kernel timer, always fires | event-driven on enqueue only | -//! | 9 | ARED bounds | conditional pre-check | unconditional post-clamp | -//! | 10 | ARED β precision | `(max_P/10)*9` (integer) | `*= 0.9` (float) | -//! | 11 | ECN | full support | not supported | -//! | 12 | Architecture | classful qdisc + child | self-contained `VecDeque` | -//! | 13 | Hard limit(s) | child `limit` (bytes) | `packet_limit` + `byte_limit` | -//! | 14 | Validation | `fls(qth)+Wlog<32` etc. | float range checks | -//! | 15 | `qcount` reset | `0` after mark | `-1` after drop | -//! | 16 | Idle-time cap | `Scell_max` | none | + use std::collections::VecDeque; From eda9b076b06a1cf8eac6a6eb743b06d56136f058 Mon Sep 17 00:00:00 2001 From: CepheusC <996390090@qq.com> Date: Wed, 22 Jul 2026 09:09:46 +0000 Subject: [PATCH 37/40] docs(red pie): add kernel field correspondence tables to Config structs --- rattan-core/src/cells/bandwidth/queue/pie.rs | 17 ++++++++++++++++ rattan-core/src/cells/bandwidth/queue/red.rs | 21 ++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/rattan-core/src/cells/bandwidth/queue/pie.rs b/rattan-core/src/cells/bandwidth/queue/pie.rs index 829be128..d96c8c0b 100644 --- a/rattan-core/src/cells/bandwidth/queue/pie.rs +++ b/rattan-core/src/cells/bandwidth/queue/pie.rs @@ -330,6 +330,23 @@ use super::serde_default; use super::{BwType, PacketQueue}; use crate::cells::Packet; +/// Configuration for a PIE (Proportional Integral controller Enhanced) queue. +/// +/// # Field correspondence with the Linux kernel +/// +/// | Field | Kernel equivalent | Notes | +/// |-------|-------------------|-------| +/// | `ref_del` | `pie_params.target` | Kernel stores in µs, converted to psched ticks internally. Here stored as seconds (`f64`). Default: 15 ms (0.015 s). | +/// | `max_burst` | `pie_vars.burst_time` | Kernel initialises to `PSCHED_TICKS_PER_SEC * 150 / 1000` (150 ms in psched ticks). Here stored as milliseconds (`f64`). Default: 150.0 ms. | +/// | `t_update` | `pie_params.tupdate` | Kernel stores in jiffies (`usecs_to_jiffies`). Here stored as `Duration`. Default: 15 ms. | +/// | `packet_limit` | `pie_params.limit` / `sch->limit` | Kernel has a single packet-count limit. Here augmented with an independent `byte_limit`. | +/// | `byte_limit` | *(no direct equivalent)* | Byte-oriented hard limit; not present in the kernel. | +/// | `bw_type` | *(no direct equivalent)* | Configures L2 overhead for bandwidth calculation; simulation-specific. | +/// | `seed` | *(no direct equivalent)* | Deterministic RNG seed; the kernel uses unseedable CSPRNG (`get_random_u64()`). | +/// +/// Kernel parameters **not present** in this struct: `alpha`, `beta` (gains +/// are hard-coded at 0.125 / 1.25), `ecn`, `bytemode`, `dq_rate_estimator`. +/// See the module-level documentation §16 for details. #[cfg_attr(feature = "serde", derive(Deserialize, Serialize), serde(default))] #[derive(Debug, Clone)] pub struct PieQueueConfig { diff --git a/rattan-core/src/cells/bandwidth/queue/red.rs b/rattan-core/src/cells/bandwidth/queue/red.rs index 42d0f7fc..acaac1ea 100644 --- a/rattan-core/src/cells/bandwidth/queue/red.rs +++ b/rattan-core/src/cells/bandwidth/queue/red.rs @@ -230,6 +230,27 @@ use super::serde_default; use super::{BwType, PacketQueue}; use crate::cells::Packet; +/// Configuration for a RED (Random Early Detection) queue. +/// +/// # Field correspondence with the Linux kernel +/// +/// | Field | Kernel equivalent | Notes | +/// |-------|-------------------|-------| +/// | `min_th` | `red_parms.qth_min` | Kernel stores as `u32`; here as `usize`. Default: 7500 (5 × 1500 B). | +/// | `max_th` | `red_parms.qth_max` | Kernel stores as `u32`; here as `usize`. Default: 22500 (15 × 1500 B). | +/// | `max_p` | `red_parms.max_P` | Kernel stores as Q0.32 fixed-point `u32`; here as `f64` in `[0.0, 1.0]`. Default: 0.02. | +/// | `w_q` | `red_parms.Wlog` | Kernel stores log₂ weight as `u8` (weight = 1/(1<limit` (from `tc_red_qopt.limit`) applied to the child qdisc. | +/// | `byte_limit` | `q->limit` / `tc_red_qopt.limit` | Kernel's limit is byte-oriented and applied to the child qdisc (bfifo); here applied directly. | +/// | `bw_type` | *(no direct equivalent)* | Configures L2 overhead for bandwidth calculation; simulation-specific. | +/// | `seed` | *(no direct equivalent)* | Deterministic RNG seed; the kernel uses unseedable CSPRNG (`get_random_u32()`). | +/// +/// Kernel parameters **not present** in this struct: `Plog` (probability scaling), +/// `Scell_log` (cell-size logarithm), `Stab[]` (precomputed idle-decay table), +/// ECN flags (`TC_RED_ECN`, `TC_RED_NODROP`). See the module-level documentation +/// for details. #[cfg_attr(feature = "serde", derive(Deserialize, Serialize), serde(default))] #[derive(Debug, Clone)] pub struct RedQueueConfig { From ff0b9d482d5da215af237b030b737015f254a20e Mon Sep 17 00:00:00 2001 From: CepheusC <996390090@qq.com> Date: Wed, 22 Jul 2026 09:47:43 +0000 Subject: [PATCH 38/40] docs(red pie): add user-facing field documentation to Config structs --- rattan-core/src/cells/bandwidth/queue/pie.rs | 92 ++++++++++++- rattan-core/src/cells/bandwidth/queue/red.rs | 130 +++++++++++++++++-- 2 files changed, 209 insertions(+), 13 deletions(-) diff --git a/rattan-core/src/cells/bandwidth/queue/pie.rs b/rattan-core/src/cells/bandwidth/queue/pie.rs index d96c8c0b..23558269 100644 --- a/rattan-core/src/cells/bandwidth/queue/pie.rs +++ b/rattan-core/src/cells/bandwidth/queue/pie.rs @@ -332,6 +332,34 @@ use crate::cells::Packet; /// Configuration for a PIE (Proportional Integral controller Enhanced) queue. /// +/// # Quick-start configuration +/// +/// The defaults follow RFC 8033 and are suitable for most Internet traffic: +/// +/// - `ref_del = 15 ms` — typical target for wired Internet paths. +/// - `t_update = 15 ms` — RFC-specified update interval. +/// - `max_burst = 150 ms` — allows a burst of roughly 10 back-to-back +/// packets before the drop probability takes effect. +/// +/// For **data-centre** links (sub-millisecond RTT), consider reducing +/// `ref_del` (e.g. 1–5 ms) and `t_update` proportionally. For +/// **satellite** or high-latency links, increase `ref_del` accordingly. +/// +/// # Constructing a config +/// +/// ```no_run +/// # use rattan_core::cells::bandwidth::queue::pie::PieQueueConfig; +/// # use rattan_core::cells::bandwidth::BwType; +/// # use std::time::Duration; +/// // Struct-literal with defaults: +/// let cfg = PieQueueConfig { ref_del: 0.005, ..Default::default() }; +/// +/// // Direct construction: +/// let cfg = PieQueueConfig::new(None, None, 0.005, 100.0, +/// Duration::from_millis(15), +/// BwType::default(), 42); +/// ``` +/// /// # Field correspondence with the Linux kernel /// /// | Field | Kernel equivalent | Notes | @@ -350,17 +378,75 @@ use crate::cells::Packet; #[cfg_attr(feature = "serde", derive(Deserialize, Serialize), serde(default))] #[derive(Debug, Clone)] pub struct PieQueueConfig { + /// Hard limit on the number of packets in the queue. Packets arriving when + /// the queue already holds this many are dropped unconditionally. + /// + /// `None` means no limit. `Some(0)` makes the queue a zero-buffer (drop + /// everything). + /// + /// Default: `None`. pub packet_limit: Option, + + /// Hard limit on the total bytes in the queue (L3 length + L2 overhead from + /// [`bw_type`](Self::bw_type)). Same semantics as + /// [`packet_limit`](Self::packet_limit). + /// + /// Default: `None`. pub byte_limit: Option, - pub ref_del: f64, // target delay (sec) - pub max_burst: f64, // MAX_BURST (ms) + + /// Target queuing delay, in **seconds**. PIE adjusts the drop probability + /// to keep the estimated queue delay near this value. + /// + /// RFC 8033 recommends 15 ms (`0.015`) for wired Internet paths. Reduce + /// for data-centre links (e.g. 0.001–0.005); increase for high-latency + /// links. + /// + /// Valid range: `> 0`, must be finite. + /// + /// Default: `0.015` (15 ms). + pub ref_del: f64, + + /// Maximum burst allowance, in **milliseconds**. + /// + /// When PIE first starts dropping, a burst allowance equal to `max_burst` + /// lets short bursts through without drops. + /// + /// RFC 8033 recommends 150 ms. Reduce for low-RTT environments to make + /// PIE react faster; increase for bursty traffic. + /// + /// Valid range: `≥ 0`, must be finite. + /// + /// Default: `150.0` (150 ms). + pub max_burst: f64, + + /// Interval between drop-probability updates. + /// + /// PIE re-evaluates the drop probability on this cadence. RFC 8033 + /// specifies 15 ms. Shorter intervals make PIE more responsive but + /// increase the update frequency; longer intervals smooth the response. + /// + /// Must be `> Duration::ZERO`. + /// + /// Default: `Duration::from_millis(15)`. #[cfg_attr(feature = "serde", serde(with = "crate::utils::serde::duration"))] - pub t_update: Duration, // update interval + pub t_update: Duration, + + /// L2 overhead mode for byte accounting. The extra length from + /// [`BwType::extra_length()`] is added to each packet's L3 length when + /// checking [`byte_limit`](Self::byte_limit). + /// + /// Default: [`BwType::NetworkLayer`] (no overhead). #[cfg_attr( feature = "serde", serde(default, skip_serializing_if = "serde_default") )] pub bw_type: BwType, + + /// Seed for the deterministic RNG used in drop decisions. Two queues with + /// identical configs, traffic, and seed make identical drop decisions, + /// enabling reproducible simulations. + /// + /// Default: `42`. #[cfg_attr(feature = "serde", serde(default = "default_pie_seed"))] pub seed: u64, } diff --git a/rattan-core/src/cells/bandwidth/queue/red.rs b/rattan-core/src/cells/bandwidth/queue/red.rs index acaac1ea..9808a470 100644 --- a/rattan-core/src/cells/bandwidth/queue/red.rs +++ b/rattan-core/src/cells/bandwidth/queue/red.rs @@ -231,6 +231,37 @@ use super::{BwType, PacketQueue}; use crate::cells::Packet; /// Configuration for a RED (Random Early Detection) queue. +/// # Quick-start configuration +/// +/// The defaults are sensible for a 100 Mbps link carrying TCP traffic with +/// 1500 B MTU. To adapt them for a different link rate **R** (Mbps): +/// +/// 1. Compute `pkt_tx_time = avpkt × 8 / R` (µs). For 1500 B packets: +/// `pkt_tx_time = 1500 * 8 / R`. At 1 Gbps this is 12 µs; at 10 Mbps, 1200 µs. +/// 2. Set `min_th` and `max_th` in **bytes** to bound the desired queuing +/// delay. With `pkt_tx_time` µs per packet, a threshold of `B` bytes +/// represents roughly `B / avpkt × pkt_tx_time` µs of extra delay. +/// Example: `min_th = 5 × avpkt` and `max_th = 15 × avpkt` (the defaults +/// at avpkt = 1500 B) give ~600 µs of buffer at `min_th` on a 100 Mbps link. +/// 3. `w_q` should be small enough that a single burst doesn't push `avg` past +/// `max_th`, but large enough that `avg` tracks the true queue length +/// within a few RTTs. The default (0.002) means the half-life is +/// `≈ 346 × pkt_tx_time` — about 42 ms at 100 Mbps with 1500 B packets. +/// 4. For most uses, enable `adaptive = true`. +/// +/// # Constructing a config +/// +/// ```no_run +/// # use rattan_core::cells::bandwidth::queue::red::RedQueueConfig; +/// // Struct-literal with defaults: +/// let cfg = RedQueueConfig { min_th: 5000, ..Default::default() }; +/// +/// // Setter chain: +/// let cfg = RedQueueConfig::default() +/// .with_min_th(5000) +/// .with_max_th(20000) +/// .with_adaptive(true); +/// ``` /// /// # Field correspondence with the Linux kernel /// @@ -254,25 +285,104 @@ use crate::cells::Packet; #[cfg_attr(feature = "serde", derive(Deserialize, Serialize), serde(default))] #[derive(Debug, Clone)] pub struct RedQueueConfig { + /// Hard limit on the number of packets in the queue. Packets arriving when + /// the queue already holds this many are dropped unconditionally. + /// + /// `None` means no limit. `Some(0)` makes the queue a zero-buffer (drop + /// everything). + /// + /// Default: `None`. pub packet_limit: Option, + + /// Hard limit on the total bytes in the queue (L3 length + L2 overhead from + /// [`bw_type`](Self::bw_type)). Same semantics as + /// [`packet_limit`](Self::packet_limit). + /// + /// Default: `None`. pub byte_limit: Option, - pub w_q: f64, // queue weight for calculating the average queue length - pub min_th: usize, // minimum threshold of average queue length - pub max_th: usize, // maximum threshold of average queue length - pub max_p: f64, // maximum probability of dropping a packet - // Packet transmission time in microseconds. - // Used to compute the number of "virtual packet departures" during an idle period - // for average queue length decay: `m = idle_time_us / pkt_tx_time`. - // The upper layer computes this from link bandwidth `C` and average packet size `avpkt` - // as `pkt_tx_time = avpkt * 8 / C` (C in Mbps), then passes it down as a fixed config. + + /// EWMA weight for the average queue length: + /// `avg = (1 − w_q) · avg + w_q · backlog`. + /// + /// Larger values make `avg` track the instantaneous queue length more + /// closely; smaller values smooth out bursts. The original RED paper + /// recommends `0.002`. + /// + /// Valid range: `(0.0, 1.0]`. `w_q = 1.0` makes `avg` equal the + /// instantaneous backlog (useful for testing). + /// + /// Default: `0.002`. + pub w_q: f64, + + /// Lower threshold for the average queue length, in **bytes**. + /// + /// When `avg < min_th`, no packets are dropped. Should be large enough to + /// absorb transient bursts without drops, but small enough to keep queuing + /// delay acceptable. A common rule of thumb is `5 × avpkt`. + /// + /// Must satisfy `min_th < max_th`. + /// + /// Default: `7500` (5 × 1500). + pub min_th: usize, + + /// Upper threshold for the average queue length, in **bytes**. + /// + /// When `avg ≥ max_th`, every packet is dropped. The original RED paper + /// suggests `max_th ≥ 3 × min_th`. + /// + /// Must satisfy `min_th < max_th`. + /// + /// Default: `22500` (3 × 7500). + pub max_th: usize, + + /// Maximum drop probability in the probabilistic region between `min_th` + /// and `max_th`. Small values mean gentle drop-back; large values mean + /// aggressive dropping. + /// + /// When `adaptive = true` (ARED mode), `max_p` is automatically tuned + /// within `[0.01, 0.5]` — the configured value is just the starting point. + /// + /// Valid range: `[0.0, 1.0]`. + /// + /// Default: `0.02` (2%). + pub max_p: f64, + + /// Transmission time of one average-sized packet, in **microseconds**. + /// + /// Used for idle-period average queue decay. Compute as + /// `avpkt (bytes) × 8 / link_rate (Mbps)`. Example: 1500 B at 100 Mbps → + /// `120 µs`. + /// + /// Valid range: `> 0`. + /// + /// Default: `120.0` (1500 B at 100 Mbps). pub pkt_tx_time: f64, - pub adaptive: bool, // enable adaptive mode (ARED): max_p is adjusted dynamically + /// Enable Adaptive RED (ARED): `max_p` is dynamically adjusted every 500 ms + /// to keep `avg` within the target band. + /// + /// Enable when you don't know the right `max_p` for your traffic mix; + /// disable for reproducible experiments. + /// + /// Default: `false`. + pub adaptive: bool, + + /// L2 overhead mode for byte accounting. The extra length from + /// [`BwType::extra_length()`] is added to each packet's L3 length when + /// checking [`byte_limit`](Self::byte_limit). + /// + /// Default: [`BwType::NetworkLayer`] (no overhead). #[cfg_attr( feature = "serde", serde(default, skip_serializing_if = "serde_default") )] pub bw_type: BwType, + + /// Seed for the deterministic RNG used in drop decisions. Two queues with + /// identical configs, traffic, and seed make identical drop decisions, + /// enabling reproducible simulations. + /// + /// Default: `42`. #[cfg_attr(feature = "serde", serde(default = "default_red_seed"))] pub seed: u64, } From c94608176e694bc14575048775e9bdd45bb8dccd Mon Sep 17 00:00:00 2001 From: CepheusC <996390090@qq.com> Date: Wed, 22 Jul 2026 09:49:24 +0000 Subject: [PATCH 39/40] style: format code with cargo fmt --- rattan-core/src/cells/bandwidth/queue/pie.rs | 1 - rattan-core/src/cells/bandwidth/queue/red.rs | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/rattan-core/src/cells/bandwidth/queue/pie.rs b/rattan-core/src/cells/bandwidth/queue/pie.rs index 23558269..b8c3d24f 100644 --- a/rattan-core/src/cells/bandwidth/queue/pie.rs +++ b/rattan-core/src/cells/bandwidth/queue/pie.rs @@ -316,7 +316,6 @@ //! | `bytemode` | `TCA_PIE_BYTEMODE` | scale drop prob by packet size | not supported | //! | `dq_rate_estimator` | `TCA_PIE_DQ_RATE_ESTIMATOR` | enable drain-rate-based delay estimation | always on | - use std::collections::VecDeque; use rand::{rngs::StdRng, RngExt, SeedableRng}; diff --git a/rattan-core/src/cells/bandwidth/queue/red.rs b/rattan-core/src/cells/bandwidth/queue/red.rs index 9808a470..7bcbd9bd 100644 --- a/rattan-core/src/cells/bandwidth/queue/red.rs +++ b/rattan-core/src/cells/bandwidth/queue/red.rs @@ -216,7 +216,6 @@ //! code easier to reason about and avoids an unnecessary special case. //! - use std::collections::VecDeque; use rand::{rngs::StdRng, RngExt, SeedableRng}; @@ -247,7 +246,7 @@ use crate::cells::Packet; /// `max_th`, but large enough that `avg` tracks the true queue length /// within a few RTTs. The default (0.002) means the half-life is /// `≈ 346 × pkt_tx_time` — about 42 ms at 100 Mbps with 1500 B packets. -/// 4. For most uses, enable `adaptive = true`. +/// 4. For most uses, enable `adaptive = true`. /// /// # Constructing a config /// From 9ffb8e4604c24e95d482ccdbe5bb495eac945be3 Mon Sep 17 00:00:00 2001 From: CepheusC <996390090@qq.com> Date: Wed, 22 Jul 2026 09:57:58 +0000 Subject: [PATCH 40/40] refactor(red pie): keep queue submodules private, import via re-export in doc-tests --- rattan-core/src/cells/bandwidth/queue/mod.rs | 4 ++-- rattan-core/src/cells/bandwidth/queue/pie.rs | 2 +- rattan-core/src/cells/bandwidth/queue/red.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/rattan-core/src/cells/bandwidth/queue/mod.rs b/rattan-core/src/cells/bandwidth/queue/mod.rs index 9c6f8e8a..2e2f6e8e 100644 --- a/rattan-core/src/cells/bandwidth/queue/mod.rs +++ b/rattan-core/src/cells/bandwidth/queue/mod.rs @@ -15,8 +15,8 @@ mod codel; mod drophead; mod droptail; mod infinite; -pub mod pie; -pub mod red; +mod pie; +mod red; pub use codel::*; pub use drophead::*; diff --git a/rattan-core/src/cells/bandwidth/queue/pie.rs b/rattan-core/src/cells/bandwidth/queue/pie.rs index b8c3d24f..a0eb571d 100644 --- a/rattan-core/src/cells/bandwidth/queue/pie.rs +++ b/rattan-core/src/cells/bandwidth/queue/pie.rs @@ -347,7 +347,7 @@ use crate::cells::Packet; /// # Constructing a config /// /// ```no_run -/// # use rattan_core::cells::bandwidth::queue::pie::PieQueueConfig; +/// # use rattan_core::cells::bandwidth::queue::PieQueueConfig; /// # use rattan_core::cells::bandwidth::BwType; /// # use std::time::Duration; /// // Struct-literal with defaults: diff --git a/rattan-core/src/cells/bandwidth/queue/red.rs b/rattan-core/src/cells/bandwidth/queue/red.rs index 7bcbd9bd..ba91de85 100644 --- a/rattan-core/src/cells/bandwidth/queue/red.rs +++ b/rattan-core/src/cells/bandwidth/queue/red.rs @@ -251,7 +251,7 @@ use crate::cells::Packet; /// # Constructing a config /// /// ```no_run -/// # use rattan_core::cells::bandwidth::queue::red::RedQueueConfig; +/// # use rattan_core::cells::bandwidth::queue::RedQueueConfig; /// // Struct-literal with defaults: /// let cfg = RedQueueConfig { min_th: 5000, ..Default::default() }; ///