-
-
Notifications
You must be signed in to change notification settings - Fork 454
Description
We are experimenting with using Quinn over slow internet connections, such as 400 Kbps. We've observed unexpected and continuous packet losses in this context. After some investigation, we've identified the pacer as the primary cause.
The current pacer is designed for typical internet connections and enforces a hard lower bound of 10 packets per 2 ms. This rate is excessive for low-bandwidth networks.
To address this, we've implemented a "dirty fix" that allows for reduced pacing capacity and slower token refill rates. With this patch, the unexpected packet losses were eliminated:
diff --git a/quinn-proto/src/connection/pacing.rs b/quinn-proto/src/connection/pacing.rs
index 2e469948..da2dbcf0 100644
--- a/quinn-proto/src/connection/pacing.rs
+++ b/quinn-proto/src/connection/pacing.rs
@@ -28,7 +28,7 @@ impl Pacer {
capacity,
last_window: window,
last_mtu: mtu,
- tokens: capacity,
+ tokens: capacity.max(mtu as u64), // build up the first token directly
prev: now,
}
}
@@ -89,11 +89,11 @@ impl Pacer {
}
let elapsed_rtts = time_elapsed.as_secs_f64() / smoothed_rtt.as_secs_f64();
- let new_tokens = window as f64 * 1.25 * elapsed_rtts;
+ let new_tokens = (window as f64 * 1.25 * elapsed_rtts).min(self.capacity as f64);
+
self.tokens = self
.tokens
- .saturating_add(new_tokens as _)
- .min(self.capacity);
+ .saturating_add(new_tokens as _);
self.prev = now;
@@ -103,7 +103,7 @@ impl Pacer {
}
let unscaled_delay = smoothed_rtt
- .checked_mul((bytes_to_send.max(self.capacity) - self.tokens) as _)
+ .checked_mul((bytes_to_send.max(self.capacity) - self.tokens).min(self.capacity) as _)
.unwrap_or(Duration::MAX)
/ window;
@@ -131,9 +131,7 @@ fn optimal_capacity(smoothed_rtt: Duration, window: u64, mtu: u16) -> u64 {
let capacity = ((window as u128 * BURST_INTERVAL_NANOS) / rtt) as u64;
- // Small bursts are less efficient (no GSO), could increase latency and don't effectively
- // use the channel's buffer capacity. Large bursts might block the connection on sending.
- capacity.clamp(MIN_BURST_SIZE * mtu as u64, MAX_BURST_SIZE * mtu as u64)
+ capacity.min(MAX_BURST_SIZE * mtu as u64)
}
/// The burst interval
@@ -144,8 +142,7 @@ fn optimal_capacity(smoothed_rtt: Duration, window: u64, mtu: u16) -> u64 {
/// more applicable.
const BURST_INTERVAL_NANOS: u128 = 2_000_000; // 2ms
-/// Allows some usage of GSO, and doesn't slow down the handshake.
-const MIN_BURST_SIZE: u64 = 10;
+const MIN_BURST_SIZE: u64 = 1;
/// Creating 256 packets took 1ms in a benchmark, so larger bursts don't make sense.
const MAX_BURST_SIZE: u64 = 256;
However, this approach has a trade-off: Quinn's transmit loop is now triggered and blocked by the pacer more frequently, leading to increased CPU usage.
We understand that this patch is not ready to be merged upstream in its current form. However, if there's interest, we're willing to invest additional time to rework it into a cleaner solution — one that supports both regular and low-bandwidth networks. For example, we could introduce a TransportConfig option to enable reduced pacer capacity where appropriate.
Would the Quinn project be interested in supporting a pacer compatible with slow network conditions?