Files
quic-go/internal/congestion/cubic_test.go
2025-06-01 09:52:47 +02:00

206 lines
7.9 KiB
Go

package congestion
import (
"math"
"testing"
"time"
"github.com/quic-go/quic-go/internal/protocol"
"github.com/stretchr/testify/require"
)
const (
numConnections uint32 = 2
nConnectionBeta float32 = (float32(numConnections) - 1 + beta) / float32(numConnections)
nConnectionBetaLastMax float32 = (float32(numConnections) - 1 + betaLastMax) / float32(numConnections)
nConnectionAlpha float32 = 3 * float32(numConnections) * float32(numConnections) * (1 - nConnectionBeta) / (1 + nConnectionBeta)
maxCubicTimeInterval = 30 * time.Millisecond
)
func renoCwnd(currentCwnd protocol.ByteCount) protocol.ByteCount {
return currentCwnd + protocol.ByteCount(float32(maxDatagramSize)*nConnectionAlpha*float32(maxDatagramSize)/float32(currentCwnd))
}
func cubicConvexCwnd(initialCwnd protocol.ByteCount, rtt, elapsedTime time.Duration) protocol.ByteCount {
offset := protocol.ByteCount((elapsedTime+rtt)/time.Microsecond) << 10 / 1000000
deltaCongestionWindow := 410 * offset * offset * offset * maxDatagramSize >> 40
return initialCwnd + deltaCongestionWindow
}
func TestCubicAboveOriginWithTighterBounds(t *testing.T) {
clock := mockClock{}
cubic := NewCubic(&clock)
cubic.SetNumConnections(int(numConnections))
// Convex growth.
const rttMin = 100 * time.Millisecond
const rttMinS = float32(rttMin/time.Millisecond) / 1000.0
currentCwnd := 10 * maxDatagramSize
initialCwnd := currentCwnd
clock.Advance(time.Millisecond)
initialTime := clock.Now()
expectedFirstCwnd := renoCwnd(currentCwnd)
currentCwnd = cubic.CongestionWindowAfterAck(maxDatagramSize, currentCwnd, rttMin, initialTime)
require.Equal(t, expectedFirstCwnd, currentCwnd)
// Normal TCP phase.
// The maximum number of expected reno RTTs can be calculated by
// finding the point where the cubic curve and the reno curve meet.
maxRenoRtts := int(math.Sqrt(float64(nConnectionAlpha/(0.4*rttMinS*rttMinS*rttMinS))) - 2)
for range maxRenoRtts {
numAcksThisEpoch := int(float32(currentCwnd/maxDatagramSize) / nConnectionAlpha)
initialCwndThisEpoch := currentCwnd
for range numAcksThisEpoch {
// Call once per ACK.
expectedNextCwnd := renoCwnd(currentCwnd)
currentCwnd = cubic.CongestionWindowAfterAck(maxDatagramSize, currentCwnd, rttMin, clock.Now())
require.Equal(t, expectedNextCwnd, currentCwnd)
}
cwndChangeThisEpoch := currentCwnd - initialCwndThisEpoch
require.InDelta(t, float64(maxDatagramSize), float64(cwndChangeThisEpoch), float64(maxDatagramSize)/2)
clock.Advance(100 * time.Millisecond)
}
for range 54 {
maxAcksThisEpoch := currentCwnd / maxDatagramSize
interval := time.Duration(100*1000/maxAcksThisEpoch) * time.Microsecond
for range int(maxAcksThisEpoch) {
clock.Advance(interval)
currentCwnd = cubic.CongestionWindowAfterAck(maxDatagramSize, currentCwnd, rttMin, clock.Now())
expectedCwnd := cubicConvexCwnd(initialCwnd, rttMin, clock.Now().Sub(initialTime))
require.Equal(t, expectedCwnd, currentCwnd)
}
}
expectedCwnd := cubicConvexCwnd(initialCwnd, rttMin, clock.Now().Sub(initialTime))
currentCwnd = cubic.CongestionWindowAfterAck(maxDatagramSize, currentCwnd, rttMin, clock.Now())
require.Equal(t, expectedCwnd, currentCwnd)
}
func TestCubicAboveOriginWithFineGrainedCubing(t *testing.T) {
clock := mockClock{}
cubic := NewCubic(&clock)
cubic.SetNumConnections(int(numConnections))
currentCwnd := 1000 * maxDatagramSize
initialCwnd := currentCwnd
rttMin := 100 * time.Millisecond
clock.Advance(time.Millisecond)
initialTime := clock.Now()
currentCwnd = cubic.CongestionWindowAfterAck(maxDatagramSize, currentCwnd, rttMin, clock.Now())
clock.Advance(600 * time.Millisecond)
currentCwnd = cubic.CongestionWindowAfterAck(maxDatagramSize, currentCwnd, rttMin, clock.Now())
for i := 0; i < 100; i++ {
clock.Advance(10 * time.Millisecond)
expectedCwnd := cubicConvexCwnd(initialCwnd, rttMin, clock.Now().Sub(initialTime))
nextCwnd := cubic.CongestionWindowAfterAck(maxDatagramSize, currentCwnd, rttMin, clock.Now())
require.Equal(t, expectedCwnd, nextCwnd)
require.Greater(t, nextCwnd, currentCwnd)
cwndDelta := nextCwnd - currentCwnd
require.Less(t, cwndDelta, maxDatagramSize/10)
currentCwnd = nextCwnd
}
}
func TestCubicHandlesPerAckUpdates(t *testing.T) {
clock := mockClock{}
cubic := NewCubic(&clock)
cubic.SetNumConnections(int(numConnections))
initialCwndPackets := 150
currentCwnd := protocol.ByteCount(initialCwndPackets) * maxDatagramSize
rttMin := 350 * time.Millisecond
clock.Advance(time.Millisecond)
rCwnd := renoCwnd(currentCwnd)
currentCwnd = cubic.CongestionWindowAfterAck(maxDatagramSize, currentCwnd, rttMin, clock.Now())
initialCwnd := currentCwnd
maxAcks := int(float32(initialCwndPackets) / nConnectionAlpha)
interval := maxCubicTimeInterval / time.Duration(maxAcks+1)
clock.Advance(interval)
rCwnd = renoCwnd(rCwnd)
require.Equal(t, currentCwnd, cubic.CongestionWindowAfterAck(maxDatagramSize, currentCwnd, rttMin, clock.Now()))
for range maxAcks - 1 {
clock.Advance(interval)
nextCwnd := cubic.CongestionWindowAfterAck(maxDatagramSize, currentCwnd, rttMin, clock.Now())
rCwnd = renoCwnd(rCwnd)
require.Greater(t, nextCwnd, currentCwnd)
require.Equal(t, rCwnd, nextCwnd)
currentCwnd = nextCwnd
}
minimumExpectedIncrease := maxDatagramSize * 9 / 10
require.Greater(t, currentCwnd, initialCwnd+minimumExpectedIncrease)
}
func TestCubicHandlesLossEvents(t *testing.T) {
clock := mockClock{}
cubic := NewCubic(&clock)
cubic.SetNumConnections(int(numConnections))
rttMin := 100 * time.Millisecond
currentCwnd := 422 * maxDatagramSize
expectedCwnd := renoCwnd(currentCwnd)
clock.Advance(time.Millisecond)
require.Equal(t, expectedCwnd, cubic.CongestionWindowAfterAck(maxDatagramSize, currentCwnd, rttMin, clock.Now()))
preLossCwnd := currentCwnd
require.Zero(t, cubic.lastMaxCongestionWindow)
expectedCwnd = protocol.ByteCount(float32(currentCwnd) * nConnectionBeta)
require.Equal(t, expectedCwnd, cubic.CongestionWindowAfterPacketLoss(currentCwnd))
require.Equal(t, preLossCwnd, cubic.lastMaxCongestionWindow)
currentCwnd = expectedCwnd
preLossCwnd = currentCwnd
expectedCwnd = protocol.ByteCount(float32(currentCwnd) * nConnectionBeta)
require.Equal(t, expectedCwnd, cubic.CongestionWindowAfterPacketLoss(currentCwnd))
currentCwnd = expectedCwnd
require.Greater(t, preLossCwnd, cubic.lastMaxCongestionWindow)
expectedLastMax := protocol.ByteCount(float32(preLossCwnd) * nConnectionBetaLastMax)
require.Equal(t, expectedLastMax, cubic.lastMaxCongestionWindow)
require.Less(t, expectedCwnd, cubic.lastMaxCongestionWindow)
currentCwnd = cubic.CongestionWindowAfterAck(maxDatagramSize, currentCwnd, rttMin, clock.Now())
require.Greater(t, cubic.lastMaxCongestionWindow, currentCwnd)
currentCwnd = cubic.lastMaxCongestionWindow - 1
preLossCwnd = currentCwnd
expectedCwnd = protocol.ByteCount(float32(currentCwnd) * nConnectionBeta)
require.Equal(t, expectedCwnd, cubic.CongestionWindowAfterPacketLoss(currentCwnd))
expectedLastMax = preLossCwnd
require.Equal(t, expectedLastMax, cubic.lastMaxCongestionWindow)
}
func TestCubicBelowOrigin(t *testing.T) {
clock := mockClock{}
cubic := NewCubic(&clock)
cubic.SetNumConnections(int(numConnections))
rttMin := 100 * time.Millisecond
currentCwnd := 422 * maxDatagramSize
expectedCwnd := renoCwnd(currentCwnd)
clock.Advance(time.Millisecond)
require.Equal(t, expectedCwnd, cubic.CongestionWindowAfterAck(maxDatagramSize, currentCwnd, rttMin, clock.Now()))
expectedCwnd = protocol.ByteCount(float32(currentCwnd) * nConnectionBeta)
require.Equal(t, expectedCwnd, cubic.CongestionWindowAfterPacketLoss(currentCwnd))
currentCwnd = expectedCwnd
currentCwnd = cubic.CongestionWindowAfterAck(maxDatagramSize, currentCwnd, rttMin, clock.Now())
for range 40 {
clock.Advance(100 * time.Millisecond)
currentCwnd = cubic.CongestionWindowAfterAck(maxDatagramSize, currentCwnd, rttMin, clock.Now())
}
expectedCwnd = 553632 * maxDatagramSize / 1460
require.Equal(t, expectedCwnd, currentCwnd)
}