diff --git a/gcc/delivery_rate_estimator.go b/gcc/delivery_rate_estimator.go new file mode 100644 index 0000000..2e176c1 --- /dev/null +++ b/gcc/delivery_rate_estimator.go @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: 2025 The Pion community +// SPDX-License-Identifier: MIT + +package gcc + +import ( + "container/heap" + "time" +) + +type deliveryRateHeapItem struct { + arrival time.Time + size int +} + +type deliveryRateHeap []deliveryRateHeapItem + +// Len implements heap.Interface. +func (d deliveryRateHeap) Len() int { + return len(d) +} + +// Less implements heap.Interface. +func (d deliveryRateHeap) Less(i int, j int) bool { + return d[i].arrival.Before(d[j].arrival) +} + +// Pop implements heap.Interface. +func (d *deliveryRateHeap) Pop() any { + old := *d + n := len(old) + x := old[n-1] + *d = old[0 : n-1] + + return x +} + +// Push implements heap.Interface. +func (d *deliveryRateHeap) Push(x any) { + // nolint + *d = append(*d, x.(deliveryRateHeapItem)) +} + +// Swap implements heap.Interface. +func (d deliveryRateHeap) Swap(i int, j int) { + d[i], d[j] = d[j], d[i] +} + +type deliveryRateEstimator struct { + window time.Duration + latestArrival time.Time + history *deliveryRateHeap +} + +func newDeliveryRateEstimator(window time.Duration) *deliveryRateEstimator { + return &deliveryRateEstimator{ + window: window, + latestArrival: time.Time{}, + history: &deliveryRateHeap{}, + } +} + +func (e *deliveryRateEstimator) onPacketAcked(arrival time.Time, size int) { + if arrival.After(e.latestArrival) { + e.latestArrival = arrival + } + heap.Push(e.history, deliveryRateHeapItem{ + arrival: arrival, + size: size, + }) +} + +func (e *deliveryRateEstimator) getRate() int { + deadline := e.latestArrival.Add(-e.window) + for len(*e.history) > 0 && (*e.history)[0].arrival.Before(deadline) { + heap.Pop(e.history) + } + earliest := e.latestArrival + sum := 0 + for _, i := range *e.history { + if i.arrival.Before(earliest) { + earliest = i.arrival + } + sum += i.size + } + d := e.latestArrival.Sub(earliest) + if d == 0 { + return 0 + } + rate := 8 * float64(sum) / d.Seconds() + + return int(rate) +} diff --git a/gcc/delivery_rate_estimator_test.go b/gcc/delivery_rate_estimator_test.go new file mode 100644 index 0000000..0324ffb --- /dev/null +++ b/gcc/delivery_rate_estimator_test.go @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: 2025 The Pion community +// SPDX-License-Identifier: MIT + +package gcc + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestDeliveryRateEstimator(t *testing.T) { + type ack struct { + arrival time.Time + size int + } + cases := []struct { + window time.Duration + acks []ack + expectedRate int + }{ + { + window: 0, + acks: []ack{}, + expectedRate: 0, + }, + { + window: time.Second, + acks: []ack{}, + expectedRate: 0, + }, + { + window: time.Second, + acks: []ack{ + {time.Time{}, 1200}, + }, + expectedRate: 0, + }, + { + window: time.Second, + acks: []ack{ + {time.Time{}.Add(time.Millisecond), 1200}, + }, + expectedRate: 0, + }, + { + window: time.Second, + acks: []ack{ + {time.Time{}.Add(time.Second), 1200}, + {time.Time{}.Add(1500 * time.Millisecond), 1200}, + {time.Time{}.Add(2 * time.Second), 1200}, + }, + expectedRate: 28800, + }, + { + window: time.Second, + acks: []ack{ + {time.Time{}.Add(500 * time.Millisecond), 1200}, + {time.Time{}.Add(time.Second), 1200}, + {time.Time{}.Add(1500 * time.Millisecond), 1200}, + {time.Time{}.Add(2 * time.Second), 1200}, + }, + expectedRate: 28800, + }, + } + for i, tc := range cases { + t.Run(fmt.Sprintf("%v", i), func(t *testing.T) { + e := newDeliveryRateEstimator(tc.window) + for _, ack := range tc.acks { + e.onPacketAcked(ack.arrival, ack.size) + } + assert.Equal(t, tc.expectedRate, e.getRate()) + }) + } +}