/// Fraction of a flow control window after which a receiver sends a window /// update. /// /// In steady-state and max utilization, a value of 4 leads to 4 window updates /// per RTT. /// /// Value aligns with [`crate::connection::params::ConnectionParameters::DEFAULT_ACK_RATIO`]. pubconst WINDOW_UPDATE_FRACTION: u64 = 4;
/// Multiplier for auto-tuning the stream receive window. /// /// See [`ReceiverFlowControl::auto_tune`]. /// /// Note that the flow control window should grow at least as fast as the /// congestion control window, in order to not unnecessarily limit throughput. const WINDOW_INCREASE_MULTIPLIER: u64 = 4;
/// Subject for flow control auto-tuning, used to avoid heap allocations /// when logging. #[derive(Debug, Clone, Copy)] enum AutoTuneSubject {
Connection,
Stream(StreamId),
}
#[derive(Debug)] pubstruct SenderFlowControl<T> where
T: Debug + Sized,
{ /// The thing that we're counting for.
subject: T, /// The limit.
limit: u64, /// How much of that limit we've used.
used: u64, /// The limit at which blocking was last reported, or `None` if never blocked. /// Updated each time the sender decides it is blocked, ensuring that blocking /// at any given limit is only reported once.
blocked_at: Option<u64>, /// Whether a blocked frame should be sent.
blocked_frame: bool,
}
impl<T> SenderFlowControl<T> where
T: Debug + Sized,
{ /// Make a new instance with the initial value and subject. pubconstfn new(subject: T, initial: u64) -> Self { Self {
subject,
limit: initial,
used: 0,
blocked_at: None,
blocked_frame: false,
}
}
/// Update the maximum. Returns `Some` with the updated available flow /// control if the change was an increase and `None` otherwise. pubfn update(&mutself, limit: u64) -> Option<usize> {
(limit > self.limit).then(|| { self.limit = limit; self.blocked_frame = false; self.available()
})
}
/// Consume flow control. pubfn consume(&mutself, count: usize) { let amt = u64::try_from(count).expect("usize fits into u64");
debug_assert!(self.used + amt <= self.limit); self.used += amt;
}
/// Get available flow control. pubfn available(&self) -> usize {
usize::try_from(self.limit - self.used).unwrap_or(usize::MAX)
}
/// How much data has been written. pubconstfn used(&self) -> u64 { self.used
}
/// Mark flow control as blocked. /// This only does something if the current limit exceeds the last reported blocking limit. pubconstfn blocked(&mutself) { iflet Some(block) = self.blocked_at
&& self.limit <= block
{ return;
} self.blocked_at = Some(self.limit); self.blocked_frame = true;
}
/// Return whether a blocking frame needs to be sent. /// This is `Some` with the active limit if `blocked` has been called, /// if a blocking frame has not been sent (or it has been lost), and /// if the blocking condition remains. fn blocked_needed(&self) -> Option<u64> { self.blocked_at
.filter(|&l| self.blocked_frame && self.limit <= l)
}
/// Clear the need to send a blocked frame. constfn blocked_sent(&mutself) { self.blocked_frame = false;
}
/// Mark a blocked frame as having been lost. /// Only send again if value of `self.blocked_at` hasn't increased since sending. /// That would imply that the limit has since increased. pubconstfn frame_lost(&mutself, limit: u64) { iflet Some(block) = self.blocked_at
&& block == limit
{ self.blocked_frame = true;
}
}
}
#[derive(Debug, Default)] pubstruct ReceiverFlowControl<T> where
T: Debug + Sized,
{ /// The thing that we're counting for.
subject: T, /// The maximum amount of items that can be active (e.g., the size of the receive buffer).
max_active: u64, /// Last max allowed sent.
max_allowed: u64, /// Last time a flow control update was sent. /// /// Used by auto-tuning logic to estimate sending rate between updates. /// This is active for both stream-level /// ([`ReceiverFlowControl<StreamId>`]) and connection-level /// ([`ReceiverFlowControl<()>`]) flow control.
last_update: Option<Instant>, /// Item received, but not retired yet. /// This will be used for byte flow control: each stream will remember its largest byte /// offset received and session flow control will remember the sum of all bytes consumed /// by all streams.
consumed: u64, /// Retired items.
retired: u64,
frame_pending: bool,
}
impl<T> ReceiverFlowControl<T> where
T: Debug + Sized,
{ /// Make a new instance with the initial value and subject. pubconstfn new(subject: T, max: u64) -> Self { Self {
subject,
max_active: max,
max_allowed: max,
last_update: None,
consumed: 0,
retired: 0,
frame_pending: false,
}
}
/// Retire some items and maybe send flow control /// update. pubconstfn retire(&mutself, retired: u64) { if retired <= self.retired { return;
}
/// This function is called when `STREAM_DATA_BLOCKED` frame is received. /// The flow control will try to send an update if possible. pubconstfn send_flowc_update(&mutself) { ifself.retired + self.max_active > self.max_allowed { self.frame_pending = true;
}
}
pubfn next_limit(&self) -> u64 {
min( self.retired + self.max_active, // Flow control limits are encoded as QUIC varints and are thus // limited to the maximum QUIC varint value.
MAX_VARINT,
)
}
/// Core auto-tuning logic for adjusting the maximum flow control window. /// /// This method is called by both connection-level and stream-level /// implementations. It increases `max_active` when the sending rate exceeds /// what the current window and RTT would allow, capping at `max_window`. fn auto_tune_inner(
&mutself,
now: Instant,
rtt: Duration,
max_window: u64,
subject: AutoTuneSubject,
) { let Some(max_allowed_sent_at) = self.last_update else { return;
};
let Ok(elapsed): Result<u64, _> = now
.duration_since(max_allowed_sent_at)
.as_micros()
.try_into() else { return;
};
let Ok(rtt): Result<NonZeroU64, _> = rtt
.as_micros()
.try_into()
.and_then(|rtt: u64| NonZeroU64::try_from(rtt)) else { // RTT is zero, no need for tuning. return;
};
// Scale the max_active window down by // [(F-1) / F]; where F=WINDOW_UPDATE_FRACTION. // // In the ideal case, each byte sent would trigger a flow control // update. However, in practice we only send updates every // WINDOW_UPDATE_FRACTION of the window. Thus, when not application // limited, in a steady state transfer it takes 1 RTT after sending 1 / // F bytes for the sender to receive the next update. The sender is // effectively limited to [(F-1) / F] bytes per RTT. // // By calculating with this effective window instead of the full // max_active, we account for the inherent delay between when the sender // would ideally receive flow control updates and when they actually // arrive due to our batched update strategy. // // Example with F=4 without adjustment: // // t=0 start sending // t=RTT/4 sent 1/4 of window total // t=RTT sent 1 window total // sender blocked for RTT/4 // t=RTT+RTT/4 receive update for 1/4 of window // // Example with F=4 with adjustment: // // t=0 start sending // t=RTT/4 sent 1/4 of window total // t=RTT sent 1 window total // t=RTT+RTT/4 sent 1+1/4 window total; receive update for 1/4 of window (just in time) let effective_window =
(self.max_active * (WINDOW_UPDATE_FRACTION - 1)) / (WINDOW_UPDATE_FRACTION);
// Compute the amount of bytes we have received in excess // of what `max_active` might allow. let window_bytes_expected = (effective_window * elapsed) / (rtt);
let window_bytes_used = self.max_active - (self.max_allowed - self.retired); let Some(excess) = window_bytes_used.checked_sub(window_bytes_expected) else { // Used below expected. No auto-tuning needed. return;
};
let prev_max_active = self.max_active; let new_max_active = min( self.max_active + excess * WINDOW_INCREASE_MULTIPLIER,
max_window,
);
if new_max_active <= prev_max_active { // Never decrease max_active, even if max_window is smaller. This // can happen if max_active was set manually. return;
}
self.max_active = new_max_active;
qdebug!( "Increasing max {subject} receive window by {} B, \
previous max_active: {} MiB, \
new max_active: {} MiB, \
last update: {:?}, \
rtt: {rtt:?}",
new_max_active - prev_max_active,
prev_max_active / 1024 / 1024, self.max_active / 1024 / 1024,
now - max_allowed_sent_at,
);
}
}
let max_allowed = self.next_limit(); if builder.write_varint_frame(&[FrameType::MaxData.into(), max_allowed]) {
stats.max_data += 1;
tokens.push(recovery::Token::Stream(StreamRecoveryToken::MaxData(
max_allowed,
))); self.frame_sent(max_allowed); self.last_update = Some(now);
}
}
/// Auto-tune [`ReceiverFlowControl::max_active`], i.e. the connection flow /// control window. /// /// If the sending rate (`window_bytes_used`) exceeds the rate allowed by /// the maximum flow control window and the current rtt /// (`window_bytes_expected`), try to increase the maximum flow control /// window ([`ReceiverFlowControl::max_active`]). fn auto_tune(&mutself, now: Instant, rtt: Duration) { self.auto_tune_inner(now, rtt, MAX_LOCAL_MAX_DATA, AutoTuneSubject::Connection);
}
/// Auto-tune [`ReceiverFlowControl::max_active`], i.e. the stream flow /// control window. /// /// If the sending rate (`window_bytes_used`) exceeds the rate allowed by /// the maximum flow control window and the current rtt /// (`window_bytes_expected`), try to increase the maximum flow control /// window ([`ReceiverFlowControl::max_active`]). fn auto_tune(&mutself, now: Instant, rtt: Duration) { self.auto_tune_inner(
now,
rtt,
MAX_LOCAL_MAX_STREAM_DATA,
AutoTuneSubject::Stream(self.subject),
);
}
impl RemoteStreamLimit { pubconstfn new(stream_type: StreamType, max_streams: u64, role: Role) -> Self { Self {
streams_fc: ReceiverFlowControl::new(stream_type, max_streams), // // This is for a stream created by a peer, therefore we use role.remote().
next_stream: StreamId::init(stream_type, role.remote()),
}
}
#[cfg(test)] #[cfg_attr(coverage_nightly, coverage(off))] mod test { #![allow(
clippy::allow_attributes,
clippy::unwrap_in_result,
reason = "OK in tests."
)]
use std::{
cmp::min,
collections::VecDeque,
time::{Duration, Instant},
};
use neqo_common::{Encoder, Role, qdebug}; use nss::random;
#[test] fn update_consume() { letmut fc = SenderFlowControl::new((), 10);
fc.consume(10);
assert_eq!(fc.available(), 0);
fc.update(5); // An update lower than the current limit does nothing.
assert_eq!(fc.available(), 0);
fc.update(15);
assert_eq!(fc.available(), 5);
fc.consume(3);
assert_eq!(fc.available(), 2);
}
#[test] fn update_clears_blocked() { letmut fc = SenderFlowControl::new((), 10);
fc.blocked();
assert_eq!(fc.blocked_needed(), Some(10));
fc.update(5); // An update lower than the current limit does nothing.
assert_eq!(fc.blocked_needed(), Some(10));
fc.update(11);
assert_eq!(fc.blocked_needed(), None);
}
#[test] fn changing_max_active() { letmut fc = ReceiverFlowControl::new((), 100);
fc.set_max_active(50);
// There is no MAX_STREAM_DATA frame needed.
assert!(!fc.frame_needed());
// We can still retire more than 50.
fc.consume(60).unwrap();
fc.retire(60);
// There is no MAX_STREAM_DATA frame needed yet.
assert!(!fc.frame_needed());
fc.consume(16).unwrap();
fc.retire(76);
assert!(fc.frame_needed());
assert_eq!(fc.next_limit(), 126);
#[test] fn trigger_factor() -> Res<()> { let rtt = Duration::from_millis(40); let now = test_fixture::now(); letmut fc =
ReceiverFlowControl::new(StreamId::new(0), INITIAL_LOCAL_MAX_STREAM_DATA as u64);
let fraction = INITIAL_LOCAL_MAX_STREAM_DATA as u64 / WINDOW_UPDATE_FRACTION;
let consumed = fc.set_consumed(fraction)?;
fc.add_retired(consumed);
assert_eq!(write_frames(&mut fc, rtt, now), 0);
#[test] fn auto_tuning_increase_no_decrease() -> Res<()> { let rtt = Duration::from_millis(40); letmut now = test_fixture::now(); letmut fc =
ReceiverFlowControl::new(StreamId::new(0), INITIAL_LOCAL_MAX_STREAM_DATA as u64); let initial_max_active = fc.max_active();
// Consume and retire multiple receive windows without increasing time. for _ in1..11 { let consumed = fc.set_consumed(fc.next_limit())?;
fc.add_retired(consumed);
write_frames(&mut fc, rtt, now);
} let increased_max_active = fc.max_active();
assert!(
initial_max_active < increased_max_active, "expect receive window auto-tuning to increase max_active on full utilization of high bdp connection"
);
// Huge idle time.
now += Duration::from_secs(60 * 60); // 1h let consumed = fc.set_consumed(fc.next_limit()).unwrap();
fc.add_retired(consumed);
assert_eq!(write_frames(&mut fc, rtt, now), 1);
assert_eq!(
increased_max_active,
fc.max_active(), "expect receive window auto-tuning never to decrease max_active on low utilization"
);
Ok(())
}
#[test] fn stream_data_blocked_triggers_auto_tuning() -> Res<()> { let rtt = Duration::from_millis(40); let now = test_fixture::now(); letmut fc =
ReceiverFlowControl::new(StreamId::new(0), INITIAL_LOCAL_MAX_STREAM_DATA as u64);
// Send first window update to give auto-tuning algorithm a baseline. let consumed = fc.set_consumed(fc.next_limit())?;
fc.add_retired(consumed);
assert_eq!(write_frames(&mut fc, rtt, now), 1);
// Use up a single byte only, i.e. way below WINDOW_UPDATE_FRACTION. let consumed = fc.set_consumed(fc.retired + 1)?;
fc.add_retired(consumed);
assert_eq!(
write_frames(&mut fc, rtt, now), 0, "expect receiver to not send window update unprompted"
);
#[expect(clippy::cast_precision_loss, reason = "This is test code.")] #[expect(clippy::too_many_lines, reason = "This is test code.")] #[test] fn auto_tuning_approximates_bandwidth_delay_product() -> Res<()> { const DATA_FRAME_SIZE: u64 = 1_500; /// Allow auto-tuning algorithm to be off from actual bandwidth-delay /// product by up to 1KiB. const TOLERANCE: u64 = 1024; const BW_TOLERANCE: f64 = 0.6;
test_fixture::fixture_init();
// Run multiple iterations with randomized bandwidth and rtt. for _ in0..100 { // Random bandwidth between 12 Mbit/s and 1 Gbit/s. Minimum 12 // Mbit/s to ensure bdp stays above DATA_FRAME_SIZE, see `assert!` // below. let bandwidth =
u64::from(u16::from_be_bytes(random::<2>()) % 1_000 + 12) * 1_000 * 1_000; // Random delay between 1 ms and 256 ms. let rtt_int = u64::from(random::<1>()[0]) + 1; let rtt = Duration::from_millis(rtt_int); let half_rtt = rtt / 2; let bdp = bandwidth * rtt_int / 1_000 / 8;
assert!(
DATA_FRAME_SIZE <= bdp, "BDP must be larger than DATA_FRAME_SIZE. Latency calculations in test assume it can transfer DATA_FRAME_SIZE bytes in1 RTT."
);
letmut last_max_active = INITIAL_LOCAL_MAX_STREAM_DATA as u64; letmut last_max_active_changed = now;
letmut sender_window = INITIAL_LOCAL_MAX_STREAM_DATA as u64; letmut fc =
ReceiverFlowControl::new(StreamId::new(0), INITIAL_LOCAL_MAX_STREAM_DATA as u64);
letmut bytes_received: u64 = 0; let start_time = now;
// Track when sender can next send. letmut next_send_time = now; loop { // Sender receives window updates. if recv_to_send.front().is_some_and(|(at, _)| *at <= now) { let (_, update) = recv_to_send.pop_front().unwrap();
sender_window += update;
}
// Sender sends data frames. let sender_progressed = if sender_window > 0 { let to_send = min(DATA_FRAME_SIZE, sender_window);
sender_window -= to_send; let time_to_send =
Duration::from_secs_f64(to_send as f64 * 8.0 / bandwidth as f64);
let send_start = next_send_time.max(now);
next_send_time = send_start + time_to_send;
// When idle, travel in (simulated) time. if !sender_progressed && !receiver_progressed {
now = [recv_to_send.front(), send_to_recv.front()]
.into_iter()
.flatten()
.map(|(at, _)| *at)
.min()
.expect("both are None");
}
// Consider auto-tuning done once receive window hasn't changed for 8 RTT. // A large amount to allow the observed bandwidth average to stabilize. if now.duration_since(last_max_active_changed) > 8 * rtt { break;
}
}
// See comment in [`ReceiverFlowControl::auto_tune_inner`] for an // explanation of the effective window. let effective_window =
(fc.max_active() * (WINDOW_UPDATE_FRACTION - 1)) / WINDOW_UPDATE_FRACTION; let at_max_stream_data = fc.max_active() == MAX_LOCAL_MAX_STREAM_DATA;
#[test] fn connection_flow_control_auto_tune() -> Res<()> { let rtt = Duration::from_millis(40); let now = test_fixture::now(); let initial_window = (INITIAL_LOCAL_MAX_STREAM_DATA * 16) as u64; letmut fc = ReceiverFlowControl::new((), initial_window); let initial_max_active = fc.max_active();
// Consume and retire multiple windows to trigger auto-tuning. // Each iteration: consume a full window, retire it, send update. for _ in1..11 { let to_consume = fc.max_active();
fc.consume(to_consume)?;
fc.add_retired(to_consume);
write_conn_frames(&mut fc, now);
} let increased_max_active = fc.max_active();
assert!(
initial_max_active < increased_max_active, "expect connection-level receive window auto-tuning to increase max_active on full utilization"
);
Ok(())
}
#[test] fn connection_flow_control_respects_max_window() -> Res<()> { let rtt = Duration::from_millis(40); let now = test_fixture::now(); let initial_window = (INITIAL_LOCAL_MAX_STREAM_DATA * 16) as u64; letmut fc = ReceiverFlowControl::new((), initial_window);
// Consume and retire many full windows to push window to the limit. // Keep consuming without advancing time to create maximum pressure. for _ in0..1000 { let prev_max = fc.max_active(); let to_consume = fc.max_active();
fc.consume(to_consume)?;
fc.add_retired(to_consume);
write_conn_frames(&mut fc);
// Stop if we've reached the maximum and it's not growing anymore if fc.max_active() == MAX_LOCAL_MAX_DATA && fc.max_active() == prev_max {
qdebug!( "Reached and stabilized at max window: {} MiB",
fc.max_active() / 1024 / 1024
); break;
}
}
assert_eq!(
fc.max_active(),
MAX_LOCAL_MAX_DATA, "expect connection-level receive window to cap at MAX_LOCAL_MAX_DATA (100 MiB), got {} MiB",
fc.max_active() / 1024 / 1024
);
#[test] fn update_same_limit_returns_none() { // `update` returns None when the new limit equals the current limit. letmut fc = SenderFlowControl::new((), 10);
assert!(fc.update(10).is_none()); // Equal — no change.
assert!(fc.update(11).is_some()); // Strictly greater — update.
}
#[test] fn set_max_active_equal_does_not_set_frame_pending() { // `set_max_active` does not mark frame pending when the value is unchanged. letmut fc = ReceiverFlowControl::new(StreamId::new(0), 100);
fc.set_max_active(100); // Same value — should not set frame_pending.
assert!(!fc.frame_needed());
fc.set_max_active(101); // Increase — should set frame_pending.
assert!(fc.frame_needed());
}
#[test] fn retire_no_op_when_not_increasing() { // `retire` is a no-op when the new value does not exceed the current retired count. // Retire 80/100 bytes to exceed the 75% threshold and trigger a flow control update. letmut fc = ReceiverFlowControl::new(StreamId::new(0), 100);
fc.set_consumed(80).unwrap();
fc.retire(80); // 20 bytes unused < 25 (25% threshold) → triggers update
assert!(fc.frame_needed());
fc.frame_sent(fc.next_limit()); // mark frame as sent, clearing pending
fc.retire(80); // same value — no-op
assert!(!fc.frame_needed());
}
#[test] fn add_retired_zero_does_not_trigger_update() { // `add_retired(0)` must not trigger a flow control update when no data was retired. letmut fc = ReceiverFlowControl::new(StreamType::UniDi, 100);
fc.add_retired(0); // count == 0: no update.
assert!(!fc.frame_needed());
fc.add_retired(1); // count > 0: retired+max_active > max_allowed → triggers update.
assert!(fc.frame_needed());
}
#[test] fn auto_tune_never_decreases_large_manually_set_max_active() -> Res<()> { let rtt = Duration::from_millis(40); let now = test_fixture::now(); letmut fc = ReceiverFlowControl::new(
StreamId::new(0), // Very large manually configured window beyond the maximum auto-tuned window.
MAX_LOCAL_MAX_STREAM_DATA * 10,
); let initial_max_active = fc.max_active();
// Consume and retire multiple windows to trigger auto-tuning. // Each iteration: consume a full window, retire it, send update. for _ in1..11 { let consumed = fc.set_consumed(fc.next_limit())?;
fc.add_retired(consumed);
write_frames(&mut fc, rtt, now);
} let increased_max_active = fc.max_active();
assert!(
initial_max_active == increased_max_active, "expect receive window auto-tuning to not decrease max_active below manually set initial value."
);
Ok(())
}
}
Messung V0.5 in Prozent
¤ Dauer der Verarbeitung: 0.24 Sekunden
(vorverarbeitet am 2026-08-25)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.