package congestion import ( "fmt" "time" "github.com/lucas-clemente/quic-go/internal/protocol" "github.com/lucas-clemente/quic-go/internal/utils" "github.com/lucas-clemente/quic-go/logging" ) const ( // maxDatagramSize is the default maximum packet size used in the Linux TCP implementation. // Used in QUIC for congestion window computations in bytes. initialMaxDatagramSize = protocol.ByteCount(protocol.InitialPacketSizeIPv4) maxBurstPackets = 3 renoBeta = 0.7 // Reno backoff factor. minCongestionWindowPackets = 2 initialCongestionWindow = 32 ) type cubicSender struct { hybridSlowStart HybridSlowStart rttStats *utils.RTTStats cubic *Cubic pacer *pacer clock Clock reno bool // Track the largest packet that has been sent. largestSentPacketNumber protocol.PacketNumber // Track the largest packet that has been acked. largestAckedPacketNumber protocol.PacketNumber // Track the largest packet number outstanding when a CWND cutback occurs. largestSentAtLastCutback protocol.PacketNumber // Whether the last loss event caused us to exit slowstart. // Used for stats collection of slowstartPacketsLost lastCutbackExitedSlowstart bool // Congestion window in bytes. congestionWindow protocol.ByteCount // Slow start congestion window in bytes, aka ssthresh. slowStartThreshold protocol.ByteCount // ACK counter for the Reno implementation. numAckedPackets uint64 initialCongestionWindow protocol.ByteCount initialMaxCongestionWindow protocol.ByteCount maxDatagramSize protocol.ByteCount lastState logging.CongestionState tracer logging.ConnectionTracer } var ( _ SendAlgorithm = &cubicSender{} _ SendAlgorithmWithDebugInfos = &cubicSender{} ) // NewCubicSender makes a new cubic sender func NewCubicSender( clock Clock, rttStats *utils.RTTStats, initialMaxDatagramSize protocol.ByteCount, reno bool, tracer logging.ConnectionTracer, ) *cubicSender { return newCubicSender( clock, rttStats, reno, initialMaxDatagramSize, initialCongestionWindow*initialMaxDatagramSize, protocol.MaxCongestionWindowPackets*initialMaxDatagramSize, tracer, ) } func newCubicSender( clock Clock, rttStats *utils.RTTStats, reno bool, initialMaxDatagramSize, initialCongestionWindow, initialMaxCongestionWindow protocol.ByteCount, tracer logging.ConnectionTracer, ) *cubicSender { c := &cubicSender{ rttStats: rttStats, largestSentPacketNumber: protocol.InvalidPacketNumber, largestAckedPacketNumber: protocol.InvalidPacketNumber, largestSentAtLastCutback: protocol.InvalidPacketNumber, initialCongestionWindow: initialCongestionWindow, initialMaxCongestionWindow: initialMaxCongestionWindow, congestionWindow: initialCongestionWindow, slowStartThreshold: protocol.MaxByteCount, cubic: NewCubic(clock), clock: clock, reno: reno, tracer: tracer, maxDatagramSize: initialMaxDatagramSize, } c.pacer = newPacer(c.BandwidthEstimate) if c.tracer != nil { c.lastState = logging.CongestionStateSlowStart c.tracer.UpdatedCongestionState(logging.CongestionStateSlowStart) } return c } // TimeUntilSend returns when the next packet should be sent. func (c *cubicSender) TimeUntilSend(_ protocol.ByteCount) time.Time { return c.pacer.TimeUntilSend() } func (c *cubicSender) HasPacingBudget() bool { return c.pacer.Budget(c.clock.Now()) >= c.maxDatagramSize } func (c *cubicSender) maxCongestionWindow() protocol.ByteCount { return c.maxDatagramSize * protocol.MaxCongestionWindowPackets } func (c *cubicSender) minCongestionWindow() protocol.ByteCount { return c.maxDatagramSize * minCongestionWindowPackets } func (c *cubicSender) OnPacketSent( sentTime time.Time, _ protocol.ByteCount, packetNumber protocol.PacketNumber, bytes protocol.ByteCount, isRetransmittable bool, ) { c.pacer.SentPacket(sentTime, bytes) if !isRetransmittable { return } c.largestSentPacketNumber = packetNumber c.hybridSlowStart.OnPacketSent(packetNumber) } func (c *cubicSender) CanSend(bytesInFlight protocol.ByteCount) bool { return bytesInFlight < c.GetCongestionWindow() } func (c *cubicSender) InRecovery() bool { return c.largestAckedPacketNumber != protocol.InvalidPacketNumber && c.largestAckedPacketNumber <= c.largestSentAtLastCutback } func (c *cubicSender) InSlowStart() bool { return c.GetCongestionWindow() < c.slowStartThreshold } func (c *cubicSender) GetCongestionWindow() protocol.ByteCount { return c.congestionWindow } func (c *cubicSender) MaybeExitSlowStart() { if c.InSlowStart() && c.hybridSlowStart.ShouldExitSlowStart(c.rttStats.LatestRTT(), c.rttStats.MinRTT(), c.GetCongestionWindow()/c.maxDatagramSize) { // exit slow start c.slowStartThreshold = c.congestionWindow c.maybeTraceStateChange(logging.CongestionStateCongestionAvoidance) } } func (c *cubicSender) OnPacketAcked( ackedPacketNumber protocol.PacketNumber, ackedBytes protocol.ByteCount, priorInFlight protocol.ByteCount, eventTime time.Time, ) { c.largestAckedPacketNumber = utils.MaxPacketNumber(ackedPacketNumber, c.largestAckedPacketNumber) if c.InRecovery() { return } c.maybeIncreaseCwnd(ackedPacketNumber, ackedBytes, priorInFlight, eventTime) if c.InSlowStart() { c.hybridSlowStart.OnPacketAcked(ackedPacketNumber) } } func (c *cubicSender) OnPacketLost(packetNumber protocol.PacketNumber, lostBytes, priorInFlight protocol.ByteCount) { // TCP NewReno (RFC6582) says that once a loss occurs, any losses in packets // already sent should be treated as a single loss event, since it's expected. if packetNumber <= c.largestSentAtLastCutback { return } c.lastCutbackExitedSlowstart = c.InSlowStart() c.maybeTraceStateChange(logging.CongestionStateRecovery) if c.reno { c.congestionWindow = protocol.ByteCount(float64(c.congestionWindow) * renoBeta) } else { c.congestionWindow = c.cubic.CongestionWindowAfterPacketLoss(c.congestionWindow) } if minCwnd := c.minCongestionWindow(); c.congestionWindow < minCwnd { c.congestionWindow = minCwnd } c.slowStartThreshold = c.congestionWindow c.largestSentAtLastCutback = c.largestSentPacketNumber // reset packet count from congestion avoidance mode. We start // counting again when we're out of recovery. c.numAckedPackets = 0 } // Called when we receive an ack. Normal TCP tracks how many packets one ack // represents, but quic has a separate ack for each packet. func (c *cubicSender) maybeIncreaseCwnd( _ protocol.PacketNumber, ackedBytes protocol.ByteCount, priorInFlight protocol.ByteCount, eventTime time.Time, ) { // Do not increase the congestion window unless the sender is close to using // the current window. if !c.isCwndLimited(priorInFlight) { c.cubic.OnApplicationLimited() c.maybeTraceStateChange(logging.CongestionStateApplicationLimited) return } if c.congestionWindow >= c.maxCongestionWindow() { return } if c.InSlowStart() { // TCP slow start, exponential growth, increase by one for each ACK. c.congestionWindow += c.maxDatagramSize c.maybeTraceStateChange(logging.CongestionStateSlowStart) return } // Congestion avoidance c.maybeTraceStateChange(logging.CongestionStateCongestionAvoidance) if c.reno { // Classic Reno congestion avoidance. c.numAckedPackets++ if c.numAckedPackets >= uint64(c.congestionWindow/c.maxDatagramSize) { c.congestionWindow += c.maxDatagramSize c.numAckedPackets = 0 } } else { c.congestionWindow = utils.MinByteCount(c.maxCongestionWindow(), c.cubic.CongestionWindowAfterAck(ackedBytes, c.congestionWindow, c.rttStats.MinRTT(), eventTime)) } } func (c *cubicSender) isCwndLimited(bytesInFlight protocol.ByteCount) bool { congestionWindow := c.GetCongestionWindow() if bytesInFlight >= congestionWindow { return true } availableBytes := congestionWindow - bytesInFlight slowStartLimited := c.InSlowStart() && bytesInFlight > congestionWindow/2 return slowStartLimited || availableBytes <= maxBurstPackets*c.maxDatagramSize } // BandwidthEstimate returns the current bandwidth estimate func (c *cubicSender) BandwidthEstimate() Bandwidth { srtt := c.rttStats.SmoothedRTT() if srtt == 0 { // If we haven't measured an rtt, the bandwidth estimate is unknown. return infBandwidth } return BandwidthFromDelta(c.GetCongestionWindow(), srtt) } // OnRetransmissionTimeout is called on an retransmission timeout func (c *cubicSender) OnRetransmissionTimeout(packetsRetransmitted bool) { c.largestSentAtLastCutback = protocol.InvalidPacketNumber if !packetsRetransmitted { return } c.hybridSlowStart.Restart() c.cubic.Reset() c.slowStartThreshold = c.congestionWindow / 2 c.congestionWindow = c.minCongestionWindow() } // OnConnectionMigration is called when the connection is migrated (?) func (c *cubicSender) OnConnectionMigration() { c.hybridSlowStart.Restart() c.largestSentPacketNumber = protocol.InvalidPacketNumber c.largestAckedPacketNumber = protocol.InvalidPacketNumber c.largestSentAtLastCutback = protocol.InvalidPacketNumber c.lastCutbackExitedSlowstart = false c.cubic.Reset() c.numAckedPackets = 0 c.congestionWindow = c.initialCongestionWindow c.slowStartThreshold = c.initialMaxCongestionWindow } func (c *cubicSender) maybeTraceStateChange(new logging.CongestionState) { if c.tracer == nil || new == c.lastState { return } c.tracer.UpdatedCongestionState(new) c.lastState = new } func (c *cubicSender) SetMaxDatagramSize(s protocol.ByteCount) { if s < c.maxDatagramSize { panic(fmt.Sprintf("congestion BUG: decreased max datagram size from %d to %d", c.maxDatagramSize, s)) } cwndIsMinCwnd := c.congestionWindow == c.minCongestionWindow() c.maxDatagramSize = s if cwndIsMinCwnd { c.congestionWindow = c.minCongestionWindow() } c.pacer.SetMaxDatagramSize(s) }