Skip to content

Commit ab7ddff

Browse files
committed
added BBRv3 algorithm, including MaxFilter from Kathleen Nichols
1 parent fe5da8a commit ab7ddff

5 files changed

Lines changed: 1818 additions & 2 deletions

File tree

perf/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ pub fn parse_byte_size(s: &str) -> Result<u64, ParseIntError> {
201201
pub enum CongestionAlgorithm {
202202
Cubic,
203203
Bbr,
204+
Bbr3,
204205
NewReno,
205206
}
206207

@@ -209,6 +210,7 @@ impl CongestionAlgorithm {
209210
match self {
210211
Self::Cubic => Arc::new(congestion::CubicConfig::default()),
211212
Self::Bbr => Arc::new(congestion::BbrConfig::default()),
213+
Self::Bbr3 => Arc::new(congestion::Bbr3Config::default()),
212214
Self::NewReno => Arc::new(congestion::NewRenoConfig::default()),
213215
}
214216
}

quinn-proto/Cargo.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ slab = { workspace = true }
5757
thiserror = { workspace = true }
5858
tinyvec = { workspace = true, features = ["alloc"] }
5959
tracing = { workspace = true }
60-
60+
rand_pcg = "0.9"
6161
# Feature flags & dependencies for wasm
6262
# wasm-bindgen is assumed for a wasm*-*-unknown target
6363
[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies]
@@ -69,7 +69,6 @@ web-time = { workspace = true }
6969
[dev-dependencies]
7070
assert_matches = { workspace = true }
7171
hex-literal = { workspace = true }
72-
rand_pcg = "0.9"
7372
rcgen = { workspace = true }
7473
tracing-subscriber = { workspace = true }
7574
wasm-bindgen-test = { workspace = true }

quinn-proto/src/congestion.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ use std::any::Any;
66
use std::sync::Arc;
77

88
mod bbr;
9+
mod bbr3;
910
mod cubic;
1011
mod new_reno;
1112

1213
pub use bbr::{Bbr, BbrConfig};
14+
pub use bbr3::{Bbr3, Bbr3Config};
1315
pub use cubic::{Cubic, CubicConfig};
1416
pub use new_reno::{NewReno, NewRenoConfig};
1517

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
use std::fmt::Debug;
2+
3+
const MAX_FILTER_LEN: usize = 3;
4+
5+
/// Based on Linux kernel code released here:
6+
/// <https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=f672258391b42a5c7cc2732c9c063e56a85c8dbe>
7+
///
8+
/// Kathleen Nichols' algorithm for tracking the maximum
9+
/// value of a data stream over some fixed time interval. (E.g.,
10+
/// the maximum Bandwidth achieved over the past 3 rounds.) It uses constant
11+
/// space and constant time per update yet almost always delivers
12+
/// the same maximum as an implementation that has to keep all the
13+
/// data in the window.
14+
///
15+
/// The algorithm keeps track of the best, 2nd best & 3rd highest max
16+
/// values, maintaining an invariant that the measurement time of
17+
/// the n'th best >= n-1'th best. It also makes sure that the three
18+
/// values are widely separated in the time window since that bounds
19+
/// the worst case error when that data is monotonically increasing
20+
/// over the window.
21+
///
22+
/// Upon getting a new max, we can forget everything earlier because
23+
/// it has no value - the new max is >= everything else in the window
24+
/// by definition, and it samples the most recent one. So we restart fresh on
25+
/// every new max and overwrites 2nd & 3rd choices. The same property
26+
/// holds for 2nd & 3rd best.
27+
#[derive(Copy, Clone, Debug)]
28+
pub(super) struct MaxFilter {
29+
window: u64,
30+
// sample on index 0 has the maximum value followed in descending order
31+
// by samples on index 1 and then 2
32+
samples: [MaxSample; MAX_FILTER_LEN],
33+
}
34+
35+
impl MaxFilter {
36+
pub(super) fn new(window: u64) -> Self {
37+
Self {
38+
window,
39+
samples: [Default::default(); MAX_FILTER_LEN],
40+
}
41+
}
42+
pub(super) fn get_max(&self) -> u64 {
43+
self.samples[0].value.unwrap_or(0)
44+
}
45+
46+
/// `current_round` represents a sequence number counting upwards, it can eventually reset to 0
47+
/// and continue counting upwards.
48+
/// `measurement` is what is tracked as the max values over time
49+
pub(super) fn update_max(&mut self, current_round: u64, measurement: u64) {
50+
let sample = MaxSample {
51+
round: current_round,
52+
value: Some(measurement),
53+
};
54+
55+
if self.samples[0].value.is_none() // uninitialised
56+
|| sample.round == 0 // wrapping around
57+
|| sample.value >= self.samples[0].value // found new max?
58+
|| sample.round.saturating_sub(self.samples[2].round) > self.window
59+
// nothing left in window?
60+
{
61+
self.samples.fill(sample); // forget earlier samples
62+
return;
63+
}
64+
65+
if sample.value >= self.samples[1].value {
66+
self.samples[1] = sample;
67+
self.samples[2] = sample;
68+
} else if sample.value >= self.samples[2].value {
69+
self.samples[2] = sample;
70+
}
71+
72+
self.subwin_update(sample);
73+
}
74+
75+
/// As time advances, update the 1st, 2nd, and 3rd choices.
76+
fn subwin_update(&mut self, sample: MaxSample) {
77+
let dt = sample.round.saturating_sub(self.samples[0].round);
78+
if dt > self.window {
79+
/*
80+
* Passed entire window without a new sample so make 2nd
81+
* choice the new sample & 3rd choice the new 2nd choice.
82+
* we may have to iterate this since our 2nd choice
83+
* may also be outside the window (we checked on entry
84+
* that the third choice was in the window).
85+
*/
86+
self.samples[0] = self.samples[1];
87+
self.samples[1] = self.samples[2];
88+
self.samples[2] = sample;
89+
if sample.round.saturating_sub(self.samples[0].round) > self.window {
90+
self.samples[0] = self.samples[1];
91+
self.samples[1] = self.samples[2];
92+
self.samples[2] = sample;
93+
}
94+
} else if self.samples[1].round == self.samples[0].round && dt > self.window / 4 {
95+
/*
96+
* We've passed a quarter of the window without a new sample
97+
* so take a 2nd choice from the 2nd quarter of the window.
98+
*/
99+
self.samples[2] = sample;
100+
self.samples[1] = sample;
101+
} else if self.samples[2].round == self.samples[1].round && dt > self.window / 2 {
102+
/*
103+
* We've passed half the window without finding a new sample
104+
* so take a 3rd choice from the last half of the window
105+
*/
106+
self.samples[2] = sample;
107+
}
108+
}
109+
}
110+
111+
impl Default for MaxFilter {
112+
fn default() -> Self {
113+
Self {
114+
window: 10,
115+
samples: [Default::default(); MAX_FILTER_LEN],
116+
}
117+
}
118+
}
119+
120+
#[derive(Debug, Copy, Clone, Default)]
121+
struct MaxSample {
122+
/// `round` count, not a timestamp as per <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-04.html#section-5.5.1>
123+
/// can also be a count of cycle as per <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-04.html#section-5.5.6>
124+
round: u64,
125+
value: Option<u64>,
126+
}
127+
128+
#[cfg(test)]
129+
mod test {
130+
use super::*;
131+
132+
#[test]
133+
fn test() {
134+
let round = 25;
135+
let mut max_filter = MaxFilter::default();
136+
max_filter.update_max(round + 1, 100);
137+
assert_eq!(100, max_filter.get_max());
138+
max_filter.update_max(round + 3, 120);
139+
assert_eq!(120, max_filter.get_max());
140+
max_filter.update_max(round + 5, 160);
141+
assert_eq!(160, max_filter.get_max());
142+
max_filter.update_max(round + 7, 100);
143+
assert_eq!(160, max_filter.get_max());
144+
max_filter.update_max(round + 10, 100);
145+
assert_eq!(160, max_filter.get_max());
146+
max_filter.update_max(round + 14, 100);
147+
assert_eq!(160, max_filter.get_max());
148+
max_filter.update_max(round + 16, 100);
149+
assert_eq!(100, max_filter.get_max());
150+
max_filter.update_max(round + 18, 130);
151+
assert_eq!(130, max_filter.get_max());
152+
max_filter.update_max(0, 90);
153+
assert_eq!(90, max_filter.get_max());
154+
max_filter.update_max(1, 80);
155+
assert_eq!(90, max_filter.get_max());
156+
max_filter.update_max(2, 100);
157+
assert_eq!(100, max_filter.get_max());
158+
}
159+
}

0 commit comments

Comments
 (0)