mirror of
https://github.com/facebookincubator/mvfst.git
synced 2025-11-24 04:01:07 +03:00
Summary: Several "Network Path Model" parameters are described in Sec. 2.9 of the BBRv2 IETF draft. These quantities evolve throughout the connection and they are useful to analyze BBRv2 performance. This diff adds inflight_hi, inflight_lo, bandwidth_hi, and bandwidth_lo to the qlog. Reviewed By: mjoras Differential Revision: D61414936 fbshipit-source-id: 2862db2a6aab336fd8a60e4ae5b358e9ab5588b4
1025 lines
33 KiB
C++
1025 lines
33 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.89; // 2 / ln(2)
|
|
constexpr float kDrainPacingGain = 1 / kStartupPacingGain;
|
|
constexpr float kProbeBwDownPacingGain = 0.9;
|
|
constexpr float kProbeBwCruiseRefillPacingGain = 1.0;
|
|
constexpr float kProbeBwUpPacingGain = 1.25;
|
|
constexpr float kProbeRttPacingGain = 1.0;
|
|
|
|
constexpr float kStartupCwndGain = 2.89;
|
|
constexpr float kProbeBwCruiseRefillCwndGain = 2.0;
|
|
constexpr float kProbeBwUpDownCwndGain = 2.0;
|
|
constexpr float kProbeRttCwndGain = 0.5;
|
|
|
|
constexpr float kBeta = 0.7;
|
|
|
|
constexpr float kLossThreshold = 0.02;
|
|
constexpr float kHeadroomFactor = 0.15;
|
|
|
|
#ifndef CLANG_LAZY_INIT
|
|
#define CLANG_LAZY_INIT
|
|
#endif
|
|
CLANG_LAZY_INIT quic::Bandwidth kMinPacingRateForSendQuantum{1200 * 1000, 1s};
|
|
// The experimental pacer currently achieves ~99% of the target rate
|
|
// we should not reduce the target by adding an extra margin.
|
|
// TODO: add the margin back if the pacer performance improves further.
|
|
constexpr uint8_t kPacingMarginPercent = 0;
|
|
|
|
Bbr2CongestionController::Bbr2CongestionController(
|
|
QuicConnectionStateBase& conn)
|
|
: conn_(conn),
|
|
// WindowedFilter window_length is expiry time which inflates the window
|
|
// length by 1
|
|
maxBwFilter_(kMaxBwFilterLen - 1, 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();
|
|
extraAckedDelivered_ = 0;
|
|
|
|
if (isProbeBwState(state_)) {
|
|
setPacing();
|
|
} else if (state_ == State::ProbeRTT) {
|
|
checkProbeRttDone();
|
|
}
|
|
}
|
|
|
|
addAndCheckOverflow(
|
|
conn_.lossState.inflightBytes, packet.metadata.encodedSize);
|
|
|
|
// Maintain cwndLimited flag. We consider the transport being cwnd limited if
|
|
// we are using > 90% of the cwnd.
|
|
if (conn_.lossState.inflightBytes > cwndBytes_ * 9 / 10) {
|
|
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_));
|
|
conn_.qLogger->addNetworkPathModelUpdate(
|
|
inflightHi_.value_or(0),
|
|
inflightLo_.value_or(0),
|
|
bandwidthHi_.has_value() ? bandwidthHi_->units : 0,
|
|
bandwidthHi_.has_value() ? bandwidthHi_->interval
|
|
: std::chrono::microseconds(1),
|
|
bandwidthLo_.has_value() ? bandwidthLo_->units : 0,
|
|
bandwidthLo_.has_value() ? bandwidthLo_->interval
|
|
: std::chrono::microseconds(1));
|
|
}
|
|
if (ackEvent) {
|
|
subtractAndCheckUnderflow(
|
|
conn_.lossState.inflightBytes, ackEvent->ackedBytes);
|
|
}
|
|
if (lossEvent) {
|
|
subtractAndCheckUnderflow(
|
|
conn_.lossState.inflightBytes, lossEvent->lostBytes);
|
|
}
|
|
SCOPE_EXIT {
|
|
VLOG(6) << "State=" << bbr2StateToString(state_)
|
|
<< " inflight=" << conn_.lossState.inflightBytes
|
|
<< " cwnd=" << getCongestionWindow() << "(gain=" << cwndGain_
|
|
<< ")";
|
|
};
|
|
|
|
if (lossEvent && lossEvent->lostPackets > 0 &&
|
|
conn_.transportSettings.ccaConfig.conservativeRecovery) {
|
|
// 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;
|
|
// Mark the connection as app-limited so bw samples during recovery are not
|
|
// taken into account.
|
|
setAppLimited();
|
|
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 (ackEvent->implicit) {
|
|
// Implicit acks should not be used for bandwidth or rtt estimation
|
|
setCwnd(ackEvent->ackedBytes, 0);
|
|
return;
|
|
}
|
|
|
|
if (appLimited_ &&
|
|
appLimitedLastSendTime_ <= ackEvent->largestNewlyAckedPacketSentTime) {
|
|
appLimited_ = false;
|
|
if (conn_.qLogger) {
|
|
conn_.qLogger->addAppUnlimitedUpdate();
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
Optional<Bandwidth> Bbr2CongestionController::getBandwidth() const {
|
|
return bandwidth_;
|
|
}
|
|
|
|
bool Bbr2CongestionController::isAppLimited() const {
|
|
return appLimited_;
|
|
}
|
|
|
|
void Bbr2CongestionController::setAppLimited() noexcept {
|
|
appLimited_ = true;
|
|
appLimitedLastSendTime_ = Clock::now();
|
|
if (conn_.qLogger) {
|
|
conn_.qLogger->addAppLimitedUpdate();
|
|
}
|
|
}
|
|
|
|
// Internals
|
|
void Bbr2CongestionController::resetCongestionSignals() {
|
|
lossBytesInRound_ = 0;
|
|
lossEventsInRound_ = 0;
|
|
bandwidthLatest_ = Bandwidth();
|
|
inflightLatest_ = 0;
|
|
}
|
|
|
|
void Bbr2CongestionController::resetLowerBounds() {
|
|
bandwidthLo_.reset();
|
|
inflightLo_.reset();
|
|
}
|
|
void Bbr2CongestionController::enterStartup() {
|
|
state_ = State::Startup;
|
|
updatePacingAndCwndGain();
|
|
}
|
|
|
|
void Bbr2CongestionController::setPacing() {
|
|
if (!conn_.transportSettings.ccaConfig.paceInitCwnd &&
|
|
conn_.lossState.totalBytesSent <
|
|
conn_.transportSettings.initCwndInMss * conn_.udpSendPacketLen) {
|
|
return;
|
|
}
|
|
uint64_t pacingWindow =
|
|
bandwidth_ * minRtt_ * pacingGain_ * (100 - kPacingMarginPercent) / 100;
|
|
VLOG(6) << "Setting pacing to "
|
|
<< Bandwidth(pacingWindow, minRtt_).normalizedDescribe()
|
|
<< " from bandwidth_=" << bandwidth_.normalizedDescribe()
|
|
<< " pacingGain_=" << pacingGain_
|
|
<< " kPacingMarginPercent=" << kPacingMarginPercent
|
|
<< " units=" << pacingWindow << " interval=" << minRtt_.count();
|
|
|
|
if (state_ == State::Startup && !filledPipe_) {
|
|
pacingWindow = std::max(
|
|
pacingWindow,
|
|
conn_.udpSendPacketLen * conn_.transportSettings.initCwndInMss);
|
|
}
|
|
conn_.pacer->refreshPacingRate(pacingWindow, minRtt_);
|
|
}
|
|
|
|
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()
|
|
auto inflightMax = addQuantizationBudget(
|
|
getBDPWithGain(cwndGain_) + maxExtraAckedFilter_.GetBest());
|
|
// BBRModulateCwndForRecovery()
|
|
if (lostBytes > 0 && !conn_.transportSettings.ccaConfig.ignoreLoss) {
|
|
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 (inflightHi_.has_value() &&
|
|
!conn_.transportSettings.ccaConfig.ignoreInflightHi) {
|
|
if (isProbeBwState(state_) && state_ != State::ProbeBw_Cruise) {
|
|
cap = *inflightHi_;
|
|
} else if (state_ == State::ProbeRTT || state_ == State::ProbeBw_Cruise) {
|
|
cap = getTargetInflightWithHeadroom();
|
|
}
|
|
}
|
|
if (inflightLo_.has_value() &&
|
|
!conn_.transportSettings.ccaConfig.ignoreLoss) {
|
|
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_) ||
|
|
conn_.lossState.inflightBytes == 0) {
|
|
// 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();
|
|
if (filledPipe_) {
|
|
startProbeBwDown();
|
|
startProbeBwCruise();
|
|
} else {
|
|
enterStartup();
|
|
}
|
|
}
|
|
|
|
void Bbr2CongestionController::updateLatestDeliverySignals(
|
|
const AckEvent& ackEvent) {
|
|
lossRoundStart_ = false;
|
|
|
|
bandwidthLatest_ =
|
|
std::max(bandwidthLatest_, getBandwidthSampleFromAck(ackEvent));
|
|
VLOG(6) << "Bandwidth latest=" << bandwidthLatest_.normalizedDescribe()
|
|
<< " AppLimited=" << 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) << "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_.has_value()) {
|
|
bandwidthLo_ = maxBwFilter_.GetBest();
|
|
}
|
|
if (!inflightLo_.has_value()) {
|
|
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() ||
|
|
conn_.transportSettings.ccaConfig.ignoreLoss) {
|
|
// 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;
|
|
updatePacingAndCwndGain();
|
|
}
|
|
|
|
void Bbr2CongestionController::checkDrain() {
|
|
if (state_ == State::Drain) {
|
|
VLOG(6) << "Current inflight" << conn_.lossState.inflightBytes
|
|
<< " target inflight " << 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_) {
|
|
// Enable one reaction to loss per probe bw cycle.
|
|
bwProbeShouldHandleLoss_ = true;
|
|
startProbeBwUp();
|
|
}
|
|
break;
|
|
case State::ProbeBw_Up:
|
|
if (hasElapsedInPhase(minRtt_) &&
|
|
conn_.lossState.inflightBytes > 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) {
|
|
/* Update BBR.inflight_hi and BBR.bw_hi. */
|
|
|
|
if (!checkInflightTooHigh(inflightBytesAtLargestAckedPacket, lostBytes)) {
|
|
if (!inflightHi_.has_value() || !bandwidthHi_.has_value()) {
|
|
// No loss has occurred yet so these values are not set and do not need to
|
|
// be raised.
|
|
return;
|
|
}
|
|
/* There is loss but it's at safe levels. The limits are populated so we
|
|
* update them */
|
|
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 high for the last ack received?
|
|
bool Bbr2CongestionController::checkInflightTooHigh(
|
|
uint64_t inflightBytesAtLargestAckedPacket,
|
|
uint64_t lostBytes) {
|
|
if (isInflightTooHigh(inflightBytesAtLargestAckedPacket, lostBytes)) {
|
|
if (bwProbeShouldHandleLoss_) {
|
|
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) {
|
|
bwProbeShouldHandleLoss_ = false;
|
|
// 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));
|
|
bandwidthHi_ = maxBwFilter_.GetBest();
|
|
}
|
|
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_.has_value()) {
|
|
return std::numeric_limits<uint64_t>::max();
|
|
} else {
|
|
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 (!inflightHi_.has_value() || !cwndLimitedInRound_ ||
|
|
cwndBytes_ < *inflightHi_) {
|
|
return; /* no inflight_hi set or not fully using inflight_hi, so don't grow
|
|
it */
|
|
}
|
|
probeUpAcks_ += ackedBytes;
|
|
if (probeUpAcks_ >= probeUpCount_) {
|
|
auto delta = probeUpAcks_ / probeUpCount_;
|
|
probeUpAcks_ -= delta * probeUpCount_;
|
|
addAndCheckOverflow(*inflightHi_, delta);
|
|
}
|
|
if (roundStart_) {
|
|
raiseInflightHiSlope();
|
|
}
|
|
}
|
|
|
|
void Bbr2CongestionController::updateMinRtt() {
|
|
if (idleRestart_) {
|
|
probeRttMinTimestamp_ = Clock::now();
|
|
probeRttMinValue_ = kDefaultMinRtt;
|
|
}
|
|
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;
|
|
updatePacingAndCwndGain();
|
|
}
|
|
|
|
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() {
|
|
Bandwidth previousBw = bandwidth_;
|
|
bandwidth_ = maxBwFilter_.GetBest();
|
|
if (state_ != State::Startup) {
|
|
if (bandwidthLo_.has_value() &&
|
|
!conn_.transportSettings.ccaConfig.ignoreLoss) {
|
|
bandwidth_ = std::min(bandwidth_, *bandwidthLo_);
|
|
}
|
|
if (bandwidthHi_.has_value() &&
|
|
!conn_.transportSettings.ccaConfig.ignoreInflightHi) {
|
|
bandwidth_ = std::min(bandwidth_, *bandwidthHi_);
|
|
}
|
|
}
|
|
if (conn_.qLogger && previousBw != bandwidth_) {
|
|
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() {
|
|
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(2000 + (folly::Random::rand32() % 1000));
|
|
|
|
probeBWCycleStart_ = Clock::now();
|
|
state_ = State::ProbeBw_Down;
|
|
updatePacingAndCwndGain();
|
|
startRound();
|
|
|
|
// This is a new ProbeBW cycle. Advance the max bw filter if we're not app
|
|
// limited
|
|
if (!isAppLimited()) {
|
|
cycleCount_++;
|
|
}
|
|
}
|
|
void Bbr2CongestionController::startProbeBwCruise() {
|
|
state_ = State::ProbeBw_Cruise;
|
|
updatePacingAndCwndGain();
|
|
}
|
|
|
|
void Bbr2CongestionController::startProbeBwRefill() {
|
|
resetLowerBounds();
|
|
probeUpRounds_ = 0;
|
|
probeUpAcks_ = 0;
|
|
state_ = State::ProbeBw_Refill;
|
|
updatePacingAndCwndGain();
|
|
startRound();
|
|
}
|
|
void Bbr2CongestionController::startProbeBwUp() {
|
|
probeBWCycleStart_ = Clock::now();
|
|
state_ = State::ProbeBw_Up;
|
|
updatePacingAndCwndGain();
|
|
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 bwSample = Bandwidth();
|
|
for (auto const& ackedPacket : ackEvent.ackedPackets) {
|
|
auto pkt = &ackedPacket;
|
|
if (ackedPacket.outstandingPacketMetadata.encodedSize == 0) {
|
|
continue;
|
|
}
|
|
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 bw(
|
|
bytesDelivered,
|
|
std::chrono::duration_cast<std::chrono::microseconds>(interval),
|
|
pkt->isAppLimited || lastSentTime < appLimitedLastSendTime_);
|
|
if (bw > bwSample) {
|
|
bwSample = bw;
|
|
}
|
|
}
|
|
return bwSample;
|
|
}
|
|
|
|
bool Bbr2CongestionController::isRenoCoexistenceProbeTime() {
|
|
if (!conn_.transportSettings.ccaConfig.enableRenoCoexistence) {
|
|
return false;
|
|
}
|
|
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_);
|
|
}
|
|
|
|
void Bbr2CongestionController::updatePacingAndCwndGain() {
|
|
switch (state_) {
|
|
case State::Startup:
|
|
pacingGain_ =
|
|
conn_.transportSettings.ccaConfig.overrideStartupPacingGain > 0
|
|
? conn_.transportSettings.ccaConfig.overrideStartupPacingGain
|
|
: kStartupPacingGain;
|
|
cwndGain_ = kStartupCwndGain;
|
|
break;
|
|
case State::Drain:
|
|
pacingGain_ = kDrainPacingGain;
|
|
cwndGain_ = kStartupCwndGain;
|
|
break;
|
|
case State::ProbeBw_Up:
|
|
pacingGain_ = kProbeBwUpPacingGain;
|
|
cwndGain_ = kProbeBwUpDownCwndGain;
|
|
break;
|
|
case State::ProbeBw_Down:
|
|
pacingGain_ = kProbeBwDownPacingGain;
|
|
cwndGain_ = kProbeBwUpDownCwndGain;
|
|
break;
|
|
case State::ProbeBw_Cruise:
|
|
case State::ProbeBw_Refill:
|
|
pacingGain_ =
|
|
conn_.transportSettings.ccaConfig.overrideCruisePacingGain > 0
|
|
? conn_.transportSettings.ccaConfig.overrideCruisePacingGain
|
|
: kProbeBwCruiseRefillPacingGain;
|
|
cwndGain_ = conn_.transportSettings.ccaConfig.overrideCruiseCwndGain > 0
|
|
? conn_.transportSettings.ccaConfig.overrideCruiseCwndGain
|
|
: kProbeBwCruiseRefillCwndGain;
|
|
break;
|
|
case State::ProbeRTT:
|
|
pacingGain_ = kProbeRttPacingGain;
|
|
cwndGain_ = kProbeRttCwndGain;
|
|
break;
|
|
}
|
|
}
|
|
|
|
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
|