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