From bb462bad69d78e37096ec7d0343ac3be4522b24d Mon Sep 17 00:00:00 2001 From: evgeny Date: Sat, 24 Oct 2026 15:53:41 +0200 Subject: [PATCH] Preserve BBR RTT filter age and full-width probe deadlines --- src/transport_layer/etcp_bbr.c | 35 ++++++----- src/transport_layer/etcp_bbr.h | 14 +++-- tests/test_etcp_bbr.c | 107 +++++++++++++++++++++++++++++++++ 3 files changed, 137 insertions(+), 19 deletions(-) diff --git a/src/transport_layer/etcp_bbr.c b/src/transport_layer/etcp_bbr.c index de0c395f..b6d964a1 100644 --- a/src/transport_layer/etcp_bbr.c +++ b/src/transport_layer/etcp_bbr.c @@ -577,36 +577,42 @@ static void bbr_check_drain(struct bbr* bbr, const struct bbr_rate_sample* rs, static void bbr_check_probe_rtt_done(struct bbr* bbr, uint32_t* cwnd) { - if (!bbr->probe_rtt_done_stamp_tb) + uint64_t now = bbr_now(bbr); + if (!bbr->probe_rtt_done_stamp_tb || now < bbr->probe_rtt_done_stamp_tb) return; - if (!((int32_t)((uint32_t)bbr_now(bbr) - (uint32_t)bbr->probe_rtt_done_stamp_tb) >= 0)) - return; - bbr->probe_rtt_min_stamp_tb = bbr_now(bbr); + DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "[L%u] PROBE_RTT complete now_tb=%llu deadline_tb=%llu min_us=%u probe_min_us=%u", + bbr->link_id, (unsigned long long)now, (unsigned long long)bbr->probe_rtt_done_stamp_tb, + bbr->min_rtt_us, bbr->probe_rtt_min_us); + bbr->probe_rtt_min_stamp_tb = now; *cwnd = (uint32_t)(*cwnd > bbr->prior_cwnd ? *cwnd : bbr->prior_cwnd); bbr_exit_probe_rtt(bbr); } +/* Обновить RTT-фильтры с наследованием возраста минимума и цикл PROBE_RTT по полным 64-битным часам. */ static void bbr_update_min_rtt(struct bbr* bbr, const struct bbr_rate_sample* rs, uint32_t* cwnd, uint32_t mss, uint32_t inflight_bytes) { - uint32_t expire; + uint64_t now = bbr_now(bbr); int probe_rtt_expired, min_rtt_expired; - expire = (uint32_t)(bbr->probe_rtt_min_stamp_tb + BBR_PROBE_RTT_WIN_MS * 10U); - probe_rtt_expired = ((int32_t)(expire - (uint32_t)bbr_now(bbr)) < 0); + probe_rtt_expired = now - bbr->probe_rtt_min_stamp_tb > BBR_PROBE_RTT_WIN_MS * 10ULL; if (rs->rtt_us > 0 && (rs->rtt_us < bbr->probe_rtt_min_us || probe_rtt_expired)) { bbr->probe_rtt_min_us = rs->rtt_us; - bbr->probe_rtt_min_stamp_tb = bbr_now(bbr); + bbr->probe_rtt_min_stamp_tb = now; } if (bbr->probe_rtt_min_us == 0) bbr->probe_rtt_min_us = ~0U; - expire = (uint32_t)(bbr->min_rtt_stamp_tb + BBR_MIN_RTT_WIN_SEC * 10000U); - min_rtt_expired = ((int32_t)(expire - (uint32_t)bbr_now(bbr)) < 0); + min_rtt_expired = now - bbr->min_rtt_stamp_tb > BBR_MIN_RTT_WIN_SEC * 10000ULL; if (bbr->probe_rtt_min_us <= bbr->min_rtt_us || min_rtt_expired) { + if (bbr->min_rtt_us != bbr->probe_rtt_min_us || bbr->min_rtt_stamp_tb != bbr->probe_rtt_min_stamp_tb) + DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "[L%u] min RTT: %u->%u sample_us=%u stamp_tb=%llu->%llu now_tb=%llu expired=%d", + bbr->link_id, bbr->min_rtt_us, bbr->probe_rtt_min_us, rs->rtt_us, + (unsigned long long)bbr->min_rtt_stamp_tb, (unsigned long long)bbr->probe_rtt_min_stamp_tb, + (unsigned long long)now, min_rtt_expired); bbr->min_rtt_us = bbr->probe_rtt_min_us; - bbr->min_rtt_stamp_tb = bbr_now(bbr); + bbr->min_rtt_stamp_tb = bbr->probe_rtt_min_stamp_tb; } if (BBR_PROBE_RTT_MS > 0 && probe_rtt_expired && @@ -623,11 +629,12 @@ static void bbr_update_min_rtt(struct bbr* bbr, const struct bbr_rate_sample* rs if (bbr->mode == BBR_PROBE_RTT) { if (!bbr->probe_rtt_done_stamp_tb && inflight_bytes <= bbr_probe_rtt_cwnd(bbr, mss)) { - bbr->probe_rtt_done_stamp_tb = (uint32_t)bbr_now(bbr) + BBR_PROBE_RTT_MS * 10U; + bbr->probe_rtt_done_stamp_tb = now + BBR_PROBE_RTT_MS * 10ULL; bbr->probe_rtt_round_done = 0; bbr->next_rtt_delivered = bbr->delivered; - DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "[L%u] PROBE_RTT drained inflight=%u target=%u hold_ms=%u", - bbr->link_id, inflight_bytes, bbr_probe_rtt_cwnd(bbr, mss), BBR_PROBE_RTT_MS); + DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "[L%u] PROBE_RTT drained inflight=%u target=%u hold_ms=%u deadline_tb=%llu", + bbr->link_id, inflight_bytes, bbr_probe_rtt_cwnd(bbr, mss), BBR_PROBE_RTT_MS, + (unsigned long long)bbr->probe_rtt_done_stamp_tb); } else if (bbr->probe_rtt_done_stamp_tb) { if (bbr->round_start) bbr->probe_rtt_round_done = 1; diff --git a/src/transport_layer/etcp_bbr.h b/src/transport_layer/etcp_bbr.h index f5bd2fd6..b688238d 100644 --- a/src/transport_layer/etcp_bbr.h +++ b/src/transport_layer/etcp_bbr.h @@ -5,6 +5,10 @@ * cwnd/inflight/delivered — байты DATA, interval/rtt — микросекунды, pacing_rate — байты в секунду. * MSS — максимальный DATA-фрагмент normalizer, без заголовков; не wire-MTU и не размер кадра STCP. * Первый достоверный RTT в STARTUP инициализирует pacing от начальных 4*MSS, включая короткие служебные DATA. + * Короткий RTT-минимум обновляется меньшим достоверным RTT либо измерением после истечения 5с. + * min_rtt наследует его значение и метку; большее значение принимается после 10с возраста min_rtt. + * Выход из PROBE_RTT возобновляет короткое окно. Копирование кеша сохраняет его метку, не задаёт текущее время. + * Метки RTT-фильтров и срок hold — uint64 TB (100мкс); окна проверяются при обработке ACK. * Минимальное окно — 4*MSS; PROBE_RTT осушает до max(4*MSS, 0.5*BDP), затем ждёт >=200мс и раунд ACK. * Транспорт применяет окно с верхним ограничением конфига и не повышает его до собственного минимума. * Целевое окно PROBE_BW включает 2*BDP и ACK-резерв, в PROBE_UP дополнительно 2*MSS. @@ -67,7 +71,7 @@ struct bbr_rate_sample { typedef void (*bbr_cwnd_update_fn)(void* ctx, uint32_t new_cwnd); struct bbr { - uint32_t min_rtt_us; + uint32_t min_rtt_us; // принятый долгий минимум RTT, мкс; UINT32_MAX — измерений ещё нет uint32_t bw_lo; uint32_t bw_hi[2]; uint32_t inflight_lo; @@ -100,9 +104,9 @@ struct bbr { uint8_t rounds_since_probe; uint32_t probe_wait_us; uint64_t cycle_mstamp_tb; - uint32_t probe_rtt_min_us; - uint64_t probe_rtt_min_stamp_tb; - uint64_t probe_rtt_done_stamp_tb; + uint32_t probe_rtt_min_us; // короткий RTT-минимум, мкс; UINT32_MAX — измерений ещё нет + uint64_t probe_rtt_min_stamp_tb; // метка короткого фильтра; обновляется его измерением и завершением PROBE_RTT + uint64_t probe_rtt_done_stamp_tb; // полный срок hold в TB; 0 — hold ещё не начат; используется в PROBE_RTT uint8_t probe_rtt_round_done : 1; uint32_t extra_acked[2]; // избыточно подтверждённые байты, без усечения на 64KiB uint32_t ack_epoch_acked; @@ -116,7 +120,7 @@ struct bbr { uint8_t loss_events_in_round : 4; uint32_t delivered; uint32_t full_bw; - uint64_t min_rtt_stamp_tb; + uint64_t min_rtt_stamp_tb; // метка короткого фильтра, принятая вместе с min_rtt_us; окно 10с uint8_t loss_round_start : 1; uint8_t full_bw_now : 1; uint8_t pad_unused : 6; diff --git a/tests/test_etcp_bbr.c b/tests/test_etcp_bbr.c index c3fb741a..2c7b477b 100644 --- a/tests/test_etcp_bbr.c +++ b/tests/test_etcp_bbr.c @@ -261,6 +261,87 @@ static int test_probe_rtt(void) PASS(); return 0; } +/* Повышение RTT не омолаживает старый минимум; проверяем основной/быстрый путь и границу 32 бит TB. */ +static int test_min_rtt_lifetime(void) { + MSEG("min RTT expires after 10s, including fast path and TB wrap"); + const uint64_t starts[] = {10000, (uint64_t)UINT32_MAX-30000, UINT64_C(1)<<40}; + for (unsigned fast = 0; fast < 2; fast++) for (unsigned i = 0; i < sizeof(starts)/sizeof(starts[0]); i++) { + struct bbr s = {.now_tb=starts[i]}; bbr_init(&s); + s.mode = BBR_PROBE_BW; s.full_bw_reached = 1; s.cycle_idx = BBR_BW_PROBE_CRUISE; + s.bw_hi[0] = BW_UNIT/10; + uint32_t cwnd = 10000, pacing = 0, replaced_ms = 0; + uint64_t cached_stamp = 0; + struct bbr_rate_sample rs = {.delivered=1000,.interval_us=100000,.rtt_us=20000, + .acked_sacked=1000,.tx_in_flight=1000,.is_app_limited=fast}; + bbr_main(&s,&rs,&cwnd,&pacing,1400,1000,0); + rs.rtt_us = 100000; + for (unsigned ms = 100; ms <= 16000; ms += 100) { + s.now_tb = starts[i]+ms*10; s.try_fast_path = fast; rs.prior_delivered = s.delivered; + bbr_main(&s,&rs,&cwnd,&pacing,1400,1000,0); + if (ms == 4900) cached_stamp = s.min_rtt_stamp_tb; + if (!replaced_ms && s.min_rtt_us == 100000) replaced_ms = ms; + } + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "RTT lifetime: start_tb=%llu fast=%u cached_stamp_tb=%llu replaced_ms=%u expected_ms=10100", + (unsigned long long)starts[i],fast,(unsigned long long)cached_stamp,replaced_ms); + if (cached_stamp != starts[i]) FAIL("cached minimum was timestamped as a new RTT observation"); + if (replaced_ms != 10100) FAIL("20ms minimum was not replaced just after its 10s window"); + } + PASS(); return 0; +} + +/* Граница 10с строгая; реальное новое измерение такого же минимума продлевает его жизнь. */ +static int test_min_rtt_refresh(void) { + MSEG("min RTT boundary, fresh equal/lower samples and unknown RTT"); + for (unsigned fresh_equal = 0; fresh_equal < 2; fresh_equal++) { + 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; + uint32_t cwnd = 10000, pacing = 0; + struct bbr_rate_sample rs = {.delivered=1000,.interval_us=100000,.rtt_us=20000, + .acked_sacked=1000,.tx_in_flight=1000}; + bbr_main(&s,&rs,&cwnd,&pacing,1400,1000,0); + s.now_tb = 60001; rs.rtt_us = fresh_equal ? 20000 : 100000; rs.prior_delivered = s.delivered; + bbr_main(&s,&rs,&cwnd,&pacing,1400,1000,0); + s.now_tb = 62001; rs.rtt_us = 100000; rs.prior_delivered = s.delivered; + bbr_main(&s,&rs,&cwnd,&pacing,1400,1000,0); // завершаем hold и ACK-раунд PROBE_RTT + uint64_t origin = fresh_equal ? 60001 : 10000; + s.now_tb = origin+100000; rs.prior_delivered = s.delivered; + bbr_main(&s,&rs,&cwnd,&pacing,1400,1000,0); + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "RTT boundary: fresh_equal=%u now_tb=%llu origin_tb=%llu min_us=%u stamp_tb=%llu", + fresh_equal,(unsigned long long)s.now_tb,(unsigned long long)origin,s.min_rtt_us, + (unsigned long long)s.min_rtt_stamp_tb); + if (s.min_rtt_us != 20000) FAIL("minimum expired at the exact boundary"); + s.now_tb++; rs.prior_delivered = s.delivered; + bbr_main(&s,&rs,&cwnd,&pacing,1400,1000,0); + if (s.min_rtt_us != 100000) FAIL("minimum did not expire one TB after the boundary"); + s.now_tb++; rs.rtt_us = 15000; rs.prior_delivered = s.delivered; + bbr_main(&s,&rs,&cwnd,&pacing,1400,1000,0); + uint64_t lower_stamp = s.min_rtt_stamp_tb; + if (s.min_rtt_us != 15000 || lower_stamp != s.now_tb) FAIL("fresh lower RTT did not replace the minimum immediately"); + s.now_tb += 1000; rs.rtt_us = 0; rs.prior_delivered = s.delivered; + bbr_main(&s,&rs,&cwnd,&pacing,1400,1000,0); + if (s.min_rtt_us != 15000 || s.min_rtt_stamp_tb != lower_stamp) FAIL("unknown RTT changed the minimum or its age"); + } + PASS(); return 0; +} + +/* Большой перерыв не теряется при сравнении младших 32 бит часов. */ +static int test_min_rtt_long_idle(void) { + MSEG("RTT filters expire after an idle longer than 32-bit TB range"); + 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; + uint32_t cwnd = 10000, pacing = 0; + struct bbr_rate_sample rs = {.delivered=1000,.interval_us=100000,.rtt_us=20000, + .acked_sacked=1000,.tx_in_flight=1000}; + bbr_main(&s,&rs,&cwnd,&pacing,1400,1000,0); + s.now_tb += UINT64_C(1)<<32; rs.rtt_us = 100000; rs.prior_delivered = s.delivered; + bbr_main(&s,&rs,&cwnd,&pacing,1400,1000,0); + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "RTT long idle: now_tb=%llu min_us=%u probe_us=%u mode=%u", + (unsigned long long)s.now_tb,s.min_rtt_us,s.probe_rtt_min_us,s.mode); + if (s.min_rtt_us != 100000 || s.probe_rtt_min_us != 100000 || s.mode != BBR_PROBE_RTT) + FAIL("old RTT survived an idle of a full 32-bit TB range"); + PASS(); return 0; +} + /* Длинный канал осушается до половины BDP, затем выдерживает 200мс и полный раунд доставки. */ static int test_probe_rtt_lifecycle(void) { MSEG("PROBE_RTT half-BDP drain, hold and window restore"); @@ -289,6 +370,28 @@ static int test_probe_rtt_lifecycle(void) { PASS(); return 0; } +/* Нулевой младший uint32 срока не означает отсутствие hold; ждать надо полные 200мс. */ +static int test_probe_rtt_deadline_wrap(void) { + MSEG("PROBE_RTT hold at a zero low-32-bit deadline"); + struct bbr s = {.now_tb=(UINT64_C(1)<<32)-2000}; bbr_init(&s); + s.mode = BBR_PROBE_RTT; s.full_bw_reached = 1; + uint32_t cwnd = 10000, pacing = 0; + struct bbr_rate_sample rs = {.delivered=1000,.interval_us=100000,.rtt_us=20000, + .acked_sacked=1000,.tx_in_flight=1000,.is_app_limited=1}; + bbr_main(&s,&rs,&cwnd,&pacing,1400,1000,0); + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "PROBE_RTT zero deadline: now_tb=%llu deadline_tb=%llu expected_tb=%llu", + (unsigned long long)s.now_tb,(unsigned long long)s.probe_rtt_done_stamp_tb, + (unsigned long long)(UINT64_C(1)<<32)); + if (!s.probe_rtt_done_stamp_tb) FAIL("zero low bits erased the active PROBE_RTT deadline"); + s.now_tb += 1999; rs.prior_delivered = s.delivered; + bbr_main(&s,&rs,&cwnd,&pacing,1400,1000,0); + if (s.mode != BBR_PROBE_RTT || !s.probe_rtt_round_done) FAIL("probe must hold for 200ms even with a complete round"); + s.now_tb++; rs.prior_delivered = s.delivered; + bbr_main(&s,&rs,&cwnd,&pacing,1400,1000,0); + if (s.mode != BBR_PROBE_BW) FAIL("probe did not finish at the 200ms deadline across TB wrap"); + PASS(); return 0; +} + /* Бюджет включает все резервы окна и не зависит от удлинения текущего RTT очередью. */ static int test_queue_budget(void) { MSEG("queue time budget includes ACK and probing reserves"); @@ -427,7 +530,11 @@ int main(void) test_loss_cut(); test_fast_path(); test_probe_rtt(); + test_min_rtt_lifetime(); + test_min_rtt_refresh(); + test_min_rtt_long_idle(); test_probe_rtt_lifecycle(); + test_probe_rtt_deadline_wrap(); test_queue_budget(); test_queue_budget_floor_and_startup(); test_queue_budget_fast_path();