diff --git a/gcc/overuse_detector.go b/gcc/overuse_detector.go new file mode 100644 index 0000000..409b797 --- /dev/null +++ b/gcc/overuse_detector.go @@ -0,0 +1,69 @@ +// SPDX-FileCopyrightText: 2025 The Pion community +// SPDX-License-Identifier: MIT + +package gcc + +import ( + "time" +) + +const ( + defaultOveruseTimeThreshold = 5 * time.Millisecond + defaultDelayThreshold = 1.5 +) + +type overuseDetector struct { + overUseTimeThreshold time.Duration + delayThreshold float64 + lastUpdate time.Time + firstOverUse time.Time + overUseCounter int + previousTrend float64 + usage usage +} + +func newOveruseDetector() *overuseDetector { + return &overuseDetector{ + overUseTimeThreshold: defaultOveruseTimeThreshold, + delayThreshold: defaultDelayThreshold, + lastUpdate: time.Time{}, + firstOverUse: time.Time{}, + overUseCounter: 0, + previousTrend: 0, + usage: usageNormal, + } +} + +func (d *overuseDetector) update(ts time.Time, trend float64) usage { + if d.lastUpdate.IsZero() { + d.lastUpdate = ts + } + + switch { + case trend > 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 + d.usage = usageOver + } + case trend < -d.delayThreshold: + d.firstOverUse = time.Time{} + d.overUseCounter = 0 + d.usage = usageUnder + default: + d.firstOverUse = time.Time{} + d.overUseCounter = 0 + d.usage = usageNormal + } + d.previousTrend = trend + d.lastUpdate = ts + + return d.usage +} diff --git a/gcc/overuse_detector_test.go b/gcc/overuse_detector_test.go new file mode 100644 index 0000000..dadeb04 --- /dev/null +++ b/gcc/overuse_detector_test.go @@ -0,0 +1,103 @@ +// SPDX-FileCopyrightText: 2025 The Pion community +// 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 + modifiedTrend float64 + } + cases := []struct { + name string + values []estimate + expected []usage + }{ + { + name: "noEstimateNoUsage", + values: []estimate{}, + expected: []usage{}, + }, + { + name: "confirmsOverUse", + values: []estimate{ + {time.Time{}, 0}, + {time.Time{}.Add(5 * time.Millisecond), 40}, + {time.Time{}.Add(20 * time.Millisecond), 90}, + }, + expected: []usage{usageNormal, usageNormal, usageOver}, + }, + { + name: "normaluse", + values: []estimate{{modifiedTrend: 0}}, + expected: []usage{usageNormal}, + }, + { + name: "confirmsUnderUse", + values: []estimate{{time.Time{}, -40}}, + expected: []usage{usageUnder}, + }, + { + name: "noOverUseBeforeDelay", + values: []estimate{ + {time.Time{}.Add(time.Millisecond), 0}, + {time.Time{}.Add(2 * time.Millisecond), 60}, + {time.Time{}.Add(30 * time.Millisecond), 150}, + }, + expected: []usage{usageNormal, usageNormal, usageOver}, + }, + { + name: "noNewOverUseIfEstimateDecreased", + values: []estimate{ + {time.Time{}.Add(time.Millisecond), 0}, + {time.Time{}.Add(10 * time.Millisecond), 80}, + {time.Time{}.Add(30 * time.Millisecond), 60}, + }, + expected: []usage{usageNormal, usageNormal, usageNormal}, + }, + { + name: "normalUseWhenTrendFallsBelowThreshold", + values: []estimate{ + {time.Time{}.Add(time.Millisecond), 0}, + {time.Time{}.Add(10 * time.Millisecond), 80}, + {time.Time{}.Add(30 * time.Millisecond), 150}, + {time.Time{}.Add(35 * time.Millisecond), 1.2}, + }, + expected: []usage{usageNormal, usageNormal, usageOver, usageNormal}, + }, + { + name: "keepsOverUseWhileTrendStaysHigh", + values: []estimate{ + {time.Time{}.Add(5 * time.Millisecond), 0}, + {time.Time{}.Add(10 * time.Millisecond), 1}, + {time.Time{}.Add(15 * time.Millisecond), 1.5}, + {time.Time{}.Add(20 * time.Millisecond), 2}, + {time.Time{}.Add(25 * time.Millisecond), 2.5}, + {time.Time{}.Add(30 * time.Millisecond), 3}, + {time.Time{}.Add(35 * time.Millisecond), 3.5}, + }, + expected: []usage{ + usageNormal, usageNormal, usageNormal, usageNormal, + usageOver, usageOver, usageOver, + }, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + od := newOveruseDetector() + received := []usage{} + for _, e := range tc.values { + u := od.update(e.ts, e.modifiedTrend) + received = append(received, u) + } + assert.Equal(t, tc.expected, received) + }) + } +}