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}