diff --git a/include/quicly.h b/include/quicly.h index f3c3e078..ef128f15 100644 --- a/include/quicly.h +++ b/include/quicly.h @@ -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. @@ -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; /** * */ diff --git a/include/quicly/cc.h b/include/quicly/cc.h index a509573d..413399a7 100644 --- a/include/quicly/cc.h +++ b/include/quicly/cc.h @@ -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. */ @@ -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. */ diff --git a/lib/cc-cubic.c b/lib/cc-cubic.c index c5a94c41..a91bc8c8 100644 --- a/lib/cc-cubic.c +++ b/lib/cc-cubic.c @@ -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; @@ -197,7 +198,7 @@ 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; } @@ -205,9 +206,9 @@ static int cubic_on_switch(quicly_cc_t *cc) 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", diff --git a/lib/cc-pico.c b/lib/cc-pico.c index e3976735..5e9cc87a 100644 --- a/lib/cc-pico.c +++ b/lib/cc-pico.c @@ -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: * @@ -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; } @@ -257,7 +262,7 @@ 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 @@ -265,18 +270,18 @@ static uint32_t cuback_bytes_per_mtu_increase(const struct st_quicly_cc_cuback_t 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; } @@ -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. */ @@ -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) @@ -385,7 +390,8 @@ 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(); @@ -393,6 +399,10 @@ static uint32_t calc_bytes_per_mtu_increase(quicly_cc_t *cc, const quicly_loss_t 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; } } @@ -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; } @@ -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, @@ -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; } @@ -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", diff --git a/lib/defaults.c b/lib/defaults.c index 7406775f..ab04aa26 100644 --- a/lib/defaults.c +++ b/lib/defaults.c @@ -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 = { @@ -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 = { diff --git a/lib/quicly.c b/lib/quicly.c index 535424fd..0c108347 100644 --- a/lib/quicly.c +++ b/lib/quicly.c @@ -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); @@ -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); diff --git a/src/cli.c b/src/cli.c index 6a9d1f58..f50a56bc 100644 --- a/src/cli.c +++ b/src/cli.c @@ -1247,6 +1247,7 @@ static void usage(const char *cmd) " -M max stream data (in bytes; default: 1MB)\n" " -m max data (in bytes; default: 16MB)\n" " --max-crypto-bytes 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" @@ -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}, @@ -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) { diff --git a/t/cc.c b/t/cc.c index eac4041a..73cb71a2 100644 --- a/t/cc.c +++ b/t/cc.c @@ -30,7 +30,7 @@ static void test_pico_undo_loss(void) quicly_loss_t loss = {.rtt = {.latest = 100, .smoothed = 100, .minimum = 100, .variance = 0}}; uint32_t mtu = 1200, initcwnd = 10 * mtu; - quicly_cc_pico_init.cb(&quicly_cc_pico_init, &cc, initcwnd, 0); + quicly_cc_pico_init.cb(&quicly_cc_pico_init, &cc, initcwnd, 0, 0); uint32_t bytes_per_mtu_increase = cc.state.pico.bytes_per_mtu_increase; cc.type->cc_on_lost(&cc, &loss, mtu, 10, 20, 1000, mtu); @@ -61,7 +61,7 @@ static void test_pico_undo_multiple_losses(void) quicly_loss_t loss = {.rtt = {.latest = 100, .smoothed = 100, .minimum = 100, .variance = 0}}; uint32_t mtu = 1200, initcwnd = 10 * mtu; - quicly_cc_pico_init.cb(&quicly_cc_pico_init, &cc, initcwnd, 0); + quicly_cc_pico_init.cb(&quicly_cc_pico_init, &cc, initcwnd, 0, 0); cc.type->cc_on_lost(&cc, &loss, mtu, 10, 20, 1000, mtu); uint32_t reduced_cwnd = cc.cwnd; @@ -94,7 +94,7 @@ static void test_pico_undo_rapid_start_loss(void) quicly_loss_t loss = {.rtt = {.latest = 100, .smoothed = 100, .minimum = 100, .variance = 0}}; uint32_t mtu = 1200, initcwnd = 10 * mtu; - quicly_cc_pico_init.cb(&quicly_cc_pico_init, &cc, initcwnd, 0); + quicly_cc_pico_init.cb(&quicly_cc_pico_init, &cc, initcwnd, 0, 0); cc.type->enable_rapid_start(&cc, 900); ok(quicly_cc_rapid_start_is_enabled(&cc.rapid_start)); @@ -118,7 +118,7 @@ static void test_pico_undo_jumpstart_loss(void) quicly_loss_t loss = {.rtt = {.latest = 100, .smoothed = 100, .minimum = 100, .variance = 0}}; uint32_t mtu = 1200, initcwnd = 10 * mtu, jumpcwnd = 24 * mtu; - quicly_cc_pico_init.cb(&quicly_cc_pico_init, &cc, initcwnd, 0); + quicly_cc_pico_init.cb(&quicly_cc_pico_init, &cc, initcwnd, 0, 0); cc.type->cc_jumpstart(&cc, jumpcwnd, 10); ok(quicly_cc_in_jumpstart(&cc)); ok(cc.cwnd == jumpcwnd); @@ -173,7 +173,7 @@ static void test_pico_ecn(void) quicly_loss_t loss = {.rtt = {.latest = 100, .smoothed = 100, .minimum = 100, .variance = 0}}; uint32_t mtu = 1200, initcwnd = 10 * mtu; - quicly_cc_pico_init.cb(&quicly_cc_pico_init, &cc, initcwnd, 0); + quicly_cc_pico_init.cb(&quicly_cc_pico_init, &cc, initcwnd, 0, 0); /* exit slow start by observing a packet loss */ cc.type->cc_on_lost(&cc, &loss, mtu, 10, 20, 1000, mtu); @@ -204,7 +204,7 @@ static void test_pico_ecn_rapid_start(void) quicly_loss_t loss = {.rtt = {.latest = 100, .smoothed = 100, .minimum = 100, .variance = 0}}; uint32_t mtu = 1200, initcwnd = 10 * mtu; - quicly_cc_pico_init.cb(&quicly_cc_pico_init, &cc, initcwnd, 0); + quicly_cc_pico_init.cb(&quicly_cc_pico_init, &cc, initcwnd, 0, 0); cc.type->enable_rapid_start(&cc, 900); /* upon a CE mark, the silence factor derived from QUICLY_BETA_ECN (i.e., 0.95x) is applied */ @@ -233,7 +233,7 @@ static void test_cubic_fast_convergence(void) quicly_loss_t loss = {.rtt = {.latest = 100, .smoothed = 100, .minimum = 100, .variance = 0}}; uint32_t mtu = 1200, initcwnd = 100 * mtu; - quicly_cc_cubic_init.cb(&quicly_cc_cubic_init, &cc, initcwnd, 0); + quicly_cc_cubic_init.cb(&quicly_cc_cubic_init, &cc, initcwnd, 0, 0); cc.type->cc_on_lost(&cc, &loss, mtu, 10, 20, 1000, mtu); ok(!cc.state.pico.cubic.fast_convergence); @@ -260,7 +260,7 @@ static void test_cubic_target_bounds(void) quicly_loss_t loss = {.rtt = {.latest = 100, .smoothed = 100, .minimum = 100, .variance = 0}}; uint32_t mtu = 1200, initcwnd = 10 * mtu; - quicly_cc_cubic_init.cb(&quicly_cc_cubic_init, &cc, initcwnd, 0); + quicly_cc_cubic_init.cb(&quicly_cc_cubic_init, &cc, initcwnd, 0, 0); cc.ssthresh = cc.cwnd; cc.state.pico.cubic.w_est = cc.cwnd; cc.state.pico.cubic.cwnd_prior = cc.cwnd; @@ -269,7 +269,7 @@ static void test_cubic_target_bounds(void) cc.type->cc_on_acked(&cc, &loss, 2 * mtu, 1, cc.cwnd, 1, 2, 1000000, mtu); ok(cc.cwnd == initcwnd + mtu); - quicly_cc_cubic_init.cb(&quicly_cc_cubic_init, &cc, initcwnd, 0); + quicly_cc_cubic_init.cb(&quicly_cc_cubic_init, &cc, initcwnd, 0, 0); cc.ssthresh = cc.cwnd / 2; cc.state.pico.cubic.cwnd_prior = cc.cwnd / 2; cc.state.pico.cubic.w_est = cc.state.pico.cubic.cwnd_prior - 1; @@ -285,14 +285,51 @@ static void test_cubic_w_est(void) struct st_quicly_cc_cubic_t state = {.w_est = 10 * mtu, .cwnd_prior = 10 * mtu}; /* At 10 MTUs, the exposed estimate stays unchanged until 10 MTUs have been acknowledged, then grows by exactly one MTU. */ - ok(cubic_update_w_est(&state, 10 * mtu, 10 * mtu, 10 * mtu - 1, mtu) == 10 * mtu); + ok(cubic_update_w_est(&state, 10 * mtu, 10 * mtu, 10 * mtu - 1, mtu, mtu) == 10 * mtu); ok(10 * mtu < state.w_est && state.w_est < 11 * mtu); - ok(cubic_update_w_est(&state, 10 * mtu, 10 * mtu, 1, mtu) == 11 * mtu); + ok(cubic_update_w_est(&state, 10 * mtu, 10 * mtu, 1, mtu, mtu) == 11 * mtu); ok(state.w_est == 11 * mtu); /* The next increase requires the new 11-MTU window to be acknowledged. */ - ok(cubic_update_w_est(&state, 11 * mtu, 10 * mtu, 11 * mtu, mtu) == 12 * mtu); + ok(cubic_update_w_est(&state, 11 * mtu, 10 * mtu, 11 * mtu, mtu, mtu) == 12 * mtu); ok(state.w_est == 12 * mtu); + + /* With normalization, one window of ACKs advances the estimate by the reference MTU, while the exposed window remains + * quantized in actual-MTU steps. */ + state = (struct st_quicly_cc_cubic_t){.w_est = 10 * mtu, .cwnd_prior = 10 * mtu}; + ok(cubic_update_w_est(&state, 10 * mtu, 10 * mtu, 10 * mtu, mtu, QUICLY_CC_REFERENCE_MTU) == 11 * mtu); + ok(state.w_est == 10 * mtu + QUICLY_CC_REFERENCE_MTU); +} + +static void test_cubic_mtu_normalization(void) +{ + quicly_cc_t cc; + quicly_loss_t loss = {.rtt = {.latest = 0, .smoothed = 0, .minimum = 0, .variance = 0}}; + uint32_t mtu = 1200, initcwnd = 10 * mtu; + + /* In the cubic region, normalization substitutes the reference MTU in W_cubic. */ + quicly_cc_cubic_init.cb(&quicly_cc_cubic_init, &cc, initcwnd, 1, 0); + cc.ssthresh = cc.cwnd; + cc.state.pico.cubic.w_est = cc.cwnd; + cc.state.pico.cubic.cwnd_prior = cc.cwnd; + cc.state.pico.cubic.epoch_start = 1000; + cc.state.pico.cubic.k = 0; + cc.type->cc_on_acked(&cc, &loss, initcwnd, 1, initcwnd, 0, 2, 2000, mtu); + ok(cwnd_is(cc.cwnd, initcwnd + QUICLY_CUBIC_C * QUICLY_CC_REFERENCE_MTU)); + + /* In the Reno-friendly region, growth uses the reference MTU but CWND is still exposed in actual-MTU steps. Five windows of + * ACKs therefore accumulate six 1200-byte steps (floor(5 * 1462 / 1200)). */ + quicly_cc_cubic_init.cb(&quicly_cc_cubic_init, &cc, initcwnd, 1, 0); + cc.ssthresh = cc.cwnd; + cc.state.pico.cubic.w_est = cc.cwnd; + cc.state.pico.cubic.cwnd_prior = cc.cwnd; + cc.state.pico.cubic.epoch_start = 1000; + cc.state.pico.cubic.k = 100; + for (size_t i = 0; i != 5; ++i) { + uint32_t cwnd = cc.cwnd; + cc.type->cc_on_acked(&cc, &loss, cwnd, i + 1, cwnd, 1, i + 2, 1000, mtu); + } + ok(cc.cwnd == initcwnd + 6 * mtu); } static void test_cubic_cc_limited(void) @@ -301,7 +338,7 @@ static void test_cubic_cc_limited(void) quicly_loss_t loss = {.rtt = {.latest = 100, .smoothed = 100, .minimum = 100, .variance = 0}}; uint32_t mtu = 1200, initcwnd = 100 * mtu; - quicly_cc_cubic_init.cb(&quicly_cc_cubic_init, &cc, initcwnd, 0); + quicly_cc_cubic_init.cb(&quicly_cc_cubic_init, &cc, initcwnd, 0, 0); cc.ssthresh = cc.cwnd; cc.state.pico.cubic.cwnd_prior = 50 * mtu; cc.state.pico.cubic.epoch_start = 1000; @@ -349,7 +386,7 @@ static void test_cubic_recovery_epoch(void) uint32_t mtu = 1200, initcwnd = 10 * mtu; /* RFC 9438 starts the epoch when congestion avoidance begins, not when congestion is detected. */ - quicly_cc_cubic_init.cb(&quicly_cc_cubic_init, &cc, initcwnd, 0); + quicly_cc_cubic_init.cb(&quicly_cc_cubic_init, &cc, initcwnd, 0, 0); cc.type->cc_on_lost(&cc, &loss, mtu, 10, 20, 1000, mtu); ok(cc.state.pico.cubic.w_est == 0); ok(cc.state.pico.cubic.epoch_start == 0); @@ -360,7 +397,7 @@ static void test_cubic_recovery_epoch(void) ok(cc.state.pico.cubic.epoch_start == 1200); /* If recovery exits while app-limited, initialize W_est but defer the wall-clock epoch until sending resumes. */ - quicly_cc_cubic_init.cb(&quicly_cc_cubic_init, &cc, initcwnd, 0); + quicly_cc_cubic_init.cb(&quicly_cc_cubic_init, &cc, initcwnd, 0, 0); cc.type->cc_on_lost(&cc, &loss, mtu, 10, 20, 1000, mtu); cc.type->cc_update_cc_limited(&cc, 0, 1050); cc.type->cc_on_acked(&cc, &loss, 0, 20, 0, 0, 21, 1200, mtu); @@ -376,7 +413,7 @@ static void test_cubic_rapid_start_epoch(void) quicly_loss_t loss = {.rtt = {.latest = 100, .smoothed = 100, .minimum = 100, .variance = 0}}; uint32_t mtu = 1200, initcwnd = 10 * mtu; - quicly_cc_cubic_init.cb(&quicly_cc_cubic_init, &cc, initcwnd, 0); + quicly_cc_cubic_init.cb(&quicly_cc_cubic_init, &cc, initcwnd, 0, 0); cc.type->enable_rapid_start(&cc, 900); cc.type->cc_on_lost(&cc, &loss, mtu, 10, 20, 1000, mtu); ok(cc.state.pico.cubic.cwnd_prior != 0); @@ -424,7 +461,7 @@ static void test_cubic_abe(void) quicly_loss_t loss = {.rtt = {.latest = 100, .smoothed = 100, .minimum = 100, .variance = 0}}; uint32_t mtu = 1200, initcwnd = 100 * mtu; - quicly_cc_cubic_init.cb(&quicly_cc_cubic_init, &cc, initcwnd, 0); + quicly_cc_cubic_init.cb(&quicly_cc_cubic_init, &cc, initcwnd, 0, 0); /* Establish a 50-MTU W_max when leaving ordinary slow start. */ cc.type->cc_on_lost(&cc, &loss, mtu, 10, 20, 1000, mtu); @@ -455,7 +492,7 @@ static void test_cubic_undo_loss(void) quicly_loss_t loss = {.rtt = {.latest = 100, .smoothed = 100, .minimum = 100, .variance = 0}}; uint32_t mtu = 1200, initcwnd = 10 * mtu; - quicly_cc_cubic_init.cb(&quicly_cc_cubic_init, &cc, initcwnd, 0); + quicly_cc_cubic_init.cb(&quicly_cc_cubic_init, &cc, initcwnd, 0, 0); cc.type->cc_on_lost(&cc, &loss, mtu, 10, 20, 1000, mtu); ok(cc.state.pico.cubic.cwnd_prior != 0); @@ -471,7 +508,7 @@ static void test_cubic_legacy_name(void) { quicly_cc_t cc; - quicly_cc_cubic_legacy_init.cb(&quicly_cc_cubic_legacy_init, &cc, 12000, 0); + quicly_cc_cubic_legacy_init.cb(&quicly_cc_cubic_legacy_init, &cc, 12000, 0, 0); ok(cc.type == &quicly_cc_type_cubic_legacy); ok(strcmp(cc.type->name, "cubic-legacy") == 0); } @@ -482,7 +519,7 @@ static void test_pico_ack_countdown(void) quicly_loss_t loss = {.rtt = {.latest = 100, .smoothed = 100, .minimum = 100, .variance = 0}}; uint32_t mtu = 1200, initcwnd = 10 * mtu; - quicly_cc_pico_init.cb(&quicly_cc_pico_init, &cc, initcwnd, 0); + quicly_cc_pico_init.cb(&quicly_cc_pico_init, &cc, initcwnd, 0, 0); cc.type->cc_on_acked(&cc, &loss, mtu - 1, 1, mtu - 1, 1, 2, 100, mtu); ok(cc.cwnd == initcwnd); ok(cc.state.pico.bytes_to_mtu_increase == 1); @@ -492,7 +529,7 @@ static void test_pico_ack_countdown(void) ok(cc.state.pico.bytes_to_mtu_increase == mtu); /* The interval switches to Pico's congestion-avoidance rate when an increase reaches ssthresh. */ - quicly_cc_pico_init.cb(&quicly_cc_pico_init, &cc, initcwnd, 0); + quicly_cc_pico_init.cb(&quicly_cc_pico_init, &cc, initcwnd, 0, 0); cc.ssthresh = initcwnd + mtu; cc.type->cc_on_acked(&cc, &loss, mtu, 1, mtu, 1, 2, 100, mtu); ok(cc.cwnd == cc.ssthresh); @@ -505,7 +542,7 @@ static void test_pico_switch_resets_ack_credit(void) quicly_loss_t loss = {.rtt = {.latest = 100, .smoothed = 100, .minimum = 100, .variance = 0}}; uint32_t mtu = 1200, initcwnd = 10 * mtu; - quicly_cc_reno_init.cb(&quicly_cc_reno_init, &cc, initcwnd, 0); + quicly_cc_reno_init.cb(&quicly_cc_reno_init, &cc, initcwnd, 0, 0); cc.ssthresh = cc.cwnd; cc.type->cc_on_acked(&cc, &loss, initcwnd - 1, 1, initcwnd - 1, 1, 2, 100, mtu); ok(cc.cwnd == initcwnd); @@ -528,7 +565,7 @@ static void test_reno(void) uint32_t mtu = 1200, initcwnd = 100 * mtu; /* Reno grows by one MTU for each current-CWND bytes acknowledged. */ - quicly_cc_reno_init.cb(&quicly_cc_reno_init, &cc, initcwnd, 0); + quicly_cc_reno_init.cb(&quicly_cc_reno_init, &cc, initcwnd, 0, 0); cc.ssthresh = cc.cwnd; cc.type->cc_on_acked(&cc, &loss, initcwnd - 1, 1, initcwnd - 1, 1, 2, 100, mtu); ok(cc.cwnd == initcwnd); @@ -536,8 +573,23 @@ static void test_reno(void) cc.type->cc_on_acked(&cc, &loss, 1, 2, 1, 1, 3, 100, mtu); ok(cc.cwnd == initcwnd + mtu); + /* Packet-size normalization shortens the ACK deficit so that actual-MTU CWND steps amortize to the reference MTU per RTT. */ + quicly_cc_reno_init.cb(&quicly_cc_reno_init, &cc, initcwnd, 1, 0); + cc.ssthresh = cc.cwnd; + uint32_t normalized_deficit = (uint64_t)initcwnd * mtu / QUICLY_CC_REFERENCE_MTU; + cc.type->cc_on_acked(&cc, &loss, normalized_deficit - 1, 1, normalized_deficit - 1, 1, 2, 100, mtu); + ok(cc.cwnd == initcwnd); + ok(cc.state.pico.bytes_to_mtu_increase == 1); + cc.type->cc_on_acked(&cc, &loss, 1, 2, 1, 1, 3, 100, mtu); + ok(cc.cwnd == initcwnd + mtu); + + /* Normalization does not alter slow start. */ + quicly_cc_reno_init.cb(&quicly_cc_reno_init, &cc, initcwnd, 1, 0); + cc.type->cc_on_acked(&cc, &loss, mtu, 1, mtu, 1, 2, 100, mtu); + ok(cc.cwnd == initcwnd + mtu); + /* Startup uses the shared 0.5 reduction; subsequent loss uses the policy beta. */ - quicly_cc_reno_init.cb(&quicly_cc_reno_init, &cc, initcwnd, 0); + quicly_cc_reno_init.cb(&quicly_cc_reno_init, &cc, initcwnd, 0, 0); cc.type->cc_on_lost(&cc, &loss, mtu, 10, 20, 1000, mtu); ok(cc.cwnd == initcwnd / 2); cc.cwnd = 40 * mtu; @@ -548,7 +600,7 @@ static void test_reno(void) ok(cc.state.pico.bytes_to_mtu_increase == cc.cwnd - mtu); /* Reno uses the same beta for ECN and packet loss. */ - quicly_cc_reno_init.cb(&quicly_cc_reno_init, &cc, initcwnd, 0); + quicly_cc_reno_init.cb(&quicly_cc_reno_init, &cc, initcwnd, 0, 0); cc.type->cc_on_lost(&cc, &loss, mtu, 10, 20, 1000, mtu); cc.cwnd = 40 * mtu; cc.type->cc_on_lost(&cc, &loss, 0, 20, 30, 1100, mtu); @@ -562,14 +614,60 @@ static void test_cuback_reno_bytes_per_mtu_increase(void) /* Reno curve, before Wmax: each MTU increase consumes the current CWND divided by the friendly alpha. */ state.bandwidth = 1e12; - ok(cwnd_is(cuback_bytes_per_mtu_increase(&state, cwnd_epoch, cwnd_epoch, mtu), + ok(cwnd_is(cuback_bytes_per_mtu_increase(&state, cwnd_epoch, cwnd_epoch, mtu, mtu), (double)cwnd_epoch / cubic_friendly_alpha[0])); - ok(cwnd_is(cuback_bytes_per_mtu_increase(&state, cwnd_epoch + mtu, cwnd_epoch, mtu), + ok(cwnd_is(cuback_bytes_per_mtu_increase(&state, cwnd_epoch + mtu, cwnd_epoch, mtu, mtu), (double)(cwnd_epoch + mtu) / cubic_friendly_alpha[0])); /* Reno curve, at and above Wmax: alpha is one, so each increase consumes the current CWND. */ - ok(cuback_bytes_per_mtu_increase(&state, w_max, cwnd_epoch, mtu) == w_max); - ok(cuback_bytes_per_mtu_increase(&state, w_max + mtu, cwnd_epoch, mtu) == w_max + mtu); + ok(cuback_bytes_per_mtu_increase(&state, w_max, cwnd_epoch, mtu, mtu) == w_max); + ok(cuback_bytes_per_mtu_increase(&state, w_max + mtu, cwnd_epoch, mtu, mtu) == w_max + mtu); + + /* Normalization scales the ACK thresholds by actual_MTU / reference_MTU without changing the actual-MTU CWND step. */ + ok(cwnd_is(cuback_bytes_per_mtu_increase(&state, cwnd_epoch, cwnd_epoch, mtu, QUICLY_CC_REFERENCE_MTU), + (double)cwnd_epoch / cubic_friendly_alpha[0] * mtu / QUICLY_CC_REFERENCE_MTU)); + ok(cwnd_is(cuback_bytes_per_mtu_increase(&state, w_max, cwnd_epoch, mtu, QUICLY_CC_REFERENCE_MTU), + (double)w_max * mtu / QUICLY_CC_REFERENCE_MTU)); +} + +static void check_cuback_cubic_bytes_per_mtu_increase(uint32_t cwnd_epoch, uint32_t w_max, uint32_t actual_mtu, + uint32_t reference_mtu) +{ + /* A two-second RTT makes the Cubic curve cheaper than the Reno curve throughout the points being tested. */ + struct st_quicly_cc_cuback_t state = {.cwnd_prior = w_max, .bandwidth = w_max / 2.}; + double k = cbrt((double)(w_max - cwnd_epoch) / (QUICLY_CUBIC_C * reference_mtu)); + + /* By point symmetry, the continuous Cubic curve is one eighth of the epoch-to-Wmax gap below Wmax at K / 2, and the same + * distance above Wmax at 3 * K / 2. Cuback exposes only whole-MTU windows, so record the times bracketing those points. */ + double gap = w_max - cwnd_epoch; + double w_half_k = w_max - gap / 8, w_three_halves_k = w_max + gap / 8; + uint32_t before_half_k = (uint32_t)(w_half_k / actual_mtu) * actual_mtu, after_half_k = before_half_k + actual_mtu; + uint32_t before_three_halves_k = (uint32_t)(w_three_halves_k / actual_mtu) * actual_mtu; + uint32_t after_three_halves_k = before_three_halves_k + actual_mtu; + uint64_t bytes = 0, bytes_before_half_k = 0, bytes_after_half_k = 0, bytes_at_w_max = 0, + bytes_before_three_halves_k = 0, bytes_after_three_halves_k = 0; + + for (uint32_t cwnd = cwnd_epoch;; cwnd += actual_mtu) { + if (cwnd == before_half_k) + bytes_before_half_k = bytes; + if (cwnd == after_half_k) + bytes_after_half_k = bytes; + if (cwnd == w_max) + bytes_at_w_max = bytes; + if (cwnd == before_three_halves_k) + bytes_before_three_halves_k = bytes; + if (cwnd == after_three_halves_k) { + bytes_after_three_halves_k = bytes; + break; + } + bytes += cuback_bytes_per_mtu_increase(&state, cwnd, cwnd_epoch, actual_mtu, reference_mtu); + } + + ok(bytes_before_half_k / state.bandwidth < k / 2); + ok(bytes_after_half_k / state.bandwidth > k / 2); + ok(fabs(bytes_at_w_max / state.bandwidth - k) / k < 1e-3); + ok(bytes_before_three_halves_k / state.bandwidth < 3 * k / 2); + ok(bytes_after_three_halves_k / state.bandwidth > 3 * k / 2); } static void test_cuback_cubic_bytes_per_mtu_increase(void) @@ -582,41 +680,8 @@ static void test_cuback_cubic_bytes_per_mtu_increase(void) for (size_t i = 0; i != PTLS_ELEMENTSOF(cases); ++i) { uint32_t cwnd_epoch = cases[i].cwnd_epoch_in_mtu * mtu, w_max = cases[i].w_max_in_mtu * mtu; - /* A two-second RTT makes the Cubic curve cheaper than the Reno curve throughout the points being tested. */ - struct st_quicly_cc_cuback_t state = {.cwnd_prior = w_max, .bandwidth = w_max / 2.}; - double k = cbrt((double)(w_max - cwnd_epoch) / (QUICLY_CUBIC_C * mtu)); - - /* By point symmetry, the continuous Cubic curve is one eighth of the epoch-to-Wmax gap below Wmax at K / 2, and the same - * distance above Wmax at 3 * K / 2. Cuback exposes only whole-MTU windows, so record the times bracketing those points. */ - double gap = w_max - cwnd_epoch; - double w_half_k = w_max - gap / 8, w_three_halves_k = w_max + gap / 8; - uint32_t before_half_k = (uint32_t)(w_half_k / mtu) * mtu, after_half_k = before_half_k + mtu; - uint32_t before_three_halves_k = (uint32_t)(w_three_halves_k / mtu) * mtu; - uint32_t after_three_halves_k = before_three_halves_k + mtu; - uint64_t bytes = 0, bytes_before_half_k = 0, bytes_after_half_k = 0, bytes_at_w_max = 0, - bytes_before_three_halves_k = 0, bytes_after_three_halves_k = 0; - - for (uint32_t cwnd = cwnd_epoch;; cwnd += mtu) { - if (cwnd == before_half_k) - bytes_before_half_k = bytes; - if (cwnd == after_half_k) - bytes_after_half_k = bytes; - if (cwnd == w_max) - bytes_at_w_max = bytes; - if (cwnd == before_three_halves_k) - bytes_before_three_halves_k = bytes; - if (cwnd == after_three_halves_k) { - bytes_after_three_halves_k = bytes; - break; - } - bytes += cuback_bytes_per_mtu_increase(&state, cwnd, cwnd_epoch, mtu); - } - - ok(bytes_before_half_k / state.bandwidth < k / 2); - ok(bytes_after_half_k / state.bandwidth > k / 2); - ok(fabs(bytes_at_w_max / state.bandwidth - k) / k < 1e-3); - ok(bytes_before_three_halves_k / state.bandwidth < 3 * k / 2); - ok(bytes_after_three_halves_k / state.bandwidth > 3 * k / 2); + check_cuback_cubic_bytes_per_mtu_increase(cwnd_epoch, w_max, mtu, mtu); + check_cuback_cubic_bytes_per_mtu_increase(cwnd_epoch, w_max, mtu, QUICLY_CC_REFERENCE_MTU); } } @@ -626,7 +691,7 @@ static void test_cuback_ack_countdown(void) quicly_loss_t loss = {.rtt = {.latest = 100, .smoothed = 100, .minimum = 100, .variance = 0}}; uint32_t mtu = 1200, w_max = 2 * mtu; - quicly_cc_cuback_init.cb(&quicly_cc_cuback_init, &cc, w_max, 0); + quicly_cc_cuback_init.cb(&quicly_cc_cuback_init, &cc, w_max, 0, 0); cc.ssthresh = cc.cwnd; cc.state.pico.cuback.cwnd_prior = w_max; cc.state.pico.cuback.bandwidth = w_max * 1000. / loss.rtt.smoothed; @@ -642,6 +707,16 @@ static void test_cuback_ack_countdown(void) cc.type->cc_on_acked(&cc, &loss, 1, 3, 1, 1, 4, 100, mtu); ok(cc.cwnd == w_max + mtu); ok(cc.state.pico.bytes_to_mtu_increase == 3 * mtu); + + /* The policy-level option selects the normalized ACK threshold while retaining actual-MTU CWND steps. */ + quicly_cc_cuback_init.cb(&quicly_cc_cuback_init, &cc, w_max, 1, 0); + cc.ssthresh = cc.cwnd; + cc.state.pico.cuback.cwnd_prior = w_max; + cc.state.pico.cuback.bandwidth = w_max * 1000. / loss.rtt.smoothed; + cc.type->cc_on_acked(&cc, &loss, 1, 1, 1, 1, 2, 100, mtu); + uint32_t normalized_deficit = (uint64_t)w_max * mtu / QUICLY_CC_REFERENCE_MTU; + ok(cc.cwnd == w_max); + ok(cc.state.pico.bytes_to_mtu_increase == normalized_deficit - 1); } static void test_cuback_deferred_bdp_estimate(void) @@ -651,13 +726,13 @@ static void test_cuback_deferred_bdp_estimate(void) uint32_t mtu = 1200, initcwnd = 10 * mtu; /* An ordinary first loss retains the estimated BDP as W_max, matching HEAD's special 0.5 startup reduction. */ - quicly_cc_cuback_init.cb(&quicly_cc_cuback_init, &cc, initcwnd, 0); + quicly_cc_cuback_init.cb(&quicly_cc_cuback_init, &cc, initcwnd, 0, 0); cc.type->cc_on_lost(&cc, &loss, mtu, 10, 20, 1000, mtu); ok(cc.state.pico.cuback.cwnd_prior == initcwnd / 2); /* Rapid Start continues adjusting CWND throughout recovery, so W_max is derived from the final CWND afterward, using twice * the BDP estimate. */ - quicly_cc_cuback_init.cb(&quicly_cc_cuback_init, &cc, initcwnd, 0); + quicly_cc_cuback_init.cb(&quicly_cc_cuback_init, &cc, initcwnd, 0, 0); cc.type->enable_rapid_start(&cc, 900); cc.type->cc_on_lost(&cc, &loss, mtu, 10, 20, 1000, mtu); ok(cc.state.pico.cuback.bandwidth > 0); @@ -692,7 +767,7 @@ static void test_zero_byte_ack_exits_rapid_start_recovery(void) for (size_t i = 0; i != PTLS_ELEMENTSOF(policies); ++i) { for (int second_by_ecn = 0; second_by_ecn != 2; ++second_by_ecn) { quicly_cc_t cc; - policies[i]->cb(policies[i], &cc, initcwnd, 0); + policies[i]->cb(policies[i], &cc, initcwnd, 0, 0); cc.type->enable_rapid_start(&cc, 900); cc.type->cc_on_lost(&cc, &loss, mtu, 10, 20, 1000, mtu); @@ -752,6 +827,7 @@ void test_cc(void) subtest("cubic-fast-convergence", test_cubic_fast_convergence); subtest("cubic-target-bounds", test_cubic_target_bounds); subtest("cubic-w-est", test_cubic_w_est); + subtest("cubic-mtu-normalization", test_cubic_mtu_normalization); subtest("cubic-cc-limited", test_cubic_cc_limited); subtest("cubic-recovery-epoch", test_cubic_recovery_epoch); subtest("cubic-rapid-start-epoch", test_cubic_rapid_start_epoch); diff --git a/t/jumpstart.c b/t/jumpstart.c index 0390c876..bcae562b 100644 --- a/t/jumpstart.c +++ b/t/jumpstart.c @@ -37,7 +37,7 @@ static void test_jumpstart_pattern(quicly_init_cc_t *init, const struct test_jum uint32_t packets_acked = 0, packets_inflight = 0; size_t ackcnt = 0; - init->cb(init, &cc, 10 * mtu, now); + init->cb(init, &cc, 10 * mtu, 0, now); ok(cc.cwnd == 10 * mtu); ok(cc.num_loss_episodes == 0); diff --git a/t/simulator.c b/t/simulator.c index cee9adae..5fc5b724 100644 --- a/t/simulator.c +++ b/t/simulator.c @@ -189,6 +189,11 @@ struct net_bottleneck { * Total number of bytes being held, and the size of the buffer that holds them. */ size_t size, capacity; + /** + * Packet size used for tail-drop admission, or zero to use the arriving packet's size. Setting this to the largest packet that + * can enter the queue prevents smaller packets from consuming the residual space in which a larger packet would not fit. + */ + size_t admission_mtu; double next_emit_at; double bytes_per_sec; /** @@ -374,8 +379,11 @@ static void net_bottleneck_forward(struct net_node *_self, struct net_packet *pa /* When the buffer is full, room is made by dropping from the head of the longest queue, so that a flow that does not slow * down cannot push the others out. Should the arrival belong to that queue itself there is nothing to be protected, hence it - * is refused; that is what always happens when the flows are not isolated. */ - while (self->size + packet->size > self->capacity) { + * is refused; that is what always happens when the flows are not isolated. + * Section 4.2.1.2 of RFC 7141 identifies reserving the last buffer space usable by a large packet as the minimum necessary to + * prevent large-packet lockout. `admission_mtu` therefore holds the largest configured packet size, unless `-B` is set. */ + size_t admission_size = self->admission_mtu != 0 ? self->admission_mtu : packet->size; + while (self->size + admission_size > self->capacity) { struct net_queue *longest = net_bottleneck_longest(self); if (longest == queue || longest->first == NULL) { net_bottleneck_drop(self, packet); @@ -584,11 +592,12 @@ static void net_bottleneck_run(struct net_node *_self) } static void net_bottleneck_init(struct net_bottleneck *self, double bytes_per_sec, double capacity_in_sec, struct net_aqm aqm, - int isolate_flows) + int isolate_flows, size_t admission_mtu) { *self = (struct net_bottleneck){ .num_queues = isolate_flows ? NET_BOTTLENECK_MAX_QUEUES : 1, .capacity = (size_t)(bytes_per_sec * capacity_in_sec), + .admission_mtu = admission_mtu, .bytes_per_sec = bytes_per_sec, .target = aqm.target, .interval = aqm.interval, @@ -792,12 +801,15 @@ static void usage(const char *cmd) "Options:\n" " -c sets congestion controller\n" " -b bottleneck bandwidth (default: 1000000, i.e., 1MB/s)\n" + " -B uses byte-fit rather than packet-size-neutral tail-drop admission\n" " -d delay added between the sender and the botteneck\n" " (default: 0.1)\n" " -i sets initial CWND (default: %" PRIu32 ")\n" " -E turns off ECN, which is otherwise used by every flow\n" " -j enables use of jumpstart using given window size\n" " -l number of seconds to simulate (default: 100)\n" + " -m sets the maximum UDP payload size\n" + " -M disables packet-size normalization of congestion-control growth\n" " -p turns on pacing\n" " -q max depth of the bottleneck queue (default: 0.1)\n" " -A queue discipline of the bottleneck: `none` (default),\n" @@ -862,11 +874,12 @@ static int parse_aqm(const char *spec, struct net_aqm *aqm) } static int parse_options(int argc, char **argv, quicly_context_t *quicctx, double *delay, double *start, double *bw, double *depth, - double *length, double *random_loss, struct net_aqm *aqm, int *isolate_flows, FILE **trace_fp) + double *length, double *random_loss, struct net_aqm *aqm, int *isolate_flows, int *byte_fit_admission, + FILE **trace_fp) { reset_getopt_state(); int ch; - while ((ch = getopt(argc, argv, "A:c:b:d:EFi:j:l:pq:r:Rs:th")) != -1) { + while ((ch = getopt(argc, argv, "A:Bc:b:d:EFi:j:l:m:Mpq:r:Rs:th")) != -1) { switch (ch) { case 'c': { @@ -890,6 +903,13 @@ static int parse_options(int argc, char **argv, quicly_context_t *quicctx, doubl return 0; } break; + case 'B': + if (byte_fit_admission == NULL) { + fprintf(stderr, "-%c is a global option and cannot be used inside a flow block\n", ch); + return 0; + } + *byte_fit_admission = 1; + break; case 'b': if (bw == NULL) { fprintf(stderr, "-%c is a global option and cannot be used inside a flow block\n", ch); @@ -928,6 +948,19 @@ static int parse_options(int argc, char **argv, quicly_context_t *quicctx, doubl return 0; } break; + case 'm': { + uint16_t max_udp_payload_size; + if (sscanf(optarg, "%" SCNu16, &max_udp_payload_size) != 1 || max_udp_payload_size < QUICLY_MIN_CLIENT_INITIAL_SIZE || + max_udp_payload_size > quicctx->transport_params.max_udp_payload_size) { + fprintf(stderr, "invalid maximum UDP payload size: %s\n", optarg); + return 0; + } + quicctx->initial_egress_max_udp_payload_size = max_udp_payload_size; + quicctx->transport_params.max_udp_payload_size = max_udp_payload_size; + } break; + case 'M': + quicctx->normalize_cc_mtu = 0; + break; case 'p': quicctx->enable_ratio.pacing = 255; break; @@ -1102,7 +1135,8 @@ int main(int argc, char **argv) /* parse args */ struct net_aqm aqm = {.type = NET_AQM_NONE}; - int isolate_flows = 0; + int isolate_flows = 0, byte_fit_admission = 0; + uint16_t queue_mtu = 0; double delay = 0.1, bw = 1e6, depth = 0.1, start = 0, random_loss = 0; double length = 100; int first_sep = find_next_separator(argc, argv, 1); @@ -1115,7 +1149,7 @@ int main(int argc, char **argv) argv[first_sep] = NULL; if (!parse_options(first_sep, argv, &quicctx, &delay, &start, &bw, &depth, &length, &random_loss, &aqm, &isolate_flows, - &quicly_trace_fp)) + &byte_fit_admission, &quicly_trace_fp)) exit(1); argv[first_sep] = "--"; @@ -1136,12 +1170,16 @@ int main(int argc, char **argv) if (seg_end < argc) argv[seg_end] = NULL; - if (!parse_options(flow_argc, flow_argv, flow_ctx, &flow_delay, &flow_start, NULL, NULL, NULL, NULL, NULL, NULL, NULL)) + if (!parse_options(flow_argc, flow_argv, flow_ctx, &flow_delay, &flow_start, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + NULL)) exit(1); flow_argv[0] = saved_argv0; if (seg_end < argc) argv[seg_end] = saved; + if (queue_mtu < flow_ctx->initial_egress_max_udp_payload_size) + queue_mtu = flow_ctx->initial_egress_max_udp_payload_size; + struct net_delay *delay_node = malloc(sizeof(*delay_node)); net_delay_init(delay_node, flow_delay); delay_node->next_node = &bottleneck_node.super; @@ -1171,7 +1209,7 @@ int main(int argc, char **argv) } /* setup bottleneck */ - net_bottleneck_init(&bottleneck_node, bw, depth, aqm, isolate_flows); + net_bottleneck_init(&bottleneck_node, bw, depth, aqm, isolate_flows, byte_fit_admission ? 0 : queue_mtu); bottleneck_node.next_node = &server_node.node.super; *node_insert_at++ = &bottleneck_node.super;