/src/h2o/deps/quicly/lib/cc-pico.c
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2021 Fastly, Kazuho Oku |
3 | | * |
4 | | * Permission is hereby granted, free of charge, to any person obtaining a copy |
5 | | * of this software and associated documentation files (the "Software"), to |
6 | | * deal in the Software without restriction, including without limitation the |
7 | | * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or |
8 | | * sell copies of the Software, and to permit persons to whom the Software is |
9 | | * furnished to do so, subject to the following conditions: |
10 | | * |
11 | | * The above copyright notice and this permission notice shall be included in |
12 | | * all copies or substantial portions of the Software. |
13 | | * |
14 | | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
15 | | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
16 | | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
17 | | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
18 | | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |
19 | | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS |
20 | | * IN THE SOFTWARE. |
21 | | */ |
22 | | #include <math.h> |
23 | | #include "quicly/pacer.h" |
24 | | #include "quicly/cc.h" |
25 | | #include "quicly.h" |
26 | | |
27 | | /** |
28 | | * Calculates the increase ratio to be used in congestion avoidance phase. |
29 | | */ |
30 | | static uint32_t calc_bytes_per_mtu_increase(uint32_t cwnd, uint32_t rtt, uint32_t mtu) |
31 | 0 | { |
32 | | /* Reno: CWND size after reduction */ |
33 | 0 | uint32_t reno = cwnd * QUICLY_RENO_BETA; |
34 | | |
35 | | /* Cubic: Cubic reaches original CWND (i.e., Wmax) in K seconds, therefore: |
36 | | * amount_to_increase = 0.3 * Wmax |
37 | | * amount_to_be_acked = K * Wmax / RTT_at_Wmax |
38 | | * where |
39 | | * K = (0.3 / 0.4 * Wmax / MTU)^(1/3) |
40 | | * |
41 | | * Hence: |
42 | | * bytes_per_mtu_increase = amount_to_be_acked / amount_to_increase * MTU |
43 | | * = (K * Wmax / RTT_at_Wmax) / (0.3 * Wmax) * MTU |
44 | | * = K * MTU / (0.3 * RTT_at_Wmax) |
45 | | * |
46 | | * In addition, we have to adjust the value to take fast convergence into account. On a path with stable capacity, 50% of |
47 | | * congestion events adjust Wmax to 0.85x of before calculating K. If that happens, the modified K (K') is: |
48 | | * |
49 | | * K' = (0.3 / 0.4 * 0.85 * Wmax / MTU)^(1/3) = 0.85^(1/3) * K |
50 | | * |
51 | | * where K' represents the time to reach 0.85 * Wmax. As the cubic curve is point symmetric at the point where this curve |
52 | | * reaches 0.85 * Wmax, it would take 2 * K' seconds to reach Wmax. |
53 | | * |
54 | | * Therefore, by amortizing the two modes, the congestion period of Cubic with fast convergence is calculated as: |
55 | | * |
56 | | * bytes_per_mtu_increase = ((1 + 0.85^(1/3) * 2) / 2) * K * MTU / (0.3 * RTT_at_Wmax) |
57 | | */ |
58 | 0 | uint32_t cubic = 1.447 / 0.3 * 1000 * cbrt(0.3 / 0.4 * cwnd / mtu) / rtt * mtu; |
59 | |
|
60 | 0 | return reno < cubic ? reno : cubic; |
61 | 0 | } |
62 | | |
63 | | /* TODO: Avoid increase if sender was application limited. */ |
64 | | static void pico_on_acked(quicly_cc_t *cc, const quicly_loss_t *loss, uint32_t bytes, uint64_t largest_acked, uint32_t inflight, |
65 | | int cc_limited, uint64_t next_pn, int64_t now, uint32_t max_udp_payload_size) |
66 | 0 | { |
67 | 0 | assert(inflight >= bytes); |
68 | | |
69 | | /* In recovery period: CWND remains the same (but either jumpstart or rapid start may handle it differently). */ |
70 | 0 | if (largest_acked < cc->recovery_end) { |
71 | 0 | if (quicly_cc_rapid_start_is_enabled(&cc->rapid_start)) { |
72 | 0 | if (cc->num_loss_episodes == 1) |
73 | 0 | quicly_cc_rapid_start_on_recovery(&cc->rapid_start, &cc->cwnd, bytes, 0); |
74 | 0 | } else { |
75 | 0 | quicly_cc_jumpstart_on_acked(cc, 1, bytes, largest_acked, inflight, next_pn); |
76 | 0 | } |
77 | 0 | return; |
78 | 0 | } |
79 | | |
80 | 0 | quicly_cc_jumpstart_on_acked(cc, 0, bytes, largest_acked, inflight, next_pn); |
81 | |
|
82 | 0 | if (!cc_limited) |
83 | 0 | return; |
84 | | |
85 | 0 | cc->state.pico.stash += bytes; |
86 | | |
87 | | /* Calculate the amount of bytes required to be acked for incrementing CWND by one MTU. */ |
88 | 0 | uint32_t bytes_per_mtu_increase; |
89 | 0 | if (cc->cwnd < cc->ssthresh) { |
90 | 0 | if (cc->num_loss_episodes == 0) |
91 | 0 | quicly_cc_rapid_start_update_rtt(&cc->rapid_start, &loss->rtt, now); |
92 | 0 | bytes_per_mtu_increase = |
93 | 0 | quicly_cc_rapid_start_use_3x(&cc->rapid_start, &loss->rtt) ? max_udp_payload_size / 2 : max_udp_payload_size; |
94 | 0 | } else { |
95 | 0 | bytes_per_mtu_increase = cc->state.pico.bytes_per_mtu_increase; |
96 | 0 | } |
97 | | |
98 | | /* Bail out if we do not yet have enough bytes being acked. */ |
99 | 0 | if (cc->state.pico.stash < bytes_per_mtu_increase) |
100 | 0 | return; |
101 | | |
102 | | /* Update CWND, reducing stash relative to the amount we've adjusted the CWND */ |
103 | 0 | uint32_t count = cc->state.pico.stash / bytes_per_mtu_increase; |
104 | 0 | cc->cwnd = quicly_u32_add_saturating(cc->cwnd, count * max_udp_payload_size); |
105 | 0 | cc->state.pico.stash -= count * bytes_per_mtu_increase; |
106 | |
|
107 | 0 | if (cc->cwnd_maximum < cc->cwnd) |
108 | 0 | cc->cwnd_maximum = cc->cwnd; |
109 | 0 | } |
110 | | |
111 | | static void pico_on_lost(quicly_cc_t *cc, const quicly_loss_t *loss, uint32_t bytes, uint64_t lost_pn, uint64_t next_pn, |
112 | | int64_t now, uint32_t max_udp_payload_size) |
113 | 0 | { |
114 | 0 | quicly_cc__update_ecn_episodes(cc, bytes, lost_pn); |
115 | | |
116 | | /* Nothing to do if loss is in recovery window (modulo when exiting rapid start, in which case CWND is further reduced relative |
117 | | * to the number of bytes lost. */ |
118 | 0 | if (lost_pn < cc->recovery_end) { |
119 | 0 | if (bytes != 0 && cc->state.pico.undo.num_packets_lost != 0) |
120 | 0 | ++cc->state.pico.undo.num_packets_lost; |
121 | 0 | if (cc->num_loss_episodes == 1 && quicly_cc_rapid_start_is_enabled(&cc->rapid_start)) { |
122 | 0 | quicly_cc_rapid_start_on_recovery(&cc->rapid_start, &cc->cwnd, 0, bytes); |
123 | 0 | goto ClampMinAndUpdateMetrics; |
124 | 0 | } |
125 | 0 | return; |
126 | 0 | } |
127 | | |
128 | | /* Zero-byte congestion reports are ECN signals, not lost packets. They still enter recovery below, but cannot be undone by |
129 | | * late ACKs because no packet was deemed lost. */ |
130 | 0 | if (bytes != 0) { |
131 | 0 | cc->state.pico.undo.num_packets_lost = 1; |
132 | 0 | cc->state.pico.undo.start_pn = lost_pn; |
133 | 0 | cc->state.pico.undo.cwnd = cc->cwnd; |
134 | 0 | if (quicly_cc_in_jumpstart(cc)) { |
135 | 0 | cc->state.pico.undo.cwnd /= 2; |
136 | 0 | if (cc->state.pico.undo.cwnd < cc->cwnd_initial) |
137 | 0 | cc->state.pico.undo.cwnd = cc->cwnd_initial; |
138 | 0 | } |
139 | 0 | cc->state.pico.undo.ssthresh = cc->ssthresh; |
140 | 0 | cc->state.pico.undo.bytes_per_mtu_increase = cc->state.pico.bytes_per_mtu_increase; |
141 | 0 | } else { |
142 | 0 | cc->state.pico.undo.num_packets_lost = 0; |
143 | 0 | } |
144 | |
|
145 | 0 | cc->recovery_end = next_pn; |
146 | 0 | ++cc->num_loss_episodes; |
147 | | |
148 | | /* end of slow start */ |
149 | 0 | if (cc->cwnd_exiting_slow_start == 0) { |
150 | 0 | assert(cc->ssthresh == UINT32_MAX); |
151 | | /* jumpstart: if detected loss during the validating phase, advance to validating phase */ |
152 | 0 | quicly_cc_jumpstart_on_first_loss(cc, lost_pn, quicly_cc_rapid_start_is_enabled(&cc->rapid_start)); |
153 | | /* save values */ |
154 | 0 | cc->cwnd_exiting_slow_start = cc->cwnd; |
155 | 0 | cc->exit_slow_start_at = now; |
156 | 0 | } |
157 | | |
158 | 0 | { /* Calculate increase rate based on CWND before reduction. When rapid start is on but the loss is observed while jump start is |
159 | | * in action, CWND is not adjusted in the code above, therefore jumpstart.bytes_acked is adopted here. */ |
160 | 0 | uint32_t bdp = cc->cwnd; |
161 | 0 | if (cc->num_loss_episodes == 1 && quicly_cc_rapid_start_is_enabled(&cc->rapid_start)) { |
162 | 0 | if (quicly_cc_is_jumpstart_ack(cc, lost_pn)) { |
163 | 0 | bdp = cc->jumpstart.bytes_acked; |
164 | 0 | } else { |
165 | 0 | bdp = cc->cwnd / 3; |
166 | 0 | } |
167 | 0 | if (bdp < cc->cwnd_initial) |
168 | 0 | bdp = cc->cwnd_initial; |
169 | 0 | } |
170 | 0 | cc->state.pico.bytes_per_mtu_increase = calc_bytes_per_mtu_increase(bdp, loss->rtt.smoothed, max_udp_payload_size); |
171 | 0 | } |
172 | | |
173 | | /* Reduce congestion window. At the end of Slow Start, 0.5x is used, because the 1 RTT delay in ACK causes the sender to |
174 | | * overshoot by 2x (note: after 0.5x reduction, CWND is still as large as BDP+QUEUE, so further reduction is preferable). |
175 | | * |
176 | | * In rapid start, upon the first loss we set CWND to 0.7x (QUICLY_RENO_BETA), then reduce proportionally to the bytes deemed |
177 | | * lost during recovery, with a lower bound of 1/3 * beta. |
178 | | * Rationale: at a small loss, reducing by beta mirrors CA's single signal behavior. With up to ~67% loss (typical for 3x |
179 | | * growth under tail-drop), CWND upon loss detection is 3 * (BDP + Q); therefore clamping to 1/3 * beta reproduces the CA |
180 | | * target. For loss >67% (i.e., beyond queue overflow), we keep the lower bound to avoid over-shrinking. */ |
181 | 0 | if (cc->ssthresh == UINT32_MAX) { |
182 | 0 | if (quicly_cc_rapid_start_is_enabled(&cc->rapid_start)) { |
183 | 0 | uint32_t base = cc->cwnd_initial; |
184 | 0 | if (base < cc->jumpstart.bytes_acked) |
185 | 0 | base = cc->jumpstart.bytes_acked; |
186 | 0 | quicly_cc_rapid_start_on_first_lost(&cc->rapid_start, &cc->cwnd, base * 0.5); |
187 | 0 | } else { |
188 | 0 | cc->cwnd *= 0.5; |
189 | 0 | } |
190 | 0 | } else { |
191 | 0 | cc->cwnd *= QUICLY_RENO_BETA; |
192 | 0 | } |
193 | |
|
194 | 0 | ClampMinAndUpdateMetrics: |
195 | | /* After CWND has been reduced, adjust if it is below permitted minimum and update metrics. */ |
196 | 0 | if (cc->cwnd < QUICLY_MIN_CWND * max_udp_payload_size) |
197 | 0 | cc->cwnd = QUICLY_MIN_CWND * max_udp_payload_size; |
198 | 0 | cc->ssthresh = cc->cwnd; |
199 | |
|
200 | 0 | if (cc->cwnd_minimum > cc->cwnd) |
201 | 0 | cc->cwnd_minimum = cc->cwnd; |
202 | 0 | } |
203 | | |
204 | | static void pico_on_late_ack(quicly_cc_t *cc, uint64_t pn, int64_t now) |
205 | 0 | { |
206 | 0 | if (!(cc->state.pico.undo.start_pn <= pn && pn < cc->recovery_end)) |
207 | 0 | return; |
208 | 0 | if (cc->state.pico.undo.num_packets_lost == 0) |
209 | 0 | return; |
210 | | |
211 | 0 | if (--cc->state.pico.undo.num_packets_lost != 0) |
212 | 0 | return; |
213 | | |
214 | 0 | int was_in_startup = cc->state.pico.undo.ssthresh == UINT32_MAX; |
215 | 0 | cc->cwnd = cc->state.pico.undo.cwnd; |
216 | 0 | cc->ssthresh = cc->state.pico.undo.ssthresh; |
217 | 0 | cc->state.pico.stash = 0; |
218 | 0 | cc->state.pico.bytes_per_mtu_increase = cc->state.pico.undo.bytes_per_mtu_increase; |
219 | 0 | cc->recovery_end = 0; |
220 | 0 | --cc->num_loss_episodes; |
221 | 0 | ++cc->num_loss_episodes_undone; |
222 | 0 | if (was_in_startup) { |
223 | 0 | ++cc->num_loss_episodes_undone_in_startup; |
224 | 0 | cc->cwnd_exiting_slow_start = 0; |
225 | 0 | cc->exit_slow_start_at = INT64_MAX; |
226 | 0 | quicly_cc_jumpstart_reset(cc); |
227 | 0 | cc->rapid_start.newest_rtt_sample_until = 0; |
228 | 0 | } |
229 | 0 | } |
230 | | |
231 | | static void pico_on_persistent_congestion(quicly_cc_t *cc, const quicly_loss_t *loss, int64_t now) |
232 | 0 | { |
233 | | /* TODO */ |
234 | 0 | } |
235 | | |
236 | | static void pico_on_sent(quicly_cc_t *cc, const quicly_loss_t *loss, uint32_t bytes, int64_t now) |
237 | 0 | { |
238 | | /* Unused */ |
239 | 0 | } |
240 | | |
241 | | static void pico_init_pico_state(quicly_cc_t *cc, uint32_t stash) |
242 | 0 | { |
243 | 0 | cc->state.pico.stash = stash; |
244 | 0 | cc->state.pico.bytes_per_mtu_increase = cc->cwnd * QUICLY_RENO_BETA; /* use Reno, for simplicity */ |
245 | 0 | } |
246 | | |
247 | | static void pico_reset(quicly_cc_t *cc, uint32_t initcwnd) |
248 | 0 | { |
249 | 0 | *cc = (quicly_cc_t){ |
250 | 0 | .type = &quicly_cc_type_pico, |
251 | 0 | .cwnd = initcwnd, |
252 | 0 | .cwnd_initial = initcwnd, |
253 | 0 | .cwnd_maximum = initcwnd, |
254 | 0 | .cwnd_minimum = UINT32_MAX, |
255 | 0 | .exit_slow_start_at = INT64_MAX, |
256 | 0 | .ssthresh = UINT32_MAX, |
257 | 0 | }; |
258 | 0 | pico_init_pico_state(cc, 0); |
259 | |
|
260 | 0 | quicly_cc_jumpstart_reset(cc); |
261 | 0 | } |
262 | | |
263 | | static int pico_on_switch(quicly_cc_t *cc) |
264 | 0 | { |
265 | 0 | if (cc->type == &quicly_cc_type_pico) { |
266 | 0 | return 1; /* nothing to do */ |
267 | 0 | } else if (cc->type == &quicly_cc_type_reno) { |
268 | 0 | cc->type = &quicly_cc_type_pico; |
269 | 0 | pico_init_pico_state(cc, cc->state.reno.stash); |
270 | 0 | return 1; |
271 | 0 | } else if (cc->type == &quicly_cc_type_cubic) { |
272 | | /* When in slow start, state can be reused as-is; otherwise, restart. */ |
273 | 0 | if (cc->cwnd_exiting_slow_start == 0) { |
274 | 0 | cc->type = &quicly_cc_type_pico; |
275 | 0 | pico_init_pico_state(cc, 0); |
276 | 0 | } else { |
277 | 0 | pico_reset(cc, cc->cwnd_initial); |
278 | 0 | } |
279 | 0 | return 1; |
280 | 0 | } |
281 | | |
282 | 0 | return 0; |
283 | 0 | } |
284 | | |
285 | | static void pico_enable_rapid_start(quicly_cc_t *cc, int64_t now) |
286 | 0 | { |
287 | 0 | quicly_cc_init_rapid_start(&cc->rapid_start, now); |
288 | 0 | } |
289 | | |
290 | | static void pico_init(quicly_init_cc_t *self, quicly_cc_t *cc, uint32_t initcwnd, int64_t now) |
291 | 0 | { |
292 | 0 | pico_reset(cc, initcwnd); |
293 | 0 | } |
294 | | |
295 | | quicly_cc_type_t quicly_cc_type_pico = {"pico", |
296 | | &quicly_cc_pico_init, |
297 | | pico_on_acked, |
298 | | pico_on_lost, |
299 | | pico_on_persistent_congestion, |
300 | | pico_on_sent, |
301 | | pico_on_switch, |
302 | | pico_on_late_ack, |
303 | | quicly_cc_jumpstart_enter, |
304 | | pico_enable_rapid_start}; |
305 | | quicly_init_cc_t quicly_cc_pico_init = {pico_init}; |