Files
quic-go/mtu_discoverer_test.go
2025-01-21 17:31:10 +08:00

202 lines
6.0 KiB
Go

package quic
import (
"fmt"
"time"
"golang.org/x/exp/rand"
"github.com/quic-go/quic-go/internal/protocol"
"github.com/quic-go/quic-go/internal/utils"
"github.com/quic-go/quic-go/logging"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("MTU Discoverer", func() {
const (
rtt = 100 * time.Millisecond
startMTU protocol.ByteCount = 1000
maxMTU protocol.ByteCount = 2000
)
var (
d *mtuFinder
rttStats *utils.RTTStats
now time.Time
discoveredMTU protocol.ByteCount
)
r := rand.New(rand.NewSource(uint64(GinkgoRandomSeed())))
BeforeEach(func() {
rttStats = &utils.RTTStats{}
rttStats.SetInitialRTT(rtt)
Expect(rttStats.SmoothedRTT()).To(Equal(rtt))
d = newMTUDiscoverer(
rttStats,
startMTU,
maxMTU,
func(s protocol.ByteCount) { discoveredMTU = s },
nil,
)
d.Start(time.Now())
now = time.Now()
})
It("only allows a probe 5 RTTs after the handshake completes", func() {
Expect(d.ShouldSendProbe(now)).To(BeFalse())
Expect(d.ShouldSendProbe(now.Add(rtt * 9 / 2))).To(BeFalse())
Expect(d.ShouldSendProbe(now.Add(rtt * 5))).To(BeTrue())
})
It("doesn't allow a probe if another probe is still in flight", func() {
ping, _ := d.GetPing(time.Now())
Expect(d.ShouldSendProbe(now.Add(10 * rtt))).To(BeFalse())
ping.Handler.OnLost(ping.Frame)
Expect(d.ShouldSendProbe(now.Add(10 * rtt))).To(BeTrue())
})
It("tries a lower size when a probe is lost", func() {
ping, size := d.GetPing(time.Now())
Expect(size).To(Equal(protocol.ByteCount(1500)))
ping.Handler.OnLost(ping.Frame)
_, size = d.GetPing(time.Now())
Expect(size).To(Equal(protocol.ByteCount(1250)))
})
It("tries a higher size and calls the callback when a probe is acknowledged", func() {
ping, size := d.GetPing(time.Now())
Expect(size).To(Equal(protocol.ByteCount(1500)))
ping.Handler.OnAcked(ping.Frame)
Expect(discoveredMTU).To(Equal(protocol.ByteCount(1500)))
_, size = d.GetPing(time.Now())
Expect(size).To(Equal(protocol.ByteCount(1750)))
})
It("stops discovery after getting close enough to the MTU", func() {
var sizes []protocol.ByteCount
t := now.Add(5 * rtt)
for d.ShouldSendProbe(t) {
ping, size := d.GetPing(time.Now())
fmt.Println("sending", size)
ping.Handler.OnAcked(ping.Frame)
sizes = append(sizes, size)
t = t.Add(5 * rtt)
}
Expect(sizes).To(Equal([]protocol.ByteCount{1500, 1750, 1875, 1937, 1968, 1984}))
Expect(d.ShouldSendProbe(t.Add(10 * rtt))).To(BeFalse())
})
It("doesn't do discovery before being started", func() {
d := newMTUDiscoverer(rttStats, startMTU, protocol.MaxByteCount, func(s protocol.ByteCount) {}, nil)
for i := 0; i < 5; i++ {
Expect(d.ShouldSendProbe(time.Now())).To(BeFalse())
}
})
It("finds the MTU", MustPassRepeatedly(300), func() {
maxMTU := protocol.ByteCount(r.Intn(int(3000-startMTU))) + startMTU + 1
currentMTU := startMTU
var tracedMTU protocol.ByteCount
var tracerDone bool
d := newMTUDiscoverer(
rttStats,
startMTU,
maxMTU,
func(s protocol.ByteCount) { currentMTU = s },
&logging.ConnectionTracer{
UpdatedMTU: func(mtu logging.ByteCount, done bool) {
tracedMTU = mtu
tracerDone = done
},
},
)
d.Start(time.Now())
now := time.Now()
realMTU := protocol.ByteCount(r.Intn(int(maxMTU-startMTU))) + startMTU
fmt.Fprintf(GinkgoWriter, "MTU: %d, max: %d\n", realMTU, maxMTU)
t := now.Add(mtuProbeDelay * rtt)
var probes []protocol.ByteCount
for d.ShouldSendProbe(t) {
if len(probes) > 24 {
Fail(fmt.Sprintf("too many iterations: %v", probes))
}
ping, size := d.GetPing(time.Now())
probes = append(probes, size)
if size <= realMTU {
ping.Handler.OnAcked(ping.Frame)
} else {
ping.Handler.OnLost(ping.Frame)
}
t = t.Add(mtuProbeDelay * rtt)
}
diff := realMTU - currentMTU
Expect(diff).To(BeNumerically(">=", 0))
if maxMTU > currentMTU+maxMTU {
Expect(tracedMTU).To(Equal(currentMTU))
Expect(tracerDone).To(BeTrue())
}
fmt.Fprintf(GinkgoWriter, "MTU discovered: %d (diff: %d)\n", currentMTU, diff)
fmt.Fprintf(GinkgoWriter, "probes sent (%d): %v\n", len(probes), probes)
Expect(diff).To(BeNumerically("<=", maxMTUDiff))
})
const maxRandomLoss = maxLostMTUProbes - 1
It(fmt.Sprintf("finds the MTU, with up to %d packets lost", maxRandomLoss), MustPassRepeatedly(500), func() {
maxMTU := protocol.ByteCount(r.Intn(int(3000-startMTU))) + startMTU + 1
currentMTU := startMTU
var tracedMTU protocol.ByteCount
var tracerDone bool
d := newMTUDiscoverer(
rttStats,
startMTU,
maxMTU,
func(s protocol.ByteCount) { currentMTU = s },
&logging.ConnectionTracer{
UpdatedMTU: func(mtu logging.ByteCount, done bool) {
tracedMTU = mtu
tracerDone = done
},
},
)
d.Start(time.Now())
now := time.Now()
realMTU := protocol.ByteCount(r.Intn(int(maxMTU-startMTU))) + startMTU
fmt.Fprintf(GinkgoWriter, "MTU: %d, max: %d\n", realMTU, maxMTU)
t := now.Add(mtuProbeDelay * rtt)
var probes, randomLosses []protocol.ByteCount
for d.ShouldSendProbe(t) {
if len(probes) > 32 {
Fail(fmt.Sprintf("too many iterations: %v", probes))
}
ping, size := d.GetPing(time.Now())
probes = append(probes, size)
packetFits := size <= realMTU
var acked bool
if packetFits {
randomLoss := r.Intn(maxLostMTUProbes) == 0 && len(randomLosses) < maxRandomLoss
if randomLoss {
randomLosses = append(randomLosses, size)
} else {
ping.Handler.OnAcked(ping.Frame)
acked = true
}
}
if !acked {
ping.Handler.OnLost(ping.Frame)
}
t = t.Add(mtuProbeDelay * rtt)
}
diff := realMTU - currentMTU
Expect(diff).To(BeNumerically(">=", 0))
if maxMTU > currentMTU+maxMTU {
Expect(tracedMTU).To(Equal(currentMTU))
Expect(tracerDone).To(BeTrue())
}
fmt.Fprintf(GinkgoWriter, "MTU discovered with random losses %v: %d (diff: %d)\n", randomLosses, currentMTU, diff)
fmt.Fprintf(GinkgoWriter, "probes sent (%d): %v\n", len(probes), probes)
Expect(diff).To(BeNumerically("<=", maxMTUDiff))
})
})