Skip to content

Commit 43c7728

Browse files
committed
proto: pacer slow mode
The pacer enters dynamically in slow mode when the inter-packet pacing exceed 10 ms. In slow mode, the pacer sends only single packet per interval and disables the default burst sending mode.
1 parent c1b181d commit 43c7728

File tree

1 file changed

+67
-17
lines changed

1 file changed

+67
-17
lines changed

quinn-proto/src/connection/pacing.rs

Lines changed: 67 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,20 @@ pub(super) struct Pacer {
1818
last_mtu: u16,
1919
tokens: u64,
2020
prev: Instant,
21+
burst_mode: bool,
2122
}
2223

2324
impl Pacer {
2425
/// Obtains a new [`Pacer`].
2526
pub(super) fn new(smoothed_rtt: Duration, window: u64, mtu: u16, now: Instant) -> Self {
26-
let capacity = optimal_capacity(smoothed_rtt, window, mtu);
27+
let capacity = optimal_capacity(smoothed_rtt, window, mtu, true);
2728
Self {
2829
capacity,
2930
last_window: window,
3031
last_mtu: mtu,
3132
tokens: capacity,
3233
prev: now,
34+
burst_mode: true,
3335
}
3436
}
3537

@@ -43,6 +45,8 @@ impl Pacer {
4345
/// If we can send a packet right away, this returns `None`. Otherwise, returns `Some(d)`,
4446
/// where `d` is the time before this function should be called again.
4547
///
48+
/// In slow mode, it fills the bucket progressively by yielding at a maximum of 10 ms.
49+
///
4650
/// The 5/4 ratio used here comes from the suggestion that N = 1.25 in the draft IETF RFC for
4751
/// QUIC.
4852
pub(super) fn delay(
@@ -59,10 +63,13 @@ impl Pacer {
5963
);
6064

6165
if window != self.last_window || mtu != self.last_mtu {
62-
self.capacity = optimal_capacity(smoothed_rtt, window, mtu);
66+
self.burst_mode = is_burst_mode(smoothed_rtt, window, mtu);
67+
self.capacity = optimal_capacity(smoothed_rtt, window, mtu, self.burst_mode);
6368

64-
// Clamp the tokens
65-
self.tokens = self.capacity.min(self.tokens);
69+
// Clamp the tokens in burst mode
70+
if self.burst_mode {
71+
self.tokens = self.capacity.min(self.tokens);
72+
}
6673
self.last_window = window;
6774
self.last_mtu = mtu;
6875
}
@@ -90,22 +97,41 @@ impl Pacer {
9097

9198
let elapsed_rtts = time_elapsed.as_secs_f64() / smoothed_rtt.as_secs_f64();
9299
let new_tokens = window as f64 * 1.25 * elapsed_rtts;
93-
self.tokens = self
94-
.tokens
95-
.saturating_add(new_tokens as _)
96-
.min(self.capacity);
97100

101+
self.tokens = self.tokens.saturating_add(new_tokens as _);
102+
103+
let token_limit = if self.burst_mode {
104+
self.capacity
105+
} else {
106+
// In slow mode, we only send one packet per interval
107+
mtu as u64
108+
};
109+
110+
self.tokens = self.tokens.min(token_limit);
98111
self.prev = now;
99112

100113
// if we can already send a packet, there is no need for delay
101114
if self.tokens >= bytes_to_send {
102115
return None;
103116
}
104117

105-
let unscaled_delay = smoothed_rtt
106-
.checked_mul((bytes_to_send.max(self.capacity) - self.tokens) as _)
107-
.unwrap_or(Duration::MAX)
108-
/ window;
118+
let unscaled_delay = if self.burst_mode {
119+
smoothed_rtt
120+
.checked_mul((bytes_to_send.max(self.capacity) - self.tokens) as _)
121+
.unwrap_or(Duration::MAX)
122+
/ window
123+
} else {
124+
// We prefer to yield at a maximum of MAX_WAIT_MS ms repetitively instead
125+
// of waiting once
126+
(smoothed_rtt
127+
.checked_mul((bytes_to_send - self.tokens) as _)
128+
.unwrap_or(Duration::MAX)
129+
/ window)
130+
.clamp(
131+
Duration::from_nanos(BURST_INTERVAL_NANOS as _),
132+
Duration::from_millis(MAX_WAIT_MS),
133+
)
134+
};
109135

110136
// divisions come before multiplications to prevent overflow
111137
// this is the time at which the pacing window becomes empty
@@ -126,14 +152,33 @@ impl Pacer {
126152
/// tokens for the extra-elapsed time can be stored.
127153
///
128154
/// Too long burst intervals make pacing less effective.
129-
fn optimal_capacity(smoothed_rtt: Duration, window: u64, mtu: u16) -> u64 {
155+
fn optimal_capacity(smoothed_rtt: Duration, window: u64, mtu: u16, burst_mode: bool) -> u64 {
130156
let rtt = smoothed_rtt.as_nanos().max(1);
131157

132-
let capacity = ((window as u128 * BURST_INTERVAL_NANOS) / rtt) as u64;
158+
let mut capacity = ((window as u128 * BURST_INTERVAL_NANOS) / rtt) as u64;
159+
160+
if burst_mode {
161+
// Small bursts are less efficient (no GSO), could increase latency and don't effectively
162+
// use the channel's buffer capacity. Large bursts might block the connection on sending.
163+
capacity = capacity.clamp(MIN_BURST_SIZE * mtu as u64, MAX_BURST_SIZE * mtu as u64);
164+
}
165+
capacity
166+
}
133167

134-
// Small bursts are less efficient (no GSO), could increase latency and don't effectively
135-
// use the channel's buffer capacity. Large bursts might block the connection on sending.
136-
capacity.clamp(MIN_BURST_SIZE * mtu as u64, MAX_BURST_SIZE * mtu as u64)
168+
/// Determine if pacer must stay in burst mode (original behavior) or switch to slow mode
169+
///
170+
/// On very slow network link, we must avoid sending packets by burst. This could lead to issues
171+
/// such as packet drop. Instead, we would prefer to space each packet by the appropriate amount
172+
/// of time.
173+
///
174+
/// We determine if the pacer must switch to slow mode depending on the packet spacing computed
175+
/// as follows:
176+
///
177+
/// `packet_spacing = RTT * MTU / cwnd`
178+
///
179+
/// Slow mode is detected if packet_spacing exceeds `SLOW_MODE_PACKET_SPACING_THRESHOLD_MS`
180+
fn is_burst_mode(smoothed_rtt: Duration, window: u64, mtu: u16) -> bool {
181+
smoothed_rtt.as_millis() as u64 * mtu as u64 / window < SLOW_MODE_PACKET_SPACING_THRESHOLD_MS
137182
}
138183

139184
/// The burst interval
@@ -149,6 +194,11 @@ const MIN_BURST_SIZE: u64 = 10;
149194

150195
/// Creating 256 packets took 1ms in a benchmark, so larger bursts don't make sense.
151196
const MAX_BURST_SIZE: u64 = 256;
197+
/// Packet spacing threshold in ms computed from the congestion window and the RTT.
198+
/// Above this threshold, we consider the network as slow and adapt the pacer accordingly
199+
const SLOW_MODE_PACKET_SPACING_THRESHOLD_MS: u64 = 10;
200+
/// Maximum amount of time the pacer can wait while in slow mode
201+
const MAX_WAIT_MS: u64 = 10;
152202

153203
#[cfg(test)]
154204
mod tests {

0 commit comments

Comments
 (0)