Skip to content

Commit c3ab570

Browse files
feat(s2n-quic-core): BBRv2 recovery and full pipe estimator (#1261)
* Move Fraction to common location and add Bandwidth to RateSample * Add new_loss_burst to congestion controller trait * Add BBR congestion control draft RFC * Add Mul implementation for u32 * Fraction * Add Debug to Bandwidth * Add recovery::State * Add full_pipe::Estimator * Add BBR skeleton * Fix compliance * Fix tests * Add tests for Fraction * Add tests for Bandwidth * Add tests for Recovery * Add tests for Full Pipe Estimator * Clippy * Remove unnecessary pub(crate)s * Use fully qualified enum * Use num_rational * Add typo exception
1 parent 07e4902 commit c3ab570

File tree

18 files changed

+4446
-18
lines changed

18 files changed

+4446
-18
lines changed

.github/config/typos.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ check-filename = true
99
[default.extend-words]
1010
# ECN Capable Transport
1111
ect = "ect"
12+
# num_rational uses "numer" as short for "numerator"
13+
numer = "numer"
1214

1315
[files]
1416
extend-exclude = [

quic/s2n-quic-core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ bytes = { version = "1", default-features = false }
2525
hex-literal = "0.3"
2626
# used for event snapshot testing - needs an internal API so we require a minimum version
2727
insta = { version = ">=1.12", optional = true }
28+
num-rational = { version = "0.4", default-features = false }
2829
num-traits = { version = "0.2", default-features = false, features = ["libm"] }
2930
s2n-codec = { version = "=0.1.0", path = "../../common/s2n-codec", default-features = false }
3031
subtle = { version = "2", default-features = false }

quic/s2n-quic-core/src/recovery/bandwidth/estimator.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
use crate::time::Timestamp;
55
use core::{cmp::max, time::Duration};
6+
use num_rational::Ratio;
67

78
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
89
/// Bandwidth-related data tracked for each sent packet
@@ -23,6 +24,37 @@ pub struct PacketInfo {
2324
pub is_app_limited: bool,
2425
}
2526

27+
#[derive(Copy, Clone, Debug, Default, PartialOrd, PartialEq)]
28+
pub struct Bandwidth {
29+
bits_per_second: u64,
30+
}
31+
32+
impl Bandwidth {
33+
pub const ZERO: Bandwidth = Bandwidth { bits_per_second: 0 };
34+
35+
pub fn new(bytes: u64, interval: Duration) -> Self {
36+
const MICRO_BITS_PER_BYTE: u64 = 8 * 1000000;
37+
38+
if interval.is_zero() {
39+
Bandwidth::ZERO
40+
} else {
41+
Self {
42+
bits_per_second: (bytes * MICRO_BITS_PER_BYTE / interval.as_micros() as u64),
43+
}
44+
}
45+
}
46+
}
47+
48+
impl core::ops::Mul<Ratio<u64>> for Bandwidth {
49+
type Output = Bandwidth;
50+
51+
fn mul(self, rhs: Ratio<u64>) -> Self::Output {
52+
Bandwidth {
53+
bits_per_second: (rhs * self.bits_per_second).to_integer(),
54+
}
55+
}
56+
}
57+
2658
#[derive(Clone, Copy, Debug, Default)]
2759
/// A bandwidth delivery rate estimate with associated metadata
2860
pub struct RateSample {
@@ -55,6 +87,11 @@ impl RateSample {
5587
self.prior_lost_bytes = packet_info.lost_bytes;
5688
self.bytes_in_flight = packet_info.bytes_in_flight;
5789
}
90+
91+
/// Gets the delivery rate of this rate sample
92+
pub fn delivery_rate(&self) -> Bandwidth {
93+
Bandwidth::new(self.delivered_bytes, self.interval)
94+
}
5895
}
5996

6097
/// Bandwidth estimator as defined in [Delivery Rate Estimation](https://datatracker.ietf.org/doc/draft-cheng-iccrg-delivery-rate-estimation/)

quic/s2n-quic-core/src/recovery/bandwidth/estimator/tests.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,29 @@
44
use super::*;
55
use crate::time::{Clock, NoopClock};
66

7+
#[test]
8+
fn bandwidth() {
9+
let result = Bandwidth::new(1000, Duration::from_secs(10));
10+
11+
assert_eq!(100 * 8, result.bits_per_second);
12+
}
13+
14+
#[test]
15+
fn bandwidth_zero_interval() {
16+
let result = Bandwidth::new(500, Duration::ZERO);
17+
18+
assert_eq!(Bandwidth::ZERO, result);
19+
}
20+
21+
#[test]
22+
fn bandwidth_mul_ratio() {
23+
let bandwidth = Bandwidth::new(7000, Duration::from_secs(1));
24+
25+
let result = bandwidth * Ratio::new(3, 7);
26+
27+
assert_eq!(result, Bandwidth::new(3000, Duration::from_secs(1)));
28+
}
29+
730
// first_sent_time and delivered_time typically hold values from recently acknowledged packets. However,
831
// when no packet has been sent yet, or there are no packets currently in flight, these values are initialized
932
// with the time when a packet is sent. This test confirms first_sent_time and delivered_time are
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
use crate::{
5+
counter::Counter,
6+
recovery::{bandwidth, bandwidth::Bandwidth, CongestionController, RttEstimator},
7+
time::Timestamp,
8+
};
9+
use num_rational::Ratio;
10+
11+
mod full_pipe;
12+
mod recovery;
13+
14+
//= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#2.8
15+
//# The maximum tolerated per-round-trip packet loss rate when probing for bandwidth (the default is 2%).
16+
const LOSS_THRESH: Ratio<u32> = Ratio::new_raw(1, 50);
17+
18+
/// A congestion controller that implements "Bottleneck Bandwidth and Round-trip propagation time"
19+
/// version 2 (BBRv2) as specified in <https://datatracker.ietf.org/doc/draft-cardwell-iccrg-bbr-congestion-control/>.
20+
///
21+
/// Based in part on the Chromium BBRv2 implementation, see <https://source.chromium.org/chromium/chromium/src/+/main:net/third_party/quiche/src/quic/core/congestion_control/bbr2_sender.cc>
22+
/// and the Linux Kernel TCP BBRv2 implementation, see <https://github.com/google/bbr/blob/v2alpha/net/ipv4/tcp_bbr2.c>
23+
#[derive(Debug, Clone)]
24+
struct BbrCongestionController {
25+
bw_estimator: bandwidth::Estimator,
26+
//= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#2.9.1
27+
//# The windowed maximum recent bandwidth sample - obtained using the BBR delivery rate sampling
28+
//# algorithm [draft-cheng-iccrg-delivery-rate-estimation] - measured during the current or
29+
//# previous bandwidth probing cycle (or during Startup, if the flow is still in that state).
30+
max_bw: Bandwidth,
31+
full_pipe_estimator: full_pipe::Estimator,
32+
//= https://www.rfc-editor.org/rfc/rfc9002#section-B.2
33+
//# The sum of the size in bytes of all sent packets
34+
//# that contain at least one ack-eliciting or PADDING frame and have
35+
//# not been acknowledged or declared lost. The size does not include
36+
//# IP or UDP overhead, but does include the QUIC header and
37+
//# Authenticated Encryption with Associated Data (AEAD) overhead.
38+
//# Packets only containing ACK frames do not count toward
39+
//# bytes_in_flight to ensure congestion control does not impede
40+
//# congestion feedback.
41+
bytes_in_flight: BytesInFlight,
42+
recovery_state: recovery::State,
43+
}
44+
45+
type BytesInFlight = Counter<u32>;
46+
47+
impl CongestionController for BbrCongestionController {
48+
type PacketInfo = bandwidth::PacketInfo;
49+
50+
fn congestion_window(&self) -> u32 {
51+
todo!()
52+
}
53+
54+
fn bytes_in_flight(&self) -> u32 {
55+
*self.bytes_in_flight
56+
}
57+
58+
fn is_congestion_limited(&self) -> bool {
59+
todo!()
60+
}
61+
62+
fn requires_fast_retransmission(&self) -> bool {
63+
self.recovery_state.requires_fast_retransmission()
64+
}
65+
66+
fn on_packet_sent(
67+
&mut self,
68+
time_sent: Timestamp,
69+
sent_bytes: usize,
70+
_rtt_estimator: &RttEstimator,
71+
) -> Self::PacketInfo {
72+
let is_app_limited = false; // TODO: determine if app limited
73+
let packet_info =
74+
self.bw_estimator
75+
.on_packet_sent(*self.bytes_in_flight, is_app_limited, time_sent);
76+
77+
if sent_bytes > 0 {
78+
self.recovery_state.on_packet_sent();
79+
80+
self.bytes_in_flight
81+
.try_add(sent_bytes)
82+
.expect("sent_bytes should not exceed u32::MAX");
83+
}
84+
85+
packet_info
86+
}
87+
88+
fn on_rtt_update(&mut self, _time_sent: Timestamp, _rtt_estimator: &RttEstimator) {
89+
todo!()
90+
}
91+
92+
fn on_ack(
93+
&mut self,
94+
newest_acked_time_sent: Timestamp,
95+
bytes_acknowledged: usize,
96+
newest_acked_packet_info: Self::PacketInfo,
97+
_rtt_estimator: &RttEstimator,
98+
ack_receive_time: Timestamp,
99+
) {
100+
self.bw_estimator.on_ack(
101+
bytes_acknowledged,
102+
newest_acked_time_sent,
103+
newest_acked_packet_info,
104+
ack_receive_time,
105+
);
106+
let round_start = false; // TODO: track rounds
107+
self.recovery_state
108+
.on_ack(round_start, newest_acked_time_sent);
109+
110+
if round_start {
111+
self.full_pipe_estimator.on_round_start(
112+
self.bw_estimator.rate_sample(),
113+
self.max_bw,
114+
self.recovery_state.in_recovery(),
115+
)
116+
}
117+
}
118+
119+
fn on_packet_lost(
120+
&mut self,
121+
lost_bytes: u32,
122+
_packet_info: Self::PacketInfo,
123+
_persistent_congestion: bool,
124+
new_loss_burst: bool,
125+
timestamp: Timestamp,
126+
) {
127+
self.bw_estimator.on_loss(lost_bytes as usize);
128+
self.recovery_state.on_congestion_event(timestamp);
129+
self.full_pipe_estimator.on_packet_lost(new_loss_burst);
130+
}
131+
132+
fn on_congestion_event(&mut self, event_time: Timestamp) {
133+
self.recovery_state.on_congestion_event(event_time);
134+
}
135+
136+
fn on_mtu_update(&mut self, _max_data_size: u16) {
137+
todo!()
138+
}
139+
140+
fn on_packet_discarded(&mut self, _bytes_sent: usize) {
141+
todo!()
142+
}
143+
144+
fn earliest_departure_time(&self) -> Option<Timestamp> {
145+
todo!()
146+
}
147+
}

0 commit comments

Comments
 (0)