| // Copyright 2023 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| //go:build go1.21 |
| |
| package quic |
| |
| import ( |
| "testing" |
| "time" |
| ) |
| |
| func TestRenoInitialCongestionWindow(t *testing.T) { |
| // https://www.rfc-editor.org/rfc/rfc9002#section-7.2-1 |
| for _, test := range []struct { |
| maxDatagramSize int |
| wantWindow int |
| }{{ |
| // "[...] ten times the maximum datagram size [...]" |
| maxDatagramSize: 1200, |
| wantWindow: 12000, |
| }, { |
| // [...] limiting the window to the larger of 14,720 bytes [...]" |
| maxDatagramSize: 1500, |
| wantWindow: 14720, |
| }, { |
| // [...] or twice the maximum datagram size." |
| maxDatagramSize: 15000, |
| wantWindow: 30000, |
| }} { |
| c := newReno(test.maxDatagramSize) |
| if got, want := c.congestionWindow, test.wantWindow; got != want { |
| t.Errorf("newReno(max_datagram_size=%v): congestion_window = %v, want %v", |
| test.maxDatagramSize, got, want) |
| } |
| } |
| } |
| |
| func TestRenoSlowStartWindowIncreases(t *testing.T) { |
| // "[...] the congestion window increases by the number of bytes acknowledged [...]" |
| // https://www.rfc-editor.org/rfc/rfc9002#section-7.3.1-2 |
| test := newRenoTest(t, 1200) |
| |
| p0 := test.packetSent(initialSpace, 1200) |
| test.wantVar("congestion_window", 12000) |
| test.packetAcked(initialSpace, p0) |
| test.packetBatchEnd(initialSpace) |
| test.wantVar("congestion_window", 12000+1200) |
| |
| p1 := test.packetSent(handshakeSpace, 600) |
| p2 := test.packetSent(handshakeSpace, 300) |
| test.packetAcked(handshakeSpace, p1) |
| test.packetAcked(handshakeSpace, p2) |
| test.packetBatchEnd(handshakeSpace) |
| test.wantVar("congestion_window", 12000+1200+600+300) |
| } |
| |
| func TestRenoSlowStartToRecovery(t *testing.T) { |
| // "The sender MUST exit slow start and enter a recovery period |
| // when a packet is lost [...]" |
| // https://www.rfc-editor.org/rfc/rfc9002#section-7.3.1-3 |
| test := newRenoTest(t, 1200) |
| |
| p0 := test.packetSent(initialSpace, 1200) |
| p1 := test.packetSent(initialSpace, 1200) |
| p2 := test.packetSent(initialSpace, 1200) |
| p3 := test.packetSent(initialSpace, 1200) |
| test.wantVar("congestion_window", 12000) |
| |
| t.Logf("# ACK triggers packet loss, sender enters recovery") |
| test.advance(1 * time.Millisecond) |
| test.packetAcked(initialSpace, p3) |
| test.packetLost(initialSpace, p0) |
| test.packetBatchEnd(initialSpace) |
| |
| // "[...] set the slow start threshold to half the value of |
| // the congestion window when loss is detected." |
| // https://www.rfc-editor.org/rfc/rfc9002#section-7.3.2-2 |
| test.wantVar("slow_start_threshold", 6000) |
| |
| t.Logf("# packet loss in recovery does not change congestion window") |
| test.packetLost(initialSpace, p1) |
| test.packetBatchEnd(initialSpace) |
| |
| t.Logf("# ack of packet from before recovery does not change congestion window") |
| test.packetAcked(initialSpace, p2) |
| test.packetBatchEnd(initialSpace) |
| |
| p4 := test.packetSent(initialSpace, 1200) |
| test.packetAcked(initialSpace, p4) |
| test.packetBatchEnd(initialSpace) |
| |
| // "The congestion window MUST be set to the reduced value of |
| // the slow start threshold before exiting the recovery period." |
| // https://www.rfc-editor.org/rfc/rfc9002#section-7.3.2-2 |
| test.wantVar("congestion_window", 6000) |
| } |
| |
| func TestRenoRecoveryToCongestionAvoidance(t *testing.T) { |
| // "A sender in congestion avoidance [limits] the increase |
| // to the congestion window to at most one maximum datagram size |
| // for each congestion window that is acknowledged." |
| // https://www.rfc-editor.org/rfc/rfc9002#section-7.3.3-2 |
| test := newRenoTest(t, 1200) |
| |
| p0 := test.packetSent(initialSpace, 1200) |
| p1 := test.packetSent(initialSpace, 1200) |
| p2 := test.packetSent(initialSpace, 1200) |
| test.advance(1 * time.Millisecond) |
| test.packetAcked(initialSpace, p1) |
| test.packetLost(initialSpace, p0) |
| test.packetBatchEnd(initialSpace) |
| |
| p3 := test.packetSent(initialSpace, 1000) |
| test.advance(1 * time.Millisecond) |
| test.packetAcked(initialSpace, p3) |
| test.packetBatchEnd(initialSpace) |
| |
| test.wantVar("congestion_window", 6000) |
| test.wantVar("slow_start_threshold", 6000) |
| test.wantVar("congestion_pending_acks", 1000) |
| |
| t.Logf("# ack of packet from before recovery does not change congestion window") |
| test.packetAcked(initialSpace, p2) |
| test.packetBatchEnd(initialSpace) |
| test.wantVar("congestion_pending_acks", 1000) |
| |
| for i := 0; i < 6; i++ { |
| p := test.packetSent(initialSpace, 1000) |
| test.packetAcked(initialSpace, p) |
| } |
| test.packetBatchEnd(initialSpace) |
| t.Logf("# congestion window increased by max_datagram_size") |
| test.wantVar("congestion_window", 6000+1200) |
| test.wantVar("congestion_pending_acks", 1000) |
| } |
| |
| func TestRenoMinimumCongestionWindow(t *testing.T) { |
| // "The RECOMMENDED [minimum congestion window] is 2 * max_datagram_size." |
| // https://www.rfc-editor.org/rfc/rfc9002.html#section-7.2-4 |
| test := newRenoTest(t, 1200) |
| |
| p0 := test.packetSent(handshakeSpace, 1200) |
| p1 := test.packetSent(handshakeSpace, 1200) |
| test.advance(1 * time.Millisecond) |
| test.packetAcked(handshakeSpace, p1) |
| test.packetLost(handshakeSpace, p0) |
| test.packetBatchEnd(handshakeSpace) |
| test.wantVar("slow_start_threshold", 6000) |
| test.wantVar("congestion_window", 6000) |
| |
| test.advance(1 * time.Millisecond) |
| p2 := test.packetSent(handshakeSpace, 1200) |
| p3 := test.packetSent(handshakeSpace, 1200) |
| test.advance(1 * time.Millisecond) |
| test.packetAcked(handshakeSpace, p3) |
| test.packetLost(handshakeSpace, p2) |
| test.packetBatchEnd(handshakeSpace) |
| test.wantVar("slow_start_threshold", 3000) |
| test.wantVar("congestion_window", 3000) |
| |
| p4 := test.packetSent(handshakeSpace, 1200) |
| p5 := test.packetSent(handshakeSpace, 1200) |
| test.advance(1 * time.Millisecond) |
| test.packetAcked(handshakeSpace, p4) |
| test.packetLost(handshakeSpace, p5) |
| test.packetBatchEnd(handshakeSpace) |
| test.wantVar("slow_start_threshold", 1500) |
| test.wantVar("congestion_window", 2400) // minimum |
| |
| p6 := test.packetSent(handshakeSpace, 1200) |
| p7 := test.packetSent(handshakeSpace, 1200) |
| test.advance(1 * time.Millisecond) |
| test.packetAcked(handshakeSpace, p7) |
| test.packetLost(handshakeSpace, p6) |
| test.packetBatchEnd(handshakeSpace) |
| test.wantVar("slow_start_threshold", 1200) // half congestion window |
| test.wantVar("congestion_window", 2400) // minimum |
| } |
| |
| func TestRenoSlowStartToCongestionAvoidance(t *testing.T) { |
| test := newRenoTest(t, 1200) |
| test.setRTT(1*time.Millisecond, 0) |
| |
| t.Logf("# enter recovery with persistent congestion") |
| p0 := test.packetSent(handshakeSpace, 1200) |
| test.advance(1 * time.Second) // larger than persistent congestion duration |
| p1 := test.packetSent(handshakeSpace, 1200) |
| p2 := test.packetSent(handshakeSpace, 1200) |
| test.advance(1 * time.Millisecond) |
| test.packetAcked(handshakeSpace, p2) |
| test.packetLost(handshakeSpace, p0) |
| test.packetLost(handshakeSpace, p1) |
| test.packetBatchEnd(handshakeSpace) |
| test.wantVar("slow_start_threshold", 6000) |
| test.wantVar("congestion_window", 2400) // minimum in persistent congestion |
| test.wantVar("congestion_pending_acks", 0) |
| |
| t.Logf("# enter slow start on new ack") |
| p3 := test.packetSent(handshakeSpace, 1200) |
| test.packetAcked(handshakeSpace, p3) |
| test.packetBatchEnd(handshakeSpace) |
| test.wantVar("congestion_window", 3600) |
| test.wantVar("congestion_pending_acks", 0) |
| |
| t.Logf("# enter congestion avoidance after reaching slow_start_threshold") |
| p4 := test.packetSent(handshakeSpace, 1200) |
| p5 := test.packetSent(handshakeSpace, 1200) |
| p6 := test.packetSent(handshakeSpace, 1200) |
| test.packetAcked(handshakeSpace, p4) |
| test.packetAcked(handshakeSpace, p5) |
| test.packetAcked(handshakeSpace, p6) |
| test.packetBatchEnd(handshakeSpace) |
| test.wantVar("congestion_window", 6000) |
| test.wantVar("congestion_pending_acks", 1200) |
| } |
| |
| func TestRenoPersistentCongestionDurationExceeded(t *testing.T) { |
| // "When persistent congestion is declared, the sender's congestion |
| // window MUST be reduced to the minimum congestion window [...]" |
| // https://www.rfc-editor.org/rfc/rfc9002.html#section-7.6.2-6 |
| test := newRenoTest(t, 1200) |
| test.setRTT(10*time.Millisecond, 3*time.Millisecond) |
| test.maxAckDelay = 25 * time.Millisecond |
| |
| t.Logf("persistent congesion duration is 3 * (10ms + 4*3ms + 25ms) = 141ms") |
| p0 := test.packetSent(handshakeSpace, 1200) |
| test.advance(142 * time.Millisecond) // larger than persistent congestion duration |
| p1 := test.packetSent(handshakeSpace, 1200) |
| p2 := test.packetSent(handshakeSpace, 1200) |
| test.packetAcked(handshakeSpace, p2) |
| test.packetLost(handshakeSpace, p0) |
| test.packetLost(handshakeSpace, p1) |
| test.packetBatchEnd(handshakeSpace) |
| test.wantVar("slow_start_threshold", 6000) |
| test.wantVar("congestion_window", 2400) // minimum in persistent congestion |
| } |
| |
| func TestRenoPersistentCongestionDurationNotExceeded(t *testing.T) { |
| test := newRenoTest(t, 1200) |
| test.setRTT(10*time.Millisecond, 3*time.Millisecond) |
| test.maxAckDelay = 25 * time.Millisecond |
| |
| t.Logf("persistent congesion duration is 3 * (10ms + 4*3ms + 25ms) = 141ms") |
| p0 := test.packetSent(handshakeSpace, 1200) |
| test.advance(140 * time.Millisecond) // smaller than persistent congestion duration |
| p1 := test.packetSent(handshakeSpace, 1200) |
| p2 := test.packetSent(handshakeSpace, 1200) |
| test.packetAcked(handshakeSpace, p2) |
| test.packetLost(handshakeSpace, p0) |
| test.packetLost(handshakeSpace, p1) |
| test.packetBatchEnd(handshakeSpace) |
| test.wantVar("slow_start_threshold", 6000) |
| test.wantVar("congestion_window", 6000) // no persistent congestion |
| } |
| |
| func TestRenoPersistentCongestionInterveningAck(t *testing.T) { |
| // "[...] none of the packets sent between the send times |
| // of these two packets are acknowledged [...]" |
| // https://www.rfc-editor.org/rfc/rfc9002#section-7.6.2-2.1 |
| test := newRenoTest(t, 1200) |
| |
| test.setRTT(10*time.Millisecond, 3*time.Millisecond) |
| test.maxAckDelay = 25 * time.Millisecond |
| |
| t.Logf("persistent congesion duration is 3 * (10ms + 4*3ms + 25ms) = 141ms") |
| p0 := test.packetSent(handshakeSpace, 1200) |
| test.advance(100 * time.Millisecond) |
| p1 := test.packetSent(handshakeSpace, 1200) |
| test.advance(42 * time.Millisecond) |
| p2 := test.packetSent(handshakeSpace, 1200) |
| p3 := test.packetSent(handshakeSpace, 1200) |
| test.packetAcked(handshakeSpace, p1) |
| test.packetAcked(handshakeSpace, p3) |
| test.packetLost(handshakeSpace, p0) |
| test.packetLost(handshakeSpace, p2) |
| test.packetBatchEnd(handshakeSpace) |
| test.wantVar("slow_start_threshold", 6000) |
| test.wantVar("congestion_window", 6000) // no persistent congestion |
| } |
| |
| func TestRenoPersistentCongestionInterveningLosses(t *testing.T) { |
| test := newRenoTest(t, 1200) |
| |
| test.setRTT(10*time.Millisecond, 3*time.Millisecond) |
| test.maxAckDelay = 25 * time.Millisecond |
| |
| t.Logf("persistent congesion duration is 3 * (10ms + 4*3ms + 25ms) = 141ms") |
| p0 := test.packetSent(handshakeSpace, 1200) |
| test.advance(50 * time.Millisecond) |
| p1 := test.packetSent(handshakeSpace, 1200, func(p *sentPacket) { |
| p.inFlight = false |
| p.ackEliciting = false |
| }) |
| test.advance(50 * time.Millisecond) |
| p2 := test.packetSent(handshakeSpace, 1200, func(p *sentPacket) { |
| p.ackEliciting = false |
| }) |
| test.advance(42 * time.Millisecond) |
| p3 := test.packetSent(handshakeSpace, 1200) |
| p4 := test.packetSent(handshakeSpace, 1200) |
| test.packetAcked(handshakeSpace, p4) |
| test.packetLost(handshakeSpace, p0) |
| test.packetLost(handshakeSpace, p1) |
| test.packetLost(handshakeSpace, p2) |
| test.packetBatchEnd(handshakeSpace) |
| test.wantVar("congestion_window", 6000) // no persistent congestion yet |
| test.packetLost(handshakeSpace, p3) |
| test.packetBatchEnd(handshakeSpace) |
| test.wantVar("congestion_window", 2400) // persistent congestion |
| } |
| |
| func TestRenoPersistentCongestionNoRTTSample(t *testing.T) { |
| // "[...] a prior RTT sample existed when these two packets were sent." |
| // https://www.rfc-editor.org/rfc/rfc9002#section-7.6.2-2.3 |
| test := newRenoTest(t, 1200) |
| |
| t.Logf("first packet sent prior to first RTT sample") |
| p0 := test.packetSent(handshakeSpace, 1200) |
| |
| test.advance(1 * time.Millisecond) |
| test.setRTT(10*time.Millisecond, 3*time.Millisecond) |
| test.maxAckDelay = 25 * time.Millisecond |
| |
| t.Logf("persistent congesion duration is 3 * (10ms + 4*3ms + 25ms) = 141ms") |
| test.advance(142 * time.Millisecond) // larger than persistent congestion duration |
| p1 := test.packetSent(handshakeSpace, 1200) |
| p2 := test.packetSent(handshakeSpace, 1200) |
| test.packetAcked(handshakeSpace, p2) |
| test.packetLost(handshakeSpace, p0) |
| test.packetLost(handshakeSpace, p1) |
| test.packetBatchEnd(handshakeSpace) |
| test.wantVar("slow_start_threshold", 6000) |
| test.wantVar("congestion_window", 6000) // no persistent congestion |
| } |
| |
| func TestRenoPersistentCongestionPacketNotAckEliciting(t *testing.T) { |
| // "These two packets MUST be ack-eliciting [...]" |
| // https://www.rfc-editor.org/rfc/rfc9002.html#section-7.6.2-3 |
| test := newRenoTest(t, 1200) |
| |
| t.Logf("first packet set prior to first RTT sample") |
| p0 := test.packetSent(handshakeSpace, 1200) |
| |
| test.advance(1 * time.Millisecond) |
| test.setRTT(10*time.Millisecond, 3*time.Millisecond) |
| test.maxAckDelay = 25 * time.Millisecond |
| |
| t.Logf("persistent congesion duration is 3 * (10ms + 4*3ms + 25ms) = 141ms") |
| test.advance(142 * time.Millisecond) // larger than persistent congestion duration |
| p1 := test.packetSent(handshakeSpace, 1200) |
| p2 := test.packetSent(handshakeSpace, 1200) |
| test.packetAcked(handshakeSpace, p2) |
| test.packetLost(handshakeSpace, p0) |
| test.packetLost(handshakeSpace, p1) |
| test.packetBatchEnd(handshakeSpace) |
| test.wantVar("slow_start_threshold", 6000) |
| test.wantVar("congestion_window", 6000) // no persistent congestion |
| } |
| |
| func TestRenoCanSend(t *testing.T) { |
| test := newRenoTest(t, 1200) |
| test.wantVar("congestion_window", 12000) |
| |
| t.Logf("controller permits sending until congestion window is full") |
| var packets []*sentPacket |
| for i := 0; i < 10; i++ { |
| test.wantVar("bytes_in_flight", i*1200) |
| test.wantCanSend(true) |
| p := test.packetSent(initialSpace, 1200) |
| packets = append(packets, p) |
| } |
| test.wantVar("bytes_in_flight", 12000) |
| |
| t.Logf("controller blocks sending when congestion window is consumed") |
| test.wantCanSend(false) |
| |
| t.Logf("loss of packet moves to recovery, reduces window") |
| test.packetLost(initialSpace, packets[0]) |
| test.packetAcked(initialSpace, packets[1]) |
| test.packetBatchEnd(initialSpace) |
| test.wantVar("bytes_in_flight", 9600) // 12000 - 2*1200 |
| test.wantVar("congestion_window", 6000) // 12000 / 2 |
| |
| t.Logf("one packet permitted on entry to recovery") |
| test.wantCanSend(true) |
| test.packetSent(initialSpace, 1200) |
| test.wantVar("bytes_in_flight", 10800) |
| test.wantCanSend(false) |
| } |
| |
| func TestRenoNonAckEliciting(t *testing.T) { |
| test := newRenoTest(t, 1200) |
| test.wantVar("congestion_window", 12000) |
| |
| t.Logf("in-flight packet") |
| p0 := test.packetSent(initialSpace, 1200) |
| test.wantVar("bytes_in_flight", 1200) |
| test.packetAcked(initialSpace, p0) |
| test.packetBatchEnd(initialSpace) |
| test.wantVar("bytes_in_flight", 0) |
| test.wantVar("congestion_window", 12000+1200) |
| |
| t.Logf("non-in-flight packet") |
| p1 := test.packetSent(initialSpace, 1200, func(p *sentPacket) { |
| p.inFlight = false |
| p.ackEliciting = false |
| }) |
| test.wantVar("bytes_in_flight", 0) |
| test.packetAcked(initialSpace, p1) |
| test.packetBatchEnd(initialSpace) |
| test.wantVar("bytes_in_flight", 0) |
| test.wantVar("congestion_window", 12000+1200) |
| } |
| |
| func TestRenoUnderutilizedCongestionWindow(t *testing.T) { |
| test := newRenoTest(t, 1200) |
| test.setUnderutilized(true) |
| test.wantVar("congestion_window", 12000) |
| |
| t.Logf("congestion window does not increase when application limited") |
| p0 := test.packetSent(initialSpace, 1200) |
| test.packetAcked(initialSpace, p0) |
| test.wantVar("congestion_window", 12000) |
| } |
| |
| func TestRenoDiscardKeys(t *testing.T) { |
| test := newRenoTest(t, 1200) |
| |
| p0 := test.packetSent(initialSpace, 1200) |
| p1 := test.packetSent(handshakeSpace, 1200) |
| test.wantVar("bytes_in_flight", 2400) |
| |
| test.packetDiscarded(initialSpace, p0) |
| test.wantVar("bytes_in_flight", 1200) |
| |
| test.packetDiscarded(handshakeSpace, p1) |
| test.wantVar("bytes_in_flight", 0) |
| } |
| |
| type ccTest struct { |
| t *testing.T |
| cc *ccReno |
| rtt rttState |
| maxAckDelay time.Duration |
| now time.Time |
| nextNum [numberSpaceCount]packetNumber |
| } |
| |
| func newRenoTest(t *testing.T, maxDatagramSize int) *ccTest { |
| test := &ccTest{ |
| t: t, |
| now: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), |
| } |
| test.cc = newReno(maxDatagramSize) |
| return test |
| } |
| |
| func (c *ccTest) setRTT(smoothedRTT, rttvar time.Duration) { |
| c.t.Helper() |
| c.t.Logf("set smoothed_rtt=%v rttvar=%v", smoothedRTT, rttvar) |
| c.rtt.smoothedRTT = smoothedRTT |
| c.rtt.rttvar = rttvar |
| if c.rtt.firstSampleTime.IsZero() { |
| c.rtt.firstSampleTime = c.now |
| } |
| } |
| |
| func (c *ccTest) setUnderutilized(v bool) { |
| c.t.Helper() |
| c.t.Logf("set underutilized = %v", v) |
| c.cc.setUnderutilized(v) |
| } |
| |
| func (c *ccTest) packetSent(space numberSpace, size int, fns ...func(*sentPacket)) *sentPacket { |
| c.t.Helper() |
| num := c.nextNum[space] |
| c.nextNum[space]++ |
| sent := &sentPacket{ |
| inFlight: true, |
| ackEliciting: true, |
| num: num, |
| size: size, |
| time: c.now, |
| } |
| for _, f := range fns { |
| f(sent) |
| } |
| c.t.Logf("packet sent: num=%v.%v, size=%v", space, sent.num, sent.size) |
| c.cc.packetSent(c.now, space, sent) |
| return sent |
| } |
| |
| func (c *ccTest) advance(d time.Duration) { |
| c.t.Helper() |
| c.t.Logf("advance time %v", d) |
| c.now = c.now.Add(d) |
| } |
| |
| func (c *ccTest) packetAcked(space numberSpace, sent *sentPacket) { |
| c.t.Helper() |
| c.t.Logf("packet acked: num=%v.%v, size=%v", space, sent.num, sent.size) |
| c.cc.packetAcked(c.now, sent) |
| } |
| |
| func (c *ccTest) packetLost(space numberSpace, sent *sentPacket) { |
| c.t.Helper() |
| c.t.Logf("packet lost: num=%v.%v, size=%v", space, sent.num, sent.size) |
| c.cc.packetLost(c.now, space, sent, &c.rtt) |
| } |
| |
| func (c *ccTest) packetDiscarded(space numberSpace, sent *sentPacket) { |
| c.t.Helper() |
| c.t.Logf("packet number space discarded: num=%v.%v, size=%v", space, sent.num, sent.size) |
| c.cc.packetDiscarded(sent) |
| } |
| |
| func (c *ccTest) packetBatchEnd(space numberSpace) { |
| c.t.Helper() |
| c.t.Logf("(end of batch)") |
| c.cc.packetBatchEnd(c.now, space, &c.rtt, c.maxAckDelay) |
| } |
| |
| func (c *ccTest) wantCanSend(want bool) { |
| if got := c.cc.canSend(); got != want { |
| c.t.Fatalf("canSend() = %v, want %v", got, want) |
| } |
| } |
| |
| func (c *ccTest) wantVar(name string, want int) { |
| c.t.Helper() |
| var got int |
| switch name { |
| case "bytes_in_flight": |
| got = c.cc.bytesInFlight |
| case "congestion_pending_acks": |
| got = c.cc.congestionPendingAcks |
| case "congestion_window": |
| got = c.cc.congestionWindow |
| case "slow_start_threshold": |
| got = c.cc.slowStartThreshold |
| default: |
| c.t.Fatalf("unknown var %q", name) |
| } |
| if got != want { |
| c.t.Fatalf("ERROR: %v = %v, want %v", name, got, want) |
| } |
| c.t.Logf("# %v = %v", name, got) |
| } |