github.com/metacubex/quic-go@v0.44.1-0.20240520163451-20b689a59136/internal/congestion/cubic_test.go (about)

     1  package congestion
     2  
     3  import (
     4  	"math"
     5  	"time"
     6  
     7  	"github.com/metacubex/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  })