From 4370441e48643382ef7470a13d49afcd25ac2155 Mon Sep 17 00:00:00 2001 From: Johnathan Corgan Date: Sun, 5 Apr 2026 10:38:59 +0000 Subject: [PATCH] Tune MMP link-layer report intervals for constrained transports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Raise the report interval floor from 100ms to 1000ms and ceiling from 2000ms to 5000ms. The old 100ms floor produced ~600 reports per 60s parent evaluation cycle — far more than the ~10 needed for EWMA convergence. The new floor yields ~60 reports/cycle, still well above the convergence threshold, while reducing BLE overhead by 10×. Add cold-start transition: first 5 SRTT samples use the 200ms floor for fast initial convergence, then switch to the 1000ms steady-state floor. Session-layer intervals unchanged (500ms–10000ms). --- src/mmp/mod.rs | 16 +++++++++++++-- src/mmp/receiver.rs | 50 +++++++++++++++++++++++++++++++++++++-------- src/mmp/sender.rs | 48 +++++++++++++++++++++++++++++++++++-------- 3 files changed, 95 insertions(+), 19 deletions(-) diff --git a/src/mmp/mod.rs b/src/mmp/mod.rs index 105b8cb..b894062 100644 --- a/src/mmp/mod.rs +++ b/src/mmp/mod.rs @@ -72,10 +72,22 @@ pub const EWMA_LONG_ALPHA: f64 = 1.0 / 32.0; pub const DEFAULT_COLD_START_INTERVAL_MS: u64 = 200; /// Minimum report interval (SRTT clamp floor). -pub const MIN_REPORT_INTERVAL_MS: u64 = 100; +/// +/// Raised from 100ms to 1000ms: parent re-evaluation runs every 60s, +/// so 60 samples/cycle is more than sufficient for EWMA convergence (~10). +/// The cold-start phase uses `DEFAULT_COLD_START_INTERVAL_MS` (200ms) for +/// fast initial SRTT convergence before transitioning to this floor. +pub const MIN_REPORT_INTERVAL_MS: u64 = 1_000; /// Maximum report interval (SRTT clamp ceiling). -pub const MAX_REPORT_INTERVAL_MS: u64 = 2_000; +pub const MAX_REPORT_INTERVAL_MS: u64 = 5_000; + +/// Number of SRTT samples before transitioning from cold-start to normal floor. +/// +/// During cold-start, report intervals use `DEFAULT_COLD_START_INTERVAL_MS` as +/// the floor to gather SRTT samples quickly. After this many updates, the floor +/// switches to `MIN_REPORT_INTERVAL_MS`. +pub const COLD_START_SAMPLES: u32 = 5; /// Default OWD ring buffer capacity. pub const DEFAULT_OWD_WINDOW_SIZE: usize = 32; diff --git a/src/mmp/receiver.rs b/src/mmp/receiver.rs index a8f201e..ad0cfcc 100644 --- a/src/mmp/receiver.rs +++ b/src/mmp/receiver.rs @@ -7,8 +7,9 @@ use std::time::{Duration, Instant}; use crate::mmp::algorithms::{JitterEstimator, OwdTrendDetector}; use crate::mmp::report::ReceiverReport; -use crate::mmp::{DEFAULT_COLD_START_INTERVAL_MS, DEFAULT_OWD_WINDOW_SIZE, - MAX_REPORT_INTERVAL_MS, MIN_REPORT_INTERVAL_MS}; +use crate::mmp::{COLD_START_SAMPLES, DEFAULT_COLD_START_INTERVAL_MS, + DEFAULT_OWD_WINDOW_SIZE, MAX_REPORT_INTERVAL_MS, + MIN_REPORT_INTERVAL_MS}; /// Grace period after rekey before resuming jitter calculation. /// @@ -179,6 +180,10 @@ pub struct ReceiverState { report_interval: Duration, /// Whether any frames have been received since the last report. interval_has_data: bool, + + // --- Cold-start tracking --- + /// Number of SRTT-based interval updates received. + srtt_sample_count: u32, } impl ReceiverState { @@ -209,6 +214,7 @@ impl ReceiverState { last_report_time: None, report_interval: Duration::from_millis(cold_start_ms), interval_has_data: false, + srtt_sample_count: 0, } } @@ -363,9 +369,18 @@ impl ReceiverState { /// Update the report interval based on SRTT (link-layer defaults). /// - /// Receiver reports at 1× SRTT, clamped to [MIN, MAX]. + /// Receiver reports at 1× SRTT clamped to [floor, MAX]. During cold-start + /// (first `COLD_START_SAMPLES` updates), the floor is the cold-start + /// interval (200ms) for fast SRTT convergence. After that, it rises to + /// `MIN_REPORT_INTERVAL_MS` (1000ms) for steady-state efficiency. pub fn update_report_interval_from_srtt(&mut self, srtt_us: i64) { - self.update_report_interval_with_bounds(srtt_us, MIN_REPORT_INTERVAL_MS, MAX_REPORT_INTERVAL_MS); + self.srtt_sample_count = self.srtt_sample_count.saturating_add(1); + let floor = if self.srtt_sample_count <= COLD_START_SAMPLES { + DEFAULT_COLD_START_INTERVAL_MS + } else { + MIN_REPORT_INTERVAL_MS + }; + self.update_report_interval_with_bounds(srtt_us, floor, MAX_REPORT_INTERVAL_MS); } /// Update the report interval based on SRTT with custom bounds. @@ -566,17 +581,36 @@ mod tests { } #[test] - fn test_update_report_interval() { + fn test_update_report_interval_cold_start() { let mut r = ReceiverState::new(32); - // 50ms SRTT → 100ms receiver interval (1× SRTT, clamped to min) + // During cold-start, floor is 200ms (DEFAULT_COLD_START_INTERVAL_MS) + // 50ms SRTT → 50ms receiver interval (1× SRTT), clamped to cold-start floor 200ms r.update_report_interval_from_srtt(50_000); - assert_eq!(r.report_interval(), Duration::from_millis(100)); + assert_eq!(r.report_interval(), Duration::from_millis(200)); - // 500ms SRTT → 500ms + // 500ms SRTT → 500ms (above cold-start floor) r.update_report_interval_from_srtt(500_000); assert_eq!(r.report_interval(), Duration::from_millis(500)); } + #[test] + fn test_update_report_interval_after_cold_start() { + let mut r = ReceiverState::new(32); + // Burn through cold-start samples + for _ in 0..COLD_START_SAMPLES { + r.update_report_interval_from_srtt(500_000); + } + + // 6th sample: steady state, floor is MIN_REPORT_INTERVAL_MS (1000ms) + // 50ms SRTT → 50ms receiver interval (1× SRTT), clamped to 1000ms + r.update_report_interval_from_srtt(50_000); + assert_eq!(r.report_interval(), Duration::from_millis(MIN_REPORT_INTERVAL_MS)); + + // 3s SRTT → 3000ms, within [1000, 5000] + r.update_report_interval_from_srtt(3_000_000); + assert_eq!(r.report_interval(), Duration::from_millis(3000)); + } + #[test] fn test_rekey_jitter_grace_suppresses_spikes() { let mut r = ReceiverState::new(32); diff --git a/src/mmp/sender.rs b/src/mmp/sender.rs index ba3b360..f4a69cc 100644 --- a/src/mmp/sender.rs +++ b/src/mmp/sender.rs @@ -6,7 +6,8 @@ use std::time::{Duration, Instant}; use crate::mmp::report::SenderReport; -use crate::mmp::{DEFAULT_COLD_START_INTERVAL_MS, MAX_REPORT_INTERVAL_MS, MIN_REPORT_INTERVAL_MS}; +use crate::mmp::{COLD_START_SAMPLES, DEFAULT_COLD_START_INTERVAL_MS, + MAX_REPORT_INTERVAL_MS, MIN_REPORT_INTERVAL_MS}; /// Per-peer sender-side MMP state. /// @@ -35,6 +36,10 @@ pub struct SenderState { // --- Send failure backoff --- /// Consecutive send failure count for backoff calculation. consecutive_send_failures: u32, + + // --- Cold-start tracking --- + /// Number of SRTT-based interval updates received. + srtt_sample_count: u32, } impl SenderState { @@ -59,6 +64,7 @@ impl SenderState { last_report_time: None, report_interval: Duration::from_millis(cold_start_ms), consecutive_send_failures: 0, + srtt_sample_count: 0, } } @@ -150,9 +156,18 @@ impl SenderState { /// Update the report interval based on SRTT (link-layer defaults). /// - /// Sender reports at 2× SRTT clamped to [MIN, MAX]. + /// Sender reports at 2× SRTT clamped to [floor, MAX]. During cold-start + /// (first `COLD_START_SAMPLES` updates), the floor is the cold-start + /// interval (200ms) for fast SRTT convergence. After that, it rises to + /// `MIN_REPORT_INTERVAL_MS` (1000ms) for steady-state efficiency. pub fn update_report_interval_from_srtt(&mut self, srtt_us: i64) { - self.update_report_interval_with_bounds(srtt_us, MIN_REPORT_INTERVAL_MS, MAX_REPORT_INTERVAL_MS); + self.srtt_sample_count = self.srtt_sample_count.saturating_add(1); + let floor = if self.srtt_sample_count <= COLD_START_SAMPLES { + DEFAULT_COLD_START_INTERVAL_MS + } else { + MIN_REPORT_INTERVAL_MS + }; + self.update_report_interval_with_bounds(srtt_us, floor, MAX_REPORT_INTERVAL_MS); } /// Update the report interval based on SRTT with custom bounds. @@ -289,18 +304,33 @@ mod tests { } #[test] - fn test_update_report_interval() { + fn test_update_report_interval_cold_start() { let mut s = SenderState::new(); - // 50ms RTT → 100ms sender interval (2× SRTT), clamped to min 100ms + // During cold-start, floor is 200ms (DEFAULT_COLD_START_INTERVAL_MS) + // 50ms RTT → 100ms sender interval (2× SRTT), clamped to cold-start floor 200ms s.update_report_interval_from_srtt(50_000); - assert_eq!(s.report_interval(), Duration::from_millis(100)); + assert_eq!(s.report_interval(), Duration::from_millis(200)); - // 500ms RTT → 1000ms sender interval + // 500ms RTT → 1000ms sender interval (above cold-start floor) s.update_report_interval_from_srtt(500_000); assert_eq!(s.report_interval(), Duration::from_millis(1000)); + } - // 2s RTT → 4s, clamped to max 2s - s.update_report_interval_from_srtt(2_000_000); + #[test] + fn test_update_report_interval_after_cold_start() { + let mut s = SenderState::new(); + // Burn through cold-start samples (COLD_START_SAMPLES = 5) + for _ in 0..COLD_START_SAMPLES { + s.update_report_interval_from_srtt(500_000); + } + + // 6th sample: now in steady state, floor is MIN_REPORT_INTERVAL_MS (1000ms) + // 50ms RTT → 100ms sender interval (2× SRTT), clamped to 1000ms + s.update_report_interval_from_srtt(50_000); + assert_eq!(s.report_interval(), Duration::from_millis(MIN_REPORT_INTERVAL_MS)); + + // 3s RTT → 6s, clamped to max 5s + s.update_report_interval_from_srtt(3_000_000); assert_eq!(s.report_interval(), Duration::from_millis(MAX_REPORT_INTERVAL_MS)); }