github.com/apernet/quic-go@v0.43.1-0.20240515053213-5e9e635fd9f0/internal/congestion/cubic_test.go (about) 1 package congestion 2 3 import ( 4 "math" 5 "time" 6 7 "github.com/apernet/quic-go/internal/protocol" 8 9 . "github.com/onsi/ginkgo/v2" 10 . "github.com/onsi/gomega" 11 ) 12 13 const ( 14 numConnections uint32 = 2 15 nConnectionBeta float32 = (float32(numConnections) - 1 + beta) / float32(numConnections) 16 nConnectionBetaLastMax float32 = (float32(numConnections) - 1 + betaLastMax) / float32(numConnections) 17 nConnectionAlpha float32 = 3 * float32(numConnections) * float32(numConnections) * (1 - nConnectionBeta) / (1 + nConnectionBeta) 18 maxCubicTimeInterval = 30 * time.Millisecond 19 ) 20 21 var _ = Describe("Cubic", func() { 22 var ( 23 clock mockClock 24 cubic *Cubic 25 ) 26 27 BeforeEach(func() { 28 clock = mockClock{} 29 cubic = NewCubic(&clock) 30 cubic.SetNumConnections(int(numConnections)) 31 }) 32 33 renoCwnd := func(currentCwnd protocol.ByteCount) protocol.ByteCount { 34 return currentCwnd + protocol.ByteCount(float32(maxDatagramSize)*nConnectionAlpha*float32(maxDatagramSize)/float32(currentCwnd)) 35 } 36 37 cubicConvexCwnd := func(initialCwnd protocol.ByteCount, rtt, elapsedTime time.Duration) protocol.ByteCount { 38 offset := protocol.ByteCount((elapsedTime+rtt)/time.Microsecond) << 10 / 1000000 39 deltaCongestionWindow := 410 * offset * offset * offset * maxDatagramSize >> 40 40 return initialCwnd + deltaCongestionWindow 41 } 42 43 It("works above origin (with tighter bounds)", func() { 44 // Convex growth. 45 const rttMin = 100 * time.Millisecond 46 const rttMinS = float32(rttMin/time.Millisecond) / 1000.0 47 currentCwnd := 10 * maxDatagramSize 48 initialCwnd := currentCwnd 49 50 clock.Advance(time.Millisecond) 51 initialTime := clock.Now() 52 expectedFirstCwnd := renoCwnd(currentCwnd) 53 currentCwnd = cubic.CongestionWindowAfterAck(maxDatagramSize, currentCwnd, rttMin, initialTime) 54 Expect(expectedFirstCwnd).To(Equal(currentCwnd)) 55 56 // Normal TCP phase. 57 // The maximum number of expected reno RTTs can be calculated by 58 // finding the point where the cubic curve and the reno curve meet. 59 maxRenoRtts := int(math.Sqrt(float64(nConnectionAlpha/(0.4*rttMinS*rttMinS*rttMinS))) - 2) 60 for i := 0; i < maxRenoRtts; i++ { 61 // Alternatively, we expect it to increase by one, every time we 62 // receive current_cwnd/Alpha acks back. (This is another way of 63 // saying we expect cwnd to increase by approximately Alpha once 64 // we receive current_cwnd number ofacks back). 65 numAcksThisEpoch := int(float32(currentCwnd/maxDatagramSize) / nConnectionAlpha) 66 67 initialCwndThisEpoch := currentCwnd 68 for n := 0; n < numAcksThisEpoch; n++ { 69 // Call once per ACK. 70 expectedNextCwnd := renoCwnd(currentCwnd) 71 currentCwnd = cubic.CongestionWindowAfterAck(maxDatagramSize, currentCwnd, rttMin, clock.Now()) 72 Expect(currentCwnd).To(Equal(expectedNextCwnd)) 73 } 74 // Our byte-wise Reno implementation is an estimate. We expect 75 // the cwnd to increase by approximately one MSS every 76 // cwnd/kDefaultTCPMSS/Alpha acks, but it may be off by as much as 77 // half a packet for smaller values of current_cwnd. 78 cwndChangeThisEpoch := currentCwnd - initialCwndThisEpoch 79 Expect(cwndChangeThisEpoch).To(BeNumerically("~", maxDatagramSize, maxDatagramSize/2)) 80 clock.Advance(100 * time.Millisecond) 81 } 82 83 for i := 0; i < 54; i++ { 84 maxAcksThisEpoch := currentCwnd / maxDatagramSize 85 interval := time.Duration(100*1000/maxAcksThisEpoch) * time.Microsecond 86 for n := 0; n < int(maxAcksThisEpoch); n++ { 87 clock.Advance(interval) 88 currentCwnd = cubic.CongestionWindowAfterAck(maxDatagramSize, currentCwnd, rttMin, clock.Now()) 89 expectedCwnd := cubicConvexCwnd(initialCwnd, rttMin, clock.Now().Sub(initialTime)) 90 // If we allow per-ack updates, every update is a small cubic update. 91 Expect(currentCwnd).To(Equal(expectedCwnd)) 92 } 93 } 94 expectedCwnd := cubicConvexCwnd(initialCwnd, rttMin, clock.Now().Sub(initialTime)) 95 currentCwnd = cubic.CongestionWindowAfterAck(maxDatagramSize, currentCwnd, rttMin, clock.Now()) 96 Expect(currentCwnd).To(Equal(expectedCwnd)) 97 }) 98 99 It("works above the origin with fine grained cubing", func() { 100 // Start the test with an artificially large cwnd to prevent Reno 101 // from over-taking cubic. 102 currentCwnd := 1000 * maxDatagramSize 103 initialCwnd := currentCwnd 104 rttMin := 100 * time.Millisecond 105 clock.Advance(time.Millisecond) 106 initialTime := clock.Now() 107 108 currentCwnd = cubic.CongestionWindowAfterAck(maxDatagramSize, currentCwnd, rttMin, clock.Now()) 109 clock.Advance(600 * time.Millisecond) 110 currentCwnd = cubic.CongestionWindowAfterAck(maxDatagramSize, currentCwnd, rttMin, clock.Now()) 111 112 // We expect the algorithm to perform only non-zero, fine-grained cubic 113 // increases on every ack in this case. 114 for i := 0; i < 100; i++ { 115 clock.Advance(10 * time.Millisecond) 116 expectedCwnd := cubicConvexCwnd(initialCwnd, rttMin, clock.Now().Sub(initialTime)) 117 nextCwnd := cubic.CongestionWindowAfterAck(maxDatagramSize, currentCwnd, rttMin, clock.Now()) 118 // Make sure we are performing cubic increases. 119 Expect(nextCwnd).To(Equal(expectedCwnd)) 120 // Make sure that these are non-zero, less-than-packet sized increases. 121 Expect(nextCwnd).To(BeNumerically(">", currentCwnd)) 122 cwndDelta := nextCwnd - currentCwnd 123 Expect(maxDatagramSize / 10).To(BeNumerically(">", cwndDelta)) 124 currentCwnd = nextCwnd 125 } 126 }) 127 128 It("handles per ack updates", func() { 129 // Start the test with a large cwnd and RTT, to force the first 130 // increase to be a cubic increase. 131 initialCwndPackets := 150 132 currentCwnd := protocol.ByteCount(initialCwndPackets) * maxDatagramSize 133 rttMin := 350 * time.Millisecond 134 135 // Initialize the epoch 136 clock.Advance(time.Millisecond) 137 // Keep track of the growth of the reno-equivalent cwnd. 138 rCwnd := renoCwnd(currentCwnd) 139 currentCwnd = cubic.CongestionWindowAfterAck(maxDatagramSize, currentCwnd, rttMin, clock.Now()) 140 initialCwnd := currentCwnd 141 142 // Simulate the return of cwnd packets in less than 143 // MaxCubicInterval() time. 144 maxAcks := int(float32(initialCwndPackets) / nConnectionAlpha) 145 interval := maxCubicTimeInterval / time.Duration(maxAcks+1) 146 147 // In this scenario, the first increase is dictated by the cubic 148 // equation, but it is less than one byte, so the cwnd doesn't 149 // change. Normally, without per-ack increases, any cwnd plateau 150 // will cause the cwnd to be pinned for MaxCubicTimeInterval(). If 151 // we enable per-ack updates, the cwnd will continue to grow, 152 // regardless of the temporary plateau. 153 clock.Advance(interval) 154 rCwnd = renoCwnd(rCwnd) 155 Expect(cubic.CongestionWindowAfterAck(maxDatagramSize, currentCwnd, rttMin, clock.Now())).To(Equal(currentCwnd)) 156 for i := 1; i < maxAcks; i++ { 157 clock.Advance(interval) 158 nextCwnd := cubic.CongestionWindowAfterAck(maxDatagramSize, currentCwnd, rttMin, clock.Now()) 159 rCwnd = renoCwnd(rCwnd) 160 // The window shoud increase on every ack. 161 Expect(nextCwnd).To(BeNumerically(">", currentCwnd)) 162 Expect(nextCwnd).To(Equal(rCwnd)) 163 currentCwnd = nextCwnd 164 } 165 166 // After all the acks are returned from the epoch, we expect the 167 // cwnd to have increased by nearly one packet. (Not exactly one 168 // packet, because our byte-wise Reno algorithm is always a slight 169 // under-estimation). Without per-ack updates, the current_cwnd 170 // would otherwise be unchanged. 171 minimumExpectedIncrease := maxDatagramSize * 9 / 10 172 Expect(currentCwnd).To(BeNumerically(">", initialCwnd+minimumExpectedIncrease)) 173 }) 174 175 It("handles loss events", func() { 176 rttMin := 100 * time.Millisecond 177 currentCwnd := 422 * maxDatagramSize 178 expectedCwnd := renoCwnd(currentCwnd) 179 // Initialize the state. 180 clock.Advance(time.Millisecond) 181 Expect(cubic.CongestionWindowAfterAck(maxDatagramSize, currentCwnd, rttMin, clock.Now())).To(Equal(expectedCwnd)) 182 183 // On the first loss, the last max congestion window is set to the 184 // congestion window before the loss. 185 preLossCwnd := currentCwnd 186 Expect(cubic.lastMaxCongestionWindow).To(BeZero()) 187 expectedCwnd = protocol.ByteCount(float32(currentCwnd) * nConnectionBeta) 188 Expect(cubic.CongestionWindowAfterPacketLoss(currentCwnd)).To(Equal(expectedCwnd)) 189 Expect(cubic.lastMaxCongestionWindow).To(Equal(preLossCwnd)) 190 currentCwnd = expectedCwnd 191 192 // On the second loss, the current congestion window has not yet 193 // reached the last max congestion window. The last max congestion 194 // window will be reduced by an additional backoff factor to allow 195 // for competition. 196 preLossCwnd = currentCwnd 197 expectedCwnd = protocol.ByteCount(float32(currentCwnd) * nConnectionBeta) 198 Expect(cubic.CongestionWindowAfterPacketLoss(currentCwnd)).To(Equal(expectedCwnd)) 199 currentCwnd = expectedCwnd 200 Expect(preLossCwnd).To(BeNumerically(">", cubic.lastMaxCongestionWindow)) 201 expectedLastMax := protocol.ByteCount(float32(preLossCwnd) * nConnectionBetaLastMax) 202 Expect(cubic.lastMaxCongestionWindow).To(Equal(expectedLastMax)) 203 Expect(expectedCwnd).To(BeNumerically("<", cubic.lastMaxCongestionWindow)) 204 // Simulate an increase, and check that we are below the origin. 205 currentCwnd = cubic.CongestionWindowAfterAck(maxDatagramSize, currentCwnd, rttMin, clock.Now()) 206 Expect(cubic.lastMaxCongestionWindow).To(BeNumerically(">", currentCwnd)) 207 208 // On the final loss, simulate the condition where the congestion 209 // window had a chance to grow nearly to the last congestion window. 210 currentCwnd = cubic.lastMaxCongestionWindow - 1 211 preLossCwnd = currentCwnd 212 expectedCwnd = protocol.ByteCount(float32(currentCwnd) * nConnectionBeta) 213 Expect(cubic.CongestionWindowAfterPacketLoss(currentCwnd)).To(Equal(expectedCwnd)) 214 expectedLastMax = preLossCwnd 215 Expect(cubic.lastMaxCongestionWindow).To(Equal(expectedLastMax)) 216 }) 217 218 It("works below origin", func() { 219 // Concave growth. 220 rttMin := 100 * time.Millisecond 221 currentCwnd := 422 * maxDatagramSize 222 expectedCwnd := renoCwnd(currentCwnd) 223 // Initialize the state. 224 clock.Advance(time.Millisecond) 225 Expect(cubic.CongestionWindowAfterAck(maxDatagramSize, currentCwnd, rttMin, clock.Now())).To(Equal(expectedCwnd)) 226 227 expectedCwnd = protocol.ByteCount(float32(currentCwnd) * nConnectionBeta) 228 Expect(cubic.CongestionWindowAfterPacketLoss(currentCwnd)).To(Equal(expectedCwnd)) 229 currentCwnd = expectedCwnd 230 // First update after loss to initialize the epoch. 231 currentCwnd = cubic.CongestionWindowAfterAck(maxDatagramSize, currentCwnd, rttMin, clock.Now()) 232 // Cubic phase. 233 for i := 0; i < 40; i++ { 234 clock.Advance(100 * time.Millisecond) 235 currentCwnd = cubic.CongestionWindowAfterAck(maxDatagramSize, currentCwnd, rttMin, clock.Now()) 236 } 237 expectedCwnd = 553632 * maxDatagramSize / 1460 238 Expect(currentCwnd).To(Equal(expectedCwnd)) 239 }) 240 })