1 // Copyright (C) 2020, 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 27 //! HyStart++ 28 //! 29 //! This implementation is based on the following I-D: 30 //! 31 //! <https://datatracker.ietf.org/doc/html/draft-ietf-tcpm-hystartplusplus-04> 32 33 use std::cmp; 34 use std::time::Duration; 35 use std::time::Instant; 36 37 use crate::packet; 38 use crate::recovery; 39 40 /// Constants from I-D. 41 const MIN_RTT_THRESH: Duration = Duration::from_millis(4); 42 43 const MAX_RTT_THRESH: Duration = Duration::from_millis(16); 44 45 pub const N_RTT_SAMPLE: usize = 8; 46 47 pub const CSS_GROWTH_DIVISOR: usize = 4; 48 49 pub const CSS_ROUNDS: usize = 5; 50 51 #[derive(Default)] 52 pub struct Hystart { 53 enabled: bool, 54 55 window_end: Option<u64>, 56 57 last_round_min_rtt: Duration, 58 59 current_round_min_rtt: Duration, 60 61 css_baseline_min_rtt: Duration, 62 63 rtt_sample_count: usize, 64 65 css_start_time: Option<Instant>, 66 67 css_round_count: usize, 68 } 69 70 impl std::fmt::Debug for Hystart { fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result71 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 72 write!(f, "window_end={:?} ", self.window_end)?; 73 write!(f, "last_round_min_rtt={:?} ", self.last_round_min_rtt)?; 74 write!(f, "current_round_min_rtt={:?} ", self.current_round_min_rtt)?; 75 write!(f, "css_baseline_min_rtt={:?} ", self.css_baseline_min_rtt)?; 76 write!(f, "rtt_sample_count={:?} ", self.rtt_sample_count)?; 77 write!(f, "css_start_time={:?} ", self.css_start_time)?; 78 write!(f, "css_round_count={:?}", self.css_round_count)?; 79 80 Ok(()) 81 } 82 } 83 84 impl Hystart { new(enabled: bool) -> Self85 pub fn new(enabled: bool) -> Self { 86 Self { 87 enabled, 88 89 last_round_min_rtt: Duration::MAX, 90 91 current_round_min_rtt: Duration::MAX, 92 93 css_baseline_min_rtt: Duration::MAX, 94 95 ..Default::default() 96 } 97 } 98 reset(&mut self)99 pub fn reset(&mut self) { 100 *self = Self::new(self.enabled); 101 } 102 enabled(&self) -> bool103 pub fn enabled(&self) -> bool { 104 self.enabled 105 } 106 css_start_time(&self) -> Option<Instant>107 pub fn css_start_time(&self) -> Option<Instant> { 108 self.css_start_time 109 } 110 in_css(&self, epoch: packet::Epoch) -> bool111 pub fn in_css(&self, epoch: packet::Epoch) -> bool { 112 self.enabled && 113 epoch == packet::Epoch::Application && 114 self.css_start_time().is_some() 115 } 116 start_round(&mut self, pkt_num: u64)117 pub fn start_round(&mut self, pkt_num: u64) { 118 if self.window_end.is_none() { 119 self.window_end = Some(pkt_num); 120 121 self.last_round_min_rtt = self.current_round_min_rtt; 122 123 self.current_round_min_rtt = Duration::MAX; 124 125 self.rtt_sample_count = 0; 126 } 127 } 128 129 // On receiving ACK. Returns true if need to enter Congestion Avoidance. on_packet_acked( &mut self, epoch: packet::Epoch, packet: &recovery::Acked, rtt: Duration, now: Instant, ) -> bool130 pub fn on_packet_acked( 131 &mut self, epoch: packet::Epoch, packet: &recovery::Acked, rtt: Duration, 132 now: Instant, 133 ) -> bool { 134 if !(self.enabled && epoch == packet::Epoch::Application) { 135 return false; 136 } 137 138 self.current_round_min_rtt = cmp::min(self.current_round_min_rtt, rtt); 139 140 self.rtt_sample_count += 1; 141 142 // Slow Start. 143 if self.css_start_time().is_none() { 144 if self.rtt_sample_count >= N_RTT_SAMPLE && 145 self.current_round_min_rtt != Duration::MAX && 146 self.last_round_min_rtt != Duration::MAX 147 { 148 // clamp(min_rtt_thresh, last_round_min_rtt/8, 149 // max_rtt_thresh) 150 let rtt_thresh = 151 cmp::max(self.last_round_min_rtt / 8, MIN_RTT_THRESH); 152 let rtt_thresh = cmp::min(rtt_thresh, MAX_RTT_THRESH); 153 154 // Check if we can exit to CSS. 155 if self.current_round_min_rtt >= 156 self.last_round_min_rtt.saturating_add(rtt_thresh) 157 { 158 self.css_baseline_min_rtt = self.current_round_min_rtt; 159 self.css_start_time = Some(now); 160 } 161 } 162 } else { 163 // Conservative Slow Start. 164 if self.rtt_sample_count >= N_RTT_SAMPLE { 165 self.rtt_sample_count = 0; 166 167 if self.current_round_min_rtt < self.css_baseline_min_rtt { 168 self.css_baseline_min_rtt = Duration::MAX; 169 170 // Back to Slow Start. 171 self.css_start_time = None; 172 self.css_round_count = 0; 173 } 174 } 175 } 176 177 // Check if we reached the end of the round. 178 if let Some(end_pkt_num) = self.window_end { 179 if packet.pkt_num >= end_pkt_num { 180 // Start of a new round. 181 self.window_end = None; 182 183 if self.css_start_time().is_some() { 184 self.css_round_count += 1; 185 186 // End of CSS - exit to congestion avoidance. 187 if self.css_round_count >= CSS_ROUNDS { 188 self.css_round_count = 0; 189 return true; 190 } 191 } 192 } 193 } 194 195 false 196 } 197 198 // Return a cwnd increment during CSS (Conservative Slow Start). css_cwnd_inc(&self, pkt_size: usize) -> usize199 pub fn css_cwnd_inc(&self, pkt_size: usize) -> usize { 200 pkt_size / CSS_GROWTH_DIVISOR 201 } 202 203 // Exit HyStart++ when entering congestion avoidance. congestion_event(&mut self)204 pub fn congestion_event(&mut self) { 205 self.window_end = None; 206 self.css_start_time = None; 207 } 208 } 209 210 #[cfg(test)] 211 mod tests { 212 use super::*; 213 214 #[test] start_round()215 fn start_round() { 216 let mut hspp = Hystart::default(); 217 let pkt_num = 100; 218 219 hspp.start_round(pkt_num); 220 221 assert_eq!(hspp.window_end, Some(pkt_num)); 222 assert_eq!(hspp.current_round_min_rtt, Duration::MAX); 223 } 224 225 #[test] css_cwnd_inc()226 fn css_cwnd_inc() { 227 let hspp = Hystart::default(); 228 let datagram_size = 1200; 229 230 let css_cwnd_inc = hspp.css_cwnd_inc(datagram_size); 231 232 assert_eq!(datagram_size / CSS_GROWTH_DIVISOR, css_cwnd_inc); 233 } 234 235 #[test] congestion_event()236 fn congestion_event() { 237 let mut hspp = Hystart::default(); 238 let pkt_num = 100; 239 240 hspp.start_round(pkt_num); 241 242 assert_eq!(hspp.window_end, Some(pkt_num)); 243 244 // When moving into CA mode, window_end should be cleared. 245 hspp.congestion_event(); 246 247 assert_eq!(hspp.window_end, None); 248 } 249 } 250