1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

// Gain factor for ECN CE mark ratio samples
// Value from https://github.com/google/bbr/blob/1a45fd4faf30229a3d3116de7bfe9d2f933d3562/net/ipv4/tcp_bbr2.c#L2290
const ECN_ALPHA_GAIN: f64 = 1.0 / 16.0;

// The maximum tolerated ratio of packets containing ECN CE markings
// Value from https://github.com/google/bbr/blob/1a45fd4faf30229a3d3116de7bfe9d2f933d3562/net/ipv4/tcp_bbr2.c#L2306
const ECN_THRESH: f64 = 0.5;

// On ECN CE markings, cut inflight_lo to (1 - ECN_FACTOR * ecn_alpha)
// Value from https://github.com/google/bbr/blob/1a45fd4faf30229a3d3116de7bfe9d2f933d3562/net/ipv4/tcp_bbr2.c#L2301
pub(super) const ECN_FACTOR: f64 = 0.33;

#[derive(Clone, Debug)]
pub(crate) struct State {
    /// The amount of explicit congestion experienced packets in the current round trip
    ce_count_in_round: u64,
    /// The amount of bytes delivered in the current round trip
    round_start_delivered_bytes: u64,
    /// Weighted average ratio of ECN CE marked packets with `ECN_ALPHA_GAIN` applied
    alpha: f64,
    /// True if the ECN CE count over the current round trip was too high
    ce_too_high: bool,
}

impl Default for State {
    fn default() -> Self {
        Self {
            ce_count_in_round: 0,
            round_start_delivered_bytes: 0,
            alpha: 1.0,
            ce_too_high: false,
        }
    }
}

impl State {
    /// Called on each new BBR round
    #[inline]
    pub(super) fn on_round_start(&mut self, delivered_bytes: u64, max_datagram_size: u16) {
        let delivered_bytes_in_round = delivered_bytes - self.round_start_delivered_bytes;
        // update alpha
        if delivered_bytes_in_round > 0 {
            let ce_ratio = ce_ratio(
                self.ce_count_in_round,
                delivered_bytes_in_round,
                max_datagram_size,
            );
            self.alpha = calculate_alpha(self.alpha, ce_ratio);
            self.ce_too_high = is_ce_too_high(ce_ratio);
        }

        self.round_start_delivered_bytes = delivered_bytes;
        self.ce_count_in_round = 0;
    }

    /// Called each time explicit congestion is recorded
    #[inline]
    pub(super) fn on_explicit_congestion(&mut self, ce_count: u64) {
        self.ce_count_in_round += ce_count;
    }

    /// Returns true if the ECN CE ratio over the latest round was too high
    #[inline]
    pub(super) fn is_ce_too_high_in_round(&self) -> bool {
        self.ce_too_high
    }

    /// Returns the ECN alpha value
    #[inline]
    pub(super) fn alpha(&self) -> f64 {
        self.alpha
    }
}

/// Calculate the ratio of ECN CE marked bytes to overall delivered bytes
#[inline]
pub(super) fn ce_ratio(ecn_ce_count: u64, delivered_bytes: u64, max_datagram_size: u16) -> f64 {
    // Estimate the number of bytes experiencing explicit congestion by multiplying
    // the ecn_ce_count by max_datagram_size
    let ecn_ce_bytes = ecn_ce_count.saturating_mul(max_datagram_size as u64) as f64;
    ecn_ce_bytes / delivered_bytes as f64
}

/// True if the given ECN `ce_ratio` exceeds the BBR ECN threshold
#[inline]
pub(super) fn is_ce_too_high(ce_ratio: f64) -> bool {
    ce_ratio > ECN_THRESH
}

/// Calculates the new ECN alpha value
///
/// Based on `bbr2_update_ecn_alpha` from the Linux TCP BBRv2 implementation
/// See https://github.com/google/bbr/blob/1a45fd4faf30229a3d3116de7bfe9d2f933d3562/net/ipv4/tcp_bbr2.c#L1392
#[inline]
fn calculate_alpha(alpha: f64, ce_ratio: f64) -> f64 {
    ((1.0 - ECN_ALPHA_GAIN) * alpha + ECN_ALPHA_GAIN * ce_ratio).min(1.0)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{assert_delta, path::MINIMUM_MAX_DATAGRAM_SIZE, recovery::bbr::ecn};

    #[test]
    fn on_round_start() {
        let mut state = ecn::State::default();
        assert_delta!(1.0, state.alpha(), 0.0001);

        let delivered_bytes = 1000;
        state.on_round_start(delivered_bytes, MINIMUM_MAX_DATAGRAM_SIZE);

        // No ECN CE yet and alpha is currently at the initial value of 1,
        // so alpha is just 1 - alpha gain
        assert_delta!(1.0 - ECN_ALPHA_GAIN, state.alpha(), 0.0001);
        assert!(!state.is_ce_too_high_in_round());
        assert_eq!(delivered_bytes, state.round_start_delivered_bytes);

        state.on_explicit_congestion(10);
        assert_eq!(10, state.ce_count_in_round);

        let alpha = state.alpha();
        let prev_delivered = delivered_bytes;
        // 20 packets delivered, 10 of which were ECN CE marked
        let delivered_bytes = prev_delivered + 20 * MINIMUM_MAX_DATAGRAM_SIZE as u64;

        state.on_round_start(delivered_bytes, MINIMUM_MAX_DATAGRAM_SIZE);

        assert_delta!(
            (1.0 - ECN_ALPHA_GAIN) * alpha + ECN_ALPHA_GAIN * 0.5,
            state.alpha(),
            0.0001
        );
        // ECN ce count is not above the 50% `ECN_THRESH`
        assert!(!state.is_ce_too_high_in_round());
        assert_eq!(delivered_bytes, state.round_start_delivered_bytes);
        // ce count is reset
        assert_eq!(0, state.ce_count_in_round);

        state.on_explicit_congestion(11);
        assert_eq!(11, state.ce_count_in_round);

        let alpha = state.alpha();
        let prev_delivered = delivered_bytes;
        let delivered_bytes = prev_delivered + 20 * MINIMUM_MAX_DATAGRAM_SIZE as u64;

        // 20 packets delivered, 11 of which were ECN CE marked
        state.on_round_start(delivered_bytes, MINIMUM_MAX_DATAGRAM_SIZE);

        assert_delta!(
            (1.0 - ECN_ALPHA_GAIN) * alpha + ECN_ALPHA_GAIN * 11.0 / 20.0,
            state.alpha(),
            0.0001
        );
        // ECN ce count is above the 50% `ECN_THRESH`
        assert!(state.is_ce_too_high_in_round());
        assert_eq!(delivered_bytes, state.round_start_delivered_bytes);
        assert_eq!(0, state.ce_count_in_round);
    }
}