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