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