Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions gcc/acknowledgment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// SPDX-FileCopyrightText: 2025 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT

package gcc

import (
"fmt"
"time"
)

// ECN represents the ECN bits of an IP packet header.
type ECN uint8

const (
// ECNNonECT signals Non ECN-Capable Transport, Non-ECT.
// nolint:misspell
ECNNonECT ECN = iota // 00

// ECNECT1 signals ECN Capable Transport, ECT(0).
// nolint:misspell
ECNECT1 // 01

// ECNECT0 signals ECN Capable Transport, ECT(1).
// nolint:misspell
ECNECT0 // 10

// ECNCE signals ECN Congestion Encountered, CE.
// nolint:misspell
ECNCE // 11
)

// An Acknowledgment stores send and receive information about a packet.
type Acknowledgment struct {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we could move this to a separate package? This would allow us to:

  1. Define a generic interface for estimators (currently OnAcks(arrival time.Time, rtt time.Duration, acks []Acknowledgment) int).
  2. Extend the interface later (e.g. provide more data).
  3. Support multiple estimators under a unified pattern.

SeqNr uint64
Size uint16
Departure time.Time
Arrived bool
Arrival time.Time
ECN ECN
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a specific reason we’re avoiding rtcp.ECN?

}

func (a Acknowledgment) String() string {
return fmt.Sprintf("seq=%v, departure=%v, arrival=%v", a.SeqNr, a.Departure, a.Arrival)
}
55 changes: 55 additions & 0 deletions gcc/arrival_group_accumulator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// SPDX-FileCopyrightText: 2025 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT

package gcc

import (
"time"
)

type arrivalGroup []Acknowledgment

type arrivalGroupAccumulator struct {
next arrivalGroup
burstInterval time.Duration
maxBurstDuration time.Duration
}

func newArrivalGroupAccumulator() *arrivalGroupAccumulator {
return &arrivalGroupAccumulator{
next: make([]Acknowledgment, 0),
burstInterval: 5 * time.Millisecond,
maxBurstDuration: 100 * time.Millisecond,
}
}

func (a *arrivalGroupAccumulator) onPacketAcked(ack Acknowledgment) arrivalGroup {
if len(a.next) == 0 {
a.next = append(a.next, ack)

return nil
}

sendTimeDelta := ack.Departure.Sub(a.next[0].Departure)
if sendTimeDelta < a.burstInterval {
a.next = append(a.next, ack)

return nil
}

arrivalTimeDeltaLast := ack.Arrival.Sub(a.next[len(a.next)-1].Arrival)
arrivalTimeDeltaFirst := ack.Arrival.Sub(a.next[0].Arrival)
propagationDelta := arrivalTimeDeltaFirst - sendTimeDelta

if propagationDelta < 0 && arrivalTimeDeltaLast <= a.burstInterval && arrivalTimeDeltaFirst < a.maxBurstDuration {
a.next = append(a.next, ack)

return nil
}

group := make(arrivalGroup, len(a.next))
copy(group, a.next)
a.next = arrivalGroup{ack}

return group
}
210 changes: 210 additions & 0 deletions gcc/arrival_group_accumulator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
// SPDX-FileCopyrightText: 2025 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT

package gcc

import (
"testing"
"time"

"github.com/stretchr/testify/assert"
)

func TestArrivalGroupAccumulator(t *testing.T) {
triggerNewGroupElement := Acknowledgment{
Departure: time.Time{}.Add(time.Second),
Arrival: time.Time{}.Add(time.Second),
}
cases := []struct {
name string
log []Acknowledgment
exp []arrivalGroup
}{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add SeqNr to every packet for clarity?

{
name: "emptyCreatesNoGroups",
log: []Acknowledgment{},
exp: []arrivalGroup{},
},
{
name: "createsSingleElementGroup",
log: []Acknowledgment{
{
Departure: time.Time{},
Arrival: time.Time{}.Add(time.Millisecond),
},
triggerNewGroupElement,
},
exp: []arrivalGroup{
{
{
Departure: time.Time{},
Arrival: time.Time{}.Add(time.Millisecond),
},
},
},
},
{
name: "createsTwoElementGroup",
log: []Acknowledgment{
{
Departure: time.Time{},
Arrival: time.Time{}.Add(15 * time.Millisecond),
},
{
Departure: time.Time{}.Add(3 * time.Millisecond),
Arrival: time.Time{}.Add(20 * time.Millisecond),
},
triggerNewGroupElement,
},
exp: []arrivalGroup{{
{
Departure: time.Time{},
Arrival: time.Time{}.Add(15 * time.Millisecond),
},
{
Departure: time.Time{}.Add(3 * time.Millisecond),
Arrival: time.Time{}.Add(20 * time.Millisecond),
},
}},
},
{
name: "createsTwoArrivalGroups1",
log: []Acknowledgment{
{
Departure: time.Time{},
Arrival: time.Time{}.Add(15 * time.Millisecond),
},
{
Departure: time.Time{}.Add(3 * time.Millisecond),
Arrival: time.Time{}.Add(20 * time.Millisecond),
},
{
Departure: time.Time{}.Add(9 * time.Millisecond),
Arrival: time.Time{}.Add(24 * time.Millisecond),
},
triggerNewGroupElement,
},
exp: []arrivalGroup{
{
{
Departure: time.Time{},
Arrival: time.Time{}.Add(15 * time.Millisecond),
},
{
Departure: time.Time{}.Add(3 * time.Millisecond),
Arrival: time.Time{}.Add(20 * time.Millisecond),
},
},
{
{
Departure: time.Time{}.Add(9 * time.Millisecond),
Arrival: time.Time{}.Add(24 * time.Millisecond),
},
},
},
},
{
name: "ignoresOutOfOrderPackets",
log: []Acknowledgment{
{
Departure: time.Time{},
Arrival: time.Time{}.Add(15 * time.Millisecond),
},
{
Departure: time.Time{}.Add(6 * time.Millisecond),
Arrival: time.Time{}.Add(34 * time.Millisecond),
},
{
Departure: time.Time{}.Add(8 * time.Millisecond),
Arrival: time.Time{}.Add(30 * time.Millisecond),
},
triggerNewGroupElement,
},
exp: []arrivalGroup{
{
{
Departure: time.Time{},
Arrival: time.Time{}.Add(15 * time.Millisecond),
},
},
{
{
Departure: time.Time{}.Add(6 * time.Millisecond),
Arrival: time.Time{}.Add(34 * time.Millisecond),
},
{
Departure: time.Time{}.Add(8 * time.Millisecond),
Arrival: time.Time{}.Add(30 * time.Millisecond),
},
},
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you clarify how out-of-order packets are being handled? I notice this packet appears in the expected output, which suggests it's being processed normally rather than ignored.

},
{
name: "newGroupBecauseOfInterDepartureTime",
log: []Acknowledgment{
{
SeqNr: 0,
Departure: time.Time{},
Arrival: time.Time{}.Add(4 * time.Millisecond),
},
{
SeqNr: 1,
Departure: time.Time{}.Add(3 * time.Millisecond),
Arrival: time.Time{}.Add(4 * time.Millisecond),
},
{
SeqNr: 2,
Departure: time.Time{}.Add(6 * time.Millisecond),
Arrival: time.Time{}.Add(10 * time.Millisecond),
},
{
SeqNr: 3,
Departure: time.Time{}.Add(9 * time.Millisecond),
Arrival: time.Time{}.Add(10 * time.Millisecond),
},
triggerNewGroupElement,
},
exp: []arrivalGroup{
{
{
SeqNr: 0,
Departure: time.Time{},
Arrival: time.Time{}.Add(4 * time.Millisecond),
},
{
SeqNr: 1,
Departure: time.Time{}.Add(3 * time.Millisecond),
Arrival: time.Time{}.Add(4 * time.Millisecond),
},
},
{
{
SeqNr: 2,
Departure: time.Time{}.Add(6 * time.Millisecond),
Arrival: time.Time{}.Add(10 * time.Millisecond),
},
{
SeqNr: 3,
Departure: time.Time{}.Add(9 * time.Millisecond),
Arrival: time.Time{}.Add(10 * time.Millisecond),
},
},
},
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any tests that cover adding extra packets in case of a burst? It seems for me like all tests cut groups because of large Departure interval.

}

for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
aga := newArrivalGroupAccumulator()
received := []arrivalGroup{}
for _, ack := range tc.log {
next := aga.onPacketAcked(ack)
if next != nil {
received = append(received, next)
}
}
assert.Equal(t, tc.exp, received)
})
}
}
Loading
Loading