Preserve BBR RTT filter age and full-width probe deadlines

This commit is contained in:
evgeny committed 2026-10-24 17:26:59 +02:00
1 parent deee734e6c
commit bb462bad69
3 files changed
+137 -19

No files matched your search

+21 -14
View File
@@ -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;
+9 -5
View File
@@ -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;
+107
View File
@@ -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();