github.com/sagernet/quic-go@v0.43.1-beta.1/internal/congestion/cubic.go (about) 1 package congestion 2 3 import ( 4 "math" 5 "time" 6 7 "github.com/sagernet/quic-go/internal/protocol" 8 "github.com/sagernet/quic-go/internal/utils" 9 ) 10 11 // This cubic implementation is based on the one found in Chromiums's QUIC 12 // implementation, in the files net/quic/congestion_control/cubic.{hh,cc}. 13 14 // Constants based on TCP defaults. 15 // The following constants are in 2^10 fractions of a second instead of ms to 16 // allow a 10 shift right to divide. 17 18 // 1024*1024^3 (first 1024 is from 0.100^3) 19 // where 0.100 is 100 ms which is the scaling round trip time. 20 const ( 21 cubeScale = 40 22 cubeCongestionWindowScale = 410 23 cubeFactor protocol.ByteCount = 1 << cubeScale / cubeCongestionWindowScale / maxDatagramSize 24 // TODO: when re-enabling cubic, make sure to use the actual packet size here 25 maxDatagramSize = protocol.ByteCount(protocol.InitialPacketSizeIPv4) 26 ) 27 28 const defaultNumConnections = 1 29 30 // Default Cubic backoff factor 31 const beta float32 = 0.7 32 33 // Additional backoff factor when loss occurs in the concave part of the Cubic 34 // curve. This additional backoff factor is expected to give up bandwidth to 35 // new concurrent flows and speed up convergence. 36 const betaLastMax float32 = 0.85 37 38 // Cubic implements the cubic algorithm from TCP 39 type Cubic struct { 40 clock Clock 41 42 // Number of connections to simulate. 43 numConnections int 44 45 // Time when this cycle started, after last loss event. 46 epoch time.Time 47 48 // Max congestion window used just before last loss event. 49 // Note: to improve fairness to other streams an additional back off is 50 // applied to this value if the new value is below our latest value. 51 lastMaxCongestionWindow protocol.ByteCount 52 53 // Number of acked bytes since the cycle started (epoch). 54 ackedBytesCount protocol.ByteCount 55 56 // TCP Reno equivalent congestion window in packets. 57 estimatedTCPcongestionWindow protocol.ByteCount 58 59 // Origin point of cubic function. 60 originPointCongestionWindow protocol.ByteCount 61 62 // Time to origin point of cubic function in 2^10 fractions of a second. 63 timeToOriginPoint uint32 64 65 // Last congestion window in packets computed by cubic function. 66 lastTargetCongestionWindow protocol.ByteCount 67 } 68 69 // NewCubic returns a new Cubic instance 70 func NewCubic(clock Clock) *Cubic { 71 c := &Cubic{ 72 clock: clock, 73 numConnections: defaultNumConnections, 74 } 75 c.Reset() 76 return c 77 } 78 79 // Reset is called after a timeout to reset the cubic state 80 func (c *Cubic) Reset() { 81 c.epoch = time.Time{} 82 c.lastMaxCongestionWindow = 0 83 c.ackedBytesCount = 0 84 c.estimatedTCPcongestionWindow = 0 85 c.originPointCongestionWindow = 0 86 c.timeToOriginPoint = 0 87 c.lastTargetCongestionWindow = 0 88 } 89 90 func (c *Cubic) alpha() float32 { 91 // TCPFriendly alpha is described in Section 3.3 of the CUBIC paper. Note that 92 // beta here is a cwnd multiplier, and is equal to 1-beta from the paper. 93 // We derive the equivalent alpha for an N-connection emulation as: 94 b := c.beta() 95 return 3 * float32(c.numConnections) * float32(c.numConnections) * (1 - b) / (1 + b) 96 } 97 98 func (c *Cubic) beta() float32 { 99 // kNConnectionBeta is the backoff factor after loss for our N-connection 100 // emulation, which emulates the effective backoff of an ensemble of N 101 // TCP-Reno connections on a single loss event. The effective multiplier is 102 // computed as: 103 return (float32(c.numConnections) - 1 + beta) / float32(c.numConnections) 104 } 105 106 func (c *Cubic) betaLastMax() float32 { 107 // betaLastMax is the additional backoff factor after loss for our 108 // N-connection emulation, which emulates the additional backoff of 109 // an ensemble of N TCP-Reno connections on a single loss event. The 110 // effective multiplier is computed as: 111 return (float32(c.numConnections) - 1 + betaLastMax) / float32(c.numConnections) 112 } 113 114 // OnApplicationLimited is called on ack arrival when sender is unable to use 115 // the available congestion window. Resets Cubic state during quiescence. 116 func (c *Cubic) OnApplicationLimited() { 117 // When sender is not using the available congestion window, the window does 118 // not grow. But to be RTT-independent, Cubic assumes that the sender has been 119 // using the entire window during the time since the beginning of the current 120 // "epoch" (the end of the last loss recovery period). Since 121 // application-limited periods break this assumption, we reset the epoch when 122 // in such a period. This reset effectively freezes congestion window growth 123 // through application-limited periods and allows Cubic growth to continue 124 // when the entire window is being used. 125 c.epoch = time.Time{} 126 } 127 128 // CongestionWindowAfterPacketLoss computes a new congestion window to use after 129 // a loss event. Returns the new congestion window in packets. The new 130 // congestion window is a multiplicative decrease of our current window. 131 func (c *Cubic) CongestionWindowAfterPacketLoss(currentCongestionWindow protocol.ByteCount) protocol.ByteCount { 132 if currentCongestionWindow+maxDatagramSize < c.lastMaxCongestionWindow { 133 // We never reached the old max, so assume we are competing with another 134 // flow. Use our extra back off factor to allow the other flow to go up. 135 c.lastMaxCongestionWindow = protocol.ByteCount(c.betaLastMax() * float32(currentCongestionWindow)) 136 } else { 137 c.lastMaxCongestionWindow = currentCongestionWindow 138 } 139 c.epoch = time.Time{} // Reset time. 140 return protocol.ByteCount(float32(currentCongestionWindow) * c.beta()) 141 } 142 143 // CongestionWindowAfterAck computes a new congestion window to use after a received ACK. 144 // Returns the new congestion window in packets. The new congestion window 145 // follows a cubic function that depends on the time passed since last 146 // packet loss. 147 func (c *Cubic) CongestionWindowAfterAck( 148 ackedBytes protocol.ByteCount, 149 currentCongestionWindow protocol.ByteCount, 150 delayMin time.Duration, 151 eventTime time.Time, 152 ) protocol.ByteCount { 153 c.ackedBytesCount += ackedBytes 154 155 if c.epoch.IsZero() { 156 // First ACK after a loss event. 157 c.epoch = eventTime // Start of epoch. 158 c.ackedBytesCount = ackedBytes // Reset count. 159 // Reset estimated_tcp_congestion_window_ to be in sync with cubic. 160 c.estimatedTCPcongestionWindow = currentCongestionWindow 161 if c.lastMaxCongestionWindow <= currentCongestionWindow { 162 c.timeToOriginPoint = 0 163 c.originPointCongestionWindow = currentCongestionWindow 164 } else { 165 c.timeToOriginPoint = uint32(math.Cbrt(float64(cubeFactor * (c.lastMaxCongestionWindow - currentCongestionWindow)))) 166 c.originPointCongestionWindow = c.lastMaxCongestionWindow 167 } 168 } 169 170 // Change the time unit from microseconds to 2^10 fractions per second. Take 171 // the round trip time in account. This is done to allow us to use shift as a 172 // divide operator. 173 elapsedTime := int64(eventTime.Add(delayMin).Sub(c.epoch)/time.Microsecond) << 10 / (1000 * 1000) 174 175 // Right-shifts of negative, signed numbers have implementation-dependent 176 // behavior, so force the offset to be positive, as is done in the kernel. 177 offset := int64(c.timeToOriginPoint) - elapsedTime 178 if offset < 0 { 179 offset = -offset 180 } 181 182 deltaCongestionWindow := protocol.ByteCount(cubeCongestionWindowScale*offset*offset*offset) * maxDatagramSize >> cubeScale 183 var targetCongestionWindow protocol.ByteCount 184 if elapsedTime > int64(c.timeToOriginPoint) { 185 targetCongestionWindow = c.originPointCongestionWindow + deltaCongestionWindow 186 } else { 187 targetCongestionWindow = c.originPointCongestionWindow - deltaCongestionWindow 188 } 189 // Limit the CWND increase to half the acked bytes. 190 targetCongestionWindow = utils.Min(targetCongestionWindow, currentCongestionWindow+c.ackedBytesCount/2) 191 192 // Increase the window by approximately Alpha * 1 MSS of bytes every 193 // time we ack an estimated tcp window of bytes. For small 194 // congestion windows (less than 25), the formula below will 195 // increase slightly slower than linearly per estimated tcp window 196 // of bytes. 197 c.estimatedTCPcongestionWindow += protocol.ByteCount(float32(c.ackedBytesCount) * c.alpha() * float32(maxDatagramSize) / float32(c.estimatedTCPcongestionWindow)) 198 c.ackedBytesCount = 0 199 200 // We have a new cubic congestion window. 201 c.lastTargetCongestionWindow = targetCongestionWindow 202 203 // Compute target congestion_window based on cubic target and estimated TCP 204 // congestion_window, use highest (fastest). 205 if targetCongestionWindow < c.estimatedTCPcongestionWindow { 206 targetCongestionWindow = c.estimatedTCPcongestionWindow 207 } 208 return targetCongestionWindow 209 } 210 211 // SetNumConnections sets the number of emulated connections 212 func (c *Cubic) SetNumConnections(n int) { 213 c.numConnections = n 214 }