Tune MMP link-layer report intervals for constrained transports

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).
This commit is contained in:
Johnathan Corgan
2026-04-05 10:38:59 +00:00
parent 864a8bcc9e
commit 4370441e48
3 changed files with 95 additions and 19 deletions

View File

@@ -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;

View File

@@ -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);

View File

@@ -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));
}