quic: add a data structure for tracking lists of sent packets
Store in-flight packets in a ring buffer.
For golang/go#58547
Change-Id: I825c4e600bb496c2f8f6c195085aaae3e847445e
Reviewed-on: https://go-review.googlesource.com/c/net/+/499285
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
Run-TryBot: Damien Neil <dneil@google.com>
diff --git a/internal/quic/sent_packet_list.go b/internal/quic/sent_packet_list.go
new file mode 100644
index 0000000..6fb712a
--- /dev/null
+++ b/internal/quic/sent_packet_list.go
@@ -0,0 +1,95 @@
+// 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
+
+// A sentPacketList is a ring buffer of sentPackets.
+//
+// Processing an ack for a packet causes all older packets past a small threshold
+// to be discarded (RFC 9002, Section 6.1.1), so the list of in-flight packets is
+// not sparse and will contain at most a few acked/lost packets we no longer
+// care about.
+type sentPacketList struct {
+ nextNum packetNumber // next packet number to add to the buffer
+ off int // offset of first packet in the buffer
+ size int // number of packets
+ p []*sentPacket
+}
+
+// start is the first packet in the list.
+func (s *sentPacketList) start() packetNumber {
+ return s.nextNum - packetNumber(s.size)
+}
+
+// end is one after the last packet in the list.
+// If the list is empty, start == end.
+func (s *sentPacketList) end() packetNumber {
+ return s.nextNum
+}
+
+// discard clears the list.
+func (s *sentPacketList) discard() {
+ *s = sentPacketList{}
+}
+
+// add appends a packet to the list.
+func (s *sentPacketList) add(sent *sentPacket) {
+ if s.nextNum != sent.num {
+ panic("inserting out-of-order packet")
+ }
+ s.nextNum++
+ if s.size >= len(s.p) {
+ s.grow()
+ }
+ i := (s.off + s.size) % len(s.p)
+ s.size++
+ s.p[i] = sent
+}
+
+// nth returns a packet by index.
+func (s *sentPacketList) nth(n int) *sentPacket {
+ index := (s.off + n) % len(s.p)
+ return s.p[index]
+}
+
+// num returns a packet by number.
+// It returns nil if the packet is not in the list.
+func (s *sentPacketList) num(num packetNumber) *sentPacket {
+ i := int(num - s.start())
+ if i < 0 || i >= s.size {
+ return nil
+ }
+ return s.nth(i)
+}
+
+// clean removes all acked or lost packets from the head of the list.
+func (s *sentPacketList) clean() {
+ for s.size > 0 {
+ sent := s.p[s.off]
+ if !sent.acked && !sent.lost {
+ return
+ }
+ sent.recycle()
+ s.p[s.off] = nil
+ s.off = (s.off + 1) % len(s.p)
+ s.size--
+ }
+ s.off = 0
+}
+
+// grow increases the buffer to hold more packaets.
+func (s *sentPacketList) grow() {
+ newSize := len(s.p) * 2
+ if newSize == 0 {
+ newSize = 64
+ }
+ p := make([]*sentPacket, newSize)
+ for i := 0; i < s.size; i++ {
+ p[i] = s.nth(i)
+ }
+ s.p = p
+ s.off = 0
+}
diff --git a/internal/quic/sent_packet_list_test.go b/internal/quic/sent_packet_list_test.go
new file mode 100644
index 0000000..2f7f4d2
--- /dev/null
+++ b/internal/quic/sent_packet_list_test.go
@@ -0,0 +1,107 @@
+// 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"
+
+func TestSentPacketListSlidingWindow(t *testing.T) {
+ // Record 1000 sent packets, acking everything outside the most recent 10.
+ list := &sentPacketList{}
+ const window = 10
+ for i := packetNumber(0); i < 1000; i++ {
+ list.add(&sentPacket{num: i})
+ if i < window {
+ continue
+ }
+ prev := i - window
+ sent := list.num(prev)
+ if sent == nil {
+ t.Fatalf("packet %v not in list", prev)
+ }
+ if sent.num != prev {
+ t.Fatalf("list.num(%v) = packet %v", prev, sent.num)
+ }
+ if got := list.nth(0); got != sent {
+ t.Fatalf("list.nth(0) != list.num(%v)", prev)
+ }
+ sent.acked = true
+ list.clean()
+ if got := list.num(prev); got != nil {
+ t.Fatalf("list.num(%v) = packet %v, expected it to be discarded", prev, got.num)
+ }
+ if got, want := list.start(), prev+1; got != want {
+ t.Fatalf("list.start() = %v, want %v", got, want)
+ }
+ if got, want := list.end(), i+1; got != want {
+ t.Fatalf("list.end() = %v, want %v", got, want)
+ }
+ if got, want := list.size, window; got != want {
+ t.Fatalf("list.size = %v, want %v", got, want)
+ }
+ }
+}
+
+func TestSentPacketListGrows(t *testing.T) {
+ // Record 1000 sent packets.
+ list := &sentPacketList{}
+ const count = 1000
+ for i := packetNumber(0); i < count; i++ {
+ list.add(&sentPacket{num: i})
+ }
+ if got, want := list.start(), packetNumber(0); got != want {
+ t.Fatalf("list.start() = %v, want %v", got, want)
+ }
+ if got, want := list.end(), packetNumber(count); got != want {
+ t.Fatalf("list.end() = %v, want %v", got, want)
+ }
+ if got, want := list.size, count; got != want {
+ t.Fatalf("list.size = %v, want %v", got, want)
+ }
+ for i := packetNumber(0); i < count; i++ {
+ sent := list.num(i)
+ if sent == nil {
+ t.Fatalf("packet %v not in list", i)
+ }
+ if sent.num != i {
+ t.Fatalf("list.num(%v) = packet %v", i, sent.num)
+ }
+ if got := list.nth(int(i)); got != sent {
+ t.Fatalf("list.nth(%v) != list.num(%v)", int(i), i)
+ }
+ }
+}
+
+func TestSentPacketListCleanAll(t *testing.T) {
+ list := &sentPacketList{}
+ // Record 10 sent packets.
+ const count = 10
+ for i := packetNumber(0); i < count; i++ {
+ list.add(&sentPacket{num: i})
+ }
+ // Mark all the packets as acked.
+ for i := packetNumber(0); i < count; i++ {
+ list.num(i).acked = true
+ }
+ list.clean()
+ if got, want := list.size, 0; got != want {
+ t.Fatalf("list.size = %v, want %v", got, want)
+ }
+ list.add(&sentPacket{num: 10})
+ if got, want := list.size, 1; got != want {
+ t.Fatalf("list.size = %v, want %v", got, want)
+ }
+ sent := list.num(10)
+ if sent == nil {
+ t.Fatalf("packet %v not in list", 10)
+ }
+ if sent.num != 10 {
+ t.Fatalf("list.num(10) = %v", sent.num)
+ }
+ if got := list.nth(0); got != sent {
+ t.Fatalf("list.nth(0) != list.num(10)")
+ }
+}