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