Browse Source
- Новые файлы: src/etcp_bbr.h/c — полный порт BBR v3 из BBR/bbr_v3.c (референс сохранён) - struct ETCP_LINK: удалены AIMD-поля (rtt_history, rtt_swm, stat_win, stats_timer, inflight_phase...), добавлены bbr*, delivered_bytes, bbr_pacing_rate - struct INFLIGHT_PACKET: +3 поля для rate_sample (delivered_at_send, inflight_at_send, is_app_limited) - etcp_ack_recv: вызов bbr_main() с rate_sample, обновление inflight_lim_bytes и pacing - etcp_request_pkt: заполнение BBR rate_sample полей при отправке - Шейпер: bandwidth обновляется из bbr_pacing_rate - Конфиг: inflight_min/max заменены на bbr_max_cwnd - Дамп: BBR поля (mode, cycle, pacing, min_rtt_us, bw_hi/lo, inflight_hi/lo) - Time injection: bbr->now_tb для детерминированных тестов - tests/test_etcp_bbr.c: 8 юнит-тестов (init, startup, full_bw/drain, loss, cwnd cap, loss cut, fast_path, probe_rtt) - Отключены test_etcp_congestion и test_etcp_reinit_inflight (старый congestion control) - 40/40 тестов пройденоetcp-inflight-fix
14 changed files with 3717 additions and 333 deletions
@ -0,0 +1,928 @@ |
|||||||
|
#include "etcp_bbr.h" |
||||||
|
#include "../lib/u_async.h" |
||||||
|
#include <stdlib.h> |
||||||
|
|
||||||
|
static uint64_t bbr_now(const struct bbr* bbr) { return bbr->now_tb ? bbr->now_tb : bbr_now(bbr); } |
||||||
|
|
||||||
|
static void bbr_start_bw_probe_down(struct bbr* bbr); |
||||||
|
static void bbr_start_bw_probe_cruise(struct bbr* bbr); |
||||||
|
static void bbr_exit_probe_rtt(struct bbr* bbr); |
||||||
|
|
||||||
|
static const int bbr_pacing_gain[] = { |
||||||
|
BBR_UNIT * 5 / 4, /* PROBE_UP: 1.25 */ |
||||||
|
BBR_UNIT * 3 / 4, /* PROBE_DOWN: 0.75 */ |
||||||
|
BBR_UNIT, /* CRUISE: 1.00 */ |
||||||
|
BBR_UNIT, /* REFILL: 1.00 */ |
||||||
|
}; |
||||||
|
|
||||||
|
static uint32_t bbr_max_bw(const struct bbr* bbr) |
||||||
|
{ |
||||||
|
return (uint32_t)(bbr->bw_hi[0] > bbr->bw_hi[1] ? bbr->bw_hi[0] : bbr->bw_hi[1]); |
||||||
|
} |
||||||
|
|
||||||
|
static uint32_t bbr_bw(const struct bbr* bbr) |
||||||
|
{ |
||||||
|
uint32_t max_bw = bbr_max_bw(bbr); |
||||||
|
return (uint32_t)(max_bw < bbr->bw_lo ? max_bw : bbr->bw_lo); |
||||||
|
} |
||||||
|
|
||||||
|
static int bbr_full_bw_reached(const struct bbr* bbr) |
||||||
|
{ |
||||||
|
return bbr->full_bw_reached; |
||||||
|
} |
||||||
|
|
||||||
|
static uint16_t bbr_extra_acked(const struct bbr* bbr) |
||||||
|
{ |
||||||
|
return (uint16_t)(bbr->extra_acked[0] > bbr->extra_acked[1] ? |
||||||
|
bbr->extra_acked[0] : bbr->extra_acked[1]); |
||||||
|
} |
||||||
|
|
||||||
|
static uint64_t bbr_rate_bytes_per_sec(uint64_t rate, int gain, int margin, uint32_t mss) |
||||||
|
{ |
||||||
|
rate *= mss; |
||||||
|
rate *= (uint64_t)gain; |
||||||
|
rate >>= BBR_SCALE; |
||||||
|
rate *= (uint64_t)(1000000 / 100) * (uint64_t)(100 - margin); |
||||||
|
rate >>= BW_SCALE; |
||||||
|
if (rate < 1) rate = 1; |
||||||
|
return rate; |
||||||
|
} |
||||||
|
|
||||||
|
static uint32_t bbr_bw_to_pacing_rate(uint32_t bw, int gain, uint32_t mss) |
||||||
|
{ |
||||||
|
uint64_t rate = bbr_rate_bytes_per_sec(bw, gain, 1, mss); |
||||||
|
return (uint32_t)rate; |
||||||
|
} |
||||||
|
|
||||||
|
static uint32_t bbr_bdp(const struct bbr* bbr, uint32_t bw, int gain, uint32_t mss) |
||||||
|
{ |
||||||
|
uint64_t w; |
||||||
|
if (bbr->min_rtt_us == ~0U) |
||||||
|
return (uint32_t)bbr->init_cwnd * mss; |
||||||
|
w = (uint64_t)bw * bbr->min_rtt_us; |
||||||
|
return (uint32_t)((((w * (uint64_t)gain) >> BBR_SCALE) + BW_UNIT - 1) / BW_UNIT) * mss; |
||||||
|
} |
||||||
|
|
||||||
|
static uint32_t bbr_quantization_budget(struct bbr* bbr, uint32_t cwnd, uint32_t mss) |
||||||
|
{ |
||||||
|
(void)mss; |
||||||
|
cwnd = (uint32_t)(cwnd > BBR_CWND_MIN_TARGET ? cwnd : BBR_CWND_MIN_TARGET); |
||||||
|
if (bbr->mode == BBR_PROBE_BW && bbr->cycle_idx == BBR_BW_PROBE_UP) |
||||||
|
cwnd += 2; |
||||||
|
return cwnd; |
||||||
|
} |
||||||
|
|
||||||
|
static uint32_t bbr_inflight(const struct bbr* bbr, uint32_t bw, int gain, uint32_t mss) |
||||||
|
{ |
||||||
|
uint32_t inflight = bbr_bdp(bbr, bw, gain, mss); |
||||||
|
return bbr_quantization_budget((struct bbr*)bbr, inflight, mss); |
||||||
|
} |
||||||
|
|
||||||
|
static uint32_t bbr_ack_aggregation_cwnd(const struct bbr* bbr, uint32_t mss) |
||||||
|
{ |
||||||
|
uint32_t max_aggr_cwnd, aggr_cwnd = 0; |
||||||
|
(void)mss; |
||||||
|
if (BBR_EXTRA_ACKED_GAIN) { |
||||||
|
max_aggr_cwnd = (uint32_t)((uint64_t)bbr_bw(bbr) * 100000U / BW_UNIT); |
||||||
|
aggr_cwnd = (uint32_t)((uint32_t)BBR_EXTRA_ACKED_GAIN * bbr_extra_acked(bbr) >> BBR_SCALE); |
||||||
|
aggr_cwnd = (uint32_t)(aggr_cwnd < max_aggr_cwnd ? aggr_cwnd : max_aggr_cwnd); |
||||||
|
} |
||||||
|
return aggr_cwnd; |
||||||
|
} |
||||||
|
|
||||||
|
static uint32_t bbr_target_inflight(const struct bbr* bbr, uint32_t cwnd, uint32_t mss) |
||||||
|
{ |
||||||
|
uint32_t bdp = bbr_inflight(bbr, bbr_bw(bbr), BBR_UNIT, mss); |
||||||
|
return (uint32_t)(bdp < cwnd ? bdp : cwnd); |
||||||
|
} |
||||||
|
|
||||||
|
static int bbr_is_probing_bandwidth(const struct bbr* bbr) |
||||||
|
{ |
||||||
|
return (bbr->mode == BBR_STARTUP) || |
||||||
|
(bbr->mode == BBR_PROBE_BW && |
||||||
|
(bbr->cycle_idx == BBR_BW_PROBE_REFILL || |
||||||
|
bbr->cycle_idx == BBR_BW_PROBE_UP)); |
||||||
|
} |
||||||
|
|
||||||
|
static int bbr_has_elapsed_in_phase(const struct bbr* bbr, uint32_t interval_us) |
||||||
|
{ |
||||||
|
uint64_t now_tb = bbr_now(bbr); |
||||||
|
uint64_t expire_tb = bbr->cycle_mstamp_tb + (uint64_t)(interval_us / 100U); |
||||||
|
return (int32_t)((uint32_t)expire_tb - (uint32_t)now_tb) <= 0; |
||||||
|
} |
||||||
|
|
||||||
|
static uint32_t bbr_update_round_start(struct bbr* bbr, const struct bbr_rate_sample* rs) |
||||||
|
{ |
||||||
|
uint32_t round_delivered = 0; |
||||||
|
bbr->round_start = 0; |
||||||
|
if (rs->interval_us > 0 && !((int32_t)(rs->prior_delivered - bbr->next_rtt_delivered) < 0)) { |
||||||
|
round_delivered = bbr->delivered - bbr->next_rtt_delivered; |
||||||
|
bbr->next_rtt_delivered = bbr->delivered; |
||||||
|
bbr->round_start = 1; |
||||||
|
} |
||||||
|
return round_delivered; |
||||||
|
} |
||||||
|
|
||||||
|
static uint32_t bbr_calculate_bw_sample(const struct bbr_rate_sample* rs) |
||||||
|
{ |
||||||
|
if (rs->interval_us > 0) |
||||||
|
return (uint32_t)(((uint64_t)rs->delivered * BW_UNIT + rs->interval_us - 1) / rs->interval_us); |
||||||
|
return 0; |
||||||
|
} |
||||||
|
|
||||||
|
static void bbr_update_latest_delivery_signals(struct bbr* bbr, |
||||||
|
const struct bbr_rate_sample* rs, uint32_t sample_bw) |
||||||
|
{ |
||||||
|
bbr->loss_round_start = 0; |
||||||
|
if (rs->interval_us <= 0 || !rs->acked_sacked) |
||||||
|
return; |
||||||
|
bbr->bw_latest = (uint32_t)(bbr->bw_latest > sample_bw ? bbr->bw_latest : sample_bw); |
||||||
|
bbr->inflight_latest = (uint32_t)(bbr->inflight_latest > (uint32_t)rs->delivered ? |
||||||
|
bbr->inflight_latest : (uint32_t)rs->delivered); |
||||||
|
if (!((int32_t)(rs->prior_delivered - bbr->loss_round_delivered) < 0)) { |
||||||
|
bbr->loss_round_delivered = bbr->delivered; |
||||||
|
bbr->loss_round_start = 1; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
static void bbr_advance_latest_delivery_signals(struct bbr* bbr, |
||||||
|
const struct bbr_rate_sample* rs, uint32_t sample_bw) |
||||||
|
{ |
||||||
|
if (bbr->loss_round_start) { |
||||||
|
bbr->bw_latest = sample_bw; |
||||||
|
bbr->inflight_latest = (uint32_t)rs->delivered; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
static void bbr_take_max_bw_sample(struct bbr* bbr, uint32_t bw) |
||||||
|
{ |
||||||
|
bbr->bw_hi[1] = (uint32_t)(bw > bbr->bw_hi[1] ? bw : bbr->bw_hi[1]); |
||||||
|
} |
||||||
|
|
||||||
|
static void bbr_init_lower_bounds(struct bbr* bbr, int init_bw, uint32_t cwnd) |
||||||
|
{ |
||||||
|
if (init_bw && bbr->bw_lo == ~0U) |
||||||
|
bbr->bw_lo = bbr_max_bw(bbr); |
||||||
|
if (bbr->inflight_lo == ~0U) |
||||||
|
bbr->inflight_lo = cwnd; |
||||||
|
} |
||||||
|
|
||||||
|
static void bbr_loss_lower_bounds(struct bbr* bbr) |
||||||
|
{ |
||||||
|
uint32_t loss_cut = BBR_UNIT - BBR_BETA; |
||||||
|
bbr->bw_lo = (uint32_t)(bbr->bw_latest > |
||||||
|
(uint32_t)((uint64_t)bbr->bw_lo * loss_cut >> BBR_SCALE) ? |
||||||
|
bbr->bw_latest : |
||||||
|
(uint32_t)((uint64_t)bbr->bw_lo * loss_cut >> BBR_SCALE)); |
||||||
|
bbr->inflight_lo = (uint32_t)(bbr->inflight_latest > |
||||||
|
(uint32_t)((uint64_t)bbr->inflight_lo * loss_cut >> BBR_SCALE) ? |
||||||
|
bbr->inflight_latest : |
||||||
|
(uint32_t)((uint64_t)bbr->inflight_lo * loss_cut >> BBR_SCALE)); |
||||||
|
} |
||||||
|
|
||||||
|
static void bbr_adapt_lower_bounds(struct bbr* bbr, uint32_t cwnd) |
||||||
|
{ |
||||||
|
if (bbr_is_probing_bandwidth(bbr)) |
||||||
|
return; |
||||||
|
if (bbr->loss_in_round) { |
||||||
|
bbr_init_lower_bounds(bbr, 1, cwnd); |
||||||
|
bbr_loss_lower_bounds(bbr); |
||||||
|
} |
||||||
|
bbr->bw_lo = (uint32_t)(bbr->bw_lo > 1U ? bbr->bw_lo : 1U); |
||||||
|
} |
||||||
|
|
||||||
|
static void bbr_reset_lower_bounds(struct bbr* bbr) |
||||||
|
{ |
||||||
|
bbr->bw_lo = ~0U; |
||||||
|
bbr->inflight_lo = ~0U; |
||||||
|
} |
||||||
|
|
||||||
|
static void bbr_reset_congestion_signals(struct bbr* bbr) |
||||||
|
{ |
||||||
|
bbr->loss_in_round = 0; |
||||||
|
bbr->loss_in_cycle = 0; |
||||||
|
bbr->bw_latest = 0; |
||||||
|
bbr->inflight_latest = 0; |
||||||
|
} |
||||||
|
|
||||||
|
static int bbr_is_inflight_too_high(const struct bbr* bbr, const struct bbr_rate_sample* rs) |
||||||
|
{ |
||||||
|
uint32_t loss_thresh; |
||||||
|
(void)bbr; |
||||||
|
if (rs->lost > 0 && rs->tx_in_flight) { |
||||||
|
loss_thresh = (uint32_t)((uint64_t)rs->tx_in_flight * BBR_LOSS_THRESH >> BBR_SCALE); |
||||||
|
if ((uint32_t)rs->lost > loss_thresh) |
||||||
|
return 1; |
||||||
|
} |
||||||
|
return 0; |
||||||
|
} |
||||||
|
|
||||||
|
static void bbr_handle_queue_too_high_in_startup(struct bbr* bbr, uint32_t mss) |
||||||
|
{ |
||||||
|
uint32_t bdp; |
||||||
|
(void)mss; |
||||||
|
bbr->full_bw_reached = 1; |
||||||
|
bdp = bbr_inflight(bbr, bbr_max_bw(bbr), BBR_UNIT, mss); |
||||||
|
bbr->inflight_hi = (uint32_t)(bdp > bbr->inflight_latest ? bdp : bbr->inflight_latest); |
||||||
|
} |
||||||
|
|
||||||
|
static void bbr_check_loss_too_high_in_startup(struct bbr* bbr, const struct bbr_rate_sample* rs, |
||||||
|
uint32_t mss) |
||||||
|
{ |
||||||
|
if (bbr->full_bw_reached) |
||||||
|
return; |
||||||
|
if (rs->lost > 0 && bbr->loss_events_in_round < 0xf) |
||||||
|
bbr->loss_events_in_round++; |
||||||
|
if (bbr->loss_round_start && |
||||||
|
bbr->loss_events_in_round >= BBR_FULL_LOSS_CNT && |
||||||
|
bbr_is_inflight_too_high(bbr, rs)) { |
||||||
|
bbr_handle_queue_too_high_in_startup(bbr, mss); |
||||||
|
return; |
||||||
|
} |
||||||
|
if (bbr->loss_round_start) |
||||||
|
bbr->loss_events_in_round = 0; |
||||||
|
} |
||||||
|
|
||||||
|
static void bbr_reset_full_bw(struct bbr* bbr) |
||||||
|
{ |
||||||
|
bbr->full_bw = 0; |
||||||
|
bbr->full_bw_cnt = 0; |
||||||
|
bbr->full_bw_now = 0; |
||||||
|
} |
||||||
|
|
||||||
|
static void bbr_advance_max_bw_filter(struct bbr* bbr) |
||||||
|
{ |
||||||
|
if (!bbr->bw_hi[1]) |
||||||
|
return; |
||||||
|
bbr->bw_hi[0] = bbr->bw_hi[1]; |
||||||
|
bbr->bw_hi[1] = 0; |
||||||
|
} |
||||||
|
|
||||||
|
static void bbr_check_full_bw_reached(struct bbr* bbr, const struct bbr_rate_sample* rs, |
||||||
|
uint32_t sample_bw) |
||||||
|
{ |
||||||
|
uint32_t bw_thresh; |
||||||
|
if (bbr->full_bw_now || rs->is_app_limited) |
||||||
|
return; |
||||||
|
bw_thresh = (uint32_t)((uint64_t)bbr->full_bw * BBR_FULL_BW_THRESH >> BBR_SCALE); |
||||||
|
if (sample_bw >= bw_thresh) { |
||||||
|
bbr_reset_full_bw(bbr); |
||||||
|
bbr->full_bw = sample_bw; |
||||||
|
return; |
||||||
|
} |
||||||
|
if (!bbr->round_start) |
||||||
|
return; |
||||||
|
bbr->full_bw_cnt = (uint8_t)(bbr->full_bw_cnt + 1); |
||||||
|
bbr->full_bw_now = (uint8_t)(bbr->full_bw_cnt >= BBR_FULL_BW_CNT ? 1 : 0); |
||||||
|
bbr->full_bw_reached |= bbr->full_bw_now; |
||||||
|
} |
||||||
|
|
||||||
|
static void bbr_raise_inflight_hi_slope(struct bbr* bbr, uint32_t cwnd) |
||||||
|
{ |
||||||
|
uint32_t growth_this_round = 1U << bbr->bw_probe_up_rounds; |
||||||
|
uint32_t cnt; |
||||||
|
bbr->bw_probe_up_rounds = (uint8_t)(bbr->bw_probe_up_rounds + 1 < 30 ? |
||||||
|
bbr->bw_probe_up_rounds + 1 : 30); |
||||||
|
cnt = cwnd / growth_this_round; |
||||||
|
cnt = (uint32_t)(cnt > 1U ? cnt : 1U); |
||||||
|
bbr->bw_probe_up_cnt = cnt; |
||||||
|
} |
||||||
|
|
||||||
|
static void bbr_set_cycle_idx(struct bbr* bbr, int cycle_idx) |
||||||
|
{ |
||||||
|
bbr->cycle_idx = (uint8_t)cycle_idx; |
||||||
|
bbr->try_fast_path = 0; |
||||||
|
} |
||||||
|
|
||||||
|
static void bbr_start_bw_probe_refill(struct bbr* bbr, uint32_t bw_probe_up_rounds) |
||||||
|
{ |
||||||
|
bbr_reset_lower_bounds(bbr); |
||||||
|
bbr->bw_probe_up_rounds = (uint8_t)bw_probe_up_rounds; |
||||||
|
bbr->bw_probe_up_acks = 0; |
||||||
|
bbr->stopped_risky_probe = 0; |
||||||
|
bbr->ack_phase = BBR_ACKS_REFILLING; |
||||||
|
bbr->next_rtt_delivered = bbr->delivered; |
||||||
|
bbr_set_cycle_idx(bbr, BBR_BW_PROBE_REFILL); |
||||||
|
} |
||||||
|
|
||||||
|
static void bbr_start_bw_probe_up(struct bbr* bbr, uint32_t sample_bw, uint32_t cwnd) |
||||||
|
{ |
||||||
|
bbr->ack_phase = BBR_ACKS_PROBE_STARTING; |
||||||
|
bbr->next_rtt_delivered = bbr->delivered; |
||||||
|
bbr->cycle_mstamp_tb = bbr_now(bbr); |
||||||
|
bbr_reset_full_bw(bbr); |
||||||
|
bbr->full_bw = sample_bw; |
||||||
|
bbr_set_cycle_idx(bbr, BBR_BW_PROBE_UP); |
||||||
|
bbr_raise_inflight_hi_slope(bbr, cwnd); |
||||||
|
} |
||||||
|
|
||||||
|
static void bbr_save_cwnd(struct bbr* bbr, uint32_t cwnd) |
||||||
|
{ |
||||||
|
if (bbr->mode != BBR_PROBE_RTT) |
||||||
|
bbr->prior_cwnd = cwnd; |
||||||
|
else |
||||||
|
bbr->prior_cwnd = (uint32_t)(bbr->prior_cwnd > cwnd ? bbr->prior_cwnd : cwnd); |
||||||
|
} |
||||||
|
|
||||||
|
static void bbr_exit_probe_rtt(struct bbr* bbr) |
||||||
|
{ |
||||||
|
bbr_reset_lower_bounds(bbr); |
||||||
|
if (bbr->full_bw_reached) { |
||||||
|
bbr->mode = BBR_PROBE_BW; |
||||||
|
bbr_start_bw_probe_down(bbr); |
||||||
|
bbr_start_bw_probe_cruise(bbr); |
||||||
|
} else { |
||||||
|
bbr->mode = BBR_STARTUP; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
static uint32_t bbr_probe_rtt_cwnd(const struct bbr* bbr) |
||||||
|
{ |
||||||
|
(void)bbr; |
||||||
|
return BBR_CWND_MIN_TARGET; |
||||||
|
} |
||||||
|
|
||||||
|
static uint32_t bbr_inflight_with_headroom(const struct bbr* bbr) |
||||||
|
{ |
||||||
|
uint32_t headroom; |
||||||
|
if (bbr->inflight_hi == ~0U) |
||||||
|
return ~0U; |
||||||
|
headroom = (uint32_t)((uint64_t)bbr->inflight_hi * BBR_INFLIGHT_HEADROOM >> BBR_SCALE); |
||||||
|
headroom = (uint32_t)(headroom > 1U ? headroom : 1U); |
||||||
|
return bbr->inflight_hi - headroom; |
||||||
|
} |
||||||
|
|
||||||
|
static void bbr_pick_probe_wait(struct bbr* bbr) |
||||||
|
{ |
||||||
|
bbr->rounds_since_probe = (uint8_t)((uint32_t)rand() % BBR_BW_PROBE_RAND_ROUNDS); |
||||||
|
bbr->probe_wait_us = BBR_BW_PROBE_BASE_US + (uint32_t)rand() % BBR_BW_PROBE_RAND_US; |
||||||
|
} |
||||||
|
|
||||||
|
static void bbr_start_bw_probe_down(struct bbr* bbr) |
||||||
|
{ |
||||||
|
bbr_reset_congestion_signals(bbr); |
||||||
|
bbr->bw_probe_up_cnt = ~0U; |
||||||
|
bbr_pick_probe_wait(bbr); |
||||||
|
bbr->cycle_mstamp_tb = bbr_now(bbr); |
||||||
|
bbr->ack_phase = BBR_ACKS_PROBE_STOPPING; |
||||||
|
bbr->next_rtt_delivered = bbr->delivered; |
||||||
|
bbr_set_cycle_idx(bbr, BBR_BW_PROBE_DOWN); |
||||||
|
} |
||||||
|
|
||||||
|
static void bbr_start_bw_probe_cruise(struct bbr* bbr) |
||||||
|
{ |
||||||
|
if (bbr->inflight_lo != ~0U) |
||||||
|
bbr->inflight_lo = (uint32_t)(bbr->inflight_lo < bbr->inflight_hi ? |
||||||
|
bbr->inflight_lo : bbr->inflight_hi); |
||||||
|
bbr_set_cycle_idx(bbr, BBR_BW_PROBE_CRUISE); |
||||||
|
} |
||||||
|
|
||||||
|
static void bbr_handle_inflight_too_high(struct bbr* bbr, const struct bbr_rate_sample* rs, |
||||||
|
uint32_t cwnd, uint32_t mss) |
||||||
|
{ |
||||||
|
const uint32_t beta = BBR_BETA; |
||||||
|
bbr->prev_probe_too_high = 1; |
||||||
|
bbr->bw_probe_samples = 0; |
||||||
|
if (!rs->is_app_limited) { |
||||||
|
uint32_t ti = bbr_target_inflight(bbr, cwnd, mss); |
||||||
|
bbr->inflight_hi = (uint32_t)(rs->tx_in_flight > |
||||||
|
(uint32_t)((uint64_t)ti * (BBR_UNIT - beta) >> BBR_SCALE) ? |
||||||
|
rs->tx_in_flight : |
||||||
|
(uint32_t)((uint64_t)ti * (BBR_UNIT - beta) >> BBR_SCALE)); |
||||||
|
} |
||||||
|
if (bbr->mode == BBR_PROBE_BW && bbr->cycle_idx == BBR_BW_PROBE_UP) |
||||||
|
bbr_start_bw_probe_down(bbr); |
||||||
|
} |
||||||
|
|
||||||
|
static void bbr_probe_inflight_hi_upward(struct bbr* bbr, const struct bbr_rate_sample* rs, |
||||||
|
int is_cwnd_limited, uint32_t cwnd) |
||||||
|
{ |
||||||
|
uint32_t delta; |
||||||
|
if (!is_cwnd_limited || cwnd < bbr->inflight_hi) |
||||||
|
return; |
||||||
|
bbr->bw_probe_up_acks += rs->acked_sacked; |
||||||
|
if (bbr->bw_probe_up_acks >= bbr->bw_probe_up_cnt && |
||||||
|
bbr->bw_probe_up_cnt > 0) { |
||||||
|
delta = bbr->bw_probe_up_acks / bbr->bw_probe_up_cnt; |
||||||
|
bbr->bw_probe_up_acks -= delta * bbr->bw_probe_up_cnt; |
||||||
|
bbr->inflight_hi += delta; |
||||||
|
bbr->try_fast_path = 0; |
||||||
|
} |
||||||
|
if (bbr->round_start) |
||||||
|
bbr_raise_inflight_hi_slope(bbr, cwnd); |
||||||
|
} |
||||||
|
|
||||||
|
static void bbr_adapt_upper_bounds(struct bbr* bbr, const struct bbr_rate_sample* rs, |
||||||
|
uint32_t sample_bw, int is_cwnd_limited, |
||||||
|
uint32_t cwnd, uint32_t mss, int* decided) |
||||||
|
{ |
||||||
|
(void)sample_bw; |
||||||
|
*decided = 0; |
||||||
|
if (bbr->ack_phase == BBR_ACKS_PROBE_STARTING && bbr->round_start) |
||||||
|
bbr->ack_phase = BBR_ACKS_PROBE_FEEDBACK; |
||||||
|
if (bbr->ack_phase == BBR_ACKS_PROBE_STOPPING && bbr->round_start) { |
||||||
|
bbr->bw_probe_samples = 0; |
||||||
|
bbr->ack_phase = BBR_ACKS_INIT; |
||||||
|
if (bbr->mode == BBR_PROBE_BW && !rs->is_app_limited) |
||||||
|
bbr_advance_max_bw_filter(bbr); |
||||||
|
if (bbr->mode == BBR_PROBE_BW && bbr->stopped_risky_probe && !bbr->prev_probe_too_high) { |
||||||
|
bbr_start_bw_probe_refill(bbr, 0); |
||||||
|
*decided = 1; |
||||||
|
return; |
||||||
|
} |
||||||
|
} |
||||||
|
if (bbr_is_inflight_too_high(bbr, rs)) { |
||||||
|
if (bbr->bw_probe_samples) |
||||||
|
bbr_handle_inflight_too_high(bbr, rs, cwnd, mss); |
||||||
|
} else { |
||||||
|
if (bbr->inflight_hi == ~0U) |
||||||
|
return; |
||||||
|
if (rs->tx_in_flight > bbr->inflight_hi) |
||||||
|
bbr->inflight_hi = rs->tx_in_flight; |
||||||
|
if (bbr->mode == BBR_PROBE_BW && bbr->cycle_idx == BBR_BW_PROBE_UP) |
||||||
|
bbr_probe_inflight_hi_upward(bbr, rs, is_cwnd_limited, cwnd); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
static int bbr_is_reno_coexistence_probe_time(struct bbr* bbr, uint32_t cwnd, uint32_t mss) |
||||||
|
{ |
||||||
|
uint32_t rounds = bbr_target_inflight(bbr, cwnd, mss); |
||||||
|
if (rounds > BBR_BW_PROBE_MAX_ROUNDS) |
||||||
|
rounds = BBR_BW_PROBE_MAX_ROUNDS; |
||||||
|
return bbr->rounds_since_probe >= rounds; |
||||||
|
} |
||||||
|
|
||||||
|
static int bbr_check_time_to_probe_bw(struct bbr* bbr, const struct bbr_rate_sample* rs, |
||||||
|
uint32_t cwnd, uint32_t mss) |
||||||
|
{ |
||||||
|
(void)rs; |
||||||
|
if (bbr_has_elapsed_in_phase(bbr, bbr->probe_wait_us) || |
||||||
|
bbr_is_reno_coexistence_probe_time(bbr, cwnd, mss)) { |
||||||
|
bbr_start_bw_probe_refill(bbr, 0); |
||||||
|
return 1; |
||||||
|
} |
||||||
|
return 0; |
||||||
|
} |
||||||
|
|
||||||
|
static int bbr_check_time_to_cruise(const struct bbr* bbr, uint32_t inflight, |
||||||
|
uint32_t bw, uint32_t mss) |
||||||
|
{ |
||||||
|
if (inflight > bbr_inflight_with_headroom(bbr)) |
||||||
|
return 0; |
||||||
|
return inflight <= bbr_inflight(bbr, bw, BBR_UNIT, mss); |
||||||
|
} |
||||||
|
|
||||||
|
static void bbr_update_cycle_phase(struct bbr* bbr, const struct bbr_rate_sample* rs, |
||||||
|
uint32_t sample_bw, uint32_t mss, |
||||||
|
uint32_t inflight_packets, int is_cwnd_limited, |
||||||
|
uint32_t cwnd) |
||||||
|
{ |
||||||
|
int decided = 0; |
||||||
|
int is_bw_probe_done; |
||||||
|
uint32_t inflight, bw; |
||||||
|
|
||||||
|
if (!bbr->full_bw_reached) |
||||||
|
return; |
||||||
|
bbr_adapt_upper_bounds(bbr, rs, sample_bw, is_cwnd_limited, cwnd, mss, &decided); |
||||||
|
if (decided) |
||||||
|
return; |
||||||
|
if (bbr->mode != BBR_PROBE_BW) |
||||||
|
return; |
||||||
|
|
||||||
|
inflight = inflight_packets; |
||||||
|
bw = bbr_max_bw(bbr); |
||||||
|
|
||||||
|
switch (bbr->cycle_idx) { |
||||||
|
case BBR_BW_PROBE_CRUISE: |
||||||
|
if (bbr_check_time_to_probe_bw(bbr, rs, cwnd, mss)) |
||||||
|
return; |
||||||
|
break; |
||||||
|
case BBR_BW_PROBE_REFILL: |
||||||
|
if (bbr->round_start) { |
||||||
|
bbr->bw_probe_samples = 1; |
||||||
|
bbr_start_bw_probe_up(bbr, sample_bw, cwnd); |
||||||
|
} |
||||||
|
break; |
||||||
|
case BBR_BW_PROBE_UP: |
||||||
|
is_bw_probe_done = 0; |
||||||
|
if (bbr->prev_probe_too_high && inflight >= bbr->inflight_hi) { |
||||||
|
bbr->stopped_risky_probe = 1; |
||||||
|
is_bw_probe_done = 1; |
||||||
|
} else { |
||||||
|
if (is_cwnd_limited && cwnd >= bbr->inflight_hi) { |
||||||
|
bbr_reset_full_bw(bbr); |
||||||
|
bbr->full_bw = sample_bw; |
||||||
|
} else if (bbr->full_bw_now) { |
||||||
|
is_bw_probe_done = 1; |
||||||
|
} |
||||||
|
} |
||||||
|
if (is_bw_probe_done) { |
||||||
|
bbr->prev_probe_too_high = 0; |
||||||
|
bbr_start_bw_probe_down(bbr); |
||||||
|
} |
||||||
|
break; |
||||||
|
case BBR_BW_PROBE_DOWN: |
||||||
|
if (bbr_check_time_to_probe_bw(bbr, rs, cwnd, mss)) |
||||||
|
return; |
||||||
|
if (bbr_check_time_to_cruise(bbr, inflight, bw, mss)) |
||||||
|
bbr_start_bw_probe_cruise(bbr); |
||||||
|
break; |
||||||
|
default: |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
static void bbr_check_drain(struct bbr* bbr, const struct bbr_rate_sample* rs, |
||||||
|
uint32_t sample_bw, uint32_t mss, uint32_t inflight_packets) |
||||||
|
{ |
||||||
|
(void)rs; (void)sample_bw; |
||||||
|
if (bbr->mode == BBR_STARTUP && bbr->full_bw_reached) { |
||||||
|
bbr->mode = BBR_DRAIN; |
||||||
|
bbr_reset_congestion_signals(bbr); |
||||||
|
} |
||||||
|
if (bbr->mode == BBR_DRAIN && |
||||||
|
inflight_packets <= bbr_inflight(bbr, bbr_max_bw(bbr), BBR_UNIT, mss)) { |
||||||
|
bbr->mode = BBR_PROBE_BW; |
||||||
|
bbr_start_bw_probe_down(bbr); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
static void bbr_check_probe_rtt_done(struct bbr* bbr, uint32_t* cwnd) |
||||||
|
{ |
||||||
|
if (!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); |
||||||
|
*cwnd = (uint32_t)(*cwnd > bbr->prior_cwnd ? *cwnd : bbr->prior_cwnd); |
||||||
|
bbr_exit_probe_rtt(bbr); |
||||||
|
} |
||||||
|
|
||||||
|
static void bbr_update_min_rtt(struct bbr* bbr, const struct bbr_rate_sample* rs, |
||||||
|
uint32_t* cwnd, uint32_t mss, uint32_t inflight_packets) |
||||||
|
{ |
||||||
|
uint32_t expire; |
||||||
|
int probe_rtt_expired, min_rtt_expired; |
||||||
|
(void)mss; |
||||||
|
|
||||||
|
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); |
||||||
|
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->has_seen_rtt = 1; |
||||||
|
} |
||||||
|
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); |
||||||
|
if (bbr->probe_rtt_min_us <= bbr->min_rtt_us || min_rtt_expired) { |
||||||
|
bbr->min_rtt_us = bbr->probe_rtt_min_us; |
||||||
|
bbr->min_rtt_stamp_tb = bbr_now(bbr); |
||||||
|
} |
||||||
|
|
||||||
|
if (BBR_PROBE_RTT_MS > 0 && probe_rtt_expired && |
||||||
|
!bbr->idle_restart && bbr->mode != BBR_PROBE_RTT) { |
||||||
|
bbr->mode = BBR_PROBE_RTT; |
||||||
|
bbr_save_cwnd(bbr, *cwnd); |
||||||
|
bbr->probe_rtt_done_stamp_tb = 0; |
||||||
|
bbr->ack_phase = BBR_ACKS_PROBE_STOPPING; |
||||||
|
bbr->next_rtt_delivered = bbr->delivered; |
||||||
|
} |
||||||
|
|
||||||
|
if (bbr->mode == BBR_PROBE_RTT) { |
||||||
|
if (!bbr->probe_rtt_done_stamp_tb && |
||||||
|
inflight_packets <= bbr_probe_rtt_cwnd(bbr)) { |
||||||
|
bbr->probe_rtt_done_stamp_tb = (uint32_t)bbr_now(bbr) + BBR_PROBE_RTT_MS * 10U; |
||||||
|
bbr->probe_rtt_round_done = 0; |
||||||
|
bbr->next_rtt_delivered = bbr->delivered; |
||||||
|
} else if (bbr->probe_rtt_done_stamp_tb) { |
||||||
|
if (bbr->round_start) |
||||||
|
bbr->probe_rtt_round_done = 1; |
||||||
|
if (bbr->probe_rtt_round_done) |
||||||
|
bbr_check_probe_rtt_done(bbr, cwnd); |
||||||
|
} |
||||||
|
} |
||||||
|
if (rs->delivered > 0) |
||||||
|
bbr->idle_restart = 0; |
||||||
|
} |
||||||
|
|
||||||
|
static void bbr_reset_startup_mode(struct bbr* bbr) |
||||||
|
{ |
||||||
|
bbr->mode = BBR_STARTUP; |
||||||
|
} |
||||||
|
|
||||||
|
static void bbr_update_gains(struct bbr* bbr) |
||||||
|
{ |
||||||
|
switch (bbr->mode) { |
||||||
|
case BBR_STARTUP: |
||||||
|
bbr->pacing_gain = BBR_STARTUP_PACING_GAIN; |
||||||
|
bbr->cwnd_gain = BBR_STARTUP_CWND_GAIN; |
||||||
|
break; |
||||||
|
case BBR_DRAIN: |
||||||
|
bbr->pacing_gain = BBR_DRAIN_GAIN; |
||||||
|
bbr->cwnd_gain = BBR_STARTUP_CWND_GAIN; |
||||||
|
break; |
||||||
|
case BBR_PROBE_BW: |
||||||
|
bbr->pacing_gain = bbr_pacing_gain[bbr->cycle_idx]; |
||||||
|
bbr->cwnd_gain = BBR_CWND_GAIN; |
||||||
|
break; |
||||||
|
case BBR_PROBE_RTT: |
||||||
|
bbr->pacing_gain = BBR_UNIT; |
||||||
|
bbr->cwnd_gain = BBR_UNIT; |
||||||
|
break; |
||||||
|
default: |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
static void bbr_update_congestion_signals(struct bbr* bbr, const struct bbr_rate_sample* rs, |
||||||
|
uint32_t sample_bw, uint32_t cwnd) |
||||||
|
{ |
||||||
|
uint32_t bw; |
||||||
|
if (rs->interval_us <= 0 || !rs->acked_sacked) |
||||||
|
return; |
||||||
|
bw = sample_bw; |
||||||
|
if (!rs->is_app_limited || bw >= bbr_max_bw(bbr)) |
||||||
|
bbr_take_max_bw_sample(bbr, bw); |
||||||
|
bbr->loss_in_round |= (rs->lost > 0); |
||||||
|
if (!bbr->loss_round_start) |
||||||
|
return; |
||||||
|
bbr_adapt_lower_bounds(bbr, cwnd); |
||||||
|
bbr->loss_in_round = 0; |
||||||
|
} |
||||||
|
|
||||||
|
static void bbr_update_ack_aggregation(struct bbr* bbr, const struct bbr_rate_sample* rs, |
||||||
|
uint32_t cwnd, uint32_t mss) |
||||||
|
{ |
||||||
|
uint32_t epoch_us, expected_acked, extra_acked; |
||||||
|
uint32_t extra_acked_win_rtts_thresh = BBR_EXTRA_ACKED_WIN_RTTS; |
||||||
|
uint64_t now_us; |
||||||
|
(void)mss; |
||||||
|
|
||||||
|
if (!BBR_EXTRA_ACKED_GAIN || rs->acked_sacked <= 0 || |
||||||
|
rs->interval_us <= 0) |
||||||
|
return; |
||||||
|
|
||||||
|
if (bbr->round_start) { |
||||||
|
bbr->extra_acked_win_rtts = (uint8_t)(bbr->extra_acked_win_rtts + 1 < 0x1F ? |
||||||
|
bbr->extra_acked_win_rtts + 1 : 0x1F); |
||||||
|
if (!bbr->full_bw_reached) |
||||||
|
extra_acked_win_rtts_thresh = 1; |
||||||
|
if (bbr->extra_acked_win_rtts >= extra_acked_win_rtts_thresh) { |
||||||
|
bbr->extra_acked_win_rtts = 0; |
||||||
|
bbr->extra_acked_win_idx = bbr->extra_acked_win_idx ? 0 : 1; |
||||||
|
bbr->extra_acked[bbr->extra_acked_win_idx] = 0; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
now_us = bbr_now(bbr) * 100ULL; |
||||||
|
if (now_us > bbr->ack_epoch_mstamp_tb * 100ULL) |
||||||
|
epoch_us = (uint32_t)(now_us - bbr->ack_epoch_mstamp_tb * 100ULL); |
||||||
|
else |
||||||
|
epoch_us = 0; |
||||||
|
expected_acked = (uint32_t)((uint64_t)bbr_bw(bbr) * epoch_us / BW_UNIT); |
||||||
|
|
||||||
|
if (bbr->ack_epoch_acked <= expected_acked || |
||||||
|
(bbr->ack_epoch_acked + rs->acked_sacked >= BBR_ACK_EPOCH_ACKED_RESET_THRESH)) { |
||||||
|
bbr->ack_epoch_acked = 0; |
||||||
|
bbr->ack_epoch_mstamp_tb = bbr_now(bbr); |
||||||
|
expected_acked = 0; |
||||||
|
} |
||||||
|
|
||||||
|
bbr->ack_epoch_acked = (uint32_t)(bbr->ack_epoch_acked + rs->acked_sacked < 0xFFFFF ? |
||||||
|
bbr->ack_epoch_acked + rs->acked_sacked : 0xFFFFF); |
||||||
|
extra_acked = bbr->ack_epoch_acked - expected_acked; |
||||||
|
extra_acked = (uint32_t)(extra_acked < cwnd ? extra_acked : cwnd); |
||||||
|
if (extra_acked > bbr->extra_acked[bbr->extra_acked_win_idx]) |
||||||
|
bbr->extra_acked[bbr->extra_acked_win_idx] = (uint16_t)extra_acked; |
||||||
|
} |
||||||
|
|
||||||
|
static void bbr_init_pacing_rate_from_rtt(struct bbr* bbr, uint32_t cwnd, |
||||||
|
uint32_t mss, uint32_t* pacing_rate_out) |
||||||
|
{ |
||||||
|
uint64_t bw; |
||||||
|
uint32_t rtt_us; |
||||||
|
|
||||||
|
if (bbr->has_seen_rtt && bbr->min_rtt_us != ~0U) { |
||||||
|
rtt_us = bbr->min_rtt_us; |
||||||
|
if (rtt_us < 1) rtt_us = 1; |
||||||
|
} else { |
||||||
|
rtt_us = 1000; |
||||||
|
} |
||||||
|
bw = (uint64_t)cwnd * BW_UNIT / rtt_us; |
||||||
|
*pacing_rate_out = (uint32_t)bbr_rate_bytes_per_sec(bw, BBR_STARTUP_PACING_GAIN, 1, mss); |
||||||
|
} |
||||||
|
|
||||||
|
static void bbr_set_pacing_rate(struct bbr* bbr, uint32_t bw, int gain, |
||||||
|
uint32_t mss, uint32_t* pacing_rate_out) |
||||||
|
{ |
||||||
|
uint32_t rate = bbr_bw_to_pacing_rate(bw, gain, mss); |
||||||
|
|
||||||
|
if (!bbr->has_seen_rtt && bbr->min_rtt_us != ~0U) |
||||||
|
bbr_init_pacing_rate_from_rtt(bbr, *pacing_rate_out > 0 ? *pacing_rate_out : 4, mss, pacing_rate_out); |
||||||
|
if (bbr->full_bw_reached || rate > *pacing_rate_out) |
||||||
|
*pacing_rate_out = rate; |
||||||
|
} |
||||||
|
|
||||||
|
static void bbr_set_cwnd(struct bbr* bbr, const struct bbr_rate_sample* rs, |
||||||
|
uint32_t acked, uint32_t bw, int gain, |
||||||
|
uint32_t mss, uint32_t* cwnd) |
||||||
|
{ |
||||||
|
uint32_t target_cwnd = 0; |
||||||
|
uint32_t local_cwnd = *cwnd; |
||||||
|
|
||||||
|
(void)rs; |
||||||
|
if (!acked) |
||||||
|
goto done; |
||||||
|
|
||||||
|
target_cwnd = bbr_bdp(bbr, bw, gain, mss); |
||||||
|
target_cwnd += bbr_ack_aggregation_cwnd(bbr, mss); |
||||||
|
target_cwnd = bbr_quantization_budget(bbr, target_cwnd, mss); |
||||||
|
|
||||||
|
bbr->try_fast_path = 0; |
||||||
|
if (bbr->full_bw_reached) { |
||||||
|
local_cwnd += acked; |
||||||
|
if (local_cwnd >= target_cwnd) { |
||||||
|
local_cwnd = target_cwnd; |
||||||
|
bbr->try_fast_path = 1; |
||||||
|
} |
||||||
|
} else if (local_cwnd < target_cwnd || local_cwnd < 2 * (uint32_t)bbr->init_cwnd * mss) { |
||||||
|
local_cwnd += acked; |
||||||
|
} else { |
||||||
|
bbr->try_fast_path = 1; |
||||||
|
} |
||||||
|
|
||||||
|
local_cwnd = (uint32_t)(local_cwnd > BBR_CWND_MIN_TARGET ? |
||||||
|
local_cwnd : BBR_CWND_MIN_TARGET); |
||||||
|
done: |
||||||
|
*cwnd = local_cwnd; |
||||||
|
if (bbr->mode == BBR_PROBE_RTT) { |
||||||
|
uint32_t pr_cwnd = bbr_probe_rtt_cwnd(bbr); |
||||||
|
*cwnd = (uint32_t)(*cwnd < pr_cwnd ? *cwnd : pr_cwnd); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
static void bbr_bound_cwnd_for_inflight_model(struct bbr* bbr, uint32_t* cwnd, uint32_t mss) |
||||||
|
{ |
||||||
|
uint32_t cap = ~0U; |
||||||
|
(void)mss; |
||||||
|
|
||||||
|
if (!bbr->initialized) |
||||||
|
return; |
||||||
|
|
||||||
|
if (bbr->mode == BBR_PROBE_BW && bbr->cycle_idx != BBR_BW_PROBE_CRUISE) |
||||||
|
cap = bbr->inflight_hi; |
||||||
|
else if (bbr->mode == BBR_PROBE_RTT || |
||||||
|
(bbr->mode == BBR_PROBE_BW && bbr->cycle_idx == BBR_BW_PROBE_CRUISE)) |
||||||
|
cap = bbr_inflight_with_headroom(bbr); |
||||||
|
|
||||||
|
cap = (uint32_t)(cap < bbr->inflight_lo ? cap : bbr->inflight_lo); |
||||||
|
cap = (uint32_t)(cap > BBR_CWND_MIN_TARGET ? cap : BBR_CWND_MIN_TARGET); |
||||||
|
*cwnd = (uint32_t)(cap < *cwnd ? cap : *cwnd); |
||||||
|
} |
||||||
|
|
||||||
|
static int bbr_run_fast_path(struct bbr* bbr, int* update_model, |
||||||
|
const struct bbr_rate_sample* rs, uint32_t sample_bw, |
||||||
|
uint32_t mss, uint32_t inflight_packets, |
||||||
|
int is_cwnd_limited, uint32_t* cwnd) |
||||||
|
{ |
||||||
|
uint32_t prev_min_rtt_us; |
||||||
|
uint8_t prev_mode; |
||||||
|
|
||||||
|
if (bbr->try_fast_path && rs->is_app_limited && |
||||||
|
sample_bw < bbr_max_bw(bbr) && !bbr->loss_in_round) { |
||||||
|
prev_mode = bbr->mode; |
||||||
|
prev_min_rtt_us = bbr->min_rtt_us; |
||||||
|
bbr_check_drain(bbr, rs, sample_bw, mss, inflight_packets); |
||||||
|
bbr_update_cycle_phase(bbr, rs, sample_bw, mss, inflight_packets, |
||||||
|
is_cwnd_limited, *cwnd); |
||||||
|
bbr_update_min_rtt(bbr, rs, cwnd, mss, inflight_packets); |
||||||
|
|
||||||
|
if (bbr->mode == prev_mode && |
||||||
|
bbr->min_rtt_us == prev_min_rtt_us && |
||||||
|
bbr->try_fast_path) |
||||||
|
return 1; |
||||||
|
*update_model = 0; |
||||||
|
} |
||||||
|
return 0; |
||||||
|
} |
||||||
|
|
||||||
|
void bbr_init(struct bbr* bbr) |
||||||
|
{ |
||||||
|
bbr->initialized = 1; |
||||||
|
bbr->init_cwnd = 0; |
||||||
|
bbr->prior_cwnd = 0; |
||||||
|
bbr->delivered = 0; |
||||||
|
bbr->next_rtt_delivered = 0; |
||||||
|
|
||||||
|
bbr->probe_rtt_done_stamp_tb = 0; |
||||||
|
bbr->probe_rtt_round_done = 0; |
||||||
|
bbr->probe_rtt_min_us = ~0U; |
||||||
|
bbr->probe_rtt_min_stamp_tb = bbr_now(bbr); |
||||||
|
bbr->min_rtt_us = ~0U; |
||||||
|
bbr->min_rtt_stamp_tb = bbr_now(bbr); |
||||||
|
|
||||||
|
bbr->has_seen_rtt = 0; |
||||||
|
|
||||||
|
bbr->round_start = 0; |
||||||
|
bbr->idle_restart = 0; |
||||||
|
bbr->full_bw_reached = 0; |
||||||
|
bbr->full_bw = 0; |
||||||
|
bbr->full_bw_cnt = 0; |
||||||
|
bbr->full_bw_now = 0; |
||||||
|
bbr->cycle_mstamp_tb = 0; |
||||||
|
bbr->cycle_idx = 0; |
||||||
|
|
||||||
|
bbr_reset_startup_mode(bbr); |
||||||
|
|
||||||
|
bbr->ack_epoch_mstamp_tb = bbr_now(bbr); |
||||||
|
bbr->ack_epoch_acked = 0; |
||||||
|
bbr->extra_acked_win_rtts = 0; |
||||||
|
bbr->extra_acked_win_idx = 0; |
||||||
|
bbr->extra_acked[0] = 0; |
||||||
|
bbr->extra_acked[1] = 0; |
||||||
|
|
||||||
|
bbr->try_fast_path = 0; |
||||||
|
bbr->loss_round_delivered = 1; |
||||||
|
bbr->loss_round_start = 0; |
||||||
|
bbr->loss_events_in_round = 0; |
||||||
|
bbr_reset_congestion_signals(bbr); |
||||||
|
bbr->bw_lo = ~0U; |
||||||
|
bbr->bw_hi[0] = 0; |
||||||
|
bbr->bw_hi[1] = 0; |
||||||
|
bbr->inflight_lo = ~0U; |
||||||
|
bbr->inflight_hi = ~0U; |
||||||
|
bbr_reset_full_bw(bbr); |
||||||
|
bbr->bw_probe_up_cnt = ~0U; |
||||||
|
bbr->bw_probe_up_acks = 0; |
||||||
|
bbr->bw_probe_up_rounds = 0; |
||||||
|
bbr->probe_wait_us = 0; |
||||||
|
bbr->stopped_risky_probe = 0; |
||||||
|
bbr->ack_phase = BBR_ACKS_INIT; |
||||||
|
bbr->rounds_since_probe = 0; |
||||||
|
bbr->bw_probe_samples = 0; |
||||||
|
bbr->prev_probe_too_high = 0; |
||||||
|
} |
||||||
|
|
||||||
|
void bbr_main(struct bbr* bbr, const struct bbr_rate_sample* rs, |
||||||
|
uint32_t* cwnd_out, uint32_t* pacing_rate_out, |
||||||
|
uint32_t mss, uint32_t inflight_bytes, int is_cwnd_limited) |
||||||
|
{ |
||||||
|
uint32_t cwnd = *cwnd_out; |
||||||
|
uint32_t sample_bw, bw; |
||||||
|
int update_model = 1; |
||||||
|
|
||||||
|
bbr->delivered += rs->delivered; |
||||||
|
bbr_update_round_start(bbr, rs); |
||||||
|
|
||||||
|
if (bbr->round_start) |
||||||
|
bbr->rounds_since_probe = (uint8_t)(bbr->rounds_since_probe + 1 < 0xFF ? |
||||||
|
bbr->rounds_since_probe + 1 : 0xFF); |
||||||
|
|
||||||
|
sample_bw = bbr_calculate_bw_sample(rs); |
||||||
|
bbr_update_latest_delivery_signals(bbr, rs, sample_bw); |
||||||
|
|
||||||
|
if (bbr_run_fast_path(bbr, &update_model, rs, sample_bw, mss, |
||||||
|
inflight_bytes, is_cwnd_limited, &cwnd)) |
||||||
|
goto out; |
||||||
|
|
||||||
|
if (update_model) { |
||||||
|
bbr_update_congestion_signals(bbr, rs, sample_bw, cwnd); |
||||||
|
bbr_update_ack_aggregation(bbr, rs, cwnd, mss); |
||||||
|
bbr_check_loss_too_high_in_startup(bbr, rs, mss); |
||||||
|
bbr_check_full_bw_reached(bbr, rs, sample_bw); |
||||||
|
bbr_check_drain(bbr, rs, sample_bw, mss, inflight_bytes); |
||||||
|
bbr_update_cycle_phase(bbr, rs, sample_bw, mss, inflight_bytes, |
||||||
|
is_cwnd_limited, cwnd); |
||||||
|
bbr_update_min_rtt(bbr, rs, &cwnd, mss, inflight_bytes); |
||||||
|
} |
||||||
|
|
||||||
|
bbr_update_gains(bbr); |
||||||
|
bw = bbr_bw(bbr); |
||||||
|
bbr_set_pacing_rate(bbr, bw, bbr->pacing_gain, mss, pacing_rate_out); |
||||||
|
bbr_set_cwnd(bbr, rs, rs->acked_sacked, bw, bbr->cwnd_gain, mss, &cwnd); |
||||||
|
bbr_bound_cwnd_for_inflight_model(bbr, &cwnd, mss); |
||||||
|
|
||||||
|
out: |
||||||
|
bbr_advance_latest_delivery_signals(bbr, rs, sample_bw); |
||||||
|
bbr->loss_in_cycle |= (rs->lost > 0); |
||||||
|
*cwnd_out = cwnd; |
||||||
|
} |
||||||
|
|
||||||
|
void bbr_note_loss(struct bbr* bbr) |
||||||
|
{ |
||||||
|
if (!bbr->loss_in_round) |
||||||
|
bbr->loss_round_delivered = bbr->delivered; |
||||||
|
bbr->loss_in_round = 1; |
||||||
|
bbr->loss_in_cycle = 1; |
||||||
|
} |
||||||
|
|
||||||
|
void bbr_tx_start(struct bbr* bbr) |
||||||
|
{ |
||||||
|
bbr->idle_restart = 1; |
||||||
|
bbr->ack_epoch_mstamp_tb = bbr_now(bbr); |
||||||
|
bbr->ack_epoch_acked = 0; |
||||||
|
} |
||||||
@ -0,0 +1,107 @@ |
|||||||
|
#pragma once |
||||||
|
#include <stdint.h> |
||||||
|
|
||||||
|
#define BBR_SCALE 8 |
||||||
|
#define BBR_UNIT (1 << BBR_SCALE) |
||||||
|
#define BW_SCALE 24 |
||||||
|
#define BW_UNIT (1 << BW_SCALE) |
||||||
|
|
||||||
|
#define BBR_STARTUP_PACING_GAIN (BBR_UNIT * 277 / 100 + 1) |
||||||
|
#define BBR_STARTUP_CWND_GAIN (BBR_UNIT * 2) |
||||||
|
#define BBR_DRAIN_GAIN (BBR_UNIT * 1000 / 2885) |
||||||
|
#define BBR_CWND_GAIN (BBR_UNIT * 2) |
||||||
|
#define BBR_PROBE_RTT_CWND_GAIN (BBR_UNIT * 1 / 2) |
||||||
|
#define BBR_CWND_MIN_TARGET 4 |
||||||
|
#define BBR_FULL_BW_THRESH (BBR_UNIT * 5 / 4) |
||||||
|
#define BBR_FULL_BW_CNT 3 |
||||||
|
#define BBR_LOSS_THRESH (BBR_UNIT * 2 / 100) |
||||||
|
#define BBR_BETA (BBR_UNIT * 30 / 100) |
||||||
|
#define BBR_INFLIGHT_HEADROOM (BBR_UNIT * 15 / 100) |
||||||
|
#define BBR_EXTRA_ACKED_GAIN BBR_UNIT |
||||||
|
#define BBR_EXTRA_ACKED_WIN_RTTS 5 |
||||||
|
#define BBR_ACK_EPOCH_ACKED_RESET_THRESH (1U << 20) |
||||||
|
#define BBR_EXTRA_ACKED_MAX_US (100 * 1000) |
||||||
|
#define BBR_FULL_LOSS_CNT 6 |
||||||
|
#define BBR_BW_PROBE_MAX_ROUNDS 63 |
||||||
|
#define BBR_BW_PROBE_RAND_ROUNDS 2 |
||||||
|
#define BBR_BW_PROBE_BASE_US (2 * 1000000) |
||||||
|
#define BBR_BW_PROBE_RAND_US (1 * 1000000) |
||||||
|
#define BBR_PROBE_RTT_WIN_MS 5000 |
||||||
|
#define BBR_MIN_RTT_WIN_SEC 10 |
||||||
|
#define BBR_PROBE_RTT_MS 200 |
||||||
|
|
||||||
|
enum bbr_mode { BBR_STARTUP = 0, BBR_DRAIN = 1, BBR_PROBE_BW = 2, BBR_PROBE_RTT = 3 }; |
||||||
|
enum bbr_bw_probe_cyc { BBR_BW_PROBE_UP = 0, BBR_BW_PROBE_DOWN = 1, BBR_BW_PROBE_CRUISE = 2, BBR_BW_PROBE_REFILL = 3 }; |
||||||
|
enum bbr_ack_phase { BBR_ACKS_INIT = 0, BBR_ACKS_REFILLING, BBR_ACKS_PROBE_STARTING, BBR_ACKS_PROBE_FEEDBACK, BBR_ACKS_PROBE_STOPPING }; |
||||||
|
|
||||||
|
struct bbr_rate_sample { |
||||||
|
uint32_t delivered; |
||||||
|
uint32_t interval_us; |
||||||
|
uint32_t rtt_us; |
||||||
|
uint32_t acked_sacked; |
||||||
|
uint32_t prior_delivered; |
||||||
|
uint32_t tx_in_flight; |
||||||
|
int lost; |
||||||
|
int is_app_limited; |
||||||
|
}; |
||||||
|
|
||||||
|
struct bbr { |
||||||
|
uint32_t min_rtt_us; |
||||||
|
uint32_t bw_lo; |
||||||
|
uint32_t bw_hi[2]; |
||||||
|
uint32_t inflight_lo; |
||||||
|
uint32_t inflight_hi; |
||||||
|
uint32_t bw_latest; |
||||||
|
uint32_t inflight_latest; |
||||||
|
uint8_t mode; |
||||||
|
uint8_t cycle_idx; |
||||||
|
uint16_t pacing_gain; |
||||||
|
uint16_t cwnd_gain; |
||||||
|
uint32_t next_rtt_delivered; |
||||||
|
uint8_t round_start : 1; |
||||||
|
uint8_t full_bw_reached : 1; |
||||||
|
uint8_t loss_in_round : 1; |
||||||
|
uint8_t loss_in_cycle : 1; |
||||||
|
uint8_t idle_restart : 1; |
||||||
|
uint8_t try_fast_path : 1; |
||||||
|
uint8_t has_seen_rtt : 1; |
||||||
|
uint8_t initialized : 1; |
||||||
|
uint32_t loss_round_delivered; |
||||||
|
uint32_t bw_probe_up_cnt; |
||||||
|
uint32_t bw_probe_up_acks; |
||||||
|
uint8_t bw_probe_up_rounds; |
||||||
|
uint8_t bw_probe_samples : 1; |
||||||
|
uint8_t prev_probe_too_high: 1; |
||||||
|
uint8_t stopped_risky_probe: 1; |
||||||
|
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; |
||||||
|
uint8_t probe_rtt_round_done : 1; |
||||||
|
uint16_t extra_acked[2]; |
||||||
|
uint32_t ack_epoch_acked; |
||||||
|
uint64_t ack_epoch_mstamp_tb; |
||||||
|
uint8_t extra_acked_win_idx : 1; |
||||||
|
uint8_t extra_acked_win_rtts : 5; |
||||||
|
uint32_t prior_cwnd; |
||||||
|
uint8_t init_cwnd : 7; |
||||||
|
uint8_t full_bw_cnt : 2; |
||||||
|
uint8_t ack_phase : 3; |
||||||
|
uint8_t loss_events_in_round : 4; |
||||||
|
uint32_t delivered; |
||||||
|
uint32_t full_bw; |
||||||
|
uint64_t min_rtt_stamp_tb; |
||||||
|
uint8_t loss_round_start : 1; |
||||||
|
uint8_t full_bw_now : 1; |
||||||
|
uint8_t pad_unused : 6; |
||||||
|
uint64_t now_tb; // 0 = real get_time_tb(); >0 = test-controlled time
|
||||||
|
}; |
||||||
|
|
||||||
|
void bbr_init(struct bbr* bbr); |
||||||
|
void bbr_main(struct bbr* bbr, const struct bbr_rate_sample* rs, |
||||||
|
uint32_t* cwnd_out, uint32_t* pacing_rate_out, |
||||||
|
uint32_t mss, uint32_t inflight_bytes, int is_cwnd_limited); |
||||||
|
void bbr_note_loss(struct bbr* bbr); |
||||||
|
void bbr_tx_start(struct bbr* bbr); |
||||||
@ -0,0 +1,161 @@ |
|||||||
|
#include "../src/etcp_bbr.h" |
||||||
|
#include <string.h> |
||||||
|
#include <stdio.h> |
||||||
|
#include <stdlib.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 --- */ |
||||||
|
|
||||||
|
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++) { |
||||||
|
s.now_tb += 10000; |
||||||
|
for (int a = 0; a < 5; a++) run_ack(&s, 1400, 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; |
||||||
|
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("cwnd capped by inflight_hi"); |
||||||
|
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_UP; |
||||||
|
s.inflight_hi = 4000; s.inflight_lo = 4000; g_cwnd = 5600; g_pacing = 0; |
||||||
|
s.now_tb += 5000; run_ack(&s, 1400, 5000, 0, 0, 5000); |
||||||
|
if (g_cwnd > 4000) FAIL("cwnd > inflight_hi"); |
||||||
|
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) FAIL("cwnd not capped"); |
||||||
|
PASS(); return 0; |
||||||
|
} |
||||||
|
|
||||||
|
int main(void) |
||||||
|
{ |
||||||
|
test_init(); |
||||||
|
test_startup_growth(); |
||||||
|
test_full_bw(); |
||||||
|
test_loss_startup(); |
||||||
|
test_cwnd_bounded(); |
||||||
|
test_loss_cut(); |
||||||
|
test_fast_path(); |
||||||
|
test_probe_rtt(); |
||||||
|
printf("\n=== Results: %d/%d passed, %d failed ===\n", test_passed, test_total, test_failed); |
||||||
|
return test_failed ? 1 : 0; |
||||||
|
} |
||||||
Loading…
Reference in new issue