Coverage Report

Created: 2026-05-30 06:23

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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 (cc->num_loss_episodes == 1 && quicly_cc_rapid_start_is_enabled(&cc->rapid_start)) {
120
0
            quicly_cc_rapid_start_on_recovery(&cc->rapid_start, &cc->cwnd, 0, bytes);
121
0
            goto ClampMinAndUpdateMetrics;
122
0
        }
123
0
        return;
124
0
    }
125
126
0
    cc->recovery_end = next_pn;
127
0
    ++cc->num_loss_episodes;
128
129
    /* end of slow start */
130
0
    if (cc->cwnd_exiting_slow_start == 0) {
131
0
        assert(cc->ssthresh == UINT32_MAX);
132
        /* jumpstart: if detected loss during the validating phase, advance to validating phase */
133
0
        quicly_cc_jumpstart_on_first_loss(cc, lost_pn, quicly_cc_rapid_start_is_enabled(&cc->rapid_start));
134
        /* save values */
135
0
        cc->cwnd_exiting_slow_start = cc->cwnd;
136
0
        cc->exit_slow_start_at = now;
137
0
    }
138
139
0
    { /* Calculate increase rate based on CWND before reduction. When rapid start is on but the loss is observed while jump start is
140
       * in action, CWND is not adjusted in the code above, therefore jumpstart.bytes_acked is adopted here. */
141
0
        uint32_t bdp = cc->cwnd;
142
0
        if (cc->num_loss_episodes == 1 && quicly_cc_rapid_start_is_enabled(&cc->rapid_start)) {
143
0
            if (quicly_cc_is_jumpstart_ack(cc, lost_pn)) {
144
0
                bdp = cc->jumpstart.bytes_acked;
145
0
            } else {
146
0
                bdp = cc->cwnd / 3;
147
0
            }
148
0
            if (bdp < cc->cwnd_initial)
149
0
                bdp = cc->cwnd_initial;
150
0
        }
151
0
        cc->state.pico.bytes_per_mtu_increase = calc_bytes_per_mtu_increase(bdp, loss->rtt.smoothed, max_udp_payload_size);
152
0
    }
153
154
    /* Reduce congestion window. At the end of Slow Start, 0.5x is used, because the 1 RTT delay in ACK causes the sender to
155
     * overshoot by 2x (note: after 0.5x reduction, CWND is still as large as BDP+QUEUE, so further reduction is preferable).
156
     *
157
     * In rapid start, upon the first loss we set CWND to 0.7x (QUICLY_RENO_BETA), then reduce proportionally to the bytes deemed
158
     * lost during recovery, with a lower bound of 1/3 * beta.
159
     * Rationale: at a small loss, reducing by beta mirrors CA's single signal behavior. With up to ~67% loss (typical for 3x
160
     * growth under tail-drop), CWND upon loss detection is 3 * (BDP + Q); therefore clamping to 1/3 * beta reproduces the CA
161
     * target. For loss >67% (i.e., beyond queue overflow), we keep the lower bound to avoid over-shrinking. */
162
0
    if (cc->ssthresh == UINT32_MAX) {
163
0
        if (quicly_cc_rapid_start_is_enabled(&cc->rapid_start)) {
164
0
            uint32_t base = cc->cwnd_initial;
165
0
            if (base < cc->jumpstart.bytes_acked)
166
0
                base = cc->jumpstart.bytes_acked;
167
0
            quicly_cc_rapid_start_on_first_lost(&cc->rapid_start, &cc->cwnd, base * 0.5);
168
0
        } else {
169
0
            cc->cwnd *= 0.5;
170
0
        }
171
0
    } else {
172
0
        cc->cwnd *= QUICLY_RENO_BETA;
173
0
    }
174
175
0
ClampMinAndUpdateMetrics:
176
    /* After CWND has been reduced, adjust if it is below permitted minimum and update metrics. */
177
0
    if (cc->cwnd < QUICLY_MIN_CWND * max_udp_payload_size)
178
0
        cc->cwnd = QUICLY_MIN_CWND * max_udp_payload_size;
179
0
    cc->ssthresh = cc->cwnd;
180
181
0
    if (cc->cwnd_minimum > cc->cwnd)
182
0
        cc->cwnd_minimum = cc->cwnd;
183
0
}
184
185
static void pico_on_persistent_congestion(quicly_cc_t *cc, const quicly_loss_t *loss, int64_t now)
186
0
{
187
    /* TODO */
188
0
}
189
190
static void pico_on_sent(quicly_cc_t *cc, const quicly_loss_t *loss, uint32_t bytes, int64_t now)
191
0
{
192
    /* Unused */
193
0
}
194
195
static void pico_init_pico_state(quicly_cc_t *cc, uint32_t stash)
196
0
{
197
0
    cc->state.pico.stash = stash;
198
0
    cc->state.pico.bytes_per_mtu_increase = cc->cwnd * QUICLY_RENO_BETA; /* use Reno, for simplicity */
199
0
}
200
201
static void pico_reset(quicly_cc_t *cc, uint32_t initcwnd)
202
0
{
203
0
    *cc = (quicly_cc_t){
204
0
        .type = &quicly_cc_type_pico,
205
0
        .cwnd = initcwnd,
206
0
        .cwnd_initial = initcwnd,
207
0
        .cwnd_maximum = initcwnd,
208
0
        .cwnd_minimum = UINT32_MAX,
209
0
        .exit_slow_start_at = INT64_MAX,
210
0
        .ssthresh = UINT32_MAX,
211
0
    };
212
0
    pico_init_pico_state(cc, 0);
213
214
0
    quicly_cc_jumpstart_reset(cc);
215
0
}
216
217
static int pico_on_switch(quicly_cc_t *cc)
218
0
{
219
0
    if (cc->type == &quicly_cc_type_pico) {
220
0
        return 1; /* nothing to do */
221
0
    } else if (cc->type == &quicly_cc_type_reno) {
222
0
        cc->type = &quicly_cc_type_pico;
223
0
        pico_init_pico_state(cc, cc->state.reno.stash);
224
0
        return 1;
225
0
    } else if (cc->type == &quicly_cc_type_cubic) {
226
        /* When in slow start, state can be reused as-is; otherwise, restart. */
227
0
        if (cc->cwnd_exiting_slow_start == 0) {
228
0
            cc->type = &quicly_cc_type_pico;
229
0
            pico_init_pico_state(cc, 0);
230
0
        } else {
231
0
            pico_reset(cc, cc->cwnd_initial);
232
0
        }
233
0
        return 1;
234
0
    }
235
236
0
    return 0;
237
0
}
238
239
static void pico_enable_rapid_start(quicly_cc_t *cc, int64_t now)
240
0
{
241
0
    quicly_cc_init_rapid_start(&cc->rapid_start, now);
242
0
}
243
244
static void pico_init(quicly_init_cc_t *self, quicly_cc_t *cc, uint32_t initcwnd, int64_t now)
245
0
{
246
0
    pico_reset(cc, initcwnd);
247
0
}
248
249
quicly_cc_type_t quicly_cc_type_pico = {"pico",         &quicly_cc_pico_init,          pico_on_acked,
250
                                        pico_on_lost,   pico_on_persistent_congestion, pico_on_sent,
251
                                        pico_on_switch, quicly_cc_jumpstart_enter,     pico_enable_rapid_start};
252
quicly_init_cc_t quicly_cc_pico_init = {pico_init};