noq_proto/congestion/bbr3/max_filter.rs
1use std::fmt::Debug;
2
3const 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)]
28pub(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
35impl 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 from 0 monotonically
47 /// `measurement` is what is tracked as the max values over time
48 pub(super) fn update_max(&mut self, current_round: u64, measurement: u64) {
49 let sample = MaxSample {
50 round: current_round,
51 value: Some(measurement),
52 };
53
54 if self.samples[0].value.is_none() // uninitialised
55 || sample.value >= self.samples[0].value // found new max?
56 || sample.round.saturating_sub(self.samples[2].round) > self.window
57 // nothing left in window?
58 {
59 self.samples.fill(sample); // forget earlier samples
60 return;
61 }
62
63 if sample.value >= self.samples[1].value {
64 self.samples[1] = sample;
65 self.samples[2] = sample;
66 } else if sample.value >= self.samples[2].value {
67 self.samples[2] = sample;
68 }
69
70 self.subwin_update(sample);
71 }
72
73 /// As time advances, update the 1st, 2nd, and 3rd choices.
74 fn subwin_update(&mut self, sample: MaxSample) {
75 let dt = sample.round.saturating_sub(self.samples[0].round);
76 if dt > self.window {
77 /*
78 * Passed entire window without a new sample so make 2nd
79 * choice the new sample & 3rd choice the new 2nd choice.
80 * we may have to iterate this since our 2nd choice
81 * may also be outside the window (we checked on entry
82 * that the third choice was in the window).
83 */
84 self.samples[0] = self.samples[1];
85 self.samples[1] = self.samples[2];
86 self.samples[2] = sample;
87 if sample.round.saturating_sub(self.samples[0].round) > self.window {
88 self.samples[0] = self.samples[1];
89 self.samples[1] = self.samples[2];
90 self.samples[2] = sample;
91 }
92 } else if self.samples[1].round == self.samples[0].round && dt > self.window / 4 {
93 /*
94 * We've passed a quarter of the window without a new sample
95 * so take a 2nd choice from the 2nd quarter of the window.
96 */
97 self.samples[2] = sample;
98 self.samples[1] = sample;
99 } else if self.samples[2].round == self.samples[1].round && dt > self.window / 2 {
100 /*
101 * We've passed half the window without finding a new sample
102 * so take a 3rd choice from the last half of the window
103 */
104 self.samples[2] = sample;
105 }
106 }
107}
108
109impl Default for MaxFilter {
110 fn default() -> Self {
111 Self {
112 window: 10,
113 samples: [Default::default(); MAX_FILTER_LEN],
114 }
115 }
116}
117
118#[derive(Debug, Copy, Clone, Default)]
119struct MaxSample {
120 /// `round` count, not a timestamp as per <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.5.1>
121 /// can also be a count of cycle as per <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.5.6>
122 round: u64,
123 value: Option<u64>,
124}
125
126#[cfg(test)]
127mod test {
128 use super::*;
129
130 #[test]
131 fn test() {
132 let round = 25;
133 let mut max_filter = MaxFilter::default();
134 max_filter.update_max(round + 1, 100);
135 assert_eq!(100, max_filter.get_max());
136 max_filter.update_max(round + 3, 120);
137 assert_eq!(120, max_filter.get_max());
138 max_filter.update_max(round + 5, 160);
139 assert_eq!(160, max_filter.get_max());
140 max_filter.update_max(round + 7, 100);
141 assert_eq!(160, max_filter.get_max());
142 max_filter.update_max(round + 10, 100);
143 assert_eq!(160, max_filter.get_max());
144 max_filter.update_max(round + 14, 100);
145 assert_eq!(160, max_filter.get_max());
146 max_filter.update_max(round + 16, 100);
147 assert_eq!(100, max_filter.get_max());
148 max_filter.update_max(round + 18, 130);
149 assert_eq!(130, max_filter.get_max());
150 }
151}