mirror of
https://github.com/facebookincubator/mvfst.git
synced 2026-01-06 03:41:10 +03:00
Summary: Original commit changeset: 337824bc37bc Original Phabricator Diff: D47722462 Reviewed By: jbeshay, terrelln, lnicco Differential Revision: D47801753 fbshipit-source-id: 795ffcccbc2223608e2a707ec2e5bcc7dd974eb3
925 lines
29 KiB
C++
925 lines
29 KiB
C++
/*
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*/
|
|
|
|
#include <quic/congestion_control/Bbr2.h>
|
|
|
|
#include <quic/congestion_control/CongestionControlFunctions.h>
|
|
#include <sys/types.h>
|
|
#include <chrono>
|
|
#include <cstdint>
|
|
#include <cstdlib>
|
|
#include <limits>
|
|
|
|
namespace quic {
|
|
|
|
constexpr uint64_t kMaxBwFilterLen = 2; // Measured in number of ProbeBW cycles
|
|
constexpr std::chrono::microseconds kMinRttFilterLen = 10s;
|
|
constexpr std::chrono::microseconds kProbeRTTInterval = 5s;
|
|
constexpr std::chrono::microseconds kProbeRttDuration = 200ms;
|
|
constexpr uint64_t kMaxExtraAckedFilterLen =
|
|
10; // Measured in packet-timed round trips
|
|
|
|
constexpr float kStartupPacingGain = 2.77; // 4 * ln(2)
|
|
constexpr float kDrainPacingGain = 1 / kStartupPacingGain;
|
|
constexpr float kProbeBwDownPacingGain = 0.9;
|
|
constexpr float kProbeBwCruisePacingGain = 1.0;
|
|
constexpr float kProbeBwRefillPacingGain = 1.0;
|
|
constexpr float kProbeBwUpPacingGain = 1.25;
|
|
constexpr float kProbeRttPacingGain = 1.0;
|
|
|
|
constexpr float kStartupCwndGain = 2.0;
|
|
constexpr float kProbeBwCwndGain = 2.0;
|
|
constexpr float kProbeRttCwndGain = 0.5;
|
|
|
|
constexpr float kBeta = 0.7;
|
|
|
|
constexpr float kLossThreshold = 0.02;
|
|
constexpr float kHeadroomFactor = 0.15;
|
|
|
|
quic::Bandwidth kMinPacingRateForSendQuantum{1200 * 1000, 1s};
|
|
constexpr uint8_t kPacingMarginPercent = 1;
|
|
|
|
Bbr2CongestionController::Bbr2CongestionController(
|
|
QuicConnectionStateBase& conn)
|
|
: conn_(conn),
|
|
maxBwFilter_(kMaxBwFilterLen, Bandwidth(), 0),
|
|
probeRttMinTimestamp_(Clock::now()),
|
|
maxExtraAckedFilter_(kMaxExtraAckedFilterLen, 0, 0),
|
|
cwndBytes_(
|
|
conn_.udpSendPacketLen * conn_.transportSettings.initCwndInMss) {
|
|
resetCongestionSignals();
|
|
resetLowerBounds();
|
|
// Start with the default pacing settings.
|
|
enterStartup();
|
|
}
|
|
|
|
// Congestion Controller Interface
|
|
|
|
void Bbr2CongestionController::onRemoveBytesFromInflight(
|
|
uint64_t bytesToRemove) {
|
|
subtractAndCheckUnderflow(conn_.lossState.inflightBytes, bytesToRemove);
|
|
}
|
|
|
|
void Bbr2CongestionController::onPacketSent(
|
|
const OutstandingPacketWrapper& packet) {
|
|
// Handle restart from idle
|
|
if (conn_.lossState.inflightBytes == 0 && isAppLimited()) {
|
|
idleRestart_ = true;
|
|
extraAckedStartTimestamp_ = Clock::now();
|
|
|
|
if (isProbeBwState(state_)) {
|
|
setPacing();
|
|
} else if (state_ == State::ProbeRTT) {
|
|
checkProbeRttDone();
|
|
}
|
|
}
|
|
|
|
addAndCheckOverflow(
|
|
conn_.lossState.inflightBytes, packet.metadata.encodedSize);
|
|
|
|
// Maintain cwndLimited flag
|
|
if (conn_.lossState.inflightBytes >= cwndBytes_) {
|
|
cwndLimitedInRound_ |= true;
|
|
}
|
|
}
|
|
|
|
void Bbr2CongestionController::onPacketAckOrLoss(
|
|
const AckEvent* FOLLY_NULLABLE ackEvent,
|
|
const LossEvent* FOLLY_NULLABLE lossEvent) {
|
|
if (conn_.qLogger) {
|
|
conn_.qLogger->addCongestionMetricUpdate(
|
|
conn_.lossState.inflightBytes,
|
|
getCongestionWindow(),
|
|
kCongestionPacketAck,
|
|
bbr2StateToString(state_));
|
|
}
|
|
if (ackEvent) {
|
|
subtractAndCheckUnderflow(
|
|
conn_.lossState.inflightBytes, ackEvent->ackedBytes);
|
|
}
|
|
if (lossEvent) {
|
|
subtractAndCheckUnderflow(
|
|
conn_.lossState.inflightBytes, lossEvent->lostBytes);
|
|
}
|
|
SCOPE_EXIT {
|
|
VLOG(6) << fmt::format(
|
|
"State={} inflight={} cwnd={} (gain={})",
|
|
bbr2StateToString(state_),
|
|
conn_.lossState.inflightBytes,
|
|
getCongestionWindow(),
|
|
cwndGain_);
|
|
};
|
|
|
|
if (lossEvent && lossEvent->lostPackets > 0) {
|
|
// The pseudo code in BBRHandleLostPacket is included in
|
|
// updateProbeBwCyclePhase. No need to repeat it here.
|
|
|
|
// We don't expose the loss type here, so always use fast recovery for
|
|
// non-persistent congestion
|
|
saveCwnd();
|
|
inPacketConservation_ = true;
|
|
packetConservationStartTime_ = Clock::now();
|
|
if (lossEvent->persistentCongestion) {
|
|
cwndBytes_ = kMinCwndInMssForBbr * conn_.udpSendPacketLen;
|
|
} else {
|
|
auto newlyAcked = ackEvent ? ackEvent->ackedBytes : 0;
|
|
cwndBytes_ = conn_.lossState.inflightBytes +
|
|
std::max(newlyAcked, conn_.udpSendPacketLen);
|
|
}
|
|
}
|
|
|
|
if (ackEvent) {
|
|
if (appLimited_ &&
|
|
appLimitedLastSendTime_ <= ackEvent->largestNewlyAckedPacketSentTime) {
|
|
appLimited_ = false;
|
|
}
|
|
|
|
if (inPacketConservation_ &&
|
|
packetConservationStartTime_ <=
|
|
ackEvent->largestNewlyAckedPacketSentTime) {
|
|
inPacketConservation_ = false;
|
|
restoreCwnd();
|
|
}
|
|
|
|
// UpdateModelAndState
|
|
updateLatestDeliverySignals(*ackEvent);
|
|
updateRound(*ackEvent);
|
|
|
|
// Update cwndLimited state
|
|
if (roundStart_) {
|
|
cwndLimitedInRound_ = false;
|
|
}
|
|
|
|
updateCongestionSignals(lossEvent);
|
|
updateAckAggregation(*ackEvent);
|
|
checkStartupDone();
|
|
checkDrain();
|
|
|
|
auto inflightBytesAtLargestAckedPacket =
|
|
ackEvent->getLargestNewlyAckedPacket()
|
|
? ackEvent->getLargestNewlyAckedPacket()
|
|
->outstandingPacketMetadata.inflightBytes
|
|
: conn_.lossState.inflightBytes;
|
|
auto lostBytes = lossEvent ? lossEvent->lostBytes : 0;
|
|
updateProbeBwCyclePhase(
|
|
ackEvent->ackedBytes, inflightBytesAtLargestAckedPacket, lostBytes);
|
|
updateMinRtt();
|
|
checkProbeRtt(ackEvent->ackedBytes);
|
|
advanceLatestDeliverySignals(*ackEvent);
|
|
boundBwForModel();
|
|
|
|
// UpdateControlParameters
|
|
setPacing();
|
|
setSendQuantum();
|
|
setCwnd(ackEvent->ackedBytes, lostBytes);
|
|
}
|
|
}
|
|
|
|
uint64_t Bbr2CongestionController::getWritableBytes() const noexcept {
|
|
return getCongestionWindow() > conn_.lossState.inflightBytes
|
|
? getCongestionWindow() - conn_.lossState.inflightBytes
|
|
: 0;
|
|
}
|
|
|
|
uint64_t Bbr2CongestionController::getCongestionWindow() const noexcept {
|
|
return cwndBytes_;
|
|
}
|
|
|
|
CongestionControlType Bbr2CongestionController::type() const noexcept {
|
|
return CongestionControlType::BBR2;
|
|
}
|
|
|
|
bool Bbr2CongestionController::isInBackgroundMode() const {
|
|
return false;
|
|
}
|
|
|
|
folly::Optional<Bandwidth> Bbr2CongestionController::getBandwidth() const {
|
|
return bandwidth_;
|
|
}
|
|
|
|
bool Bbr2CongestionController::isAppLimited() const {
|
|
return appLimited_;
|
|
}
|
|
|
|
void Bbr2CongestionController::setAppLimited() noexcept {
|
|
appLimited_ = true;
|
|
appLimitedLastSendTime_ = Clock::now();
|
|
}
|
|
|
|
// Internals
|
|
void Bbr2CongestionController::resetCongestionSignals() {
|
|
lossBytesInRound_ = 0;
|
|
lossEventsInRound_ = 0;
|
|
bandwidthLatest_ = Bandwidth();
|
|
inflightLatest_ = 0;
|
|
}
|
|
|
|
void Bbr2CongestionController::resetLowerBounds() {
|
|
bandwidthLo_ = Bandwidth(std::numeric_limits<uint64_t>::max(), 1us);
|
|
inflightLo_ = std::numeric_limits<uint64_t>::max();
|
|
}
|
|
void Bbr2CongestionController::enterStartup() {
|
|
state_ = State::Startup;
|
|
pacingGain_ = kStartupPacingGain;
|
|
cwndGain_ = kStartupCwndGain;
|
|
}
|
|
|
|
void Bbr2CongestionController::setPacing() {
|
|
auto rate = bandwidth_ * pacingGain_ * (100 - kPacingMarginPercent) / 100;
|
|
VLOG(6) << fmt::format(
|
|
"Setting pacing to {}. From bandwdith_={} pacingGain_={} kPacingMarginPercent={} units={} interval={}",
|
|
rate.normalizedDescribe(),
|
|
bandwidth_.normalizedDescribe(),
|
|
pacingGain_,
|
|
kPacingMarginPercent,
|
|
rate.units,
|
|
rate.interval.count());
|
|
conn_.pacer->refreshPacingRate(rate.units, rate.interval);
|
|
}
|
|
|
|
void Bbr2CongestionController::setSendQuantum() {
|
|
auto rate = bandwidth_ * pacingGain_ * (100 - kPacingMarginPercent) / 100;
|
|
auto floor = 2 * conn_.udpSendPacketLen;
|
|
if (rate < kMinPacingRateForSendQuantum) {
|
|
floor = conn_.udpSendPacketLen;
|
|
}
|
|
auto rateIn1Ms = rate * 1ms;
|
|
sendQuantum_ = std::min(rateIn1Ms, decltype(rateIn1Ms)(64 * 1024));
|
|
sendQuantum_ = std::max(sendQuantum_, floor);
|
|
}
|
|
|
|
void Bbr2CongestionController::setCwnd(
|
|
uint64_t ackedBytes,
|
|
uint64_t lostBytes) {
|
|
// BBRUpdateMaxInflight()
|
|
inflightMax_ = addQuantizationBudget(
|
|
getBDPWithGain(cwndGain_) + maxExtraAckedFilter_.GetBest());
|
|
// BBRModulateCwndForRecovery()
|
|
if (lostBytes > 0) {
|
|
cwndBytes_ = std::max(
|
|
cwndBytes_ - std::min(lostBytes, cwndBytes_),
|
|
kMinCwndInMssForBbr * conn_.udpSendPacketLen);
|
|
}
|
|
if (inPacketConservation_) {
|
|
cwndBytes_ =
|
|
std::max(cwndBytes_, conn_.lossState.inflightBytes + ackedBytes);
|
|
}
|
|
|
|
if (!inPacketConservation_) {
|
|
if (filledPipe_) {
|
|
cwndBytes_ = std::min(cwndBytes_ + ackedBytes, inflightMax_);
|
|
} else if (
|
|
cwndBytes_ < inflightMax_ ||
|
|
conn_.lossState.totalBytesAcked <
|
|
conn_.transportSettings.initCwndInMss * conn_.udpSendPacketLen) {
|
|
cwndBytes_ += ackedBytes;
|
|
}
|
|
cwndBytes_ =
|
|
std::max(cwndBytes_, kMinCwndInMssForBbr * conn_.udpSendPacketLen);
|
|
}
|
|
|
|
// BBRBoundCwndForProbeRTT()
|
|
if (state_ == State::ProbeRTT) {
|
|
cwndBytes_ = std::min(cwndBytes_, getProbeRTTCwnd());
|
|
}
|
|
|
|
// BBRBoundCwndForModel()
|
|
auto cap = std::numeric_limits<uint64_t>::max();
|
|
if (isProbeBwState(state_) && state_ != State::ProbeBw_Cruise) {
|
|
cap = inflightHi_;
|
|
} else if (state_ == State::ProbeRTT || state_ == State::ProbeBw_Cruise) {
|
|
cap = getTargetInflightWithHeadroom();
|
|
}
|
|
cap = std::min(cap, inflightLo_);
|
|
cap = std::max(cap, kMinCwndInMssForBbr * conn_.udpSendPacketLen);
|
|
cwndBytes_ = std::min(cwndBytes_, cap);
|
|
}
|
|
|
|
void Bbr2CongestionController::checkProbeRttDone() {
|
|
auto timeNow = Clock::now();
|
|
if (probeRttDoneTimestamp_ && timeNow > *probeRttDoneTimestamp_) {
|
|
// Schedule the next ProbeRTT
|
|
probeRttMinTimestamp_ = timeNow;
|
|
restoreCwnd();
|
|
exitProbeRtt();
|
|
}
|
|
}
|
|
|
|
void Bbr2CongestionController::restoreCwnd() {
|
|
cwndBytes_ = std::max(cwndBytes_, previousCwndBytes_);
|
|
VLOG(6) << "Restored cwnd: " << cwndBytes_;
|
|
}
|
|
void Bbr2CongestionController::exitProbeRtt() {
|
|
resetLowerBounds();
|
|
cwndGain_ = kProbeBwCwndGain;
|
|
if (filledPipe_) {
|
|
startProbeBwDown();
|
|
startProbeBwCruise();
|
|
} else {
|
|
enterStartup();
|
|
}
|
|
}
|
|
|
|
void Bbr2CongestionController::updateLatestDeliverySignals(
|
|
const AckEvent& ackEvent) {
|
|
lossRoundStart_ = false;
|
|
|
|
bandwidthLatest_ =
|
|
std::max(bandwidthLatest_, getBandwidthSampleFromAck(ackEvent));
|
|
VLOG(6) << fmt::format(
|
|
"Bandwidth latest= {} AppLimited={}",
|
|
bandwidthLatest_.normalizedDescribe(),
|
|
bandwidthLatest_.isAppLimited);
|
|
inflightLatest_ = std::max(inflightLatest_, bandwidthLatest_.units);
|
|
|
|
auto pkt = ackEvent.getLargestNewlyAckedPacket();
|
|
if (pkt &&
|
|
pkt->outstandingPacketMetadata.totalBytesSent > lossRoundEndBytesSent_) {
|
|
// Uses bytes sent instead of ACKed in the spec. This doesn't affect the
|
|
// round counting
|
|
lossPctInLastRound_ = static_cast<float>(lossBytesInRound_) /
|
|
static_cast<float>(conn_.lossState.totalBytesSent -
|
|
lossRoundEndBytesSent_);
|
|
lossEventsInLastRound_ = lossEventsInRound_;
|
|
lossRoundEndBytesSent_ = conn_.lossState.totalBytesSent;
|
|
lossRoundStart_ = true;
|
|
}
|
|
}
|
|
|
|
void Bbr2CongestionController::updateCongestionSignals(
|
|
const LossEvent* FOLLY_NULLABLE lossEvent) {
|
|
// Update max bandwidth
|
|
if (bandwidthLatest_ > maxBwFilter_.GetBest() ||
|
|
!bandwidthLatest_.isAppLimited) {
|
|
VLOG(6) << fmt::format(
|
|
"Updating bandwidth filter with sample: {}",
|
|
bandwidthLatest_.normalizedDescribe());
|
|
maxBwFilter_.Update(bandwidthLatest_, cycleCount_);
|
|
}
|
|
|
|
// Update loss signal
|
|
if (lossEvent && lossEvent->lostBytes > 0) {
|
|
lossBytesInRound_ += lossEvent->lostBytes;
|
|
lossEventsInRound_ += 1;
|
|
}
|
|
|
|
if (!lossRoundStart_) {
|
|
return; // we're still within the same round
|
|
}
|
|
// AdaptLowerBoundsFromCongestion - once per round-trip
|
|
if (state_ == State::ProbeBw_Up) {
|
|
return;
|
|
}
|
|
if (lossBytesInRound_ > 0) {
|
|
// InitLowerBounds
|
|
if (!bandwidthLo_) {
|
|
bandwidthLo_ = maxBwFilter_.GetBest();
|
|
}
|
|
if (!inflightLo_) {
|
|
inflightLo_ = cwndBytes_;
|
|
}
|
|
|
|
// LossLowerBounds
|
|
bandwidthLo_ = std::max(bandwidthLatest_, bandwidthLo_ * kBeta);
|
|
inflightLo_ =
|
|
std::max(inflightLatest_, static_cast<uint64_t>(inflightLo_ * kBeta));
|
|
}
|
|
|
|
lossBytesInRound_ = 0;
|
|
lossEventsInRound_ = 0;
|
|
}
|
|
|
|
void Bbr2CongestionController::updateAckAggregation(const AckEvent& ackEvent) {
|
|
/* Find excess ACKed beyond expected amount over this interval */
|
|
auto interval =
|
|
Clock::now() - extraAckedStartTimestamp_.value_or(TimePoint());
|
|
auto expectedDelivered = bandwidth_ *
|
|
std::chrono::duration_cast<std::chrono::microseconds>(interval);
|
|
/* Reset interval if ACK rate is below expected rate: */
|
|
if (extraAckedDelivered_ < expectedDelivered) {
|
|
extraAckedDelivered_ = 0;
|
|
extraAckedStartTimestamp_ = Clock::now();
|
|
expectedDelivered = 0;
|
|
}
|
|
extraAckedDelivered_ += ackEvent.ackedBytes;
|
|
auto extra = extraAckedDelivered_ - expectedDelivered;
|
|
extra = std::min(extra, cwndBytes_);
|
|
maxExtraAckedFilter_.Update(extra, roundCount_);
|
|
}
|
|
void Bbr2CongestionController::checkStartupDone() {
|
|
checkStartupFullBandwidth();
|
|
checkStartupHighLoss();
|
|
|
|
if (state_ == State::Startup && filledPipe_) {
|
|
enterDrain();
|
|
}
|
|
}
|
|
|
|
void Bbr2CongestionController::checkStartupFullBandwidth() {
|
|
if (filledPipe_ || !roundStart_ || isAppLimited()) {
|
|
return; /* no need to check for a full pipe now */
|
|
}
|
|
if (maxBwFilter_.GetBest() >=
|
|
filledPipeBandwidth_ * 1.25) { /* still growing? */
|
|
filledPipeBandwidth_ =
|
|
maxBwFilter_.GetBest(); /* record new baseline level */
|
|
filledPipeCount_ = 0;
|
|
return;
|
|
}
|
|
filledPipeCount_++; /* another round w/o much growth */
|
|
if (filledPipeCount_ >= 3) {
|
|
filledPipe_ = true;
|
|
}
|
|
}
|
|
|
|
void Bbr2CongestionController::checkStartupHighLoss() {
|
|
/*
|
|
Our implementation differs from the spec a bit here. The conditions in the
|
|
spec are:
|
|
1. The connection has been in fast recovery for at least one full round trip.
|
|
2. The loss rate over the time scale of a single full round trip exceeds
|
|
BBRLossThresh (2%).
|
|
3. There are at least BBRStartupFullLossCnt=3 noncontiguous sequence ranges
|
|
lost in that round trip.
|
|
|
|
For 1,2 we use the loss pct from the last loss round which means we could exit
|
|
before a full RTT. For 3, we check we received three separate loss events
|
|
which servers a similar purpose to discontiguous ranges but it's not exactly
|
|
the same.
|
|
*/
|
|
if (filledPipe_ || !roundStart_ || isAppLimited()) {
|
|
// TODO: the appLimited condition means we could tolerate losses in startup
|
|
// if we haven't found the full bandwidth. This may need to be revisited.
|
|
|
|
return; /* no need to check for a the loss exit condition now */
|
|
}
|
|
if (lossPctInLastRound_ > kLossThreshold && lossEventsInLastRound_ >= 3) {
|
|
filledPipe_ = true;
|
|
}
|
|
}
|
|
|
|
void Bbr2CongestionController::enterDrain() {
|
|
state_ = State::Drain;
|
|
pacingGain_ = kDrainPacingGain; // Slow down pacing
|
|
cwndGain_ = kStartupCwndGain; // maintain cwnd
|
|
}
|
|
|
|
void Bbr2CongestionController::checkDrain() {
|
|
if (state_ == State::Drain) {
|
|
VLOG(6) << fmt::format(
|
|
"Current inflight {} target inflight {}",
|
|
conn_.lossState.inflightBytes,
|
|
getTargetInflightWithGain(1.0));
|
|
}
|
|
if (state_ == State::Drain &&
|
|
conn_.lossState.inflightBytes <= getTargetInflightWithGain(1.0)) {
|
|
enterProbeBW(); /* BBR estimates the queue was drained */
|
|
}
|
|
}
|
|
void Bbr2CongestionController::updateProbeBwCyclePhase(
|
|
uint64_t ackedBytes,
|
|
uint64_t inflightBytesAtLargestAckedPacket,
|
|
uint64_t lostBytes) {
|
|
/* The core state machine logic for ProbeBW: */
|
|
if (!filledPipe_) {
|
|
return; /* only handling steady-state behavior here */
|
|
}
|
|
adaptUpperBounds(ackedBytes, inflightBytesAtLargestAckedPacket, lostBytes);
|
|
if (!isProbeBwState(state_)) {
|
|
return; /* only handling ProbeBW states here: */
|
|
}
|
|
switch (state_) {
|
|
case State::ProbeBw_Down:
|
|
|
|
if (checkTimeToProbeBW()) {
|
|
return; /* already decided state transition */
|
|
}
|
|
if (checkTimeToCruise()) {
|
|
startProbeBwCruise();
|
|
}
|
|
|
|
break;
|
|
|
|
case State::ProbeBw_Cruise:
|
|
if (checkTimeToProbeBW()) {
|
|
return; /* already decided state transition */
|
|
}
|
|
break;
|
|
case State::ProbeBw_Refill:
|
|
/* After one round of REFILL, start UP */
|
|
if (roundStart_) {
|
|
bwProbeSamples_ = 1;
|
|
startProbeBwUp();
|
|
}
|
|
break;
|
|
case State::ProbeBw_Up:
|
|
|
|
if (hasElapsedInPhase(minRtt_) &&
|
|
inflightLatest_ > getTargetInflightWithGain(1.25)) {
|
|
startProbeBwDown();
|
|
}
|
|
break;
|
|
default:
|
|
throw QuicInternalException(
|
|
"BBR2: Unexpected state in ProbeBW phase: " +
|
|
bbr2StateToString(state_),
|
|
LocalErrorCode::CONGESTION_CONTROL_ERROR);
|
|
}
|
|
}
|
|
|
|
void Bbr2CongestionController::adaptUpperBounds(
|
|
uint64_t ackedBytes,
|
|
uint64_t inflightBytesAtLargestAckedPacket,
|
|
uint64_t lostBytes) {
|
|
/* Advance probeBw round if necessary and update BBR.max_bw window and
|
|
* BBR.inflight_hi and BBR.bw_hi. */
|
|
if (state_ == State::ProbeBw_Down && roundStart_) {
|
|
/* end of samples from bw probing phase */
|
|
if (!isAppLimited()) {
|
|
cycleCount_++;
|
|
}
|
|
}
|
|
|
|
if (!checkInflightTooHigh(inflightBytesAtLargestAckedPacket, lostBytes)) {
|
|
/* Loss rate is safe. Adjust upper bounds
|
|
upward. */
|
|
if (inflightHi_ == std::numeric_limits<uint64_t>::max() ||
|
|
bandwidthHi_.units == std::numeric_limits<uint64_t>::max()) {
|
|
return; /* no upper bounds to raise */
|
|
}
|
|
|
|
if (inflightBytesAtLargestAckedPacket > inflightHi_) {
|
|
inflightHi_ = inflightBytesAtLargestAckedPacket;
|
|
}
|
|
if (bandwidthLatest_ > bandwidthHi_) {
|
|
bandwidthHi_ = bandwidthLatest_;
|
|
}
|
|
if (state_ == State::ProbeBw_Up) {
|
|
probeInflightHiUpward(ackedBytes);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool Bbr2CongestionController::checkTimeToProbeBW() {
|
|
if (hasElapsedInPhase(bwProbeWait_) || isRenoCoexistenceProbeTime()) {
|
|
startProbeBwRefill();
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool Bbr2CongestionController::checkTimeToCruise() {
|
|
if (conn_.lossState.inflightBytes > getTargetInflightWithHeadroom()) {
|
|
return false; /* not enough headroom */
|
|
} else if (conn_.lossState.inflightBytes <= getTargetInflightWithGain()) {
|
|
return true; /* inflight <= estimated BDP */
|
|
}
|
|
// Neither conditions met. Do not cruise yet.
|
|
return false;
|
|
}
|
|
|
|
bool Bbr2CongestionController::hasElapsedInPhase(
|
|
std::chrono::microseconds interval) {
|
|
return Clock::now() > probeBWCycleStart_ + interval;
|
|
}
|
|
|
|
// Was the loss percent too hight for the last ack received?
|
|
bool Bbr2CongestionController::checkInflightTooHigh(
|
|
uint64_t inflightBytesAtLargestAckedPacket,
|
|
uint64_t lostBytes) {
|
|
if (isInflightTooHigh(inflightBytesAtLargestAckedPacket, lostBytes)) {
|
|
if (bwProbeSamples_) {
|
|
handleInFlightTooHigh(inflightBytesAtLargestAckedPacket);
|
|
}
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool Bbr2CongestionController::isInflightTooHigh(
|
|
uint64_t inflightBytesAtLargestAckedPacket,
|
|
uint64_t lostBytes) {
|
|
return static_cast<float>(lostBytes) >
|
|
static_cast<float>(inflightBytesAtLargestAckedPacket) * kLossThreshold;
|
|
}
|
|
|
|
void Bbr2CongestionController::handleInFlightTooHigh(
|
|
uint64_t inflightBytesAtLargestAckedPacket) {
|
|
bwProbeSamples_ = 0;
|
|
// TODO: Should this be the app limited state of the largest acknowledged
|
|
// packet?
|
|
if (!isAppLimited()) {
|
|
inflightHi_ = std::max(
|
|
inflightBytesAtLargestAckedPacket,
|
|
static_cast<uint64_t>(
|
|
static_cast<float>(getTargetInflightWithGain()) * kBeta));
|
|
}
|
|
if (state_ == State::ProbeBw_Up) {
|
|
startProbeBwDown();
|
|
}
|
|
}
|
|
|
|
uint64_t Bbr2CongestionController::getTargetInflightWithHeadroom() const {
|
|
/* Return a volume of data that tries to leave free
|
|
* headroom in the bottleneck buffer or link for
|
|
* other flows, for fairness convergence and lower
|
|
* RTTs and loss */
|
|
if (inflightHi_ == std::numeric_limits<uint64_t>::max()) {
|
|
return inflightHi_;
|
|
}
|
|
auto headroom = static_cast<uint64_t>(
|
|
std::max(1.0f, kHeadroomFactor * static_cast<float>(inflightHi_)));
|
|
return std::max(
|
|
inflightHi_ - headroom,
|
|
quic::kMinCwndInMssForBbr * conn_.udpSendPacketLen);
|
|
}
|
|
|
|
void Bbr2CongestionController::probeInflightHiUpward(uint64_t ackedBytes) {
|
|
if (!cwndLimitedInRound_ || cwndBytes_ < inflightHi_) {
|
|
return; /* not fully using inflight_hi, so don't grow it */
|
|
}
|
|
probeUpAcks_ += ackedBytes;
|
|
if (probeUpAcks_ >= probeUpCount_) {
|
|
auto delta = probeUpAcks_ / probeUpCount_;
|
|
probeUpAcks_ -= delta * probeUpCount_;
|
|
inflightHi_ += delta;
|
|
}
|
|
if (roundStart_) {
|
|
raiseInflightHiSlope();
|
|
}
|
|
}
|
|
|
|
void Bbr2CongestionController::updateMinRtt() {
|
|
probeRttExpired_ = probeRttMinTimestamp_
|
|
? Clock::now() > (probeRttMinTimestamp_.value() + kProbeRTTInterval)
|
|
: true;
|
|
auto& lrtt = conn_.lossState.lrtt;
|
|
if (lrtt > 0us && (lrtt < probeRttMinValue_ || probeRttExpired_)) {
|
|
probeRttMinValue_ = lrtt;
|
|
probeRttMinTimestamp_ = Clock::now();
|
|
}
|
|
|
|
auto minRttExpired = minRttTimestamp_
|
|
? Clock::now() > (minRttTimestamp_.value() + kMinRttFilterLen)
|
|
: true;
|
|
if (probeRttMinValue_ < minRtt_ || minRttExpired) {
|
|
minRtt_ = probeRttMinValue_;
|
|
minRttTimestamp_ = probeRttMinTimestamp_;
|
|
}
|
|
}
|
|
|
|
void Bbr2CongestionController::checkProbeRtt(uint64_t ackedBytes) {
|
|
if (state_ != State::ProbeRTT && probeRttExpired_ && !idleRestart_) {
|
|
enterProbeRtt();
|
|
saveCwnd();
|
|
probeRttDoneTimestamp_.reset();
|
|
startRound();
|
|
}
|
|
if (state_ == State::ProbeRTT) {
|
|
handleProbeRtt();
|
|
}
|
|
if (ackedBytes > 0) {
|
|
idleRestart_ = false;
|
|
}
|
|
}
|
|
|
|
void Bbr2CongestionController::enterProbeRtt() {
|
|
state_ = State::ProbeRTT;
|
|
pacingGain_ = kProbeRttPacingGain;
|
|
cwndGain_ = kProbeRttCwndGain;
|
|
}
|
|
|
|
void Bbr2CongestionController::handleProbeRtt() {
|
|
/* Ignore low rate samples during ProbeRTT: */
|
|
// TODO: I don't understand the logic in the spec in
|
|
// MarkConnectionAppLimited() but just setting app limited is reasonable
|
|
setAppLimited();
|
|
|
|
if (!probeRttDoneTimestamp_ &&
|
|
conn_.lossState.inflightBytes <= getProbeRTTCwnd()) {
|
|
/* Wait for at least ProbeRTTDuration to elapse: */
|
|
probeRttDoneTimestamp_ = Clock::now() + kProbeRttDuration;
|
|
/* Wait for at least one round to elapse: */
|
|
// Is this needed? BBR.probe_rtt_round_done = false
|
|
startRound();
|
|
} else if (probeRttDoneTimestamp_) {
|
|
if (roundStart_) {
|
|
checkProbeRttDone();
|
|
}
|
|
}
|
|
}
|
|
|
|
void Bbr2CongestionController::advanceLatestDeliverySignals(
|
|
const AckEvent& ackEvent) {
|
|
if (lossRoundStart_) {
|
|
bandwidthLatest_ = getBandwidthSampleFromAck(ackEvent);
|
|
inflightLatest_ = bandwidthLatest_.units;
|
|
}
|
|
}
|
|
|
|
uint64_t Bbr2CongestionController::getProbeRTTCwnd() {
|
|
return std::max(
|
|
getBDPWithGain(kProbeRttCwndGain),
|
|
quic::kMinCwndInMssForBbr * conn_.udpSendPacketLen);
|
|
}
|
|
void Bbr2CongestionController::boundCwndForProbeRTT() {
|
|
if (state_ == State::ProbeRTT) {
|
|
cwndBytes_ = std::min(cwndBytes_, getProbeRTTCwnd());
|
|
}
|
|
}
|
|
|
|
void Bbr2CongestionController::boundBwForModel() {
|
|
if (state_ == State::Startup) {
|
|
bandwidth_ = maxBwFilter_.GetBest();
|
|
} else {
|
|
bandwidth_ =
|
|
std::min(std::min(maxBwFilter_.GetBest(), bandwidthLo_), bandwidthHi_);
|
|
}
|
|
if (conn_.qLogger) {
|
|
conn_.qLogger->addBandwidthEstUpdate(bandwidth_.units, bandwidth_.interval);
|
|
}
|
|
}
|
|
|
|
uint64_t Bbr2CongestionController::addQuantizationBudget(uint64_t input) const {
|
|
// BBRUpdateOffloadBudget()
|
|
auto offloadBudget = 3 * sendQuantum_;
|
|
input = std::max(input, offloadBudget);
|
|
input = std::max(input, quic::kMinCwndInMssForBbr * conn_.udpSendPacketLen);
|
|
if (state_ == State::ProbeBw_Up) {
|
|
// This number is arbitrary from the spec. It's probably to guarantee that
|
|
// probing up is more aggressive (?)
|
|
input += 2 * conn_.udpSendPacketLen;
|
|
}
|
|
return input;
|
|
}
|
|
|
|
void Bbr2CongestionController::saveCwnd() {
|
|
if (!inLossRecovery_ && state_ != State::ProbeRTT) {
|
|
previousCwndBytes_ = cwndBytes_;
|
|
} else {
|
|
previousCwndBytes_ = std::max(cwndBytes_, previousCwndBytes_);
|
|
}
|
|
VLOG(6) << "Saved cwnd: " << previousCwndBytes_;
|
|
}
|
|
|
|
uint64_t Bbr2CongestionController::getTargetInflightWithGain(float gain) const {
|
|
return addQuantizationBudget(getBDPWithGain(gain));
|
|
}
|
|
|
|
uint64_t Bbr2CongestionController::getBDPWithGain(float gain) const {
|
|
if (minRtt_ == kDefaultMinRtt) {
|
|
return uint64_t(
|
|
gain * conn_.transportSettings.initCwndInMss * conn_.udpSendPacketLen);
|
|
} else {
|
|
return uint64_t(gain * (minRtt_ * bandwidth_));
|
|
}
|
|
}
|
|
|
|
void Bbr2CongestionController::enterProbeBW() {
|
|
cwndGain_ = kProbeBwCwndGain;
|
|
startProbeBwDown();
|
|
}
|
|
|
|
void Bbr2CongestionController::startRound() {
|
|
nextRoundDelivered_ = conn_.lossState.totalBytesAcked;
|
|
}
|
|
void Bbr2CongestionController::updateRound(const AckEvent& ackEvent) {
|
|
auto pkt = ackEvent.getLargestNewlyAckedPacket();
|
|
if (pkt && pkt->lastAckedPacketInfo &&
|
|
pkt->lastAckedPacketInfo->totalBytesAcked >= nextRoundDelivered_) {
|
|
startRound();
|
|
roundCount_++;
|
|
roundsSinceBwProbe_++;
|
|
roundStart_ = true;
|
|
} else {
|
|
roundStart_ = false;
|
|
}
|
|
}
|
|
|
|
void Bbr2CongestionController::startProbeBwDown() {
|
|
resetCongestionSignals();
|
|
probeUpCount_ =
|
|
std::numeric_limits<uint64_t>::max(); /* not growing inflight_hi */
|
|
/* Decide random round-trip bound for wait: */
|
|
roundsSinceBwProbe_ = folly::Random::rand32() % 2;
|
|
/* Decide the random wall clock bound for wait: between 2-3 seconds */
|
|
bwProbeWait_ = std::chrono::milliseconds(2 + folly::Random::rand32() % 1000);
|
|
|
|
probeBWCycleStart_ = Clock::now();
|
|
state_ = State::ProbeBw_Down;
|
|
pacingGain_ = kProbeBwDownPacingGain;
|
|
startRound();
|
|
}
|
|
void Bbr2CongestionController::startProbeBwCruise() {
|
|
state_ = State::ProbeBw_Cruise;
|
|
pacingGain_ = kProbeBwCruisePacingGain;
|
|
}
|
|
|
|
void Bbr2CongestionController::startProbeBwRefill() {
|
|
resetLowerBounds();
|
|
probeUpRounds_ = 0;
|
|
probeUpAcks_ = 0;
|
|
state_ = State::ProbeBw_Refill;
|
|
pacingGain_ = kProbeBwRefillPacingGain;
|
|
startRound();
|
|
}
|
|
void Bbr2CongestionController::startProbeBwUp() {
|
|
probeBWCycleStart_ = Clock::now();
|
|
state_ = State::ProbeBw_Up;
|
|
pacingGain_ = kProbeBwUpPacingGain;
|
|
startRound();
|
|
raiseInflightHiSlope();
|
|
}
|
|
|
|
void Bbr2CongestionController::raiseInflightHiSlope() {
|
|
auto growthThisRound = conn_.udpSendPacketLen << probeUpRounds_;
|
|
probeUpRounds_ = std::min(probeUpRounds_ + 1, decltype(probeUpRounds_)(30));
|
|
probeUpCount_ =
|
|
std::max(cwndBytes_ / growthThisRound, decltype(cwndBytes_)(1));
|
|
}
|
|
|
|
// Utilities
|
|
bool Bbr2CongestionController::isProbeBwState(
|
|
const Bbr2CongestionController::State state) {
|
|
return (
|
|
state == Bbr2CongestionController::State::ProbeBw_Down ||
|
|
state == Bbr2CongestionController::State::ProbeBw_Cruise ||
|
|
state == Bbr2CongestionController::State::ProbeBw_Refill ||
|
|
state == Bbr2CongestionController::State::ProbeBw_Up);
|
|
}
|
|
|
|
Bandwidth Bbr2CongestionController::getBandwidthSampleFromAck(
|
|
const AckEvent& ackEvent) {
|
|
auto ackTime = ackEvent.adjustedAckTime;
|
|
auto pkt = ackEvent.getLargestNewlyAckedPacket();
|
|
if (!pkt) {
|
|
return Bandwidth();
|
|
}
|
|
auto& lastAckedPacket = pkt->lastAckedPacketInfo;
|
|
auto lastSentTime =
|
|
lastAckedPacket ? lastAckedPacket->sentTime : conn_.connectionTime;
|
|
|
|
auto sendElapsed = pkt->outstandingPacketMetadata.time - lastSentTime;
|
|
|
|
auto lastAckTime =
|
|
lastAckedPacket ? lastAckedPacket->adjustedAckTime : conn_.connectionTime;
|
|
auto ackElapsed = ackTime - lastAckTime;
|
|
auto interval = std::max(ackElapsed, sendElapsed);
|
|
if (interval == 0us) {
|
|
return Bandwidth();
|
|
}
|
|
auto lastBytesDelivered =
|
|
lastAckedPacket ? lastAckedPacket->totalBytesAcked : 0;
|
|
auto bytesDelivered = ackEvent.totalBytesAcked - lastBytesDelivered;
|
|
Bandwidth bwSample(
|
|
bytesDelivered,
|
|
std::chrono::duration_cast<std::chrono::microseconds>(interval),
|
|
pkt->isAppLimited || lastSentTime < appLimitedLastSendTime_);
|
|
return bwSample;
|
|
}
|
|
|
|
bool Bbr2CongestionController::isRenoCoexistenceProbeTime() {
|
|
auto renoBdpInPackets = std::min(getTargetInflightWithGain(), cwndBytes_) /
|
|
conn_.udpSendPacketLen;
|
|
auto roundsBeforeRenoProbe =
|
|
std::min(renoBdpInPackets, decltype(renoBdpInPackets)(63));
|
|
return roundsSinceBwProbe_ >= roundsBeforeRenoProbe;
|
|
}
|
|
|
|
Bbr2CongestionController::State Bbr2CongestionController::getState()
|
|
const noexcept {
|
|
return state_;
|
|
}
|
|
|
|
void Bbr2CongestionController::getStats(
|
|
CongestionControllerStats& stats) const {
|
|
stats.bbr2Stats.state = uint8_t(state_);
|
|
}
|
|
|
|
std::string bbr2StateToString(Bbr2CongestionController::State state) {
|
|
switch (state) {
|
|
case Bbr2CongestionController::State::Startup:
|
|
return "Startup";
|
|
case Bbr2CongestionController::State::Drain:
|
|
return "Drain";
|
|
case Bbr2CongestionController::State::ProbeBw_Down:
|
|
return "ProbeBw_Down";
|
|
case Bbr2CongestionController::State::ProbeBw_Cruise:
|
|
return "ProbeBw_Cruise";
|
|
case Bbr2CongestionController::State::ProbeBw_Refill:
|
|
return "ProbeBw_Refill";
|
|
case Bbr2CongestionController::State::ProbeBw_Up:
|
|
return "ProbeBw_Up";
|
|
case Bbr2CongestionController::State::ProbeRTT:
|
|
return "ProbeRTT";
|
|
}
|
|
folly::assume_unreachable();
|
|
}
|
|
} // namespace quic
|