diff --git a/bin/CMakeLists.txt b/bin/CMakeLists.txt index c856bd04..4aaa7f0c 100644 --- a/bin/CMakeLists.txt +++ b/bin/CMakeLists.txt @@ -34,6 +34,8 @@ add_executable(duck_server duck_server.c prog.c test_common.c test_cert.c) add_executable(duck_client duck_client.c prog.c test_common.c test_cert.c) add_executable(perf_client perf_client.c prog.c test_common.c test_cert.c) add_executable(perf_server perf_server.c prog.c test_common.c test_cert.c) +add_executable(cbr_server cbr_server.c prog.c test_common.c test_cert.c) +add_executable(cbr_client cbr_client.c prog.c test_common.c test_cert.c) IF (NOT MSVC) @@ -69,6 +71,8 @@ TARGET_LINK_LIBRARIES(duck_server ${LIBS}) TARGET_LINK_LIBRARIES(duck_client ${LIBS}) TARGET_LINK_LIBRARIES(perf_client ${LIBS}) TARGET_LINK_LIBRARIES(perf_server ${LIBS}) +TARGET_LINK_LIBRARIES(cbr_server ${LIBS}) +TARGET_LINK_LIBRARIES(cbr_client ${LIBS}) INCLUDE(CheckFunctionExists) diff --git a/bin/cbr_client.c b/bin/cbr_client.c new file mode 100644 index 00000000..ae3f7749 --- /dev/null +++ b/bin/cbr_client.c @@ -0,0 +1,327 @@ +/* Copyright (c) 2017 - 2026 LiteSpeed Technologies Inc. See LICENSE. */ +/* + * cbr_client.c -- Constant-bitrate client. + * + * Receives a file from the CBR server, optionally saves it to disk, and + * reports bandwidth and connection statistics for testing. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef WIN32 +#include +#else +#include "vc_compat.h" +#include "getopt.h" +#include +#pragma warning(disable:4028) +#pragma warning(disable:4996) /* POSIX name (open/write/close) deprecated */ +#endif + +#include + +#include "lsquic.h" +#include "test_common.h" +#include "prog.h" + +#include "../src/liblsquic/lsquic_logger.h" + + +#define CBR_DF_LOG_INTERVAL 1 + + +struct cbr_client_ctx +{ + struct prog *prog; + unsigned log_interval; + const char *output_path; +}; + + +struct lsquic_conn_ctx +{ + lsquic_conn_t *conn; + struct cbr_client_ctx *client_ctx; +}; + + +struct lsquic_stream_ctx +{ + lsquic_stream_t *stream; + struct cbr_client_ctx *client_ctx; + int out_fd; + uint64_t bytes_read; + uint64_t file_size; /* 0 if unknown */ + time_t start_time; + time_t last_log; +}; + + +static lsquic_conn_ctx_t * +cbr_client_on_new_conn (void *stream_if_ctx, struct lsquic_conn *conn) +{ + struct cbr_client_ctx *client_ctx = stream_if_ctx; + struct lsquic_conn_ctx *conn_ctx; + + conn_ctx = calloc(1, sizeof(*conn_ctx)); + if (!conn_ctx) { + perror("calloc"); + exit(EXIT_FAILURE); + } + conn_ctx->conn = conn; + conn_ctx->client_ctx = client_ctx; + + LSQ_INFO("New connection, creating stream"); + lsquic_conn_make_stream(conn); + return conn_ctx; +} + + +static void +cbr_client_on_conn_closed (struct lsquic_conn *conn) +{ + struct lsquic_conn_ctx *conn_ctx = lsquic_conn_get_ctx(conn); + struct lsquic_conn_info info; + + if (0 == lsquic_conn_get_info(conn, &info)) + { + LSQ_NOTICE("Connection closed:" + " bytes_rcvd=%"PRIu64" bytes_sent=%"PRIu64 + " pkts_rcvd=%"PRIu64" pkts_sent=%"PRIu64 + " pkts_lost=%"PRIu64" pkts_retx=%"PRIu64 + " bw_est=%.2f Mbps", + info.lci_bytes_rcvd, info.lci_bytes_sent, + info.lci_pkts_rcvd, info.lci_pkts_sent, + info.lci_pkts_lost, info.lci_pkts_retx, + info.lci_bw_estimate / 1000000.0); + } + + struct prog *prog = conn_ctx->client_ctx->prog; + lsquic_conn_set_ctx(conn, NULL); + free(conn_ctx); + prog_stop(prog); +} + + +static struct lsquic_stream_ctx * +cbr_client_on_new_stream (void *stream_if_ctx, struct lsquic_stream *stream) +{ + struct cbr_client_ctx *client_ctx = stream_if_ctx; + struct lsquic_stream_ctx *stream_ctx; + + if (!stream) + { + LSQ_NOTICE("null stream"); + return NULL; + } + + stream_ctx = calloc(1, sizeof(*stream_ctx)); + if (!stream_ctx) { + perror("calloc"); + exit(EXIT_FAILURE); + } + stream_ctx->stream = stream; + stream_ctx->client_ctx = client_ctx; + stream_ctx->start_time = time(NULL); + stream_ctx->last_log = stream_ctx->start_time; + stream_ctx->out_fd = -1; + + if (client_ctx->output_path) + { + stream_ctx->out_fd = open(client_ctx->output_path, + O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (stream_ctx->out_fd < 0) + { + LSQ_ERROR("cannot open '%s': %s", + client_ctx->output_path, strerror(errno)); + exit(EXIT_FAILURE); + } + } + + lsquic_stream_wantread(stream, 1); + lsquic_stream_wantwrite(stream, 1); + return stream_ctx; +} + + +static void +cbr_client_on_read (struct lsquic_stream *stream, + struct lsquic_stream_ctx *stream_ctx) +{ + char buf[65536]; + ssize_t nr; + time_t now; + + nr = lsquic_stream_read(stream, buf, sizeof(buf)); + if (nr > 0) + { + stream_ctx->bytes_read += nr; + if (stream_ctx->out_fd >= 0) + { + ssize_t nw = write(stream_ctx->out_fd, buf, nr); + if (nw < 0) + { + LSQ_ERROR("file write error: %s", strerror(errno)); + exit(EXIT_FAILURE); + } + } + + now = time(NULL); + if (now - stream_ctx->last_log >= stream_ctx->client_ctx->log_interval) + { + double elapsed = now - stream_ctx->start_time; + LSQ_NOTICE("recv: elapsed=%.0fs bytes=%"PRIu64 + " rate=%.2f Mbps", + elapsed, stream_ctx->bytes_read, + elapsed > 0 + ? (stream_ctx->bytes_read * 8.0) + / elapsed / 1000000.0 + : 0.0); + stream_ctx->last_log = now; + } + } + else if (nr == 0) + { + LSQ_NOTICE("stream FIN received, total bytes read: %"PRIu64, + stream_ctx->bytes_read); + + if (stream_ctx->out_fd >= 0) + { + close(stream_ctx->out_fd); + stream_ctx->out_fd = -1; + } + + lsquic_stream_shutdown(stream, 0); + lsquic_stream_shutdown(stream, 1); + lsquic_stream_wantread(stream, 0); + } + else + { + LSQ_WARN("read error: %s", strerror(errno)); + lsquic_stream_wantread(stream, 0); + } +} + + +static void +cbr_client_on_write (struct lsquic_stream *stream, + struct lsquic_stream_ctx *stream_ctx) +{ + static const char announce_byte = 0; + ssize_t nw = lsquic_stream_write(stream, &announce_byte, 1); + if (nw == 1) + { + lsquic_stream_flush(stream); + lsquic_stream_wantwrite(stream, 0); + } + else if (nw < 0 && errno != EAGAIN) + LSQ_WARN("announce write error: %s", strerror(errno)); +} + + +static void +cbr_client_on_close (struct lsquic_stream *stream, + struct lsquic_stream_ctx *stream_ctx) +{ + LSQ_DEBUG("stream closed, total bytes: %"PRIu64, stream_ctx->bytes_read); + + if (stream_ctx->out_fd >= 0) + close(stream_ctx->out_fd); + lsquic_conn_close(lsquic_stream_conn(stream)); + free(stream_ctx); +} + + +const struct lsquic_stream_if cbr_stream_if = { + .on_new_conn = cbr_client_on_new_conn, + .on_conn_closed = cbr_client_on_conn_closed, + .on_new_stream = cbr_client_on_new_stream, + .on_read = cbr_client_on_read, + .on_write = cbr_client_on_write, + .on_close = cbr_client_on_close, +}; + + +static void +usage (const char *prog) +{ + const char *const slash = strrchr(prog, '/'); + if (slash) + prog = slash + 1; + printf( +"Usage: %s [opts]\n" +"\n" +"Receive a file from the CBR server and report statistics.\n" +"\n" +"Options:\n" +" -O FILE Save received data to FILE (default: discard)\n" +" -n SECONDS Stats log interval in seconds (default: %u)\n" +" -h Print this help screen\n" +"\n", + prog, CBR_DF_LOG_INTERVAL); +} + + +int +main (int argc, char **argv) +{ + int opt, s; + struct prog prog; + struct sport_head sports; + struct cbr_client_ctx client_ctx; + + memset(&client_ctx, 0, sizeof(client_ctx)); + + TAILQ_INIT(&sports); + prog_init(&prog, 0, &sports, &cbr_stream_if, &client_ctx); + prog.prog_api.ea_alpn = "cbr"; + client_ctx.prog = &prog; + client_ctx.log_interval = CBR_DF_LOG_INTERVAL; + + while (-1 != (opt = getopt(argc, argv, PROG_OPTS "O:n:h"))) + { + switch (opt) { + case 'O': + client_ctx.output_path = optarg; + break; + case 'n': + client_ctx.log_interval = atoi(optarg); + if (client_ctx.log_interval < 1) + client_ctx.log_interval = 1; + break; + case 'h': + usage(argv[0]); + prog_print_common_options(&prog, stdout); + exit(0); + default: + if (0 != prog_set_opt(&prog, opt, optarg)) + exit(1); + } + } + + if (0 != prog_prep(&prog)) + { + LSQ_ERROR("could not prep"); + exit(EXIT_FAILURE); + } + + if (0 != prog_connect(&prog, NULL, 0)) + { + LSQ_ERROR("could not connect"); + exit(EXIT_FAILURE); + } + + LSQ_DEBUG("entering event loop"); + s = prog_run(&prog); + prog_cleanup(&prog); + + exit(0 == s ? EXIT_SUCCESS : EXIT_FAILURE); +} diff --git a/bin/cbr_server.c b/bin/cbr_server.c new file mode 100644 index 00000000..2c4f7a52 --- /dev/null +++ b/bin/cbr_server.c @@ -0,0 +1,511 @@ +/* Copyright (c) 2017 - 2026 LiteSpeed Technologies Inc. See LICENSE. */ +/* + * cbr_server.c -- Constant-bitrate server. + * + * Reads a file and sends it to the client at a constant bitrate, simulating + * a live video stream. Periodically logs BBR bandwidth estimation and + * connection statistics for testing the bw_probe_fill feature. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef WIN32 +#include +#else +#include "vc_compat.h" +#include "getopt.h" +#include +#pragma warning(disable:4028) +#pragma warning(disable:4996) /* POSIX name (open/read/close) deprecated */ +#endif + +#include + +#include "lsquic.h" +#include "test_common.h" +#include "../src/liblsquic/lsquic_hash.h" +#include "test_cert.h" +#include "prog.h" + +#include "../src/liblsquic/lsquic_logger.h" + + +#define CBR_DF_BITRATE 5000000 +#define CBR_DF_INTERVAL_MS 100 +#define CBR_DF_LOG_INTERVAL 1 +#define CBR_MAX_PENDING_FRAMES 10 + + +struct cbr_server_ctx +{ + struct event_base *event_base; + struct lsquic_engine *engine; + struct prog *prog; + unsigned log_interval; + unsigned frame_size; + unsigned interval_ms; + const char *file_path; +}; + + +struct lsquic_conn_ctx +{ + lsquic_conn_t *conn; + struct cbr_server_ctx *server_ctx; +}; + + +struct lsquic_stream_ctx +{ + lsquic_stream_t *stream; + struct cbr_server_ctx *server_ctx; + struct event *timer; + unsigned frame_size; + unsigned interval_ms; + int fd; + char buf[65536]; + size_t buf_off; + size_t buf_end; + uint64_t file_size; + uint64_t bytes_sent; + uint64_t frame_pending; + time_t start_time; + time_t last_log; + int write_shutdown; +}; + + +/* Forward declarations */ +static void +cbr_stream_timer_cb (evutil_socket_t, short, void *); + + +static lsquic_conn_ctx_t * +cbr_server_on_new_conn (void *stream_if_ctx, lsquic_conn_t *conn) +{ + struct cbr_server_ctx *server_ctx = stream_if_ctx; + struct lsquic_conn_ctx *conn_ctx; + + conn_ctx = calloc(1, sizeof(*conn_ctx)); + if (!conn_ctx) { + perror("calloc"); + exit(EXIT_FAILURE); + } + conn_ctx->conn = conn; + conn_ctx->server_ctx = server_ctx; + + lsquic_conn_set_param(conn, LSQCP_ENABLE_BW_SAMPLER, + &(int){ 1 }, sizeof(int)); + + LSQ_INFO("New connection, waiting for client stream"); + return conn_ctx; +} + + +static void +cbr_server_on_conn_closed (lsquic_conn_t *conn) +{ + struct lsquic_conn_ctx *conn_ctx = lsquic_conn_get_ctx(conn); + + LSQ_INFO("Connection closed"); + lsquic_conn_set_ctx(conn, NULL); + free(conn_ctx); +} + + +static struct lsquic_stream_ctx * +cbr_server_on_new_stream (void *stream_if_ctx, struct lsquic_stream *stream) +{ + struct cbr_server_ctx *server_ctx = stream_if_ctx; + struct lsquic_stream_ctx *stream_ctx; + + stream_ctx = calloc(1, sizeof(*stream_ctx)); + if (!stream_ctx) { + perror("calloc"); + exit(EXIT_FAILURE); + } + + stream_ctx->stream = stream; + stream_ctx->server_ctx = server_ctx; + stream_ctx->frame_size = server_ctx->frame_size; + stream_ctx->interval_ms = server_ctx->interval_ms; + stream_ctx->start_time = time(NULL); + stream_ctx->last_log = stream_ctx->start_time; + + if (server_ctx->file_path) + { + stream_ctx->fd = open(server_ctx->file_path, O_RDONLY); + if (stream_ctx->fd < 0) + { + LSQ_ERROR("cannot open file '%s': %s", + server_ctx->file_path, strerror(errno)); + exit(EXIT_FAILURE); + } + struct stat st; + if (0 != fstat(stream_ctx->fd, &st)) + { + LSQ_ERROR("fstat: %s", strerror(errno)); + exit(EXIT_FAILURE); + } + stream_ctx->file_size = st.st_size; + } + else + { + stream_ctx->fd = -1; + stream_ctx->file_size = 100 * 1024 * 1024; + } + + lsquic_stream_wantread(stream, 1); + + stream_ctx->timer = evtimer_new(server_ctx->event_base, + cbr_stream_timer_cb, stream_ctx); + if (stream_ctx->timer) + { + struct timeval tv = { + .tv_sec = stream_ctx->interval_ms / 1000, + .tv_usec = (stream_ctx->interval_ms % 1000) * 1000, + }; + event_add(stream_ctx->timer, &tv); + } + + LSQ_NOTICE("stream created: file_size=%"PRIu64" frame_size=%u" + " interval_ms=%u", + stream_ctx->file_size, stream_ctx->frame_size, + stream_ctx->interval_ms); + return stream_ctx; +} + + +static void +cbr_server_on_read (struct lsquic_stream *stream, + struct lsquic_stream_ctx *stream_ctx) +{ + char buf[256]; + ssize_t nr = lsquic_stream_read(stream, buf, sizeof(buf)); + if (nr > 0) + LSQ_DEBUG("read %zd bytes from client (ignored)", nr); + else if (nr == 0) + { + LSQ_DEBUG("client closed stream for reading"); + lsquic_stream_wantread(stream, 0); + } + else if (nr < 0) + { + LSQ_WARN("read error: %s", strerror(errno)); + lsquic_stream_wantread(stream, 0); + } +} + + +static void +cbr_server_try_shutdown (struct lsquic_stream_ctx *stream_ctx) +{ + if (stream_ctx->bytes_sent >= stream_ctx->file_size + && !stream_ctx->write_shutdown) + { + lsquic_stream_shutdown(stream_ctx->stream, 1); + stream_ctx->write_shutdown = 1; + LSQ_DEBUG("file sent completely, write shutdown"); + } +} + + +static void +cbr_server_do_write (struct lsquic_stream_ctx *stream_ctx) +{ + struct lsquic_stream *stream = stream_ctx->stream; + size_t to_write; + ssize_t nw; + uint64_t remaining; + + /* Refill buffer if empty */ + if (stream_ctx->buf_off == stream_ctx->buf_end) + { + remaining = stream_ctx->file_size - stream_ctx->bytes_sent; + to_write = sizeof(stream_ctx->buf); + if (to_write > remaining) + to_write = (size_t)remaining; + if (to_write == 0) + return; + + if (stream_ctx->fd >= 0) + { + ssize_t nread = read(stream_ctx->fd, stream_ctx->buf, to_write); + if (nread < 0) + { + LSQ_ERROR("file read error: %s", strerror(errno)); + return; + } + stream_ctx->buf_end = nread; + } + else + { + memset(stream_ctx->buf, 0, to_write); + stream_ctx->buf_end = to_write; + } + stream_ctx->buf_off = 0; + } + + to_write = stream_ctx->buf_end - stream_ctx->buf_off; + { + uint64_t pending = stream_ctx->frame_pending; + if (pending < to_write) + to_write = (size_t)pending; + } + + if (to_write == 0) + return; + + nw = lsquic_stream_write(stream, + stream_ctx->buf + stream_ctx->buf_off, + to_write); + if (nw > 0) + { + stream_ctx->bytes_sent += nw; + stream_ctx->frame_pending -= nw; + stream_ctx->buf_off += nw; + } + else if (nw < 0) + { + if (errno != EAGAIN) + LSQ_WARN("write error: %s", strerror(errno)); + } +} + + +static void +cbr_server_on_write (struct lsquic_stream *stream, + struct lsquic_stream_ctx *stream_ctx) +{ + if (stream_ctx->write_shutdown) + return; + cbr_server_try_shutdown(stream_ctx); + if (stream_ctx->write_shutdown) + return; + if (stream_ctx->frame_pending == 0) + { + lsquic_stream_wantwrite(stream, 0); + return; + } + + cbr_server_do_write(stream_ctx); + cbr_server_try_shutdown(stream_ctx); + if (stream_ctx->write_shutdown) + return; + if (stream_ctx->frame_pending > 0) + lsquic_stream_wantwrite(stream, 1); +} + + +static void +cbr_server_on_close (lsquic_stream_t *stream, + lsquic_stream_ctx_t *stream_ctx) +{ + struct lsquic_conn_info info; + + if (0 == lsquic_conn_get_info(lsquic_stream_conn(stream), &info)) + { + LSQ_NOTICE("stream closed: sent=%"PRIu64"/%"PRIu64 + " lost=%"PRIu64" retx=%"PRIu64 + " bw_est=%.2f Mbps pacing=%.2f Mbps" + " cwnd=%u rtt=%u", + stream_ctx->bytes_sent, stream_ctx->file_size, + info.lci_pkts_lost, info.lci_pkts_retx, + info.lci_bw_estimate / 1000000.0, + info.lci_pacing_rate * 8 / 1000000.0, + info.lci_cwnd, info.lci_rtt); + } + + if (stream_ctx->timer) + event_free(stream_ctx->timer); + if (stream_ctx->fd >= 0) + close(stream_ctx->fd); + free(stream_ctx); +} + + +const struct lsquic_stream_if cbr_server_stream_if = { + .on_new_conn = cbr_server_on_new_conn, + .on_conn_closed = cbr_server_on_conn_closed, + .on_new_stream = cbr_server_on_new_stream, + .on_read = cbr_server_on_read, + .on_write = cbr_server_on_write, + .on_close = cbr_server_on_close, +}; + + +static void +cbr_stream_timer_cb (evutil_socket_t fd, short what, void *arg) +{ + struct lsquic_stream_ctx *stream_ctx = arg; + time_t now = time(NULL); + + if (now - stream_ctx->last_log >= stream_ctx->server_ctx->log_interval) + { + struct lsquic_conn_info info; + if (0 == lsquic_conn_get_info(lsquic_stream_conn(stream_ctx->stream), + &info)) + { + double elapsed = now - stream_ctx->start_time; + LSQ_NOTICE("elapsed=%.0fs sent=%"PRIu64"/%"PRIu64 + " rate=%.2f bw=%.2f pacing=%.2f Mbps" + " lost=%"PRIu64" retx=%"PRIu64 + " cwnd=%u rtt=%u", + elapsed, + stream_ctx->bytes_sent, stream_ctx->file_size, + elapsed > 0 + ? (stream_ctx->bytes_sent * 8.0) + / elapsed / 1000000.0 + : 0.0, + info.lci_bw_estimate / 1000000.0, + info.lci_pacing_rate * 8 / 1000000.0, + info.lci_pkts_lost, info.lci_pkts_retx, + info.lci_cwnd, info.lci_rtt); + } + stream_ctx->last_log = now; + } + + cbr_server_try_shutdown(stream_ctx); + + if (!stream_ctx->write_shutdown) + { + uint64_t remaining = stream_ctx->file_size - stream_ctx->bytes_sent; + stream_ctx->frame_pending += stream_ctx->frame_size; + if (stream_ctx->frame_pending > remaining) + stream_ctx->frame_pending = remaining; + { + uint64_t max_pending = (uint64_t)stream_ctx->frame_size + * CBR_MAX_PENDING_FRAMES; + if (stream_ctx->frame_pending > max_pending) + stream_ctx->frame_pending = max_pending; + } + lsquic_stream_wantwrite(stream_ctx->stream, 1); + prog_process_conns(stream_ctx->server_ctx->prog); + + struct timeval tv = { + .tv_sec = stream_ctx->interval_ms / 1000, + .tv_usec = (stream_ctx->interval_ms % 1000) * 1000, + }; + event_add(stream_ctx->timer, &tv); + } +} + + +static void +usage (const char *prog) +{ + const char *const slash = strrchr(prog, '/'); + if (slash) + prog = slash + 1; + printf( +"Usage: %s [opts]\n" +"\n" +"Send a file at constant bitrate to test BBR bandwidth estimation.\n" +"\n" +"Options:\n" +" -f FILE File to send (required for real file transfer)\n" +" -b BITRATE Target bitrate in bits per second (default: %u = %.f Mbps)\n" +" -I MS Frame interval in milliseconds (default: %u)\n" +" -r BYTES Override per-frame payload size (default: automatic from bitrate)\n" +" -t SECONDS Stats log interval in seconds (default: %u)\n" +" -h Print this help screen\n" +"\n" +"Example:\n" +" cbr_server -f video.bin -b 8000000 -I 50 -A 4\n" +"\n", + prog, + CBR_DF_BITRATE, CBR_DF_BITRATE / 1000000.0, + CBR_DF_INTERVAL_MS, + CBR_DF_LOG_INTERVAL); +} + + +int +main (int argc, char **argv) +{ + int opt; + struct prog prog; + struct sport_head sports; + struct cbr_server_ctx server_ctx; + uint64_t bitrate = CBR_DF_BITRATE; + unsigned interval_ms = CBR_DF_INTERVAL_MS; + unsigned frame_size = 0; + + memset(&server_ctx, 0, sizeof(server_ctx)); + + TAILQ_INIT(&sports); + prog_init(&prog, LSENG_SERVER, &sports, &cbr_server_stream_if, &server_ctx); + + while (-1 != (opt = getopt(argc, argv, PROG_OPTS "f:b:I:r:t:h"))) + { + switch (opt) { + case 'f': + server_ctx.file_path = optarg; + break; + case 'b': + bitrate = strtoull(optarg, NULL, 10); + break; + case 'I': + interval_ms = atoi(optarg); + if (interval_ms < 10) + interval_ms = 10; + break; + case 'r': + frame_size = atoi(optarg); + break; + case 't': + server_ctx.log_interval = atoi(optarg); + if (server_ctx.log_interval < 1) + server_ctx.log_interval = 1; + break; + case 'h': + usage(argv[0]); + prog_print_common_options(&prog, stdout); + exit(0); + default: + if (0 != prog_set_opt(&prog, opt, optarg)) + exit(1); + } + } + + if (!server_ctx.log_interval) + server_ctx.log_interval = CBR_DF_LOG_INTERVAL; + + if (!frame_size) + frame_size = (unsigned)(bitrate * interval_ms / 8 / 1000); + if (frame_size < 1) + frame_size = 1400; + + server_ctx.frame_size = frame_size; + server_ctx.interval_ms = interval_ms; + + LSQ_INFO("bitrate=%"PRIu64" bps, interval=%u ms, frame_size=%u", + bitrate, interval_ms, frame_size); + + add_alpn("cbr"); + if (0 != prog_prep(&prog)) + { + LSQ_ERROR("could not prep"); + exit(EXIT_FAILURE); + } + + server_ctx.event_base = prog.prog_eb; + server_ctx.engine = prog.prog_engine; + server_ctx.prog = &prog; + + LSQ_DEBUG("entering event loop"); + prog_run(&prog); + prog_cleanup(&prog); + + exit(EXIT_SUCCESS); +} diff --git a/bin/prog.c b/bin/prog.c index 0facdcdd..04481749 100644 --- a/bin/prog.c +++ b/bin/prog.c @@ -173,6 +173,7 @@ prog_print_common_options (const struct prog *prog, FILE *out) " 1: Cubic\n" " 2: BBRv1\n" " 3: Adaptive congestion control (this is the default).\n" +" 4: BBRv1-Copilot.\n" ); #if HAVE_SENDMMSG @@ -224,6 +225,9 @@ prog_print_common_options (const struct prog *prog, FILE *out) LSQUIC_DF_CLOCK_GRANULARITY ); fprintf(out, +" -o name=val Set engine options, such as -o max_batch_size=100\n" + ); + fprintf(out, " -h Print this help screen and exit\n" ); } @@ -269,7 +273,7 @@ prog_set_opt (struct prog *prog, int opt, const char *arg) prog->prog_use_stock_pmi = 1; return 0; case 'A': - prog->prog_settings.es_cc_algo = atoi(optarg); + prog->prog_settings.es_cc_algo = (enum lsquic_cc) atoi(optarg); return 0; case 'c': if (prog->prog_engine_flags & LSENG_SERVER) diff --git a/bin/test_common.c b/bin/test_common.c index 97e3b97f..761667b1 100644 --- a/bin/test_common.c +++ b/bin/test_common.c @@ -1988,7 +1988,7 @@ set_engine_option (struct lsquic_engine_settings *settings, } else if (0 == strncmp(name, "cc_algo", 7)) { - settings->es_cc_algo = atoi(val); + settings->es_cc_algo = (enum lsquic_cc) atoi(val); return 0; } else if (0 == strncmp(name, "ql_bits", 7)) diff --git a/docs/apiref.rst b/docs/apiref.rst index c894efd8..33e64526 100644 --- a/docs/apiref.rst +++ b/docs/apiref.rst @@ -67,6 +67,16 @@ types are included in a single enum: Special value indicating the number of versions in the enum. It may be used as argument to :func:`lsquic_engine_connect()`. +.. type:: enum lsquic_cc + + Congestion control algorithm. + + .. member:: LSQUIC_CC_DEFAULT + .. member:: LSQUIC_CC_CUBIC + .. member:: LSQUIC_CC_BBR + .. member:: LSQUIC_CC_ADAPTIVE + .. member:: LSQUIC_CC_BBR_COPILOT + Several version lists (as bitmasks) are defined in :file:`lsquic.h`: .. macro:: LSQUIC_SUPPORTED_VERSIONS @@ -707,7 +717,7 @@ settings structure: IETF QUIC only. - .. member:: unsigned es_cc_algo + .. member:: enum lsquic_cc es_cc_algo Congestion control algorithm to use. @@ -715,6 +725,7 @@ settings structure: - 1: Cubic - 2: BBRv1 - 3: Adaptive congestion control. + - 4: BBRv1-Copilot. Adaptive congestion control adapts to the environment. It figures out whether to use Cubic or BBRv1 based on the RTT. @@ -2182,6 +2193,16 @@ available through engine settings. **Note:** :func:`lsquic_conn_get_info()` enables the sampler if needed. + .. member:: LSQCP_CC_ALGO + + Select the congestion control algorithm for this connection. + + **Type:** ``enum lsquic_cc`` + + **Default:** inherited from :member:`lsquic_engine_settings.es_cc_algo` + + The values are the same as for ``es_cc_algo``. + .. function:: int lsquic_conn_set_param (lsquic_conn_t *conn, enum lsquic_conn_param param, const void *value, size_t value_len) Set a connection parameter. @@ -2222,6 +2243,14 @@ available through engine settings. lsquic_conn_set_param(conn, LSQCP_ENABLE_BW_SAMPLER, &on, sizeof(on)); + **Example - Selecting BBR-Copilot for this connection:** + + :: + + enum lsquic_cc cc_algo = LSQUIC_CC_BBR_COPILOT; + lsquic_conn_set_param(conn, LSQCP_CC_ALGO, + &cc_algo, sizeof(cc_algo)); + **Note:** For :member:`LSQCP_MAX_PACING_RATE`, pacing must be enabled via :member:`lsquic_engine_settings.es_pace_packets` for this parameter to have any effect. diff --git a/docs/congestion-control.rst b/docs/congestion-control.rst new file mode 100644 index 00000000..0f46faaf --- /dev/null +++ b/docs/congestion-control.rst @@ -0,0 +1,96 @@ +****************** +Congestion Control +****************** + +LSQUIC supports several congestion controllers. An application can select +one controller for every connection created by an engine or override the +selection for an individual connection. + +Available Controllers +===================== + +Cubic +----- + +Cubic is a loss-based congestion controller. Select it using +``LSQUIC_CC_CUBIC``. + +BBRv1 +----- + +BBRv1 models bottleneck bandwidth and round-trip propagation time. Select it +using ``LSQUIC_CC_BBR``. + +Adaptive +-------- + +Adaptive initializes both Cubic and BBRv1, measures the connection RTT, and +then selects one of them. The threshold is configured using +:member:`lsquic_engine_settings.es_cc_rtt_thresh`. Select Adaptive using +``LSQUIC_CC_ADAPTIVE``. It is the default controller. + +BBR-Copilot +----------- + +BBR-Copilot is a variant of BBRv1 intended for applications such as live +streaming that produce on-off traffic. When BBR is probing for additional +bandwidth but the application has no data to send, BBR-Copilot sends padded, +ack-eliciting packets to complete the probe and obtain a useful bandwidth +sample. + +This behavior can improve BBR's bandwidth estimate when application-limited +periods would otherwise interrupt active probing. It also deliberately sends +data that the application did not request, so applications should account for +the additional network traffic. Adaptive never selects BBR-Copilot. + +Select BBR-Copilot using ``LSQUIC_CC_BBR_COPILOT``. + +Selecting a Controller +====================== + +Engine-Wide Selection +--------------------- + +Set :member:`lsquic_engine_settings.es_cc_algo` before creating the engine: + +.. code-block:: c + + struct lsquic_engine_settings settings; + + lsquic_engine_init_settings(&settings, 0); + settings.es_cc_algo = LSQUIC_CC_BBR_COPILOT; + + engine_api.ea_settings = &settings; + +The example programs expose this setting through ``-A``. BBR-Copilot is +controller number 4: + +.. code-block:: console + + cbr_server -A 4 + +Per-Connection Selection +------------------------ + +Use :func:`lsquic_conn_set_param()` to override the controller for one +connection, typically from the ``on_new_conn`` callback: + +.. code-block:: c + + enum lsquic_cc cc = LSQUIC_CC_BBR_COPILOT; + + if (0 != lsquic_conn_set_param(conn, LSQCP_CC_ALGO, + &cc, sizeof(cc))) + { + /* Handle invalid or unsupported parameter. */ + } + +Passing ``LSQUIC_CC_DEFAULT`` selects the library default. The current +controller can be read using :func:`lsquic_conn_get_param()`. + +Reference +========= + +Xu Yan, Tong Li, Bo Wu, Cheng Luo, Jiuxiang Zhu, and Laizhong Cui, +"When BBR Meets Live Streaming," *Frontiers of Networking Technologies +(CCF ChinaNet)*, pp. 11-27, 2026. diff --git a/docs/index.rst b/docs/index.rst index 8181608e..fd754147 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -64,6 +64,7 @@ Contents gettingstarted tutorial + congestion-control apiref devel internals diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 56bb581c..546cba12 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -941,7 +941,7 @@ is parsed to see which setting to alter. } /* ... */ else if (0 == strncmp(optarg, "cc_algo=", val - optarg)) - settings.es_cc_algo = atoi(val); + settings.es_cc_algo = (enum lsquic_cc) atoi(val); /* ... */ } diff --git a/include/lsquic.h b/include/lsquic.h index 88d20db8..8173623e 100644 --- a/include/lsquic.h +++ b/include/lsquic.h @@ -419,8 +419,20 @@ typedef struct ssl_ctx_st * (*lsquic_lookup_cert_f)( /** default anti-amplification factor is 3 */ #define LSQUIC_DF_AMP_FACTOR 3 +enum lsquic_cc +{ + /* This enum has explicit values so that "es_cc_algo" is backwards- + * compatible. + */ + LSQUIC_CC_DEFAULT = 0, LSQUIC_CC_FIRST = LSQUIC_CC_DEFAULT, + LSQUIC_CC_CUBIC = 1, + LSQUIC_CC_BBR = 2, + LSQUIC_CC_ADAPTIVE = 3, + LSQUIC_CC_BBR_COPILOT = 4, LSQUIC_CC_LAST = LSQUIC_CC_BBR_COPILOT, +}; + /* Use Adaptive CC by default */ -#define LSQUIC_DF_CC_ALGO 3 +#define LSQUIC_DF_CC_ALGO LSQUIC_CC_ADAPTIVE /* Default value of the CC RTT threshold is 1.5 ms */ #define LSQUIC_DF_CC_RTT_THRESH 1500 @@ -730,8 +742,9 @@ struct lsquic_engine_settings { * 1: Cubic * 2: BBRv1 * 3: Adaptive (Cubic or BBRv1) + * 4: BBRv1-Copilot */ - unsigned es_cc_algo; + enum lsquic_cc es_cc_algo; /** * Congestion controller RTT threshold in microseconds. @@ -2184,6 +2197,15 @@ enum lsquic_conn_param * lsquic_conn_get_info() enables sampling if needed for functionality. */ LSQCP_ENABLE_BW_SAMPLER = 2, + /** + * Congestion control algorithm to use for this connection. + * + * Type: enum lsquic_cc + * Default: inherited from es_cc_algo. + * + * Values are the same as for es_cc_algo. + */ + LSQCP_CC_ALGO = 3, }; struct lsquic_conn_info diff --git a/src/liblsquic/lsquic_adaptive_cc.c b/src/liblsquic/lsquic_adaptive_cc.c index 38906805..83e16154 100644 --- a/src/liblsquic/lsquic_adaptive_cc.c +++ b/src/liblsquic/lsquic_adaptive_cc.c @@ -201,6 +201,13 @@ adaptive_cc_process_bw_sample (void *cong_ctl, struct bw_sample *sample) } +static int +adaptive_cc_bw_probe_fill_wanted (void *cong_ctl) +{ + return 0; +} + + const struct cong_ctl_if lsquic_cong_adaptive_if = { .cci_ack = adaptive_cc_ack, @@ -217,4 +224,5 @@ const struct cong_ctl_if lsquic_cong_adaptive_if = .cci_sent = adaptive_cc_sent, .cci_process_bw_sample = adaptive_cc_process_bw_sample, .cci_was_quiet = adaptive_cc_was_quiet, + .cci_bw_probe_fill_wanted = adaptive_cc_bw_probe_fill_wanted, }; diff --git a/src/liblsquic/lsquic_bbr.c b/src/liblsquic/lsquic_bbr.c index d9387b4b..73ee20f8 100644 --- a/src/liblsquic/lsquic_bbr.c +++ b/src/liblsquic/lsquic_bbr.c @@ -204,6 +204,7 @@ init_bbr (struct lsquic_bbr *bbr) bbr->bbr_flags &= ~BBR_FLAG_LAST_SAMPLE_APP_LIMITED; bbr->bbr_flags &= ~BBR_FLAG_HAS_NON_APP_LIMITED; bbr->bbr_flags &= ~BBR_FLAG_FLEXIBLE_APP_LIMITED; + bbr->bbr_flags &= ~BBR_BW_SAMPLE_INVALID_PROBE_RTT; bbr->bbr_total_acked = 0; set_startup_values(bbr); } @@ -354,6 +355,28 @@ lsquic_bbr_ack (void *cong_ctl, struct lsquic_packet_out *packet_out, bbr->bbr_total_acked += packet_sz; } + +static void +bbr_copilot_ack (void *cong_ctl, struct lsquic_packet_out *packet_out, + unsigned packet_sz, lsquic_time_t now, int app_limited) +{ + struct lsquic_bbr *const bbr = cong_ctl; + + assert(bbr->bbr_flags & BBR_FLAG_IN_ACK); + + if (bbr->bbr_flags & BBR_BW_SAMPLE_INVALID_PROBE_RTT + && bbr->bbr_mode != BBR_MODE_PROBE_RTT + && packet_out->po_packno > bbr->bbr_probe_rtt_app_limited_until) + { + LSQ_DEBUG("exit probe_rtt app-limited at packno %"PRIu64, + packet_out->po_packno); + bbr->bbr_flags &= ~BBR_BW_SAMPLE_INVALID_PROBE_RTT; + } + + lsquic_bbr_ack(cong_ctl, packet_out, packet_sz, now, app_limited); +} + + static void lsquic_bbr_process_bw_sample (void *cong_ctl, struct bw_sample *sample) { @@ -379,6 +402,23 @@ lsquic_bbr_sent (void *cong_ctl, struct lsquic_packet_out *packet_out, } +static void +bbr_copilot_sent (void *cong_ctl, struct lsquic_packet_out *packet_out, + uint64_t in_flight, int app_limited) +{ + struct lsquic_bbr *const bbr = cong_ctl; + + lsquic_bbr_sent(cong_ctl, packet_out, in_flight, app_limited); + + if (bbr->bbr_mode == BBR_MODE_PROBE_RTT) + bbr->bbr_probe_rtt_app_limited_until = bbr->bbr_last_sent_packno; + + if ((bbr->bbr_flags & BBR_BW_SAMPLE_INVALID_PROBE_RTT) + && (packet_out->po_bwp_state)) + packet_out->po_bwp_state->bwps_send_state.is_app_limited = 1; +} + + static void lsquic_bbr_lost (void *cong_ctl, struct lsquic_packet_out *UNUSED_packet_out, unsigned packet_sz) @@ -796,6 +836,9 @@ maybe_enter_or_exit_probe_rtt (struct lsquic_bbr *bbr, lsquic_time_t now, // Do not decide on the time to exit PROBE_RTT until the // |bytes_in_flight| is at the target small value. bbr->bbr_exit_probe_rtt_at = 0; + /* Used by BBR-Copilot: */ + bbr->bbr_flags |= BBR_BW_SAMPLE_INVALID_PROBE_RTT; + bbr->bbr_probe_rtt_app_limited_until = bbr->bbr_last_sent_packno; } if (bbr->bbr_mode == BBR_MODE_PROBE_RTT) @@ -1049,6 +1092,45 @@ lsquic_bbr_end_ack (void *cong_ctl, uint64_t in_flight) calculate_pacing_rate(bbr); calculate_cwnd(bbr, bytes_acked, excess_acked); calculate_recovery_window(bbr, bytes_acked, bytes_lost, in_flight); + + LSQ_DEBUG("BBR: mode=%s pacing_rate=%"PRIu64" bps bw=%"PRIu64" bps" + " pacing_gain=%.3f cwnd=%"PRIu64" cwnd_gain=%.3f" + " srtt=%"PRIu64" us min_rtt=%"PRIu64" us" + " round=%"PRIu64" cycle_off=%u in_flight=%"PRIu64 + " acked=%"PRIu64" lost=%"PRIu64" recovery=%d full_bw=%s", + mode2str[bbr->bbr_mode], + BW_VALUE(&bbr->bbr_pacing_rate), + minmax_get(&bbr->bbr_max_bandwidth), + bbr->bbr_pacing_gain, + lsquic_bbr_get_cwnd(cong_ctl), + bbr->bbr_cwnd_gain, + lsquic_rtt_stats_get_srtt(bbr->bbr_rtt_stats), + bbr->bbr_min_rtt, + bbr->bbr_round_count, + bbr->bbr_cycle_current_offset, + in_flight, + bytes_acked, + bytes_lost, + (int) bbr->bbr_recovery_state, + (bbr->bbr_flags & BBR_FLAG_IS_AT_FULL_BANDWIDTH) ? "yes" : "no"); +} + + +static int +bbr_copilot_bw_probe_fill_wanted (void *cong_ctl) +{ + struct lsquic_bbr *const bbr = cong_ctl; + + if (bbr->bbr_pacing_gain <= 1.0) + return 0; + return 1; +} + + +static int +lsquic_bbr_bw_probe_fill_wanted (void *cong_ctl) +{ + return 0; } @@ -1085,4 +1167,25 @@ const struct cong_ctl_if lsquic_cong_bbr_if = .cci_timeout = lsquic_bbr_timeout, .cci_sent = lsquic_bbr_sent, .cci_was_quiet = lsquic_bbr_was_quiet, + .cci_bw_probe_fill_wanted = lsquic_bbr_bw_probe_fill_wanted, +}; + + +const struct cong_ctl_if lsquic_cong_bbr_copilot_if = +{ + .cci_ack = bbr_copilot_ack, + .cci_begin_ack = lsquic_bbr_begin_ack, + .cci_end_ack = lsquic_bbr_end_ack, + .cci_cleanup = lsquic_bbr_cleanup, + .cci_get_cwnd = lsquic_bbr_get_cwnd, + .cci_init = lsquic_bbr_init, + .cci_pacing_rate = lsquic_bbr_pacing_rate, + .cci_loss = lsquic_bbr_loss, + .cci_lost = lsquic_bbr_lost, + .cci_process_bw_sample = lsquic_bbr_process_bw_sample, + .cci_reinit = lsquic_bbr_reinit, + .cci_timeout = lsquic_bbr_timeout, + .cci_sent = bbr_copilot_sent, + .cci_was_quiet = lsquic_bbr_was_quiet, + .cci_bw_probe_fill_wanted = bbr_copilot_bw_probe_fill_wanted, }; diff --git a/src/liblsquic/lsquic_bbr.h b/src/liblsquic/lsquic_bbr.h index fc62929a..7ec3942f 100644 --- a/src/liblsquic/lsquic_bbr.h +++ b/src/liblsquic/lsquic_bbr.h @@ -83,6 +83,8 @@ struct lsquic_bbr = 1 << 15, // When true, disables packet conservation in STARTUP. BBR_FLAG_RATE_BASED_STARTUP = 1 << 16, + + BBR_BW_SAMPLE_INVALID_PROBE_RTT = 1 << 17, } bbr_flags; // Number of round-trips in PROBE_BW mode, used for determining the current @@ -202,8 +204,16 @@ struct lsquic_bbr uint64_t in_flight; int has_losses; } bbr_ack_state; + + /* Packets sent up to and including this packno (and later acked) will + * be marked app-limited in their po_bwp_state. Used to mark ProbeRTT + * period packets as app-limited for bandwidth sampling purposes. + * When a packet with packno > this value is acked, the phase ends. + */ + lsquic_packno_t bbr_probe_rtt_app_limited_until; }; extern const struct cong_ctl_if lsquic_cong_bbr_if; +extern const struct cong_ctl_if lsquic_cong_bbr_copilot_if; #endif diff --git a/src/liblsquic/lsquic_cong_ctl.h b/src/liblsquic/lsquic_cong_ctl.h index 6a53b39c..ec14755f 100644 --- a/src/liblsquic/lsquic_cong_ctl.h +++ b/src/liblsquic/lsquic_cong_ctl.h @@ -70,6 +70,9 @@ struct cong_ctl_if void (*cci_cleanup) (void *cong_ctl); + + int + (*cci_bw_probe_fill_wanted) (void *cong_ctl); }; #endif diff --git a/src/liblsquic/lsquic_cubic.c b/src/liblsquic/lsquic_cubic.c index 39981531..8af4957d 100644 --- a/src/liblsquic/lsquic_cubic.c +++ b/src/liblsquic/lsquic_cubic.c @@ -278,6 +278,12 @@ lsquic_cubic_pacing_rate (void *cong_ctl, int in_recovery) } +static int +lsquic_cubic_bw_probe_fill_wanted (void *cong_ctl) +{ + return 0; +} + const struct cong_ctl_if lsquic_cong_cubic_if = { @@ -290,4 +296,5 @@ const struct cong_ctl_if lsquic_cong_cubic_if = .cci_reinit = lsquic_cubic_reinit, .cci_timeout = lsquic_cubic_timeout, .cci_was_quiet = lsquic_cubic_was_quiet, + .cci_bw_probe_fill_wanted = lsquic_cubic_bw_probe_fill_wanted, }; diff --git a/src/liblsquic/lsquic_engine.c b/src/liblsquic/lsquic_engine.c index 3d50c607..81849e1a 100644 --- a/src/liblsquic/lsquic_engine.c +++ b/src/liblsquic/lsquic_engine.c @@ -467,11 +467,12 @@ lsquic_engine_check_settings (const struct lsquic_engine_settings *settings, return -1; } - if (settings->es_cc_algo > 3) + if (settings->es_cc_algo < LSQUIC_CC_FIRST + || settings->es_cc_algo > LSQUIC_CC_LAST) { if (err_buf) snprintf(err_buf, err_buf_sz, "Invalid congestion control " - "algorithm value %u", settings->es_cc_algo); + "algorithm value %d", (int) settings->es_cc_algo); return -1; } @@ -515,6 +516,7 @@ lsquic_engine_check_settings (const struct lsquic_engine_settings *settings, (unsigned) UCHAR_MAX); return -1; } + #if LSQUIC_WEBTRANSPORT_SERVER_SUPPORT if(settings->es_webtransport_server) { diff --git a/src/liblsquic/lsquic_full_conn.c b/src/liblsquic/lsquic_full_conn.c index a2a07841..7cbebf08 100644 --- a/src/liblsquic/lsquic_full_conn.c +++ b/src/liblsquic/lsquic_full_conn.c @@ -3394,6 +3394,38 @@ full_conn_ci_user_stream_progress (struct lsquic_conn *lconn) } +static struct lsquic_packet_out * +full_conn_bw_probe_fill (void *conn_ctx, const struct network_path *path) +{ + struct full_conn *const conn = conn_ctx; + struct lsquic_packet_out *packet_out; + int sz; + + packet_out = lsquic_send_ctl_new_packet_out(&conn->fc_send_ctl, + 1, PNS_APP, path); + if (!packet_out) + return NULL; + + sz = conn->fc_conn.cn_pf->pf_gen_ping_frame( + packet_out->po_data + packet_out->po_data_sz, + lsquic_packet_out_avail(packet_out)); + if (sz < 0) { + ABORT_ERROR("gen_ping_frame failed"); + return NULL; + } + lsquic_send_ctl_incr_pack_sz(&conn->fc_send_ctl, packet_out, sz); + packet_out->po_frame_types |= 1 << QUIC_FRAME_PING; + LSQ_DEBUG("wrote PING frame"); + if (!(conn->fc_flags & FC_SERVER)) + log_conn_flow_control(conn); + + lsquic_packet_out_set_pns(packet_out, PNS_APP); + lsquic_packet_out_zero_pad(packet_out); + + return packet_out; +} + + static enum tick_st full_conn_ci_tick (lsquic_conn_t *lconn, lsquic_time_t now) { @@ -3582,7 +3614,8 @@ full_conn_ci_tick (lsquic_conn_t *lconn, lsquic_time_t now) if (!handshake_done_or_doing_sess_resume(conn)) { process_hsk_stream_write_events(conn); - lsquic_send_ctl_maybe_app_limited(&conn->fc_send_ctl, &conn->fc_path); + lsquic_send_ctl_maybe_app_limited(&conn->fc_send_ctl, &conn->fc_path, + NULL, NULL); goto end_write; } @@ -3608,7 +3641,8 @@ full_conn_ci_tick (lsquic_conn_t *lconn, lsquic_time_t now) if (!TAILQ_EMPTY(&conn->fc_pub.write_streams)) process_streams_write_events(conn, 0); - lsquic_send_ctl_maybe_app_limited(&conn->fc_send_ctl, &conn->fc_path); + lsquic_send_ctl_maybe_app_limited(&conn->fc_send_ctl, &conn->fc_path, + full_conn_bw_probe_fill, conn); end_write: @@ -4366,6 +4400,7 @@ full_conn_ci_set_param (lsquic_conn_t *lconn, enum lsquic_conn_param param, { struct full_conn *conn = (struct full_conn *) lconn; uint64_t rate; + enum lsquic_cc cc_algo; int enable_bw_sampler; switch (param) @@ -4385,6 +4420,11 @@ full_conn_ci_set_param (lsquic_conn_t *lconn, enum lsquic_conn_param param, LSQ_INFO("bw sampler %s", enable_bw_sampler ? "enabled" : "disabled"); return 0; + case LSQCP_CC_ALGO: + if (value_len != sizeof(enum lsquic_cc)) + return -1; + memcpy(&cc_algo, value, sizeof(cc_algo)); + return lsquic_send_ctl_set_cc_algo(&conn->fc_send_ctl, cc_algo); default: return -1; } @@ -4397,6 +4437,7 @@ full_conn_ci_get_param (lsquic_conn_t *lconn, enum lsquic_conn_param param, { struct full_conn *conn = (struct full_conn *) lconn; uint64_t rate; + enum lsquic_cc cc_algo; int enable_bw_sampler; switch (param) @@ -4416,6 +4457,13 @@ full_conn_ci_get_param (lsquic_conn_t *lconn, enum lsquic_conn_param param, memcpy(value, &enable_bw_sampler, sizeof(enable_bw_sampler)); *value_len = sizeof(enable_bw_sampler); return 0; + case LSQCP_CC_ALGO: + if (*value_len < sizeof(enum lsquic_cc)) + return -1; + cc_algo = lsquic_send_ctl_get_cc_algo(&conn->fc_send_ctl); + memcpy(value, &cc_algo, sizeof(cc_algo)); + *value_len = sizeof(cc_algo); + return 0; default: return -1; } diff --git a/src/liblsquic/lsquic_full_conn_ietf.c b/src/liblsquic/lsquic_full_conn_ietf.c index e8333434..2870d99a 100644 --- a/src/liblsquic/lsquic_full_conn_ietf.c +++ b/src/liblsquic/lsquic_full_conn_ietf.c @@ -8196,6 +8196,38 @@ ietf_full_conn_ci_user_stream_progress (struct lsquic_conn *lconn) } +static struct lsquic_packet_out * +ietf_full_conn_bw_probe_fill (void *conn_ctx, const struct network_path *path) +{ + struct ietf_full_conn *const conn = conn_ctx; + struct lsquic_packet_out *packet_out; + int sz; + + packet_out = lsquic_send_ctl_new_packet_out(&conn->ifc_send_ctl, + 1, PNS_APP, path); + if (!packet_out) + return NULL; + + sz = conn->ifc_conn.cn_pf->pf_gen_ping_frame( + packet_out->po_data + packet_out->po_data_sz, + lsquic_packet_out_avail(packet_out)); + if (sz < 0) { + ABORT_ERROR("gen_ping_frame failed"); + return NULL; + } + lsquic_send_ctl_incr_pack_sz(&conn->ifc_send_ctl, packet_out, sz); + packet_out->po_frame_types |= 1 << QUIC_FRAME_PING; + LSQ_DEBUG("wrote PING frame"); + if (!(conn->ifc_flags & IFC_SERVER)) + log_conn_flow_control(conn); + + lsquic_packet_out_set_pns(packet_out, PNS_APP); + lsquic_packet_out_zero_pad(packet_out); + + return packet_out; +} + + static enum tick_st ietf_full_conn_ci_tick (struct lsquic_conn *lconn, lsquic_time_t now) { @@ -8360,7 +8392,7 @@ ietf_full_conn_ci_tick (struct lsquic_conn *lconn, lsquic_time_t now) if (!(conn->ifc_mflags & MF_DOING_0RTT)) { lsquic_send_ctl_maybe_app_limited(&conn->ifc_send_ctl, - CUR_NPATH(conn)); + CUR_NPATH(conn), NULL, NULL); goto end_write; } } @@ -8391,7 +8423,8 @@ ietf_full_conn_ci_tick (struct lsquic_conn *lconn, lsquic_time_t now) if (!TAILQ_EMPTY(&conn->ifc_pub.write_streams)) process_streams_write_events(conn, 0); - lsquic_send_ctl_maybe_app_limited(&conn->ifc_send_ctl, CUR_NPATH(conn)); + lsquic_send_ctl_maybe_app_limited(&conn->ifc_send_ctl, CUR_NPATH(conn), + ietf_full_conn_bw_probe_fill, conn); end_write: if ((conn->ifc_flags & IFC_CLOSING) @@ -8827,6 +8860,7 @@ ietf_full_conn_ci_set_param (lsquic_conn_t *lconn, enum lsquic_conn_param param, { struct ietf_full_conn *conn = (struct ietf_full_conn *) lconn; uint64_t rate; + enum lsquic_cc cc_algo; int enable_bw_sampler; switch (param) @@ -8846,6 +8880,11 @@ ietf_full_conn_ci_set_param (lsquic_conn_t *lconn, enum lsquic_conn_param param, LSQ_INFO("bw sampler %s", enable_bw_sampler ? "enabled" : "disabled"); return 0; + case LSQCP_CC_ALGO: + if (value_len != sizeof(enum lsquic_cc)) + return -1; + memcpy(&cc_algo, value, sizeof(cc_algo)); + return lsquic_send_ctl_set_cc_algo(&conn->ifc_send_ctl, cc_algo); default: return -1; } @@ -8858,6 +8897,7 @@ ietf_full_conn_ci_get_param (lsquic_conn_t *lconn, enum lsquic_conn_param param, { struct ietf_full_conn *conn = (struct ietf_full_conn *) lconn; uint64_t rate; + enum lsquic_cc cc_algo; int enable_bw_sampler; switch (param) @@ -8877,6 +8917,13 @@ ietf_full_conn_ci_get_param (lsquic_conn_t *lconn, enum lsquic_conn_param param, memcpy(value, &enable_bw_sampler, sizeof(enable_bw_sampler)); *value_len = sizeof(enable_bw_sampler); return 0; + case LSQCP_CC_ALGO: + if (*value_len < sizeof(enum lsquic_cc)) + return -1; + cc_algo = lsquic_send_ctl_get_cc_algo(&conn->ifc_send_ctl); + memcpy(value, &cc_algo, sizeof(cc_algo)); + *value_len = sizeof(cc_algo); + return 0; default: return -1; } diff --git a/src/liblsquic/lsquic_packet_out.h b/src/liblsquic/lsquic_packet_out.h index c652080b..9bece410 100644 --- a/src/liblsquic/lsquic_packet_out.h +++ b/src/liblsquic/lsquic_packet_out.h @@ -134,6 +134,7 @@ typedef struct lsquic_packet_out PO_LOST = (1 <<29), /* On lost queue */ #define POSPIN_SHIFT 30 PO_SPIN_BIT = (1 <<30), /* Value of the spin bit */ + PO_BW_PROBE_FILL = (1u <<31), /* Extra data to improve bandwidth estimation accuracy */ } po_flags; unsigned short po_data_sz; /* Number of usable bytes in data */ unsigned short po_enc_data_sz; /* Number of usable bytes in data */ diff --git a/src/liblsquic/lsquic_send_ctl.c b/src/liblsquic/lsquic_send_ctl.c index 77f34d49..915b28eb 100644 --- a/src/liblsquic/lsquic_send_ctl.c +++ b/src/liblsquic/lsquic_send_ctl.c @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -451,6 +452,63 @@ send_ctl_pick_initial_packno (struct lsquic_send_ctl *ctl) } +static const struct +{ + const struct cong_ctl_if *ccd_if; + size_t ccd_off; + const char *ccd_name; +} cong_ctl_descs[] = +{ + [LSQUIC_CC_CUBIC] = { + &lsquic_cong_cubic_if, + offsetof(struct adaptive_cc, acc_cubic), + "Cubic", + }, + [LSQUIC_CC_BBR] = { + &lsquic_cong_bbr_if, + offsetof(struct adaptive_cc, acc_bbr), + "BBRv1", + }, + [LSQUIC_CC_ADAPTIVE] = { + &lsquic_cong_adaptive_if, + 0, + "Adaptive", + }, + [LSQUIC_CC_BBR_COPILOT] = { + &lsquic_cong_bbr_copilot_if, + offsetof(struct adaptive_cc, acc_bbr), + "BBRv1-Copilot", + }, +}; + + +static void +send_ctl_use_cc_algo (struct lsquic_send_ctl *ctl, enum lsquic_cc cc_algo) +{ + if (cc_algo == LSQUIC_CC_DEFAULT) + cc_algo = LSQUIC_DF_CC_ALGO; + + ctl->sc_ci = cong_ctl_descs[cc_algo].ccd_if; + ctl->sc_cong_ctl = (char *) &ctl->sc_adaptive_cc + + cong_ctl_descs[cc_algo].ccd_off; +} + + +enum lsquic_cc +lsquic_send_ctl_get_cc_algo (const lsquic_send_ctl_t *ctl) +{ + enum lsquic_cc cc_algo; + + for (cc_algo = LSQUIC_CC_CUBIC; cc_algo < sizeof(cong_ctl_descs) + / sizeof(cong_ctl_descs[0]); ++cc_algo) + if (cong_ctl_descs[cc_algo].ccd_if == ctl->sc_ci) + return cc_algo; + + assert(0); + return LSQUIC_CC_DEFAULT; +} + + void lsquic_send_ctl_init (lsquic_send_ctl_t *ctl, struct lsquic_alarmset *alset, struct lsquic_engine_public *enpub, const struct ver_neg *ver_neg, @@ -492,22 +550,7 @@ lsquic_send_ctl_init (lsquic_send_ctl_t *ctl, struct lsquic_alarmset *alset, if (!(ctl->sc_conn_pub->lconn->cn_flags & LSCONN_SERVER)) ctl->sc_senhist.sh_last_sent = ctl->sc_cur_packno; #endif - switch (enpub->enp_settings.es_cc_algo) - { - case 1: - ctl->sc_ci = &lsquic_cong_cubic_if; - ctl->sc_cong_ctl = &ctl->sc_adaptive_cc.acc_cubic; - break; - case 2: - ctl->sc_ci = &lsquic_cong_bbr_if; - ctl->sc_cong_ctl = &ctl->sc_adaptive_cc.acc_bbr; - break; - case 3: - default: - ctl->sc_ci = &lsquic_cong_adaptive_if; - ctl->sc_cong_ctl = &ctl->sc_adaptive_cc; - break; - } + send_ctl_use_cc_algo(ctl, enpub->enp_settings.es_cc_algo); if (enpub->enp_settings.es_enable_bw_sampler) ctl->sc_flags |= SC_KEEP_BW_SAMPLER; send_ctl_apply_bw_sampler_policy(ctl); @@ -541,6 +584,66 @@ lsquic_send_ctl_init (lsquic_send_ctl_t *ctl, struct lsquic_alarmset *alset, } +static int +is_cc_bbr (enum lsquic_cc cc) +{ + return cc == LSQUIC_CC_BBR || cc == LSQUIC_CC_BBR_COPILOT; +} + + +int +lsquic_send_ctl_set_cc_algo (lsquic_send_ctl_t *ctl, enum lsquic_cc cc_algo) +{ + enum lsquic_cc old_cc_algo; + + if (cc_algo == LSQUIC_CC_DEFAULT) + cc_algo = LSQUIC_DF_CC_ALGO; + else if (cc_algo < LSQUIC_CC_CUBIC || cc_algo > LSQUIC_CC_LAST) + return -1; + + old_cc_algo = lsquic_send_ctl_get_cc_algo(ctl); + if (old_cc_algo == cc_algo) + { + LSQ_INFO("cc is already %s: the switch is a noop", + cong_ctl_descs[cc_algo].ccd_name); + return 0; + } + + if (is_cc_bbr(old_cc_algo) && is_cc_bbr(cc_algo)) + send_ctl_use_cc_algo(ctl, cc_algo); + else if (old_cc_algo == LSQUIC_CC_ADAPTIVE) + { + if (cc_algo == LSQUIC_CC_CUBIC) + ctl->sc_flags |= SC_CLEANUP_BBR; + else + { + lsquic_cong_cubic_if.cci_cleanup( + &ctl->sc_adaptive_cc.acc_cubic); + ctl->sc_flags &= ~SC_CLEANUP_BBR; + } + send_ctl_use_cc_algo(ctl, cc_algo); + send_ctl_apply_bw_sampler_policy(ctl); + } + else + { + ctl->sc_ci->cci_cleanup(CGP(ctl)); + if (ctl->sc_flags & SC_CLEANUP_BBR) + lsquic_cong_bbr_if.cci_cleanup(&ctl->sc_adaptive_cc.acc_bbr); + + ctl->sc_flags &= ~SC_CLEANUP_BBR; + memset(&ctl->sc_adaptive_cc, 0, sizeof(ctl->sc_adaptive_cc)); + send_ctl_use_cc_algo(ctl, cc_algo); + send_ctl_apply_bw_sampler_policy(ctl); + ctl->sc_ci->cci_init(CGP(ctl), ctl->sc_conn_pub); + } + + LSQ_INFO("switched congestion controller from %s to %s", + cong_ctl_descs[old_cc_algo].ccd_name, + cong_ctl_descs[cc_algo].ccd_name); + return 0; +} + + static lsquic_time_t calculate_packet_rto (lsquic_send_ctl_t *ctl) { @@ -637,7 +740,8 @@ set_retx_alarm (struct lsquic_send_ctl *ctl, enum packnum_space pns, lsquic_alarmset_set(ctl->sc_alset, AL_RETX_INIT + pns, now + delay); if (PNS_APP == pns - && ctl->sc_ci == &lsquic_cong_bbr_if + && (ctl->sc_ci == &lsquic_cong_bbr_if + || ctl->sc_ci == &lsquic_cong_bbr_copilot_if) && lsquic_alarmset_is_inited(ctl->sc_alset, AL_PACK_TOL) && !lsquic_alarmset_is_set(ctl->sc_alset, AL_PACK_TOL)) lsquic_alarmset_set(ctl->sc_alset, AL_PACK_TOL, now + delay); @@ -1201,7 +1305,8 @@ send_ctl_handle_regular_lost_packet (struct lsquic_send_ctl *ctl, lsquic_send_ctl_disable_ecn(ctl); } - if (packet_out->po_frame_types & ctl->sc_retx_frames) + if ((packet_out->po_frame_types & ctl->sc_retx_frames) && + (packet_out->po_flags & PO_BW_PROBE_FILL) == 0) { LSQ_DEBUG("lost retransmittable packet #%"PRIu64, packet_out->po_packno); @@ -1970,9 +2075,37 @@ send_ctl_pacing_capped (const struct lsquic_send_ctl *ctl) void lsquic_send_ctl_maybe_app_limited (struct lsquic_send_ctl *ctl, - const struct network_path *path) + const struct network_path *path, + lsquic_bw_probe_fill_f bw_probe_fill_cb, + void *conn_ctx) { const struct lsquic_packet_out *packet_out; + struct lsquic_packet_out *probe_packet; + unsigned num_probing = 0; + + if (ctl->sc_ci->cci_bw_probe_fill_wanted(CGP(ctl)) && bw_probe_fill_cb) + { + while (ctl->sc_ci->cci_bw_probe_fill_wanted(CGP(ctl)) + && lsquic_send_ctl_can_send(ctl)) + { + probe_packet = bw_probe_fill_cb(conn_ctx, path); + if (!probe_packet) + { + LSQ_DEBUG("cannot get new packet to improve bandwidth estimation accuracy"); + break; + } + probe_packet->po_flags |= PO_BW_PROBE_FILL; + lsquic_send_ctl_scheduled_one(ctl, probe_packet); + ++num_probing; + } + + if (num_probing > 0) + { + LSQ_DEBUG("sent %u extra packet%s to probe bandwidth", + num_probing, num_probing != 1 ? "s" : ""); + return; + } + } packet_out = lsquic_send_ctl_last_scheduled(ctl, PNS_APP, path, 0); if ((packet_out && lsquic_packet_out_avail(packet_out) > 10) diff --git a/src/liblsquic/lsquic_send_ctl.h b/src/liblsquic/lsquic_send_ctl.h index 988f59cd..041a6c02 100644 --- a/src/liblsquic/lsquic_send_ctl.h +++ b/src/liblsquic/lsquic_send_ctl.h @@ -4,6 +4,7 @@ #include +#include "lsquic.h" #include "lsquic_types.h" #include "lsquic_packet_out.h" @@ -156,6 +157,12 @@ lsquic_send_ctl_init (lsquic_send_ctl_t *, struct lsquic_alarmset *, struct lsquic_engine_public *, const struct ver_neg *, struct lsquic_conn_public *, enum send_ctl_flags); +int +lsquic_send_ctl_set_cc_algo (lsquic_send_ctl_t *, enum lsquic_cc cc_algo); + +enum lsquic_cc +lsquic_send_ctl_get_cc_algo (const lsquic_send_ctl_t *); + int lsquic_send_ctl_sent_packet (lsquic_send_ctl_t *, struct lsquic_packet_out *); @@ -460,9 +467,14 @@ lsquic_send_ctl_1rtt_acked (const struct lsquic_send_ctl *ctl) return (ctl)->sc_flags & SC_1RTT_ACKED; } +typedef struct lsquic_packet_out * +(*lsquic_bw_probe_fill_f)(void *conn_ctx, const struct network_path *path); + void lsquic_send_ctl_maybe_app_limited (struct lsquic_send_ctl *, - const struct network_path *); + const struct network_path *, + lsquic_bw_probe_fill_f bw_probe_fill_cb, + void *conn_ctx); static inline void lsquic_send_ctl_do_ql_bits (struct lsquic_send_ctl *ctl) diff --git a/tests/test_h3_framing.c b/tests/test_h3_framing.c index 4f23775f..f9e914a0 100644 --- a/tests/test_h3_framing.c +++ b/tests/test_h3_framing.c @@ -437,7 +437,7 @@ init_test_objs (struct test_objs *tobjs, unsigned initial_conn_window, tobjs->conn_pub.conn_stats = &s_conn_stats; #endif tobjs->initial_stream_window = initial_stream_window; - tobjs->eng_pub.enp_settings.es_cc_algo = 1; /* Cubic */ + tobjs->eng_pub.enp_settings.es_cc_algo = LSQUIC_CC_CUBIC; tobjs->eng_pub.enp_hsi_if = &tobjs->hsi_if; lsquic_send_ctl_init(&tobjs->send_ctl, &tobjs->alset, &tobjs->eng_pub, &tobjs->ver_neg, &tobjs->conn_pub, 0); diff --git a/tests/test_send_ctl_accounting.c b/tests/test_send_ctl_accounting.c index 651a6155..a63ebb09 100644 --- a/tests/test_send_ctl_accounting.c +++ b/tests/test_send_ctl_accounting.c @@ -71,7 +71,7 @@ init_test (struct accounting_test *t) t->lconn.cn_esf_c = &lsquic_enc_session_common_ietf_v1; lsquic_engine_init_settings(&t->enpub.enp_settings, 0); - t->enpub.enp_settings.es_cc_algo = 1; /* Cubic */ + t->enpub.enp_settings.es_cc_algo = LSQUIC_CC_CUBIC; lsquic_mm_init(&t->enpub.enp_mm); lsquic_alarmset_init(&t->alset, 0); diff --git a/tests/test_send_ctl_bw_lifecycle.c b/tests/test_send_ctl_bw_lifecycle.c index 0b2c6573..3cc26c81 100644 --- a/tests/test_send_ctl_bw_lifecycle.c +++ b/tests/test_send_ctl_bw_lifecycle.c @@ -21,6 +21,7 @@ #include "lsquic_stream.h" #include "lsquic_mm.h" #include "lsquic_conn_public.h" +#include "lsquic_cong_ctl.h" #include "lsquic_parse.h" #include "lsquic_conn.h" #include "lsquic_engine_public.h" @@ -52,7 +53,7 @@ struct bw_lifecycle_test static void -init_test (struct bw_lifecycle_test *t, unsigned cc_algo, +init_test (struct bw_lifecycle_test *t, enum lsquic_cc cc_algo, unsigned cc_rtt_thresh, int enable_bw_sampler) { /* Build a minimal send_ctl environment with configurable CC policy. */ @@ -141,7 +142,7 @@ test_cubic_lazy_enable (void) struct bw_lifecycle_test t; struct lsquic_packet_out *packet_out; - init_test(&t, 1, 100000, 0); + init_test(&t, LSQUIC_CC_CUBIC, 100000, 0); assert(t.send_ctl.sc_ci == &lsquic_cong_cubic_if); assert(!(t.send_ctl.sc_flags & SC_BW_SAMPLER_INIT)); @@ -169,7 +170,7 @@ test_adaptive_switch_without_info_drops_sampler (void) struct bw_lifecycle_test t; struct lsquic_packet_out *packet_out; - init_test(&t, 3, 100000, 0); + init_test(&t, LSQUIC_CC_ADAPTIVE, 100000, 0); assert(t.send_ctl.sc_ci == &lsquic_cong_adaptive_if); assert(t.send_ctl.sc_flags & SC_BW_SAMPLER_INIT); @@ -182,6 +183,7 @@ test_adaptive_switch_without_info_drops_sampler (void) ack_one(&t, 1, 2000, 2000, 1); assert(t.send_ctl.sc_ci == &lsquic_cong_cubic_if); + assert(LSQUIC_CC_CUBIC == lsquic_send_ctl_get_cc_algo(&t.send_ctl)); assert(!(t.send_ctl.sc_flags & SC_BW_SAMPLER_INIT)); cleanup_test(&t); @@ -195,7 +197,7 @@ test_adaptive_switch_with_info_keeps_sampler (void) struct bw_lifecycle_test t; struct lsquic_packet_out *packet_out; - init_test(&t, 3, 100000, 0); + init_test(&t, LSQUIC_CC_ADAPTIVE, 100000, 0); (void) lsquic_send_ctl_get_bw(&t.send_ctl); assert(t.send_ctl.sc_flags & SC_KEEP_BW_SAMPLER); @@ -225,7 +227,7 @@ test_drop_clears_inflight_packet_states (void) struct bw_lifecycle_test t; struct lsquic_packet_out *packet_out_1, *packet_out_2; - init_test(&t, 3, 100000, 0); + init_test(&t, LSQUIC_CC_ADAPTIVE, 100000, 0); packet_out_1 = new_packet(&t, 1, 1000); packet_out_2 = new_packet(&t, 2, 1200); @@ -250,7 +252,7 @@ test_reinit_after_drop_via_get_bw (void) struct bw_lifecycle_test t; struct lsquic_packet_out *packet_out; - init_test(&t, 3, 100000, 0); + init_test(&t, LSQUIC_CC_ADAPTIVE, 100000, 0); packet_out = new_packet(&t, 1, 1000); assert(0 == lsquic_send_ctl_sent_packet(&t.send_ctl, packet_out)); @@ -271,7 +273,7 @@ test_engine_setting_enables_sampler (void) struct bw_lifecycle_test t; struct lsquic_packet_out *packet_out; - init_test(&t, 1, 100000, 1); + init_test(&t, LSQUIC_CC_CUBIC, 100000, 1); assert(t.send_ctl.sc_ci == &lsquic_cong_cubic_if); assert(t.send_ctl.sc_flags & SC_KEEP_BW_SAMPLER); @@ -292,7 +294,7 @@ test_engine_setting_keeps_sampler_across_adaptive_to_cubic (void) struct bw_lifecycle_test t; struct lsquic_packet_out *packet_out; - init_test(&t, 3, 100000, 1); + init_test(&t, LSQUIC_CC_ADAPTIVE, 100000, 1); assert(t.send_ctl.sc_ci == &lsquic_cong_adaptive_if); assert(t.send_ctl.sc_flags & SC_KEEP_BW_SAMPLER); @@ -316,6 +318,140 @@ test_engine_setting_keeps_sampler_across_adaptive_to_cubic (void) } +static void +test_cc_algo_selection (void) +{ + struct bw_lifecycle_test t; + + init_test(&t, LSQUIC_CC_CUBIC, 100000, 0); + assert(LSQUIC_CC_CUBIC == lsquic_send_ctl_get_cc_algo(&t.send_ctl)); + assert(t.send_ctl.sc_ci == &lsquic_cong_cubic_if); + assert(0 == t.send_ctl.sc_ci->cci_bw_probe_fill_wanted( + t.send_ctl.sc_cong_ctl)); + cleanup_test(&t); + + init_test(&t, LSQUIC_CC_BBR, 100000, 0); + assert(LSQUIC_CC_BBR == lsquic_send_ctl_get_cc_algo(&t.send_ctl)); + assert(t.send_ctl.sc_ci == &lsquic_cong_bbr_if); + assert(0 == t.send_ctl.sc_ci->cci_bw_probe_fill_wanted( + t.send_ctl.sc_cong_ctl)); + cleanup_test(&t); + + init_test(&t, LSQUIC_CC_ADAPTIVE, 100000, 0); + assert(LSQUIC_CC_ADAPTIVE == lsquic_send_ctl_get_cc_algo(&t.send_ctl)); + assert(t.send_ctl.sc_ci == &lsquic_cong_adaptive_if); + assert(0 == t.send_ctl.sc_ci->cci_bw_probe_fill_wanted( + t.send_ctl.sc_cong_ctl)); + cleanup_test(&t); + + init_test(&t, LSQUIC_CC_BBR_COPILOT, 100000, 0); + assert(LSQUIC_CC_BBR_COPILOT + == lsquic_send_ctl_get_cc_algo(&t.send_ctl)); + assert(t.send_ctl.sc_ci == &lsquic_cong_bbr_copilot_if); + assert(1 == t.send_ctl.sc_ci->cci_bw_probe_fill_wanted( + t.send_ctl.sc_cong_ctl)); + cleanup_test(&t); +} + + +static void +test_bbr_copilot_switch_preserves_state (void) +{ + struct bw_lifecycle_test t; + + init_test(&t, LSQUIC_CC_BBR, 100000, 0); + t.send_ctl.sc_adaptive_cc.acc_bbr.bbr_pacing_gain = 1.25; + + assert(0 == lsquic_send_ctl_set_cc_algo(&t.send_ctl, + LSQUIC_CC_BBR_COPILOT)); + assert(LSQUIC_CC_BBR_COPILOT + == lsquic_send_ctl_get_cc_algo(&t.send_ctl)); + assert(t.send_ctl.sc_ci == &lsquic_cong_bbr_copilot_if); + assert(1.25 == t.send_ctl.sc_adaptive_cc.acc_bbr.bbr_pacing_gain); + assert(1 == t.send_ctl.sc_ci->cci_bw_probe_fill_wanted( + t.send_ctl.sc_cong_ctl)); + + assert(0 == lsquic_send_ctl_set_cc_algo(&t.send_ctl, LSQUIC_CC_BBR)); + assert(LSQUIC_CC_BBR == lsquic_send_ctl_get_cc_algo(&t.send_ctl)); + assert(t.send_ctl.sc_ci == &lsquic_cong_bbr_if); + assert(1.25 == t.send_ctl.sc_adaptive_cc.acc_bbr.bbr_pacing_gain); + assert(0 == t.send_ctl.sc_ci->cci_bw_probe_fill_wanted( + t.send_ctl.sc_cong_ctl)); + + cleanup_test(&t); +} + + +static void +test_adaptive_switch_preserves_state (void) +{ + struct bw_lifecycle_test t; + uint64_t cwnd; + + init_test(&t, LSQUIC_CC_ADAPTIVE, 100000, 0); + cwnd = t.send_ctl.sc_adaptive_cc.acc_cubic.cu_cwnd + 1234; + t.send_ctl.sc_adaptive_cc.acc_cubic.cu_cwnd = cwnd; + assert(0 == lsquic_send_ctl_set_cc_algo(&t.send_ctl, + LSQUIC_CC_CUBIC)); + assert(cwnd == t.send_ctl.sc_adaptive_cc.acc_cubic.cu_cwnd); + assert(t.send_ctl.sc_cong_ctl + == &t.send_ctl.sc_adaptive_cc.acc_cubic); + assert(t.send_ctl.sc_flags & SC_CLEANUP_BBR); + cleanup_test(&t); + + init_test(&t, LSQUIC_CC_ADAPTIVE, 100000, 0); + t.send_ctl.sc_adaptive_cc.acc_bbr.bbr_pacing_gain = 1.25; + assert(0 == lsquic_send_ctl_set_cc_algo(&t.send_ctl, + LSQUIC_CC_BBR_COPILOT)); + assert(1.25 == t.send_ctl.sc_adaptive_cc.acc_bbr.bbr_pacing_gain); + assert(t.send_ctl.sc_cong_ctl == &t.send_ctl.sc_adaptive_cc.acc_bbr); + assert(0 == (t.send_ctl.sc_flags & SC_CLEANUP_BBR)); + cleanup_test(&t); +} + + +static void +test_cc_algo_family_switch (void) +{ + struct bw_lifecycle_test t; + + init_test(&t, LSQUIC_CC_CUBIC, 100000, 0); + assert(0 == lsquic_send_ctl_set_cc_algo(&t.send_ctl, + LSQUIC_CC_BBR_COPILOT)); + assert(t.send_ctl.sc_ci == &lsquic_cong_bbr_copilot_if); + assert(t.send_ctl.sc_flags & SC_BW_SAMPLER_INIT); + + t.send_ctl.sc_adaptive_cc.acc_cubic.cu_cwnd = 1; + t.send_ctl.sc_adaptive_cc.acc_flags |= ACC_CUBIC; + assert(0 == lsquic_send_ctl_set_cc_algo(&t.send_ctl, + LSQUIC_CC_ADAPTIVE)); + assert(t.send_ctl.sc_ci == &lsquic_cong_adaptive_if); + assert(0 == t.send_ctl.sc_adaptive_cc.acc_flags); + assert(t.send_ctl.sc_adaptive_cc.acc_cubic.cu_cwnd > 1); + + assert(-1 == lsquic_send_ctl_set_cc_algo(&t.send_ctl, + (enum lsquic_cc) 5)); + assert(LSQUIC_CC_ADAPTIVE == lsquic_send_ctl_get_cc_algo(&t.send_ctl)); + assert(t.send_ctl.sc_ci == &lsquic_cong_adaptive_if); + + cleanup_test(&t); +} + + +static void +test_cc_algo_default (void) +{ + struct bw_lifecycle_test t; + + init_test(&t, LSQUIC_CC_CUBIC, 100000, 0); + assert(0 == lsquic_send_ctl_set_cc_algo(&t.send_ctl, + LSQUIC_CC_DEFAULT)); + assert(LSQUIC_DF_CC_ALGO + == lsquic_send_ctl_get_cc_algo(&t.send_ctl)); + cleanup_test(&t); +} + + int main (void) { @@ -327,5 +463,10 @@ main (void) test_reinit_after_drop_via_get_bw(); test_engine_setting_enables_sampler(); test_engine_setting_keeps_sampler_across_adaptive_to_cubic(); + test_cc_algo_selection(); + test_bbr_copilot_switch_preserves_state(); + test_adaptive_switch_preserves_state(); + test_cc_algo_family_switch(); + test_cc_algo_default(); return EXIT_SUCCESS; }