Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion include/quicly.h
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ QUICLY_CALLBACK_TYPE(quicly_error_t, generate_resumption_token, quicly_conn_t *c
* called to initialize a congestion controller for a new connection.
* should in turn call one of the quicly_cc_*_init functions from cc.h with customized parameters.
*/
QUICLY_CALLBACK_TYPE(void, init_cc, quicly_cc_t *cc, uint32_t initcwnd, int64_t now);
QUICLY_CALLBACK_TYPE(void, init_cc, quicly_cc_t *cc, uint32_t initcwnd, int normalize_mtu, int64_t now);
/**
* reference counting.
* delta must be either 1 or -1.
Expand Down Expand Up @@ -384,6 +384,11 @@ struct st_quicly_context_t {
* expand client hello so that it does not fit into one datagram
*/
unsigned expand_client_hello : 1;
/**
* if CC growth should be normalized to the reference packet size rather than the path's maximum UDP payload size; enabled in
* the default contexts
*/
unsigned normalize_cc_mtu : 1;
/**
*
*/
Expand Down
9 changes: 9 additions & 0 deletions include/quicly/cc.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ extern "C" {
#include "quicly/loss.h"

#define QUICLY_MIN_CWND 2
/**
* Reference maximum UDP payload size used for packet-size-neutral congestion control. This is the midpoint of the maximum UDP
* payload sizes of IPv4 and IPv6 packets carried at an IP MTU of 1500 bytes.
*/
#define QUICLY_CC_REFERENCE_MTU 1462
/**
* Default beta used when a packet is lost; 0.7 is used to achieve fairness with Cubic.
*/
Expand Down Expand Up @@ -178,6 +183,10 @@ typedef struct st_quicly_cc_t {
* If the most recent loss episode was signalled by ECN only (i.e., no packet loss).
*/
unsigned episode_by_ecn : 1;
/**
* Whether growth is normalized to QUICLY_CC_REFERENCE_MTU.
*/
unsigned normalize_mtu : 1;
/**
* State information specific to the congestion controller implementation.
*/
Expand Down
9 changes: 5 additions & 4 deletions lib/cc-cubic.c
Original file line number Diff line number Diff line change
Expand Up @@ -175,10 +175,11 @@ static void cubic_on_sent(quicly_cc_t *cc, const quicly_loss_t *loss, uint32_t b
cc->state.cubic.last_sent_time = now;
}

static void cubic_reset(quicly_cc_t *cc, uint32_t initcwnd)
static void cubic_reset(quicly_cc_t *cc, uint32_t initcwnd, int normalize_mtu)
{
memset(cc, 0, sizeof(quicly_cc_t));
cc->type = &quicly_cc_type_cubic_legacy;
cc->normalize_mtu = normalize_mtu;
cc->cwnd = cc->cwnd_initial = cc->cwnd_maximum = initcwnd;
cc->ssthresh = cc->cwnd_minimum = UINT32_MAX;
cc->exit_slow_start_at = INT64_MAX;
Expand All @@ -197,17 +198,17 @@ static int cubic_on_switch(quicly_cc_t *cc)
if (cc->cwnd_exiting_slow_start == 0) {
cc->type = &quicly_cc_type_cubic_legacy;
} else {
cubic_reset(cc, cc->cwnd_initial);
cubic_reset(cc, cc->cwnd_initial, cc->normalize_mtu);
}
return 1;
}

return 0;
}

static void cubic_init(quicly_init_cc_t *self, quicly_cc_t *cc, uint32_t initcwnd, int64_t now)
static void cubic_init(quicly_init_cc_t *self, quicly_cc_t *cc, uint32_t initcwnd, int normalize_mtu, int64_t now)
{
cubic_reset(cc, initcwnd);
cubic_reset(cc, initcwnd, normalize_mtu);
}

quicly_cc_type_t quicly_cc_type_cubic_legacy = {"cubic-legacy",
Expand Down
79 changes: 46 additions & 33 deletions lib/cc-pico.c
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,10 @@ static uint32_t pico_bytes_per_mtu_increase(uint32_t cwnd, double rtt, uint32_t
*
* Using the `bytes_sent` function, the congestion controller calculates bytes needed to be acked before incrementing the CWND
* (`bytes_sent(cwnd + mtu) - bytes_sent(cwnd)`) and drives congestion avoidance, the same way as in the case of Reno.
* When packet-size normalization is enabled, the reference MTU replaces MSS in the Cubic coefficient and in the denominator of
* the Reno sums, while `w` remains spaced by the actual MTU. In particular, the normalized pre-Wmax Reno inverse is:
*
* bytes_reno(w) = (w - Wepoch) * (w + Wepoch - actual_MTU) / (2 * alpha * reference_MTU)
*
* In addition to being simple, the approach has two positive side-effects:
*
Expand All @@ -240,15 +244,16 @@ static uint32_t pico_bytes_per_mtu_increase(uint32_t cwnd, double rtt, uint32_t
* wall clock to the ACK clock.
*/
static double cuback_cwnd_to_bytes_sent(double w, double w_epoch, double w_max, double cwnd_prior, double bandwidth, double k,
uint32_t mtu, double friendly_alpha)
uint32_t actual_mtu, uint32_t reference_mtu, double friendly_alpha)
{
double bytes_cubic = (k + fast_cbrt((w - w_max) / (QUICLY_CUBIC_C * mtu))) * bandwidth;
double bytes_cubic = (k + fast_cbrt((w - w_max) / (QUICLY_CUBIC_C * reference_mtu))) * bandwidth;
/* RFC 9438, Section 4.3 switches alpha to one after the Reno-friendly estimate reaches the congestion window prior to
* reduction. Bandwidth cancels when converting the Reno time to bytes sent. */
double w_friendly = w < cwnd_prior ? w : cwnd_prior;
double bytes_reno = (w_friendly - w_epoch) * (w_friendly + w_epoch - mtu) / (2 * friendly_alpha * mtu);
double bytes_reno =
(w_friendly - w_epoch) * (w_friendly + w_epoch - actual_mtu) / (2 * friendly_alpha * reference_mtu);
if (w > cwnd_prior)
bytes_reno += (w - cwnd_prior) * (w + cwnd_prior - mtu) / (2 * mtu);
bytes_reno += (w - cwnd_prior) * (w + cwnd_prior - actual_mtu) / (2 * reference_mtu);
double bytes = bytes_cubic < bytes_reno ? bytes_cubic : bytes_reno;
return bytes > 0 ? bytes : 0;
}
Expand All @@ -257,26 +262,26 @@ static double cuback_cwnd_to_bytes_sent(double w, double w_epoch, double w_max,
* Calculates the number of bytes that have to be acked for incrementing CWND by one MTU, when Cuback is used.
*/
static uint32_t cuback_bytes_per_mtu_increase(const struct st_quicly_cc_cuback_t *state, uint32_t cwnd, uint32_t cwnd_epoch,
uint32_t mtu)
uint32_t actual_mtu, uint32_t reference_mtu)
{
/* Fast convergence: derive Wmax as the midpoint of cwnd_prior and cwnd_epoch rather than hard-coding it to 0.85 * cwnd_prior.
* Otherwise, with ABE using QUICLY_BETA_ECN (0.85), Wmax equals cwnd_epoch and K becomes zero, causing the CC to skip the
* concave region and start at the plateau. */
double w_max = state->fast_convergence ? cubic_fast_convergence_w_max(state->cwnd_prior, cwnd_epoch) : state->cwnd_prior;
/* Seconds taken by the cubic curve to climb from `cwnd_epoch` back to W_max. Derived from the gap between the two rather than
* from W_max alone, as fast convergence and the varying reduction ratios move them apart. */
double k = fast_cbrt((w_max - cwnd_epoch) / (QUICLY_CUBIC_C * mtu));
double k = fast_cbrt((w_max - cwnd_epoch) / (QUICLY_CUBIC_C * reference_mtu));

double bytes0 = cuback_cwnd_to_bytes_sent(cwnd, cwnd_epoch, w_max, state->cwnd_prior, state->bandwidth, k, mtu,
cubic_friendly_alpha[state->by_ecn]);
double bytes1 = cuback_cwnd_to_bytes_sent((double)cwnd + mtu, cwnd_epoch, w_max, state->cwnd_prior, state->bandwidth, k, mtu,
cubic_friendly_alpha[state->by_ecn]);
double bytes0 = cuback_cwnd_to_bytes_sent(cwnd, cwnd_epoch, w_max, state->cwnd_prior, state->bandwidth, k, actual_mtu,
reference_mtu, cubic_friendly_alpha[state->by_ecn]);
double bytes1 = cuback_cwnd_to_bytes_sent((double)cwnd + actual_mtu, cwnd_epoch, w_max, state->cwnd_prior, state->bandwidth, k,
actual_mtu, reference_mtu, cubic_friendly_alpha[state->by_ecn]);
double bytes = bytes1 - bytes0;

/* Past W_max the curve grows without bound, therefore the increase is capped at 50% of CWND per RTT as RFC 9438 does. Below
* W_max the cap is not applied, it being the climb back to a window that the path had already sustained. */
if (cwnd > w_max && bytes < (double)mtu * 2)
bytes = (double)mtu * 2;
if (cwnd > w_max && bytes < (double)actual_mtu * 2)
bytes = (double)actual_mtu * 2;

return bytes < 1 ? 1 : bytes > UINT32_MAX ? UINT32_MAX : (uint32_t)bytes;
}
Expand Down Expand Up @@ -323,26 +328,26 @@ static uint32_t cubic_quantized_w_est(const struct st_quicly_cc_cubic_t *state,
}

static uint32_t cubic_update_w_est(struct st_quicly_cc_cubic_t *state, uint32_t cwnd, uint32_t cwnd_epoch, uint32_t bytes,
uint32_t mtu)
uint32_t actual_mtu, uint32_t reference_mtu)
{
double alpha = state->w_est >= state->cwnd_prior ? 1 : cubic_friendly_alpha[state->by_ecn];
state->w_est += alpha * bytes / cwnd * mtu;
return cubic_quantized_w_est(state, cwnd_epoch, mtu);
state->w_est += alpha * bytes / cwnd * reference_mtu;
return cubic_quantized_w_est(state, cwnd_epoch, actual_mtu);
}

static void cubic_on_acked(struct st_quicly_cc_cubic_t *state, uint32_t *cwnd, uint32_t cwnd_epoch, uint32_t bytes, int cc_limited,
uint32_t rtt, int64_t now, uint32_t mtu)
uint32_t rtt, int64_t now, uint32_t actual_mtu, uint32_t reference_mtu)
{
uint32_t w_est = cubic_quantized_w_est(state, cwnd_epoch, mtu);
uint32_t w_est = cubic_quantized_w_est(state, cwnd_epoch, actual_mtu);
if (cc_limited)
w_est = cubic_update_w_est(state, *cwnd, cwnd_epoch, bytes, mtu);
w_est = cubic_update_w_est(state, *cwnd, cwnd_epoch, bytes, actual_mtu, reference_mtu);

/* W_est is ACK-clocked even while the CUBIC clock is stopped, but CWND does not grow while currently app-limited. */
if (!cubic_start_epoch(state, *cwnd, cwnd_epoch, mtu) || bytes == 0)
if (!cubic_start_epoch(state, *cwnd, cwnd_epoch, reference_mtu) || bytes == 0)
return;

double t_sec = (now - state->epoch_start) / 1000.;
double w_cubic = cubic_calc_w(state, cwnd_epoch, t_sec, mtu);
double w_cubic = cubic_calc_w(state, cwnd_epoch, t_sec, reference_mtu);

if (w_cubic < w_est) {
/* RFC 9438, Section 4.3; Reno-Friendly Region. */
Expand All @@ -351,7 +356,7 @@ static void cubic_on_acked(struct st_quicly_cc_cubic_t *state, uint32_t *cwnd, u
/* RFC 9438, Sections 4.4 and 4.5; Concave and Convex Regions, but the amount added to CWND is `(target - cwnd) / cwnd`
* per MTU acked rather than per ACK. The formula smoothes CWND by using W(t + RTT) as the target to be reached 1 RTT after.
* But for such a design to work, the adjustment needs to be made for every MTU acked. */
double target = cubic_calc_w(state, cwnd_epoch, t_sec + rtt / 1000., mtu);
double target = cubic_calc_w(state, cwnd_epoch, t_sec + rtt / 1000., reference_mtu);
if (target < *cwnd)
target = *cwnd;
if (target > 1.5 * *cwnd)
Expand Down Expand Up @@ -385,14 +390,19 @@ static uint32_t calc_bytes_per_mtu_increase(quicly_cc_t *cc, const quicly_loss_t
* while in congestion avoidance. Use Reno until then. */
return cc->cwnd;
}
return cuback_bytes_per_mtu_increase(&cc->state.pico.cuback, cc->cwnd, cc->ssthresh, max_udp_payload_size);
uint32_t reference_mtu = cc->normalize_mtu ? QUICLY_CC_REFERENCE_MTU : max_udp_payload_size;
return cuback_bytes_per_mtu_increase(&cc->state.pico.cuback, cc->cwnd, cc->ssthresh, max_udp_payload_size, reference_mtu);
} else if (cc->type == &quicly_cc_type_cubic) {
assert(!"Cubic congestion avoidance bypasses the byte-counter path");
abort();
} else if (cc->type == &quicly_cc_type_pico) {
return cc->state.pico.bytes_per_mtu_increase;
} else {
assert(cc->type == &quicly_cc_type_reno);
if (cc->normalize_mtu) {
uint64_t bytes = (uint64_t)cc->cwnd * max_udp_payload_size / QUICLY_CC_REFERENCE_MTU;
return bytes < 1 ? 1 : bytes > UINT32_MAX ? UINT32_MAX : (uint32_t)bytes;
}
return cc->cwnd;
}
}
Expand Down Expand Up @@ -430,7 +440,9 @@ static void pico_on_acked(quicly_cc_t *cc, const quicly_loss_t *loss, uint32_t b
state->epoch_start = state->cc_limited ? now : 0;
state->k = NAN;
}
cubic_on_acked(state, &cc->cwnd, cc->ssthresh, bytes, cc_limited, loss->rtt.smoothed, now, max_udp_payload_size);
uint32_t reference_mtu = cc->normalize_mtu ? QUICLY_CC_REFERENCE_MTU : max_udp_payload_size;
cubic_on_acked(state, &cc->cwnd, cc->ssthresh, bytes, cc_limited, loss->rtt.smoothed, now, max_udp_payload_size,
reference_mtu);
goto Cleanup;
}

Expand Down Expand Up @@ -681,10 +693,11 @@ static void pico_init_pico_state(quicly_cc_t *cc)
}
}

static void pico_reset(quicly_cc_t *cc, quicly_cc_type_t *type, uint32_t initcwnd)
static void pico_reset(quicly_cc_t *cc, quicly_cc_type_t *type, uint32_t initcwnd, int normalize_mtu)
{
*cc = (quicly_cc_t){
.type = type,
.normalize_mtu = normalize_mtu,
.cwnd = initcwnd,
.cwnd_initial = initcwnd,
.cwnd_maximum = initcwnd,
Expand Down Expand Up @@ -715,7 +728,7 @@ static int switch_to(quicly_cc_t *cc, quicly_cc_type_t *type)
cc->type = type;
pico_init_pico_state(cc);
} else {
pico_reset(cc, type, cc->cwnd_initial);
pico_reset(cc, type, cc->cwnd_initial, cc->normalize_mtu);
}
return 1;
}
Expand Down Expand Up @@ -753,24 +766,24 @@ static void cubic_update_cc_limited(quicly_cc_t *cc, int cc_limited, int64_t now
cubic_set_cc_limited(&cc->state.pico.cubic, cc_limited, now);
}

static void pico_init(quicly_init_cc_t *self, quicly_cc_t *cc, uint32_t initcwnd, int64_t now)
static void pico_init(quicly_init_cc_t *self, quicly_cc_t *cc, uint32_t initcwnd, int normalize_mtu, int64_t now)
{
pico_reset(cc, &quicly_cc_type_pico, initcwnd);
pico_reset(cc, &quicly_cc_type_pico, initcwnd, normalize_mtu);
}

static void reno_init(quicly_init_cc_t *self, quicly_cc_t *cc, uint32_t initcwnd, int64_t now)
static void reno_init(quicly_init_cc_t *self, quicly_cc_t *cc, uint32_t initcwnd, int normalize_mtu, int64_t now)
{
pico_reset(cc, &quicly_cc_type_reno, initcwnd);
pico_reset(cc, &quicly_cc_type_reno, initcwnd, normalize_mtu);
}

static void cubic_init(quicly_init_cc_t *self, quicly_cc_t *cc, uint32_t initcwnd, int64_t now)
static void cubic_init(quicly_init_cc_t *self, quicly_cc_t *cc, uint32_t initcwnd, int normalize_mtu, int64_t now)
{
pico_reset(cc, &quicly_cc_type_cubic, initcwnd);
pico_reset(cc, &quicly_cc_type_cubic, initcwnd, normalize_mtu);
}

static void cuback_init(quicly_init_cc_t *self, quicly_cc_t *cc, uint32_t initcwnd, int64_t now)
static void cuback_init(quicly_init_cc_t *self, quicly_cc_t *cc, uint32_t initcwnd, int normalize_mtu, int64_t now)
{
pico_reset(cc, &quicly_cc_type_cuback, initcwnd);
pico_reset(cc, &quicly_cc_type_cuback, initcwnd, normalize_mtu);
}

quicly_cc_type_t quicly_cc_type_pico = {"pico",
Expand Down
2 changes: 2 additions & 0 deletions lib/defaults.c
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
/* profile that employs IETF specified values */
const quicly_context_t quicly_spec_context = {
.initial_egress_max_udp_payload_size = DEFAULT_INITIAL_EGRESS_MAX_UDP_PAYLOAD_SIZE,
.normalize_cc_mtu = 1,
.loss = QUICLY_LOSS_SPEC_CONF,
.transport_params =
{
Expand Down Expand Up @@ -75,6 +76,7 @@ const quicly_context_t quicly_spec_context = {
/* profile with a focus on reducing latency for the HTTP use case */
const quicly_context_t quicly_performant_context = {
.initial_egress_max_udp_payload_size = DEFAULT_INITIAL_EGRESS_MAX_UDP_PAYLOAD_SIZE,
.normalize_cc_mtu = 1,
.loss = QUICLY_LOSS_PERFORMANT_CONF,
.transport_params =
{
Expand Down
6 changes: 4 additions & 2 deletions lib/quicly.c
Original file line number Diff line number Diff line change
Expand Up @@ -2138,7 +2138,8 @@ static quicly_error_t promote_path(quicly_conn_t *conn, size_t path_index)
/* reset CC (FIXME flush sentmap and reset loss recovery) */
conn->egress.cc.type->cc_init->cb(
conn->egress.cc.type->cc_init, &conn->egress.cc,
quicly_cc_calc_initial_cwnd(conn->super.ctx->initcwnd_packets, conn->egress.max_udp_payload_size), conn->stash.now);
quicly_cc_calc_initial_cwnd(conn->super.ctx->initcwnd_packets, conn->egress.max_udp_payload_size),
conn->egress.cc.normalize_mtu, conn->stash.now);
if (conn->super.stats.num_rapid_start != 0 && conn->egress.cc.type->enable_rapid_start != NULL)
conn->egress.cc.type->enable_rapid_start(&conn->egress.cc, conn->stash.now);

Expand Down Expand Up @@ -2850,7 +2851,8 @@ static quicly_conn_t *create_connection(quicly_context_t *ctx, uint32_t protocol
conn->egress.ack_frequency.update_at = INT64_MAX;
conn->egress.send_ack_at = INT64_MAX;
conn->egress.send_probe_at = INT64_MAX;
conn->super.ctx->init_cc->cb(conn->super.ctx->init_cc, &conn->egress.cc, initcwnd, conn->stash.now);
conn->super.ctx->init_cc->cb(conn->super.ctx->init_cc, &conn->egress.cc, initcwnd,
conn->super.ctx->normalize_cc_mtu, conn->stash.now);
if (conn->egress.cc.type->enable_rapid_start != NULL &&
enable_with_ratio255(conn->super.ctx->enable_ratio.rapid_start, conn->super.ctx->tls->random_bytes)) {
conn->egress.cc.type->enable_rapid_start(&conn->egress.cc, conn->stash.now);
Expand Down
4 changes: 4 additions & 0 deletions src/cli.c
Original file line number Diff line number Diff line change
Expand Up @@ -1247,6 +1247,7 @@ static void usage(const char *cmd)
" -M <bytes> max stream data (in bytes; default: 1MB)\n"
" -m <bytes> max data (in bytes; default: 16MB)\n"
" --max-crypto-bytes <N> maximum permitted length of a CRYPTO stream\n"
" --no-normalize-cc-mtu disables packet-size normalization of congestion-control growth\n"
" -N enforce HelloRetryRequest (client-only)\n"
" -n enforce version negotiation (client-only)\n"
" -O suppress output\n"
Expand Down Expand Up @@ -1538,6 +1539,7 @@ int main(int argc, char **argv)
{"jumpstart-default", required_argument, NULL, 0},
{"jumpstart-max", required_argument, NULL, 0},
{"max-crypto-bytes", required_argument, NULL, 0},
{"no-normalize-cc-mtu", no_argument, NULL, 0},
{"rapid-start", no_argument, NULL, 0},
{"sockfd", required_argument, NULL, 0},
{"exit-after-handshake", no_argument, NULL, 0},
Expand Down Expand Up @@ -1572,6 +1574,8 @@ int main(int argc, char **argv)
fprintf(stderr, "failed to parse max-crypto-bytes: %s\n", optarg);
exit(1);
}
} else if (strcmp(longopts[opt_index].name, "no-normalize-cc-mtu") == 0) {
ctx.normalize_cc_mtu = 0;
} else if (strcmp(longopts[opt_index].name, "rapid-start") == 0) {
ctx.enable_ratio.rapid_start = 255;
} else if (strcmp(longopts[opt_index].name, "sockfd") == 0) {
Expand Down
Loading
Loading