Skip to content

Commit 7278e97

Browse files
committed
added BBRv3 algorithm, including MaxFilter from Kathleen Nichols
1 parent db28244 commit 7278e97

File tree

5 files changed

+1814
-1
lines changed

5 files changed

+1814
-1
lines changed

perf/src/lib.rs

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

207208
impl CongestionAlgorithm {
208209
pub fn build(self) -> Arc<dyn ControllerFactory + Send + Sync + 'static> {
209210
match self {
210211
Self::Cubic => Arc::new(congestion::CubicConfig::default()),
212+
Self::Bbr3 => Arc::new(congestion::Bbr3Config::default()),
211213
Self::Bbr => Arc::new(congestion::BbrConfig::default()),
212214
Self::NewReno => Arc::new(congestion::NewRenoConfig::default()),
213215
}

quinn-proto/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ slab = { workspace = true }
5757
thiserror = { workspace = true }
5858
tinyvec = { workspace = true, features = ["alloc"] }
5959
tracing = { workspace = true }
60+
rand_pcg = "0.9"
6061

6162
# Feature flags & dependencies for wasm
6263
# wasm-bindgen is assumed for a wasm*-*-unknown target
@@ -69,7 +70,6 @@ web-time = { workspace = true }
6970
[dev-dependencies]
7071
assert_matches = { workspace = true }
7172
hex-literal = { workspace = true }
72-
rand_pcg = "0.9"
7373
rcgen = { workspace = true }
7474
tracing-subscriber = { workspace = true }
7575
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: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
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+
///
28+
#[derive(Copy, Clone, Debug)]
29+
pub(super) struct MaxFilter {
30+
window: u64,
31+
// sample on index 0 has the maximum value followed in descending order
32+
// by samples on index 1 and then 2
33+
samples: [MaxSample; MAX_FILTER_LEN],
34+
}
35+
36+
impl MaxFilter {
37+
pub(super) fn new(window: u64) -> Self {
38+
Self {
39+
window,
40+
samples: [Default::default(); MAX_FILTER_LEN],
41+
}
42+
}
43+
pub(super) fn get_max(&self) -> u64 {
44+
self.samples[0].value.unwrap_or(0)
45+
}
46+
47+
pub(super) fn update_max(&mut self, current_round: u64, measurement: u64) {
48+
let sample = MaxSample {
49+
round: current_round,
50+
value: Some(measurement),
51+
};
52+
53+
if self.samples[0].value.is_none() /* uninitialised */
54+
|| /* found new max? */ sample.value >= self.samples[0].value
55+
|| /* nothing left in window? */ sample.round - self.samples[2].round > self.window
56+
{
57+
self.samples.fill(sample); /* forget earlier samples */
58+
return;
59+
}
60+
61+
if sample.value >= self.samples[1].value {
62+
self.samples[1] = sample;
63+
self.samples[2] = sample;
64+
} else if sample.value >= self.samples[2].value {
65+
self.samples[2] = sample;
66+
}
67+
68+
self.subwin_update(sample);
69+
}
70+
71+
/* As time advances, update the 1st, 2nd, and 3rd choices. */
72+
fn subwin_update(&mut self, sample: MaxSample) {
73+
let dt = sample.round - self.samples[0].round;
74+
if dt > self.window {
75+
/*
76+
* Passed entire window without a new sample so make 2nd
77+
* choice the new sample & 3rd choice the new 2nd choice.
78+
* we may have to iterate this since our 2nd choice
79+
* may also be outside the window (we checked on entry
80+
* that the third choice was in the window).
81+
*/
82+
self.samples[0] = self.samples[1];
83+
self.samples[1] = self.samples[2];
84+
self.samples[2] = sample;
85+
if sample.round - self.samples[0].round > self.window {
86+
self.samples[0] = self.samples[1];
87+
self.samples[1] = self.samples[2];
88+
self.samples[2] = sample;
89+
}
90+
} else if self.samples[1].round == self.samples[0].round && dt > self.window / 4 {
91+
/*
92+
* We've passed a quarter of the window without a new sample
93+
* so take a 2nd choice from the 2nd quarter of the window.
94+
*/
95+
self.samples[2] = sample;
96+
self.samples[1] = sample;
97+
} else if self.samples[2].round == self.samples[1].round && dt > self.window / 2 {
98+
/*
99+
* We've passed half the window without finding a new sample
100+
* so take a 3rd choice from the last half of the window
101+
*/
102+
self.samples[2] = sample;
103+
}
104+
}
105+
}
106+
107+
impl Default for MaxFilter {
108+
fn default() -> Self {
109+
Self {
110+
window: 10,
111+
samples: [Default::default(); MAX_FILTER_LEN],
112+
}
113+
}
114+
}
115+
116+
#[derive(Debug, Copy, Clone, Default)]
117+
struct MaxSample {
118+
/// `round` count, not a timestamp as per <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-04.html#section-5.5.1>
119+
/// 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>
120+
round: u64,
121+
value: Option<u64>,
122+
}
123+
124+
#[cfg(test)]
125+
mod test {
126+
use super::*;
127+
128+
#[test]
129+
fn test() {
130+
let round = 25;
131+
let mut max_filter = MaxFilter::default();
132+
max_filter.update_max(round + 1, 100);
133+
assert_eq!(100, max_filter.get_max());
134+
max_filter.update_max(round + 3, 120);
135+
assert_eq!(120, max_filter.get_max());
136+
max_filter.update_max(round + 5, 160);
137+
assert_eq!(160, max_filter.get_max());
138+
max_filter.update_max(round + 7, 100);
139+
assert_eq!(160, max_filter.get_max());
140+
max_filter.update_max(round + 10, 100);
141+
assert_eq!(160, max_filter.get_max());
142+
max_filter.update_max(round + 14, 100);
143+
assert_eq!(160, max_filter.get_max());
144+
max_filter.update_max(round + 16, 100);
145+
assert_eq!(100, max_filter.get_max());
146+
max_filter.update_max(round + 18, 130);
147+
assert_eq!(130, max_filter.get_max());
148+
}
149+
}

0 commit comments

Comments
 (0)