chore: add new bbr implementation
This commit is contained in:
927
transport/tuic/congestion_v2/bbr_sender.go
Normal file
927
transport/tuic/congestion_v2/bbr_sender.go
Normal file
@ -0,0 +1,927 @@
|
||||
package congestion
|
||||
|
||||
// src from https://github.com/google/quiche/blob/e7872fc9e12bb1d46a118949c3d4da36de58aa44/quiche/quic/core/congestion_control/bbr_sender.cc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/metacubex/quic-go/congestion"
|
||||
|
||||
"github.com/zhangyunhao116/fastrand"
|
||||
)
|
||||
|
||||
// BbrSender implements BBR congestion control algorithm. BBR aims to estimate
|
||||
// the current available Bottleneck Bandwidth and RTT (hence the name), and
|
||||
// regulates the pacing rate and the size of the congestion window based on
|
||||
// those signals.
|
||||
//
|
||||
// BBR relies on pacing in order to function properly. Do not use BBR when
|
||||
// pacing is disabled.
|
||||
//
|
||||
|
||||
const (
|
||||
invalidPacketNumber = -1
|
||||
initialCongestionWindowPackets = 32
|
||||
|
||||
// Constants based on TCP defaults.
|
||||
// The minimum CWND to ensure delayed acks don't reduce bandwidth measurements.
|
||||
// Does not inflate the pacing rate.
|
||||
defaultMinimumCongestionWindow = 4 * congestion.ByteCount(congestion.InitialPacketSizeIPv4)
|
||||
|
||||
// The gain used for the STARTUP, equal to 2/ln(2).
|
||||
defaultHighGain = 2.885
|
||||
// The newly derived gain for STARTUP, equal to 4 * ln(2)
|
||||
derivedHighGain = 2.773
|
||||
// The newly derived CWND gain for STARTUP, 2.
|
||||
derivedHighCWNDGain = 2.0
|
||||
)
|
||||
|
||||
// The cycle of gains used during the PROBE_BW stage.
|
||||
var pacingGain = [...]float64{1.25, 0.75, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0}
|
||||
|
||||
const (
|
||||
// The length of the gain cycle.
|
||||
gainCycleLength = len(pacingGain)
|
||||
// The size of the bandwidth filter window, in round-trips.
|
||||
bandwidthWindowSize = gainCycleLength + 2
|
||||
|
||||
// The time after which the current min_rtt value expires.
|
||||
minRttExpiry = 10 * time.Second
|
||||
// The minimum time the connection can spend in PROBE_RTT mode.
|
||||
probeRttTime = 200 * time.Millisecond
|
||||
// If the bandwidth does not increase by the factor of |kStartupGrowthTarget|
|
||||
// within |kRoundTripsWithoutGrowthBeforeExitingStartup| rounds, the connection
|
||||
// will exit the STARTUP mode.
|
||||
startupGrowthTarget = 1.25
|
||||
roundTripsWithoutGrowthBeforeExitingStartup = int64(3)
|
||||
|
||||
// Flag.
|
||||
defaultStartupFullLossCount = 8
|
||||
quicBbr2DefaultLossThreshold = 0.02
|
||||
maxBbrBurstPackets = 3
|
||||
)
|
||||
|
||||
type bbrMode int
|
||||
|
||||
const (
|
||||
// Startup phase of the connection.
|
||||
bbrModeStartup = iota
|
||||
// After achieving the highest possible bandwidth during the startup, lower
|
||||
// the pacing rate in order to drain the queue.
|
||||
bbrModeDrain
|
||||
// Cruising mode.
|
||||
bbrModeProbeBw
|
||||
// Temporarily slow down sending in order to empty the buffer and measure
|
||||
// the real minimum RTT.
|
||||
bbrModeProbeRtt
|
||||
)
|
||||
|
||||
// Indicates how the congestion control limits the amount of bytes in flight.
|
||||
type bbrRecoveryState int
|
||||
|
||||
const (
|
||||
// Do not limit.
|
||||
bbrRecoveryStateNotInRecovery = iota
|
||||
// Allow an extra outstanding byte for each byte acknowledged.
|
||||
bbrRecoveryStateConservation
|
||||
// Allow two extra outstanding bytes for each byte acknowledged (slow
|
||||
// start).
|
||||
bbrRecoveryStateGrowth
|
||||
)
|
||||
|
||||
type bbrSender struct {
|
||||
rttStats congestion.RTTStatsProvider
|
||||
clock Clock
|
||||
pacer *Pacer
|
||||
|
||||
mode bbrMode
|
||||
|
||||
// Bandwidth sampler provides BBR with the bandwidth measurements at
|
||||
// individual points.
|
||||
sampler *bandwidthSampler
|
||||
|
||||
// The number of the round trips that have occurred during the connection.
|
||||
roundTripCount roundTripCount
|
||||
|
||||
// The packet number of the most recently sent packet.
|
||||
lastSentPacket congestion.PacketNumber
|
||||
// Acknowledgement of any packet after |current_round_trip_end_| will cause
|
||||
// the round trip counter to advance.
|
||||
currentRoundTripEnd congestion.PacketNumber
|
||||
|
||||
// Number of congestion events with some losses, in the current round.
|
||||
numLossEventsInRound uint64
|
||||
|
||||
// Number of total bytes lost in the current round.
|
||||
bytesLostInRound congestion.ByteCount
|
||||
|
||||
// The filter that tracks the maximum bandwidth over the multiple recent
|
||||
// round-trips.
|
||||
maxBandwidth *WindowedFilter[Bandwidth, roundTripCount]
|
||||
|
||||
// Minimum RTT estimate. Automatically expires within 10 seconds (and
|
||||
// triggers PROBE_RTT mode) if no new value is sampled during that period.
|
||||
minRtt time.Duration
|
||||
// The time at which the current value of |min_rtt_| was assigned.
|
||||
minRttTimestamp time.Time
|
||||
|
||||
// The maximum allowed number of bytes in flight.
|
||||
congestionWindow congestion.ByteCount
|
||||
|
||||
// The initial value of the |congestion_window_|.
|
||||
initialCongestionWindow congestion.ByteCount
|
||||
|
||||
// The largest value the |congestion_window_| can achieve.
|
||||
maxCongestionWindow congestion.ByteCount
|
||||
|
||||
// The smallest value the |congestion_window_| can achieve.
|
||||
minCongestionWindow congestion.ByteCount
|
||||
|
||||
// The pacing gain applied during the STARTUP phase.
|
||||
highGain float64
|
||||
|
||||
// The CWND gain applied during the STARTUP phase.
|
||||
highCwndGain float64
|
||||
|
||||
// The pacing gain applied during the DRAIN phase.
|
||||
drainGain float64
|
||||
|
||||
// The current pacing rate of the connection.
|
||||
pacingRate Bandwidth
|
||||
|
||||
// The gain currently applied to the pacing rate.
|
||||
pacingGain float64
|
||||
// The gain currently applied to the congestion window.
|
||||
congestionWindowGain float64
|
||||
|
||||
// The gain used for the congestion window during PROBE_BW. Latched from
|
||||
// quic_bbr_cwnd_gain flag.
|
||||
congestionWindowGainConstant float64
|
||||
// The number of RTTs to stay in STARTUP mode. Defaults to 3.
|
||||
numStartupRtts int64
|
||||
|
||||
// Number of round-trips in PROBE_BW mode, used for determining the current
|
||||
// pacing gain cycle.
|
||||
cycleCurrentOffset int
|
||||
// The time at which the last pacing gain cycle was started.
|
||||
lastCycleStart time.Time
|
||||
|
||||
// Indicates whether the connection has reached the full bandwidth mode.
|
||||
isAtFullBandwidth bool
|
||||
// Number of rounds during which there was no significant bandwidth increase.
|
||||
roundsWithoutBandwidthGain int64
|
||||
// The bandwidth compared to which the increase is measured.
|
||||
bandwidthAtLastRound Bandwidth
|
||||
|
||||
// Set to true upon exiting quiescence.
|
||||
exitingQuiescence bool
|
||||
|
||||
// Time at which PROBE_RTT has to be exited. Setting it to zero indicates
|
||||
// that the time is yet unknown as the number of packets in flight has not
|
||||
// reached the required value.
|
||||
exitProbeRttAt time.Time
|
||||
// Indicates whether a round-trip has passed since PROBE_RTT became active.
|
||||
probeRttRoundPassed bool
|
||||
|
||||
// Indicates whether the most recent bandwidth sample was marked as
|
||||
// app-limited.
|
||||
lastSampleIsAppLimited bool
|
||||
// Indicates whether any non app-limited samples have been recorded.
|
||||
hasNoAppLimitedSample bool
|
||||
|
||||
// Current state of recovery.
|
||||
recoveryState bbrRecoveryState
|
||||
// Receiving acknowledgement of a packet after |end_recovery_at_| will cause
|
||||
// BBR to exit the recovery mode. A value above zero indicates at least one
|
||||
// loss has been detected, so it must not be set back to zero.
|
||||
endRecoveryAt congestion.PacketNumber
|
||||
// A window used to limit the number of bytes in flight during loss recovery.
|
||||
recoveryWindow congestion.ByteCount
|
||||
// If true, consider all samples in recovery app-limited.
|
||||
isAppLimitedRecovery bool // not used
|
||||
|
||||
// When true, pace at 1.5x and disable packet conservation in STARTUP.
|
||||
slowerStartup bool // not used
|
||||
// When true, disables packet conservation in STARTUP.
|
||||
rateBasedStartup bool // not used
|
||||
|
||||
// When true, add the most recent ack aggregation measurement during STARTUP.
|
||||
enableAckAggregationDuringStartup bool
|
||||
// When true, expire the windowed ack aggregation values in STARTUP when
|
||||
// bandwidth increases more than 25%.
|
||||
expireAckAggregationInStartup bool
|
||||
|
||||
// If true, will not exit low gain mode until bytes_in_flight drops below BDP
|
||||
// or it's time for high gain mode.
|
||||
drainToTarget bool
|
||||
|
||||
// If true, slow down pacing rate in STARTUP when overshooting is detected.
|
||||
detectOvershooting bool
|
||||
// Bytes lost while detect_overshooting_ is true.
|
||||
bytesLostWhileDetectingOvershooting congestion.ByteCount
|
||||
// Slow down pacing rate if
|
||||
// bytes_lost_while_detecting_overshooting_ *
|
||||
// bytes_lost_multiplier_while_detecting_overshooting_ > IW.
|
||||
bytesLostMultiplierWhileDetectingOvershooting uint8
|
||||
// When overshooting is detected, do not drop pacing_rate_ below this value /
|
||||
// min_rtt.
|
||||
cwndToCalculateMinPacingRate congestion.ByteCount
|
||||
|
||||
// Max congestion window when adjusting network parameters.
|
||||
maxCongestionWindowWithNetworkParametersAdjusted congestion.ByteCount // not used
|
||||
|
||||
// Params.
|
||||
maxDatagramSize congestion.ByteCount
|
||||
// Recorded on packet sent. equivalent |unacked_packets_->bytes_in_flight()|
|
||||
bytesInFlight congestion.ByteCount
|
||||
}
|
||||
|
||||
var _ congestion.CongestionControl = &bbrSender{}
|
||||
|
||||
func NewBbrSender(
|
||||
clock Clock,
|
||||
initialMaxDatagramSize congestion.ByteCount,
|
||||
initialCongestionWindowPackets congestion.ByteCount,
|
||||
) *bbrSender {
|
||||
return newBbrSender(
|
||||
clock,
|
||||
initialMaxDatagramSize,
|
||||
initialCongestionWindowPackets*initialMaxDatagramSize,
|
||||
congestion.MaxCongestionWindowPackets*initialMaxDatagramSize,
|
||||
)
|
||||
}
|
||||
|
||||
func newBbrSender(
|
||||
clock Clock,
|
||||
initialMaxDatagramSize,
|
||||
initialCongestionWindow,
|
||||
initialMaxCongestionWindow congestion.ByteCount,
|
||||
) *bbrSender {
|
||||
b := &bbrSender{
|
||||
clock: clock,
|
||||
mode: bbrModeStartup,
|
||||
sampler: newBandwidthSampler(roundTripCount(bandwidthWindowSize)),
|
||||
lastSentPacket: invalidPacketNumber,
|
||||
currentRoundTripEnd: invalidPacketNumber,
|
||||
maxBandwidth: NewWindowedFilter(roundTripCount(bandwidthWindowSize), MaxFilter[Bandwidth]),
|
||||
congestionWindow: initialCongestionWindow,
|
||||
initialCongestionWindow: initialCongestionWindow,
|
||||
maxCongestionWindow: initialMaxCongestionWindow,
|
||||
minCongestionWindow: defaultMinimumCongestionWindow,
|
||||
highGain: defaultHighGain,
|
||||
highCwndGain: defaultHighGain,
|
||||
drainGain: 1.0 / defaultHighGain,
|
||||
pacingGain: 1.0,
|
||||
congestionWindowGain: 1.0,
|
||||
congestionWindowGainConstant: 2.0,
|
||||
numStartupRtts: roundTripsWithoutGrowthBeforeExitingStartup,
|
||||
recoveryState: bbrRecoveryStateNotInRecovery,
|
||||
endRecoveryAt: invalidPacketNumber,
|
||||
recoveryWindow: initialMaxCongestionWindow,
|
||||
bytesLostMultiplierWhileDetectingOvershooting: 2,
|
||||
cwndToCalculateMinPacingRate: initialCongestionWindow,
|
||||
maxCongestionWindowWithNetworkParametersAdjusted: initialMaxCongestionWindow,
|
||||
maxDatagramSize: initialMaxDatagramSize,
|
||||
}
|
||||
b.pacer = NewPacer(func() congestion.ByteCount {
|
||||
// Pacer wants bytes per second, but Bandwidth is in bits per second.
|
||||
return congestion.ByteCount(float64(b.bandwidthEstimate()) * b.congestionWindowGain / float64(BytesPerSecond))
|
||||
})
|
||||
|
||||
/*
|
||||
if b.tracer != nil {
|
||||
b.lastState = logging.CongestionStateStartup
|
||||
b.tracer.UpdatedCongestionState(logging.CongestionStateStartup)
|
||||
}
|
||||
*/
|
||||
|
||||
b.enterStartupMode(b.clock.Now())
|
||||
b.setHighCwndGain(derivedHighCWNDGain)
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *bbrSender) SetRTTStatsProvider(provider congestion.RTTStatsProvider) {
|
||||
b.rttStats = provider
|
||||
}
|
||||
|
||||
// TimeUntilSend implements the SendAlgorithm interface.
|
||||
func (b *bbrSender) TimeUntilSend(bytesInFlight congestion.ByteCount) time.Time {
|
||||
return b.pacer.TimeUntilSend()
|
||||
}
|
||||
|
||||
// HasPacingBudget implements the SendAlgorithm interface.
|
||||
func (b *bbrSender) HasPacingBudget(now time.Time) bool {
|
||||
return b.pacer.Budget(now) >= b.maxDatagramSize
|
||||
}
|
||||
|
||||
// OnPacketSent implements the SendAlgorithm interface.
|
||||
func (b *bbrSender) OnPacketSent(
|
||||
sentTime time.Time,
|
||||
bytesInFlight congestion.ByteCount,
|
||||
packetNumber congestion.PacketNumber,
|
||||
bytes congestion.ByteCount,
|
||||
isRetransmittable bool,
|
||||
) {
|
||||
b.pacer.SentPacket(sentTime, bytes)
|
||||
|
||||
b.lastSentPacket = packetNumber
|
||||
b.bytesInFlight = bytesInFlight
|
||||
|
||||
if bytesInFlight == 0 {
|
||||
b.exitingQuiescence = true
|
||||
}
|
||||
|
||||
b.sampler.OnPacketSent(sentTime, packetNumber, bytes, bytesInFlight, isRetransmittable)
|
||||
}
|
||||
|
||||
// CanSend implements the SendAlgorithm interface.
|
||||
func (b *bbrSender) CanSend(bytesInFlight congestion.ByteCount) bool {
|
||||
return bytesInFlight < b.GetCongestionWindow()
|
||||
}
|
||||
|
||||
// MaybeExitSlowStart implements the SendAlgorithm interface.
|
||||
func (b *bbrSender) MaybeExitSlowStart() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
// OnPacketAcked implements the SendAlgorithm interface.
|
||||
func (b *bbrSender) OnPacketAcked(number congestion.PacketNumber, ackedBytes, priorInFlight congestion.ByteCount, eventTime time.Time) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
// OnPacketLost implements the SendAlgorithm interface.
|
||||
func (b *bbrSender) OnPacketLost(number congestion.PacketNumber, lostBytes, priorInFlight congestion.ByteCount) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
// OnRetransmissionTimeout implements the SendAlgorithm interface.
|
||||
func (b *bbrSender) OnRetransmissionTimeout(packetsRetransmitted bool) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
// SetMaxDatagramSize implements the SendAlgorithm interface.
|
||||
func (b *bbrSender) SetMaxDatagramSize(s congestion.ByteCount) {
|
||||
if s < b.maxDatagramSize {
|
||||
panic(fmt.Sprintf("congestion BUG: decreased max datagram size from %d to %d", b.maxDatagramSize, s))
|
||||
}
|
||||
cwndIsMinCwnd := b.congestionWindow == b.minCongestionWindow
|
||||
b.maxDatagramSize = s
|
||||
if cwndIsMinCwnd {
|
||||
b.congestionWindow = b.minCongestionWindow
|
||||
}
|
||||
b.pacer.SetMaxDatagramSize(s)
|
||||
}
|
||||
|
||||
// InSlowStart implements the SendAlgorithmWithDebugInfos interface.
|
||||
func (b *bbrSender) InSlowStart() bool {
|
||||
return b.mode == bbrModeStartup
|
||||
}
|
||||
|
||||
// InRecovery implements the SendAlgorithmWithDebugInfos interface.
|
||||
func (b *bbrSender) InRecovery() bool {
|
||||
return b.recoveryState != bbrRecoveryStateNotInRecovery
|
||||
}
|
||||
|
||||
// GetCongestionWindow implements the SendAlgorithmWithDebugInfos interface.
|
||||
func (b *bbrSender) GetCongestionWindow() congestion.ByteCount {
|
||||
if b.mode == bbrModeProbeRtt {
|
||||
return b.probeRttCongestionWindow()
|
||||
}
|
||||
|
||||
if b.InRecovery() {
|
||||
return Min(b.congestionWindow, b.recoveryWindow)
|
||||
}
|
||||
|
||||
return b.congestionWindow
|
||||
}
|
||||
|
||||
func (b *bbrSender) OnCongestionEvent(number congestion.PacketNumber, lostBytes, priorInFlight congestion.ByteCount) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
func (b *bbrSender) OnCongestionEventEx(priorInFlight congestion.ByteCount, eventTime time.Time, ackedPackets []congestion.AckedPacketInfo, lostPackets []congestion.LostPacketInfo) {
|
||||
totalBytesAckedBefore := b.sampler.TotalBytesAcked()
|
||||
totalBytesLostBefore := b.sampler.TotalBytesLost()
|
||||
|
||||
var isRoundStart, minRttExpired bool
|
||||
var excessAcked, bytesLost congestion.ByteCount
|
||||
|
||||
// The send state of the largest packet in acked_packets, unless it is
|
||||
// empty. If acked_packets is empty, it's the send state of the largest
|
||||
// packet in lost_packets.
|
||||
var lastPacketSendState sendTimeState
|
||||
|
||||
b.maybeApplimited(priorInFlight)
|
||||
|
||||
// Update bytesInFlight
|
||||
b.bytesInFlight = priorInFlight
|
||||
for _, p := range ackedPackets {
|
||||
b.bytesInFlight -= p.BytesAcked
|
||||
}
|
||||
for _, p := range lostPackets {
|
||||
b.bytesInFlight -= p.BytesLost
|
||||
}
|
||||
|
||||
if len(ackedPackets) != 0 {
|
||||
lastAckedPacket := ackedPackets[len(ackedPackets)-1].PacketNumber
|
||||
isRoundStart = b.updateRoundTripCounter(lastAckedPacket)
|
||||
b.updateRecoveryState(lastAckedPacket, len(lostPackets) != 0, isRoundStart)
|
||||
}
|
||||
|
||||
sample := b.sampler.OnCongestionEvent(eventTime,
|
||||
ackedPackets, lostPackets, b.maxBandwidth.GetBest(), infBandwidth, b.roundTripCount)
|
||||
if sample.lastPacketSendState.isValid {
|
||||
b.lastSampleIsAppLimited = sample.lastPacketSendState.isAppLimited
|
||||
b.hasNoAppLimitedSample = b.hasNoAppLimitedSample || !b.lastSampleIsAppLimited
|
||||
}
|
||||
// Avoid updating |max_bandwidth_| if a) this is a loss-only event, or b) all
|
||||
// packets in |acked_packets| did not generate valid samples. (e.g. ack of
|
||||
// ack-only packets). In both cases, sampler_.total_bytes_acked() will not
|
||||
// change.
|
||||
if totalBytesAckedBefore != b.sampler.TotalBytesAcked() {
|
||||
if !sample.sampleIsAppLimited || sample.sampleMaxBandwidth > b.maxBandwidth.GetBest() {
|
||||
b.maxBandwidth.Update(sample.sampleMaxBandwidth, b.roundTripCount)
|
||||
}
|
||||
}
|
||||
|
||||
if sample.sampleRtt != infRTT {
|
||||
minRttExpired = b.maybeUpdateMinRtt(eventTime, sample.sampleRtt)
|
||||
}
|
||||
bytesLost = b.sampler.TotalBytesLost() - totalBytesLostBefore
|
||||
|
||||
excessAcked = sample.extraAcked
|
||||
lastPacketSendState = sample.lastPacketSendState
|
||||
|
||||
if len(lostPackets) != 0 {
|
||||
b.numLossEventsInRound++
|
||||
b.bytesLostInRound += bytesLost
|
||||
}
|
||||
|
||||
// Handle logic specific to PROBE_BW mode.
|
||||
if b.mode == bbrModeProbeBw {
|
||||
b.updateGainCyclePhase(eventTime, priorInFlight, len(lostPackets) != 0)
|
||||
}
|
||||
|
||||
// Handle logic specific to STARTUP and DRAIN modes.
|
||||
if isRoundStart && !b.isAtFullBandwidth {
|
||||
b.checkIfFullBandwidthReached(&lastPacketSendState)
|
||||
}
|
||||
|
||||
b.maybeExitStartupOrDrain(eventTime)
|
||||
|
||||
// Handle logic specific to PROBE_RTT.
|
||||
b.maybeEnterOrExitProbeRtt(eventTime, isRoundStart, minRttExpired)
|
||||
|
||||
// Calculate number of packets acked and lost.
|
||||
bytesAcked := b.sampler.TotalBytesAcked() - totalBytesAckedBefore
|
||||
|
||||
// After the model is updated, recalculate the pacing rate and congestion
|
||||
// window.
|
||||
b.calculatePacingRate(bytesLost)
|
||||
b.calculateCongestionWindow(bytesAcked, excessAcked)
|
||||
b.calculateRecoveryWindow(bytesAcked, bytesLost)
|
||||
|
||||
// Cleanup internal state.
|
||||
if len(lostPackets) != 0 {
|
||||
lastLostPacket := lostPackets[len(lostPackets)-1].PacketNumber
|
||||
b.sampler.RemoveObsoletePackets(lastLostPacket)
|
||||
}
|
||||
if isRoundStart {
|
||||
b.numLossEventsInRound = 0
|
||||
b.bytesLostInRound = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (b *bbrSender) PacingRate() Bandwidth {
|
||||
if b.pacingRate == 0 {
|
||||
return Bandwidth(b.highGain * float64(
|
||||
BandwidthFromDelta(b.initialCongestionWindow, b.getMinRtt())))
|
||||
}
|
||||
|
||||
return b.pacingRate
|
||||
}
|
||||
|
||||
func (b *bbrSender) hasGoodBandwidthEstimateForResumption() bool {
|
||||
return b.hasNonAppLimitedSample()
|
||||
}
|
||||
|
||||
func (b *bbrSender) hasNonAppLimitedSample() bool {
|
||||
return b.hasNoAppLimitedSample
|
||||
}
|
||||
|
||||
// Sets the pacing gain used in STARTUP. Must be greater than 1.
|
||||
func (b *bbrSender) setHighGain(highGain float64) {
|
||||
b.highGain = highGain
|
||||
if b.mode == bbrModeStartup {
|
||||
b.pacingGain = highGain
|
||||
}
|
||||
}
|
||||
|
||||
// Sets the CWND gain used in STARTUP. Must be greater than 1.
|
||||
func (b *bbrSender) setHighCwndGain(highCwndGain float64) {
|
||||
b.highCwndGain = highCwndGain
|
||||
if b.mode == bbrModeStartup {
|
||||
b.congestionWindowGain = highCwndGain
|
||||
}
|
||||
}
|
||||
|
||||
// Sets the gain used in DRAIN. Must be less than 1.
|
||||
func (b *bbrSender) setDrainGain(drainGain float64) {
|
||||
b.drainGain = drainGain
|
||||
}
|
||||
|
||||
// What's the current estimated bandwidth in bytes per second.
|
||||
func (b *bbrSender) bandwidthEstimate() Bandwidth {
|
||||
return b.maxBandwidth.GetBest()
|
||||
}
|
||||
|
||||
// Returns the current estimate of the RTT of the connection. Outside of the
|
||||
// edge cases, this is minimum RTT.
|
||||
func (b *bbrSender) getMinRtt() time.Duration {
|
||||
if b.minRtt != 0 {
|
||||
return b.minRtt
|
||||
}
|
||||
// min_rtt could be available if the handshake packet gets neutered then
|
||||
// gets acknowledged. This could only happen for QUIC crypto where we do not
|
||||
// drop keys.
|
||||
minRtt := b.rttStats.MinRTT()
|
||||
if minRtt == 0 {
|
||||
return 100 * time.Millisecond
|
||||
} else {
|
||||
return minRtt
|
||||
}
|
||||
}
|
||||
|
||||
// Computes the target congestion window using the specified gain.
|
||||
func (b *bbrSender) getTargetCongestionWindow(gain float64) congestion.ByteCount {
|
||||
bdp := bdpFromRttAndBandwidth(b.getMinRtt(), b.bandwidthEstimate())
|
||||
congestionWindow := congestion.ByteCount(gain * float64(bdp))
|
||||
|
||||
// BDP estimate will be zero if no bandwidth samples are available yet.
|
||||
if congestionWindow == 0 {
|
||||
congestionWindow = congestion.ByteCount(gain * float64(b.initialCongestionWindow))
|
||||
}
|
||||
|
||||
return Max(congestionWindow, b.minCongestionWindow)
|
||||
}
|
||||
|
||||
// The target congestion window during PROBE_RTT.
|
||||
func (b *bbrSender) probeRttCongestionWindow() congestion.ByteCount {
|
||||
return b.minCongestionWindow
|
||||
}
|
||||
|
||||
func (b *bbrSender) maybeUpdateMinRtt(now time.Time, sampleMinRtt time.Duration) bool {
|
||||
// Do not expire min_rtt if none was ever available.
|
||||
minRttExpired := b.minRtt != 0 && now.After(b.minRttTimestamp.Add(minRttExpiry))
|
||||
if minRttExpired || sampleMinRtt < b.minRtt || b.minRtt == 0 {
|
||||
b.minRtt = sampleMinRtt
|
||||
b.minRttTimestamp = now
|
||||
}
|
||||
|
||||
return minRttExpired
|
||||
}
|
||||
|
||||
// Enters the STARTUP mode.
|
||||
func (b *bbrSender) enterStartupMode(now time.Time) {
|
||||
b.mode = bbrModeStartup
|
||||
// b.maybeTraceStateChange(logging.CongestionStateStartup)
|
||||
b.pacingGain = b.highGain
|
||||
b.congestionWindowGain = b.highCwndGain
|
||||
}
|
||||
|
||||
// Enters the PROBE_BW mode.
|
||||
func (b *bbrSender) enterProbeBandwidthMode(now time.Time) {
|
||||
b.mode = bbrModeProbeBw
|
||||
// b.maybeTraceStateChange(logging.CongestionStateProbeBw)
|
||||
b.congestionWindowGain = b.congestionWindowGainConstant
|
||||
|
||||
// Pick a random offset for the gain cycle out of {0, 2..7} range. 1 is
|
||||
// excluded because in that case increased gain and decreased gain would not
|
||||
// follow each other.
|
||||
b.cycleCurrentOffset = int(fastrand.Int31n(congestion.PacketsPerConnectionID)) % (gainCycleLength - 1)
|
||||
if b.cycleCurrentOffset >= 1 {
|
||||
b.cycleCurrentOffset += 1
|
||||
}
|
||||
|
||||
b.lastCycleStart = now
|
||||
b.pacingGain = pacingGain[b.cycleCurrentOffset]
|
||||
}
|
||||
|
||||
// Updates the round-trip counter if a round-trip has passed. Returns true if
|
||||
// the counter has been advanced.
|
||||
func (b *bbrSender) updateRoundTripCounter(lastAckedPacket congestion.PacketNumber) bool {
|
||||
if b.currentRoundTripEnd == invalidPacketNumber || lastAckedPacket > b.currentRoundTripEnd {
|
||||
b.roundTripCount++
|
||||
b.currentRoundTripEnd = b.lastSentPacket
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Updates the current gain used in PROBE_BW mode.
|
||||
func (b *bbrSender) updateGainCyclePhase(now time.Time, priorInFlight congestion.ByteCount, hasLosses bool) {
|
||||
// In most cases, the cycle is advanced after an RTT passes.
|
||||
shouldAdvanceGainCycling := now.After(b.lastCycleStart.Add(b.getMinRtt()))
|
||||
// If the pacing gain is above 1.0, the connection is trying to probe the
|
||||
// bandwidth by increasing the number of bytes in flight to at least
|
||||
// pacing_gain * BDP. Make sure that it actually reaches the target, as long
|
||||
// as there are no losses suggesting that the buffers are not able to hold
|
||||
// that much.
|
||||
if b.pacingGain > 1.0 && !hasLosses && priorInFlight < b.getTargetCongestionWindow(b.pacingGain) {
|
||||
shouldAdvanceGainCycling = false
|
||||
}
|
||||
|
||||
// If pacing gain is below 1.0, the connection is trying to drain the extra
|
||||
// queue which could have been incurred by probing prior to it. If the number
|
||||
// of bytes in flight falls down to the estimated BDP value earlier, conclude
|
||||
// that the queue has been successfully drained and exit this cycle early.
|
||||
if b.pacingGain < 1.0 && b.bytesInFlight <= b.getTargetCongestionWindow(1) {
|
||||
shouldAdvanceGainCycling = true
|
||||
}
|
||||
|
||||
if shouldAdvanceGainCycling {
|
||||
b.cycleCurrentOffset = (b.cycleCurrentOffset + 1) % gainCycleLength
|
||||
b.lastCycleStart = now
|
||||
// Stay in low gain mode until the target BDP is hit.
|
||||
// Low gain mode will be exited immediately when the target BDP is achieved.
|
||||
if b.drainToTarget && b.pacingGain < 1 &&
|
||||
pacingGain[b.cycleCurrentOffset] == 1 &&
|
||||
b.bytesInFlight > b.getTargetCongestionWindow(1) {
|
||||
return
|
||||
}
|
||||
b.pacingGain = pacingGain[b.cycleCurrentOffset]
|
||||
}
|
||||
}
|
||||
|
||||
// Tracks for how many round-trips the bandwidth has not increased
|
||||
// significantly.
|
||||
func (b *bbrSender) checkIfFullBandwidthReached(lastPacketSendState *sendTimeState) {
|
||||
if b.lastSampleIsAppLimited {
|
||||
return
|
||||
}
|
||||
|
||||
target := Bandwidth(float64(b.bandwidthAtLastRound) * startupGrowthTarget)
|
||||
if b.bandwidthEstimate() >= target {
|
||||
b.bandwidthAtLastRound = b.bandwidthEstimate()
|
||||
b.roundsWithoutBandwidthGain = 0
|
||||
if b.expireAckAggregationInStartup {
|
||||
// Expire old excess delivery measurements now that bandwidth increased.
|
||||
b.sampler.ResetMaxAckHeightTracker(0, b.roundTripCount)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
b.roundsWithoutBandwidthGain++
|
||||
if b.roundsWithoutBandwidthGain >= b.numStartupRtts ||
|
||||
b.shouldExitStartupDueToLoss(lastPacketSendState) {
|
||||
b.isAtFullBandwidth = true
|
||||
}
|
||||
}
|
||||
|
||||
func (b *bbrSender) maybeApplimited(bytesInFlight congestion.ByteCount) {
|
||||
congestionWindow := b.GetCongestionWindow()
|
||||
if bytesInFlight >= congestionWindow {
|
||||
return
|
||||
}
|
||||
availableBytes := congestionWindow - bytesInFlight
|
||||
drainLimited := b.mode == bbrModeDrain && bytesInFlight > congestionWindow/2
|
||||
if !drainLimited || availableBytes > maxBbrBurstPackets*b.maxDatagramSize {
|
||||
b.sampler.OnAppLimited()
|
||||
}
|
||||
}
|
||||
|
||||
// Transitions from STARTUP to DRAIN and from DRAIN to PROBE_BW if
|
||||
// appropriate.
|
||||
func (b *bbrSender) maybeExitStartupOrDrain(now time.Time) {
|
||||
if b.mode == bbrModeStartup && b.isAtFullBandwidth {
|
||||
b.mode = bbrModeDrain
|
||||
// b.maybeTraceStateChange(logging.CongestionStateDrain)
|
||||
b.pacingGain = b.drainGain
|
||||
b.congestionWindowGain = b.highCwndGain
|
||||
}
|
||||
if b.mode == bbrModeDrain && b.bytesInFlight <= b.getTargetCongestionWindow(1) {
|
||||
b.enterProbeBandwidthMode(now)
|
||||
}
|
||||
}
|
||||
|
||||
// Decides whether to enter or exit PROBE_RTT.
|
||||
func (b *bbrSender) maybeEnterOrExitProbeRtt(now time.Time, isRoundStart, minRttExpired bool) {
|
||||
if minRttExpired && !b.exitingQuiescence && b.mode != bbrModeProbeRtt {
|
||||
b.mode = bbrModeProbeRtt
|
||||
// b.maybeTraceStateChange(logging.CongestionStateProbRtt)
|
||||
b.pacingGain = 1.0
|
||||
// Do not decide on the time to exit PROBE_RTT until the |bytes_in_flight|
|
||||
// is at the target small value.
|
||||
b.exitProbeRttAt = time.Time{}
|
||||
}
|
||||
|
||||
if b.mode == bbrModeProbeRtt {
|
||||
b.sampler.OnAppLimited()
|
||||
// b.maybeTraceStateChange(logging.CongestionStateApplicationLimited)
|
||||
|
||||
if b.exitProbeRttAt.IsZero() {
|
||||
// If the window has reached the appropriate size, schedule exiting
|
||||
// PROBE_RTT. The CWND during PROBE_RTT is kMinimumCongestionWindow, but
|
||||
// we allow an extra packet since QUIC checks CWND before sending a
|
||||
// packet.
|
||||
if b.bytesInFlight < b.probeRttCongestionWindow()+congestion.MaxPacketBufferSize {
|
||||
b.exitProbeRttAt = now.Add(probeRttTime)
|
||||
b.probeRttRoundPassed = false
|
||||
}
|
||||
} else {
|
||||
if isRoundStart {
|
||||
b.probeRttRoundPassed = true
|
||||
}
|
||||
if now.Sub(b.exitProbeRttAt) >= 0 && b.probeRttRoundPassed {
|
||||
b.minRttTimestamp = now
|
||||
if !b.isAtFullBandwidth {
|
||||
b.enterStartupMode(now)
|
||||
} else {
|
||||
b.enterProbeBandwidthMode(now)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
b.exitingQuiescence = false
|
||||
}
|
||||
|
||||
// Determines whether BBR needs to enter, exit or advance state of the
|
||||
// recovery.
|
||||
func (b *bbrSender) updateRecoveryState(lastAckedPacket congestion.PacketNumber, hasLosses, isRoundStart bool) {
|
||||
// Disable recovery in startup, if loss-based exit is enabled.
|
||||
if !b.isAtFullBandwidth {
|
||||
return
|
||||
}
|
||||
|
||||
// Exit recovery when there are no losses for a round.
|
||||
if hasLosses {
|
||||
b.endRecoveryAt = b.lastSentPacket
|
||||
}
|
||||
|
||||
switch b.recoveryState {
|
||||
case bbrRecoveryStateNotInRecovery:
|
||||
if hasLosses {
|
||||
b.recoveryState = bbrRecoveryStateConservation
|
||||
// This will cause the |recovery_window_| to be set to the correct
|
||||
// value in CalculateRecoveryWindow().
|
||||
b.recoveryWindow = 0
|
||||
// Since the conservation phase is meant to be lasting for a whole
|
||||
// round, extend the current round as if it were started right now.
|
||||
b.currentRoundTripEnd = b.lastSentPacket
|
||||
}
|
||||
case bbrRecoveryStateConservation:
|
||||
if isRoundStart {
|
||||
b.recoveryState = bbrRecoveryStateGrowth
|
||||
}
|
||||
fallthrough
|
||||
case bbrRecoveryStateGrowth:
|
||||
// Exit recovery if appropriate.
|
||||
if !hasLosses && lastAckedPacket > b.endRecoveryAt {
|
||||
b.recoveryState = bbrRecoveryStateNotInRecovery
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Determines the appropriate pacing rate for the connection.
|
||||
func (b *bbrSender) calculatePacingRate(bytesLost congestion.ByteCount) {
|
||||
if b.bandwidthEstimate() == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
targetRate := Bandwidth(b.pacingGain * float64(b.bandwidthEstimate()))
|
||||
if b.isAtFullBandwidth {
|
||||
b.pacingRate = targetRate
|
||||
return
|
||||
}
|
||||
|
||||
// Pace at the rate of initial_window / RTT as soon as RTT measurements are
|
||||
// available.
|
||||
if b.pacingRate == 0 && b.rttStats.MinRTT() != 0 {
|
||||
b.pacingRate = BandwidthFromDelta(b.initialCongestionWindow, b.rttStats.MinRTT())
|
||||
return
|
||||
}
|
||||
|
||||
if b.detectOvershooting {
|
||||
b.bytesLostWhileDetectingOvershooting += bytesLost
|
||||
// Check for overshooting with network parameters adjusted when pacing rate
|
||||
// > target_rate and loss has been detected.
|
||||
if b.pacingRate > targetRate && b.bytesLostWhileDetectingOvershooting > 0 {
|
||||
if b.hasNoAppLimitedSample ||
|
||||
b.bytesLostWhileDetectingOvershooting*congestion.ByteCount(b.bytesLostMultiplierWhileDetectingOvershooting) > b.initialCongestionWindow {
|
||||
// We are fairly sure overshoot happens if 1) there is at least one
|
||||
// non app-limited bw sample or 2) half of IW gets lost. Slow pacing
|
||||
// rate.
|
||||
b.pacingRate = Max(targetRate, BandwidthFromDelta(b.cwndToCalculateMinPacingRate, b.rttStats.MinRTT()))
|
||||
b.bytesLostWhileDetectingOvershooting = 0
|
||||
b.detectOvershooting = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Do not decrease the pacing rate during startup.
|
||||
b.pacingRate = Max(b.pacingRate, targetRate)
|
||||
}
|
||||
|
||||
// Determines the appropriate congestion window for the connection.
|
||||
func (b *bbrSender) calculateCongestionWindow(bytesAcked, excessAcked congestion.ByteCount) {
|
||||
if b.mode == bbrModeProbeRtt {
|
||||
return
|
||||
}
|
||||
|
||||
targetWindow := b.getTargetCongestionWindow(b.congestionWindowGain)
|
||||
if b.isAtFullBandwidth {
|
||||
// Add the max recently measured ack aggregation to CWND.
|
||||
targetWindow += b.sampler.MaxAckHeight()
|
||||
} else if b.enableAckAggregationDuringStartup {
|
||||
// Add the most recent excess acked. Because CWND never decreases in
|
||||
// STARTUP, this will automatically create a very localized max filter.
|
||||
targetWindow += excessAcked
|
||||
}
|
||||
|
||||
// Instead of immediately setting the target CWND as the new one, BBR grows
|
||||
// the CWND towards |target_window| by only increasing it |bytes_acked| at a
|
||||
// time.
|
||||
if b.isAtFullBandwidth {
|
||||
b.congestionWindow = Min(targetWindow, b.congestionWindow+bytesAcked)
|
||||
} else if b.congestionWindow < targetWindow ||
|
||||
b.sampler.TotalBytesAcked() < b.initialCongestionWindow {
|
||||
// If the connection is not yet out of startup phase, do not decrease the
|
||||
// window.
|
||||
b.congestionWindow += bytesAcked
|
||||
}
|
||||
|
||||
// Enforce the limits on the congestion window.
|
||||
b.congestionWindow = Max(b.congestionWindow, b.minCongestionWindow)
|
||||
b.congestionWindow = Min(b.congestionWindow, b.maxCongestionWindow)
|
||||
}
|
||||
|
||||
// Determines the appropriate window that constrains the in-flight during recovery.
|
||||
func (b *bbrSender) calculateRecoveryWindow(bytesAcked, bytesLost congestion.ByteCount) {
|
||||
if b.recoveryState == bbrRecoveryStateNotInRecovery {
|
||||
return
|
||||
}
|
||||
|
||||
// Set up the initial recovery window.
|
||||
if b.recoveryWindow == 0 {
|
||||
b.recoveryWindow = b.bytesInFlight + bytesAcked
|
||||
b.recoveryWindow = Max(b.minCongestionWindow, b.recoveryWindow)
|
||||
return
|
||||
}
|
||||
|
||||
// Remove losses from the recovery window, while accounting for a potential
|
||||
// integer underflow.
|
||||
if b.recoveryWindow >= bytesLost {
|
||||
b.recoveryWindow = b.recoveryWindow - bytesLost
|
||||
} else {
|
||||
b.recoveryWindow = b.maxDatagramSize
|
||||
}
|
||||
|
||||
// In CONSERVATION mode, just subtracting losses is sufficient. In GROWTH,
|
||||
// release additional |bytes_acked| to achieve a slow-start-like behavior.
|
||||
if b.recoveryState == bbrRecoveryStateGrowth {
|
||||
b.recoveryWindow += bytesAcked
|
||||
}
|
||||
|
||||
// Always allow sending at least |bytes_acked| in response.
|
||||
b.recoveryWindow = Max(b.recoveryWindow, b.bytesInFlight+bytesAcked)
|
||||
b.recoveryWindow = Max(b.minCongestionWindow, b.recoveryWindow)
|
||||
}
|
||||
|
||||
// Return whether we should exit STARTUP due to excessive loss.
|
||||
func (b *bbrSender) shouldExitStartupDueToLoss(lastPacketSendState *sendTimeState) bool {
|
||||
if b.numLossEventsInRound < defaultStartupFullLossCount || !lastPacketSendState.isValid {
|
||||
return false
|
||||
}
|
||||
|
||||
inflightAtSend := lastPacketSendState.bytesInFlight
|
||||
|
||||
if inflightAtSend > 0 && b.bytesLostInRound > 0 {
|
||||
if b.bytesLostInRound > congestion.ByteCount(float64(inflightAtSend)*quicBbr2DefaultLossThreshold) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func bdpFromRttAndBandwidth(rtt time.Duration, bandwidth Bandwidth) congestion.ByteCount {
|
||||
return congestion.ByteCount(rtt) * congestion.ByteCount(bandwidth) / congestion.ByteCount(BytesPerSecond) / congestion.ByteCount(time.Second)
|
||||
}
|
||||
|
||||
func GetInitialPacketSize(addr net.Addr) congestion.ByteCount {
|
||||
// If this is not a UDP address, we don't know anything about the MTU.
|
||||
// Use the minimum size of an Initial packet as the max packet size.
|
||||
if udpAddr, ok := addr.(*net.UDPAddr); ok {
|
||||
if udpAddr.IP.To4() != nil {
|
||||
return congestion.InitialPacketSizeIPv4
|
||||
} else {
|
||||
return congestion.InitialPacketSizeIPv6
|
||||
}
|
||||
} else {
|
||||
return congestion.MinInitialPacketSize
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user