diff --git a/internal/quic/sent_packet_list.go b/internal/quic/sent_packet_list.go new file mode 100644 index 000000000..6fb712a7a --- /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 000000000..2f7f4d2c6 --- /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)") + } +}