Skip to content
Draft
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
100 changes: 100 additions & 0 deletions gcc/overuse_detector.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// SPDX-FileCopyrightText: 2025 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT

package gcc

import (
"math"
"time"
)

const (
kUp = 0.0087
kDown = 0.039

minNumDeltas = 60
)

const (
defaultThresholdGain = 4.0
defaultOveruseTimeThreshold = 5 * time.Millisecond
)

type overuseDetector struct {
adaptiveThreshold bool
thresholdGain float64
overUseTimeThreshold time.Duration
delayThreshold float64
lastUpdate time.Time
firstOverUse time.Time
overUseCounter int
previousTrend float64
}

func newOveruseDetector(adaptive bool) *overuseDetector {
return &overuseDetector{
adaptiveThreshold: adaptive,
thresholdGain: defaultThresholdGain,
overUseTimeThreshold: defaultOveruseTimeThreshold,
delayThreshold: 6,
lastUpdate: time.Time{},
firstOverUse: time.Time{},
overUseCounter: 0,
previousTrend: 0,
}
}

func (d *overuseDetector) update(ts time.Time, trend float64, numDeltas int) usage {
if d.lastUpdate.IsZero() {
d.lastUpdate = ts
}
if numDeltas < 2 {
return usageNormal
}
modifiedTrend := float64(min(numDeltas, minNumDeltas)) * trend * d.thresholdGain

var currentUsage usage
switch {
case modifiedTrend > d.delayThreshold:
if d.firstOverUse.IsZero() {
delta := ts.Sub(d.lastUpdate)
d.firstOverUse = ts.Add(-delta / 2)
}
d.overUseCounter++
if ts.Sub(d.firstOverUse) > d.overUseTimeThreshold && d.overUseCounter > 1 && trend >= d.previousTrend {
d.firstOverUse = time.Time{}
d.overUseCounter = 0
currentUsage = usageOver
}
case modifiedTrend < -d.delayThreshold:
d.firstOverUse = time.Time{}
d.overUseCounter = 0
currentUsage = usageUnder
default:
d.firstOverUse = time.Time{}
d.overUseCounter = 0
currentUsage = usageNormal
}
d.adaptThreshold(ts, modifiedTrend)
d.previousTrend = trend
d.lastUpdate = ts

return currentUsage
}

func (d *overuseDetector) adaptThreshold(ts time.Time, modifiedTrend float64) {
if !d.adaptiveThreshold {
return
}
if math.Abs(modifiedTrend) > d.delayThreshold+15 {
return
}
k := kUp
if math.Abs(modifiedTrend) < d.delayThreshold {
k = kDown
}
delta := min(ts.Sub(d.lastUpdate), 100*time.Millisecond)
d.delayThreshold += k * (math.Abs(modifiedTrend) - d.delayThreshold) * float64(delta.Milliseconds())
d.delayThreshold = min(d.delayThreshold, 600.0)
d.delayThreshold = max(d.delayThreshold, 6.0)
}
194 changes: 194 additions & 0 deletions gcc/overuse_detector_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
// SPDX-FileCopyrightText: 2025 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT

package gcc

import (
"testing"
"time"

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

func TestOveruseDetectorUpdate(t *testing.T) {
type estimate struct {
ts time.Time
estimate float64
numDeltas int
}
cases := []struct {
name string
adaptive bool
values []estimate
expected []usage
}{
{
name: "noEstimateNoUsageStatic",
adaptive: false,
values: []estimate{},
expected: []usage{},
},
{
name: "overuseStatic",
adaptive: false,
values: []estimate{
{time.Time{}, 1.0, 1},
{time.Time{}.Add(5 * time.Millisecond), 20, 2},
{time.Time{}.Add(20 * time.Millisecond), 30, 3},
},
expected: []usage{usageNormal, usageNormal, usageOver},
},
{
name: "normaluseStatic",
adaptive: false,
values: []estimate{{estimate: 0}},
expected: []usage{usageNormal},
},
{
name: "underuseStatic",
adaptive: false,
values: []estimate{{time.Time{}, -20, 2}},
expected: []usage{usageUnder},
},
{
name: "noOverUseBeforeDelayStatic",
adaptive: false,
values: []estimate{
{time.Time{}.Add(time.Millisecond), 20, 1},
{time.Time{}.Add(2 * time.Millisecond), 30, 2},
{time.Time{}.Add(30 * time.Millisecond), 50, 3},
},
expected: []usage{usageNormal, usageNormal, usageOver},
},
{
name: "noOverUseIfEstimateDecreasedStatic",
adaptive: false,
values: []estimate{
{time.Time{}.Add(time.Millisecond), 20, 1},
{time.Time{}.Add(10 * time.Millisecond), 40, 2},
{time.Time{}.Add(30 * time.Millisecond), 50, 3},
{time.Time{}.Add(35 * time.Millisecond), 3, 4},
},
expected: []usage{usageNormal, usageNormal, usageOver, usageNormal},
},
{
name: "noEstimateNoUsageAdaptive",
adaptive: true,
values: []estimate{},
expected: []usage{},
},
{
name: "overuseAdaptive",
adaptive: true,
values: []estimate{
{time.Time{}, 1, 1},
{time.Time{}.Add(5 * time.Millisecond), 20, 2},
{time.Time{}.Add(20 * time.Millisecond), 30, 3},
},
expected: []usage{usageNormal, usageNormal, usageOver},
},
{
name: "normaluseAdaptive",
adaptive: true,
values: []estimate{{estimate: 0}},
expected: []usage{usageNormal},
},
{
name: "underuseAdaptive",
adaptive: true,
values: []estimate{{time.Time{}, -20, 2}},
expected: []usage{usageUnder},
},
{
name: "noOverUseBeforeDelayAdaptive",
adaptive: true,
values: []estimate{
{time.Time{}.Add(time.Millisecond), 20, 1},
{time.Time{}.Add(2 * time.Millisecond), 30, 2},
{time.Time{}.Add(30 * time.Millisecond), 50, 3},
},
expected: []usage{usageNormal, usageNormal, usageOver},
},
{
name: "noOverUseIfEstimateDecreasedAdaptive",
adaptive: true,
values: []estimate{
{time.Time{}.Add(time.Millisecond), 20, 1},
{time.Time{}.Add(10 * time.Millisecond), 40, 2},
{time.Time{}.Add(30 * time.Millisecond), 50, 3},
{time.Time{}.Add(35 * time.Millisecond), 3, 4},
},
expected: []usage{usageNormal, usageNormal, usageOver, usageNormal},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
od := newOveruseDetector(tc.adaptive)
received := []usage{}
for _, e := range tc.values {
u := od.update(e.ts, e.estimate, e.numDeltas)
received = append(received, u)
}
assert.Equal(t, tc.expected, received)
})
}
}

func TestOveruseDetectorAdaptThreshold(t *testing.T) {
cases := []struct {
name string
od *overuseDetector
ts time.Time
estimate float64
expectedThreshold float64
}{
{
name: "minThreshold",
od: &overuseDetector{
adaptiveThreshold: true,
},
ts: time.Time{},
estimate: 0,
expectedThreshold: 6,
},
{
name: "increase",
od: &overuseDetector{
adaptiveThreshold: true,
delayThreshold: 12.5,
lastUpdate: time.Time{}.Add(time.Second),
},
ts: time.Time{}.Add(2 * time.Second),
estimate: 25,
expectedThreshold: 23.375,
},
{
name: "maxThreshold",
od: &overuseDetector{
adaptiveThreshold: true,
delayThreshold: 600,
lastUpdate: time.Time{},
},
ts: time.Time{}.Add(time.Second),
estimate: 610,
expectedThreshold: 600,
},
{
name: "decrease",
od: &overuseDetector{
adaptiveThreshold: true,
delayThreshold: 12.5,
lastUpdate: time.Time{},
},
ts: time.Time{}.Add(10 * time.Millisecond),
estimate: 1,
expectedThreshold: 8.015,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
tc.od.adaptThreshold(tc.ts, tc.estimate)
assert.Equal(t, tc.expectedThreshold, tc.od.delayThreshold)
})
}
}
Loading