Skip to main content

quiche/recovery/gcongestion/bbr2/rtt_jump_detector/
global_min.rs

1// Copyright (C) 2026, Cloudflare, Inc.
2// All rights reserved.
3//
4// Redistribution and use in source and binary forms, with or without
5// modification, are permitted provided that the following conditions are
6// met:
7//
8//     * Redistributions of source code must retain the above copyright notice,
9//       this list of conditions and the following disclaimer.
10//
11//     * Redistributions in binary form must reproduce the above copyright
12//       notice, this list of conditions and the following disclaimer in the
13//       documentation and/or other materials provided with the distribution.
14//
15// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
16// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
17// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
18// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
19// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
20// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
21// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
22// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
23// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
24// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
27use std::time::Duration;
28use std::time::Instant;
29
30use super::RttJumpUpdate;
31
32// An RTT jump is a sustained RTT increase above the connection-lifetime minimum
33// RTT. The GlobalMin detector treats samples above 3x that baseline as
34// elevated, counts a jump only after repeated elevated samples persist for a
35// baseline-scaled dwell period, and clears the episode when RTT falls back to
36// 3x baseline or below.
37//
38// GlobalMin episode lifecycle:
39//
40//     elevated        sustained elevated       clear
41// Idle -------> Active -----------------> Persistent
42//   ^              |                           |
43//   |              | clear                     | clear
44//   +--------------+---------------------------+
45//
46// Sample trace:
47//
48//           __A__                __P__
49//      __A__|    |       __A__A__|     |__P__
50//     |         |       |                   |
51//  I__|         |___I___|                   |___I
52// I__|
53//
54// I = Idle
55// A = Active
56// P = Persistent
57
58/// Tracks the lifecycle of a global-min RTT jump episode.
59#[derive(Debug, Default, Clone, Copy)]
60enum GlobalMinEpisode {
61    /// No elevation currently observed.
62    #[default]
63    Idle,
64    /// A jump has been observed but not yet sustained long enough to be
65    /// considered persistent.
66    Active {
67        elevated_samples: usize,
68        episode_start_time: Instant,
69    },
70    /// The elevated RTT has been confirmed as a persistent network condition.
71    Persistent,
72}
73
74/// Multiplicative factor over the global-min RTT baseline above which a sample
75/// is classified as an RTT jump.
76const GLOBAL_MIN_JUMP_THRESHOLD: f32 = 3.0;
77
78/// Elevated RTT samples required before an active episode becomes persistent.
79const GLOBAL_MIN_CONFIRM_SAMPLES: usize = 3;
80
81/// Minimum episode duration, expressed as a multiple of the global-min RTT
82/// baseline, required before an active episode becomes persistent.
83const GLOBAL_MIN_CONFIRM_DURATION_MULTIPLIER: u32 = 3;
84
85#[derive(Debug, Default)]
86pub(super) struct GlobalMinDetector {
87    /// Connection-lifetime minimum RTT sample used by the detector.
88    baseline: Option<Duration>,
89    /// Lifecycle state used to distinguish transient RTT spikes from persistent
90    /// increases.
91    episode: GlobalMinEpisode,
92}
93
94impl GlobalMinDetector {
95    /// Global-min RTT jump detector step: connection-lifetime minimum baseline
96    /// with a strict multiplicative jump test and a sample/duration
97    /// confirmation gate.
98    pub(super) fn on_rtt_sample(
99        &mut self, rtt_sample: Duration, event_time: Instant,
100        full_bandwidth_reached: bool,
101    ) -> RttJumpUpdate {
102        if !full_bandwidth_reached {
103            self.update_baseline(rtt_sample);
104            return RttJumpUpdate::None;
105        }
106
107        let Some(baseline) = self.baseline else {
108            self.update_baseline(rtt_sample);
109            return RttJumpUpdate::None;
110        };
111
112        let is_jump = rtt_sample > baseline.mul_f32(GLOBAL_MIN_JUMP_THRESHOLD);
113        let is_clear = !is_jump;
114        let mut update = RttJumpUpdate::None;
115
116        match self.episode {
117            GlobalMinEpisode::Idle =>
118                if is_jump {
119                    self.episode = GlobalMinEpisode::Active {
120                        elevated_samples: 1,
121                        episode_start_time: event_time,
122                    };
123                },
124
125            GlobalMinEpisode::Active {
126                elevated_samples,
127                episode_start_time,
128            } =>
129                if is_clear {
130                    self.episode = GlobalMinEpisode::Idle;
131                } else {
132                    let elevated_samples = elevated_samples + 1;
133                    let dwell = baseline * GLOBAL_MIN_CONFIRM_DURATION_MULTIPLIER;
134                    let samples_ok =
135                        elevated_samples >= GLOBAL_MIN_CONFIRM_SAMPLES;
136                    let time_ok = event_time
137                        .saturating_duration_since(episode_start_time) >=
138                        dwell;
139
140                    if samples_ok && time_ok {
141                        update = RttJumpUpdate::PersistentConfirmed {
142                            episode_start_time,
143                        };
144                        self.episode = GlobalMinEpisode::Persistent;
145                    } else {
146                        self.episode = GlobalMinEpisode::Active {
147                            elevated_samples,
148                            episode_start_time,
149                        };
150                    }
151                },
152
153            GlobalMinEpisode::Persistent =>
154                if is_clear {
155                    self.episode = GlobalMinEpisode::Idle;
156                },
157        }
158
159        self.update_baseline(rtt_sample);
160        update
161    }
162
163    fn update_baseline(&mut self, rtt_sample: Duration) {
164        self.baseline =
165            Some(self.baseline.map_or(rtt_sample, |min| min.min(rtt_sample)));
166    }
167
168    #[cfg(test)]
169    pub(super) fn is_rtt_jump_active(&self) -> bool {
170        !matches!(self.episode, GlobalMinEpisode::Idle)
171    }
172
173    #[cfg(test)]
174    pub(super) fn is_rtt_jump_persistent(&self) -> bool {
175        matches!(self.episode, GlobalMinEpisode::Persistent)
176    }
177}
178
179#[cfg(test)]
180#[path = "global_min_tests.rs"]
181mod tests;