Files
utun2/tests/test_etcp_bbr.c
T

379 lines
19 KiB
C

#include "etcp_bbr.h"
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include "../lib/debug_config.h"
#define MSEG(s) do { test_total++; printf("TEST %d: %-50s ", test_total, s); fflush(stdout); } while(0)
#define PASS() do { puts("PASS"); test_passed++; } while(0)
#define FAIL(m) do { printf("FAIL: %s\n", m); test_failed++; return -1; } while(0)
static int test_total = 0;
static int test_passed = 0;
static int test_failed = 0;
static uint32_t g_cwnd;
static uint32_t g_pacing;
static uint32_t run_ack_ex(struct bbr* bbr, uint32_t acked, uint32_t rtt_us,
int app, int lost, uint32_t inflight, uint32_t prior_del)
{
bbr->now_tb += rtt_us / 100;
struct bbr_rate_sample rs = {
.delivered = acked, .interval_us = rtt_us, .rtt_us = rtt_us,
.acked_sacked = acked, .prior_delivered = prior_del,
.tx_in_flight = inflight > 0 ? inflight : acked,
.lost = lost, .is_app_limited = app,
};
bbr_main(bbr, &rs, &g_cwnd, &g_pacing, 1400, inflight, inflight >= g_cwnd);
return g_cwnd;
}
static uint32_t run_ack(struct bbr* bbr, uint32_t acked, uint32_t rtt_us,
int app, int lost, uint32_t inflight)
{ return run_ack_ex(bbr, acked, rtt_us, app, lost, inflight, bbr->delivered); }
/* --- tests --- */
/* Одинаковая доставка байтов обязана давать одну скорость независимо от MTU. */
static int test_byte_rate(void) {
MSEG("byte rate and BDP are independent of MSS");
const uint32_t sizes[] = {576, 1400};
for (unsigned i = 0; i < sizeof(sizes)/sizeof(sizes[0]); i++) {
struct bbr s = { .now_tb = 10000 }; bbr_init(&s);
s.mode = BBR_PROBE_BW; s.full_bw_reached = 1; s.cycle_idx = BBR_BW_PROBE_CRUISE;
s.cycle_mstamp_tb = s.now_tb; s.probe_wait_us = 2000000;
uint32_t cwnd = 100000, pacing = 0;
struct bbr_rate_sample rs = { .delivered = 10000, .interval_us = 10000, .rtt_us = 20000,
.acked_sacked = 1000, .tx_in_flight = 40000 };
bbr_main(&s, &rs, &cwnd, &pacing, sizes[i], 40000, 0);
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "BBR byte sample: MSS=%u delivered=10000 interval_us=10000 pace=%u cwnd=%u",
sizes[i], pacing, cwnd);
if (pacing != 990000) FAIL("1MB/s delivery must produce 990000B/s cruise pacing");
if (cwnd != 40000) FAIL("20ms queue budget at 20ms RTT must cap the window at 40000 bytes");
}
PASS(); return 0;
}
/* Байтовый Q16 сохраняет многогигабитные скорости; выход uint32 насыщается, а не оборачивается. */
static int test_large_rate(void) {
MSEG("multi-gigabit rate and pacing saturation");
const uint32_t intervals[] = {5, 1}, expected[] = {1980000000, UINT32_MAX};
for (unsigned i = 0; i < 2; i++) {
struct bbr s = { .now_tb = 10000 }; bbr_init(&s);
s.mode = BBR_PROBE_BW; s.full_bw_reached = 1; s.cycle_idx = BBR_BW_PROBE_CRUISE;
s.cycle_mstamp_tb = s.now_tb; s.probe_wait_us = 2000000;
uint32_t cwnd = 100000, pacing = 0;
struct bbr_rate_sample rs = { .delivered = 10000, .interval_us = intervals[i], .rtt_us = 20000,
.acked_sacked = 1000, .tx_in_flight = 40000 };
bbr_main(&s, &rs, &cwnd, &pacing, 1400, 40000, 0);
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "BBR high rate: interval_us=%u pacing=%u expected=%u", intervals[i], pacing, expected[i]);
if (pacing != expected[i]) FAIL("high rate was truncated or wrapped");
}
PASS(); return 0;
}
/* PROBE_UP должен добавлять один MSS за подтверждённое окно, а не один байт. */
static int test_probe_growth_bytes(void) {
MSEG("PROBE_UP grows one MSS per acknowledged window");
struct bbr s = { .now_tb = 10000 }; bbr_init(&s);
s.mode = BBR_PROBE_BW; s.full_bw_reached = 1; s.cycle_idx = BBR_BW_PROBE_REFILL;
s.inflight_hi = 56000; s.bw_hi[0] = (uint32_t)(560000ULL * BW_UNIT / 1000000);
s.min_rtt_us = s.probe_rtt_min_us = 100000; s.has_seen_rtt = 1;
uint32_t cwnd = 56000, pacing = 0;
struct bbr_rate_sample rs = { .delivered = 1400, .interval_us = 10000, .rtt_us = 100000,
.acked_sacked = 1400, .tx_in_flight = 56000 };
bbr_main(&s, &rs, &cwnd, &pacing, 1400, 56000, 1);
if (s.cycle_idx != BBR_BW_PROBE_UP) FAIL("REFILL did not start UP");
for (unsigned i = 0; i < 40; i++) {
s.now_tb += 10; rs.tx_in_flight = cwnd;
bbr_main(&s, &rs, &cwnd, &pacing, 1400, cwnd, 1);
}
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "BBR probe growth: initial=56000 acknowledged=56000 inflight_hi=%u", s.inflight_hi);
if (s.inflight_hi != 57400) FAIL("one acknowledged window must raise inflight_hi by 1400 bytes");
PASS(); return 0;
}
static int test_probe_packet_budget(void) {
MSEG("PROBE_UP reserves two MSS; Reno rounds use packets");
struct bbr s = { .now_tb = 10000 }; bbr_init(&s);
s.queue_budget_us = 0; // здесь проверяем отдельный резерв PROBE_UP без ограничения по времени очереди
s.mode = BBR_PROBE_BW; s.full_bw_reached = 1; s.cycle_idx = BBR_BW_PROBE_UP;
uint32_t cwnd = 100000, pacing = 0;
struct bbr_rate_sample rs = { .delivered = 10000, .interval_us = 10000, .rtt_us = 20000,
.acked_sacked = 1000, .tx_in_flight = 40000 };
bbr_main(&s, &rs, &cwnd, &pacing, 1400, 40000, 0);
if (cwnd != 43800) FAIL("two MSS probing reserve must add 2800 bytes to 41000");
memset(&s, 0, sizeof(s)); s.now_tb = 10000; bbr_init(&s);
s.mode = BBR_PROBE_BW; s.full_bw_reached = 1; s.cycle_idx = BBR_BW_PROBE_CRUISE;
s.min_rtt_us = s.probe_rtt_min_us = 14000; s.has_seen_rtt = 1;
s.bw_hi[0] = BW_UNIT; s.rounds_since_probe = 9; s.cycle_mstamp_tb = s.now_tb; s.probe_wait_us = 2000000;
cwnd = 56000; pacing = 0; rs.rtt_us = 14000;
bbr_main(&s, &rs, &cwnd, &pacing, 1400, 14000, 0);
if (s.cycle_idx != BBR_BW_PROBE_REFILL) FAIL("ten-packet BDP should probe after ten packet rounds");
PASS(); return 0;
}
static int test_large_ack_aggregation(void) {
MSEG("ACK aggregation above 64KiB keeps byte precision");
struct bbr s = { .now_tb = 10000 }; bbr_init(&s);
s.queue_budget_us = 0; // размер ACK-резерва проверяется отдельно от политики времени очереди
s.mode = BBR_PROBE_BW; s.full_bw_reached = 1; s.cycle_idx = BBR_BW_PROBE_CRUISE;
s.cycle_mstamp_tb = s.now_tb; s.probe_wait_us = 2000000;
uint32_t cwnd = 300000, pacing = 0;
struct bbr_rate_sample rs = { .delivered = 80000, .interval_us = 80000, .rtt_us = 100000,
.acked_sacked = 80000, .tx_in_flight = 200000 };
bbr_main(&s, &rs, &cwnd, &pacing, 1400, 200000, 0);
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "ACK aggregation: 2*BDP=200000 ACKed=80000 cwnd=%u", cwnd);
if (cwnd != 280000) FAIL("80000-byte ACK aggregation must not wrap at 65536");
PASS(); return 0;
}
/* Большой BDP и прибавление ACK не должны оборачивать окно до нескольких пакетов. */
static int test_cwnd_saturation(void) {
MSEG("large BDP and acknowledged bytes saturate cwnd");
struct bbr s = { .now_tb = 10000 }; bbr_init(&s);
s.mode = BBR_PROBE_BW; s.full_bw_reached = 1; s.cycle_idx = BBR_BW_PROBE_CRUISE;
s.cycle_mstamp_tb = s.now_tb; s.probe_wait_us = 2000000;
uint32_t cwnd = UINT32_MAX - 500, pacing = 0;
struct bbr_rate_sample rs = { .delivered = 10000, .interval_us = 1, .rtt_us = 1000000,
.acked_sacked = 1000, .tx_in_flight = UINT32_MAX - 500 };
bbr_main(&s, &rs, &cwnd, &pacing, 1400, UINT32_MAX - 500, 0);
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "BBR saturated window: cwnd=%u expected=%u", cwnd, UINT32_MAX);
if (cwnd != UINT32_MAX) FAIL("BDP plus ACK and cwnd plus ACK must saturate, not wrap");
if (s.queue_cwnd_cap != UINT32_MAX) FAIL("large queue budget cap must saturate, not wrap");
PASS(); return 0;
}
static int test_init(void)
{
MSEG("bbr_init — startup mode");
struct bbr s; memset(&s, 0xFF, sizeof(s)); bbr_init(&s);
if (s.mode != BBR_STARTUP) FAIL("mode");
if (s.cycle_idx != 0) FAIL("cycle_idx");
if (s.min_rtt_us != ~0U) FAIL("min_rtt_us");
if (s.bw_lo != ~0U) FAIL("bw_lo");
if (s.inflight_lo != ~0U) FAIL("inflight_lo");
if (s.inflight_hi != ~0U) FAIL("inflight_hi");
if (s.initialized != 1) FAIL("initialized");
if (s.full_bw_reached) FAIL("full_bw_reached");
PASS(); return 0;
}
static int test_startup_growth(void)
{
MSEG("startup — cwnd grows");
struct bbr s; memset(&s, 0, sizeof(s)); s.now_tb = 10000; bbr_init(&s);
s.init_cwnd = 4; g_cwnd = 5600; g_pacing = 0;
uint32_t prev = g_cwnd;
for (int r = 0; r < 4; r++) {
// Подтверждаем целое окно за RTT: доступная полоса растёт вместе с окном, STARTUP должен её освоить.
run_ack(&s, prev, 10000, 0, 0, prev);
if (g_cwnd <= prev) FAIL("cwnd did not grow");
prev = g_cwnd;
}
PASS(); return 0;
}
static int test_full_bw(void)
{
MSEG("full_bw → DRAIN → PROBE_BW");
struct bbr s; memset(&s, 0, sizeof(s)); s.now_tb = 10000; bbr_init(&s);
s.init_cwnd = 4; g_cwnd = 5600; g_pacing = 0;
// 4 rounds: first resets full_bw (sample_bw >= 0*1.25), next 3 count up
for (int r = 0; r < 4; r++) {
s.now_tb += 10000;
uint32_t rd = s.delivered;
for (int a = 0; a < 4; a++) run_ack_ex(&s, 1400, 10000, 0, 0, g_cwnd, rd);
}
if (!s.full_bw_reached) FAIL("full_bw_reached");
// Mode transitions STARTUP→DRAIN→PROBE_BW might all happen in one ACK
if (s.mode != BBR_DRAIN && s.mode != BBR_PROBE_BW) FAIL("mode not DRAIN/PROBE_BW after full_bw");
PASS(); return 0;
}
static int test_loss_startup(void)
{
MSEG("loss events in STARTUP → early full_bw");
struct bbr s; memset(&s, 0, sizeof(s)); s.now_tb = 10000; bbr_init(&s);
s.init_cwnd = 4; g_cwnd = 5600; g_pacing = 0; s.loss_round_start = 1;
for (int i = 0; i < 6; i++) { s.now_tb += 1000; run_ack(&s, 1400, 5000, 0, 1, 5600); }
if (!s.full_bw_reached) FAIL("full_bw_reached not set");
PASS(); return 0;
}
static int test_cwnd_bounded(void)
{
MSEG("inflight bounds respect the four-MSS minimum");
const uint32_t caps[] = {4000, 8000}, expected[] = {5600, 8000};
for (unsigned i = 0; i < 2; i++) {
struct bbr s = { .now_tb = 10000 }; bbr_init(&s);
s.queue_budget_us = 0;
s.mode = BBR_PROBE_BW; s.full_bw_reached = 1; s.cycle_idx = BBR_BW_PROBE_UP;
s.inflight_hi = s.inflight_lo = caps[i]; g_cwnd = 14000; g_pacing = 0;
s.now_tb += 5000; run_ack(&s, 1400, 5000, 0, 0, 14000);
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "BBR window bound: cap=%u minimum=5600 cwnd=%u", caps[i], g_cwnd);
if (g_cwnd != expected[i]) FAIL("inflight bounds must not reduce cwnd below four MSS");
}
PASS(); return 0;
}
static int test_loss_cut(void)
{
MSEG("loss cuts inflight_lo");
struct bbr s; memset(&s, 0, sizeof(s)); s.now_tb = 10000; bbr_init(&s);
s.mode = BBR_PROBE_BW; s.full_bw_reached = 1; s.cycle_idx = BBR_BW_PROBE_CRUISE;
s.inflight_lo = 5000; s.bw_lo = 1000;
s.bw_latest = 500; s.inflight_latest = 3000;
s.delivered = 0; s.loss_round_delivered = 0;
s.cycle_mstamp_tb = s.now_tb; // prevent phase advance
s.probe_wait_us = 2000000;
uint32_t prev = s.inflight_lo;
s.now_tb += 5000; run_ack_ex(&s, 1400, 5000, 0, 1, 5000, 0);
if (s.inflight_lo >= prev) FAIL("inflight_lo not reduced");
PASS(); return 0;
}
static int test_fast_path(void)
{
MSEG("app-limited ACK triggers fast_path");
struct bbr s; memset(&s, 0, sizeof(s)); s.now_tb = 10000; bbr_init(&s);
s.init_cwnd = 4; s.full_bw_reached = 1;
s.mode = BBR_PROBE_BW; s.cycle_idx = BBR_BW_PROBE_CRUISE;
s.try_fast_path = 1; s.bw_hi[0] = 10000; s.bw_lo = 5000;
g_cwnd = 5600; g_pacing = 0;
s.now_tb += 1000; run_ack(&s, 1400, 5000, 1, 0, 2000);
PASS(); return 0;
}
static int test_probe_rtt(void)
{
MSEG("PROBE_RTT entry after 5s");
struct bbr s; memset(&s, 0, sizeof(s)); s.now_tb = 10000; bbr_init(&s);
s.mode = BBR_PROBE_BW; s.full_bw_reached = 1; s.cycle_idx = BBR_BW_PROBE_CRUISE;
s.probe_rtt_min_us = ~0U; s.probe_rtt_min_stamp_tb = 10000;
s.min_rtt_us = ~0U; s.min_rtt_stamp_tb = 10000;
g_cwnd = 5600; g_pacing = 0;
s.now_tb = 10000 + 5000U * 10; // +5 seconds in 0.1ms
run_ack(&s, 1400, 1000, 0, 0, 5600);
if (s.mode != BBR_PROBE_RTT) FAIL("mode not PROBE_RTT");
if (g_cwnd > BBR_CWND_MIN_TARGET * 1400 + 1400) FAIL("cwnd not capped");
PASS(); return 0;
}
/* Длинный канал осушается до половины BDP, затем выдерживает 200мс и полный раунд доставки. */
static int test_probe_rtt_lifecycle(void) {
MSEG("PROBE_RTT half-BDP drain, hold and window restore");
struct bbr s = { .now_tb = 10000 }; bbr_init(&s);
s.queue_budget_us = 0; // восстановление исходного окна проверяем без последующего ограничения бюджета
s.mode = BBR_PROBE_BW; s.full_bw_reached = 1; s.cycle_idx = BBR_BW_PROBE_CRUISE;
s.bw_hi[0] = BW_UNIT; s.min_rtt_us = s.probe_rtt_min_us = 200000; s.has_seen_rtt = 1;
s.now_tb += BBR_PROBE_RTT_WIN_MS * 10 + 1;
uint32_t cwnd = 400000, pacing = 1000000;
struct bbr_rate_sample rs = { .delivered = 20000, .interval_us = 20000, .rtt_us = 200000,
.acked_sacked = 1000, .tx_in_flight = 400000 };
bbr_main(&s, &rs, &cwnd, &pacing, 1400, 200001, 0);
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "PROBE_RTT drain: BDP=200000 cwnd=%u timer=%llu", cwnd,
(unsigned long long)s.probe_rtt_done_stamp_tb);
if (s.mode != BBR_PROBE_RTT || cwnd != 100000) FAIL("probe window must be half of 200000-byte BDP");
if (s.probe_rtt_done_stamp_tb) FAIL("probe hold started before flight drained");
s.now_tb++; rs.prior_delivered = s.delivered; rs.is_app_limited = 1;
bbr_main(&s, &rs, &cwnd, &pacing, 1400, 100000, 0);
if (!s.probe_rtt_done_stamp_tb) FAIL("probe hold did not start at half-BDP");
s.now_tb += BBR_PROBE_RTT_MS * 10 - 1; rs.prior_delivered = s.delivered;
bbr_main(&s, &rs, &cwnd, &pacing, 1400, 100000, 0);
if (s.mode != BBR_PROBE_RTT || cwnd != 100000) FAIL("probe ended before 200ms");
s.now_tb++; rs.prior_delivered = s.delivered;
bbr_main(&s, &rs, &cwnd, &pacing, 1400, 100000, 0);
if (s.mode != BBR_PROBE_BW || cwnd < 400000) FAIL("probe did not restore the previous window after hold and round");
PASS(); return 0;
}
/* Бюджет включает все резервы окна и не зависит от удлинения текущего RTT очередью. */
static int test_queue_budget(void) {
MSEG("queue time budget includes ACK and probing reserves");
const uint32_t rtts[] = {20000, 200000, 20000, 20000};
const uint32_t budgets[] = {20000, 20000, 5000, 0};
const uint32_t expected[] = {40000, 220000, 25000, 43800};
for (unsigned i = 0; i < 4; i++) {
struct bbr s = { .now_tb = 10000 }; bbr_init(&s);
s.queue_budget_us = budgets[i]; s.mode = BBR_PROBE_BW; s.full_bw_reached = 1;
s.cycle_idx = BBR_BW_PROBE_UP; s.min_rtt_us = s.probe_rtt_min_us = rtts[i]; s.has_seen_rtt = 1;
s.bw_hi[0] = BW_UNIT;
uint32_t cwnd = 1000000, pacing = 0;
struct bbr_rate_sample rs = { .delivered = 10000, .interval_us = 10000, .rtt_us = rtts[i] * 2,
.acked_sacked = 1000, .tx_in_flight = 1000000 };
bbr_main(&s, &rs, &cwnd, &pacing, 1400, 100000, 0);
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "Queue budget: min_rtt=%u budget=%u cwnd=%u expected=%u",
rtts[i], budgets[i], cwnd, expected[i]);
if (cwnd != expected[i]) FAIL("queue budget must bound the entire window using min_rtt");
}
PASS(); return 0;
}
static int test_queue_budget_floor_and_startup(void) {
MSEG("queue budget preserves minimum window and STARTUP");
struct bbr s = { .now_tb = 10000 }; bbr_init(&s);
s.mode = BBR_PROBE_BW; s.full_bw_reached = 1; s.cycle_idx = BBR_BW_PROBE_UP;
uint32_t cwnd = 1000000, pacing = 0;
struct bbr_rate_sample rs = { .delivered = 100, .interval_us = 100000, .rtt_us = 20000,
.acked_sacked = 100, .tx_in_flight = 1000000 };
bbr_main(&s, &rs, &cwnd, &pacing, 1400, 100000, 0);
if (cwnd != 5600) FAIL("minimum four-MSS window must survive a tiny estimated BDP");
memset(&s, 0, sizeof(s)); s.now_tb = 10000; bbr_init(&s);
cwnd = 5600; pacing = 0; rs.delivered = rs.acked_sacked = 5600;
rs.interval_us = rs.rtt_us = 200000; rs.tx_in_flight = 5600;
bbr_main(&s, &rs, &cwnd, &pacing, 1400, 5600, 1);
if (s.mode != BBR_STARTUP || cwnd != 11200) FAIL("queue budget must not prevent STARTUP window growth");
PASS(); return 0;
}
static void record_window(void* arg, uint32_t window) { *(uint32_t*)arg = window; }
static int test_queue_budget_fast_path(void) {
MSEG("queue budget also bounds fast path and callback");
struct bbr s = { .now_tb = 10000 }; bbr_init(&s);
s.mode = BBR_PROBE_BW; s.full_bw_reached = 1; s.cycle_idx = BBR_BW_PROBE_CRUISE;
s.try_fast_path = 1; s.min_rtt_us = s.probe_rtt_min_us = 20000; s.has_seen_rtt = 1;
s.cycle_mstamp_tb = s.now_tb; s.probe_wait_us = 2000000; s.bw_hi[0] = BW_UNIT; s.queue_budget_us = 5000;
uint32_t cwnd = 100000, pacing = 990000, observed = 0;
s.on_cwnd_update = record_window; s.cwnd_update_ctx = &observed;
struct bbr_rate_sample rs = { .delivered = 1000, .interval_us = 20000, .rtt_us = 20000,
.acked_sacked = 1000, .is_app_limited = 1, .tx_in_flight = 100000 };
bbr_main(&s, &rs, &cwnd, &pacing, 1400, 10000, 0);
if (cwnd != 25000 || observed != cwnd || s.queue_cwnd_cap != cwnd) FAIL("fast path callback must see the bounded window");
s.queue_budget_us = 0; cwnd = 100000; rs.prior_delivered = s.delivered;
bbr_main(&s, &rs, &cwnd, &pacing, 1400, 10000, 0);
if (cwnd != 100000 || observed != cwnd || s.queue_cwnd_cap) FAIL("disabling budget must clear its active cap");
s.queue_budget_us = 5000; s.min_rtt_us = s.probe_rtt_min_us = UINT32_MAX; rs.rtt_us = 0;
rs.prior_delivered = s.delivered;
bbr_main(&s, &rs, &cwnd, &pacing, 1400, 10000, 0);
if (s.queue_cwnd_cap || cwnd != 100000) FAIL("unknown min RTT must not create a fabricated queue cap");
PASS(); return 0;
}
int main(void)
{
debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); debug_set_category_level(DEBUG_CATEGORY_DEBUG, DEBUG_LEVEL_INFO);
test_byte_rate();
test_large_rate();
test_probe_growth_bytes();
test_probe_packet_budget();
test_large_ack_aggregation();
test_cwnd_saturation();
test_init();
test_startup_growth();
test_full_bw();
test_loss_startup();
test_cwnd_bounded();
test_loss_cut();
test_fast_path();
test_probe_rtt();
test_probe_rtt_lifecycle();
test_queue_budget();
test_queue_budget_floor_and_startup();
test_queue_budget_fast_path();
printf("\n=== Results: %d/%d passed, %d failed ===\n", test_passed, test_total, test_failed);
return test_failed ? 1 : 0;
}