From 286e42770f9fa608d02c1f09ce0017243d79b141 Mon Sep 17 00:00:00 2001 From: Mathis Engelbart Date: Fri, 20 Feb 2026 10:53:56 +0100 Subject: [PATCH] Add refactored loss based controller --- gcc/gcc.go | 6 +++ gcc/loss_rate_controller.go | 63 ++++++++++++++++++++++ gcc/loss_rate_controller_test.go | 89 ++++++++++++++++++++++++++++++++ 3 files changed, 158 insertions(+) create mode 100644 gcc/gcc.go create mode 100644 gcc/loss_rate_controller.go create mode 100644 gcc/loss_rate_controller_test.go diff --git a/gcc/gcc.go b/gcc/gcc.go new file mode 100644 index 0000000..c10a0fd --- /dev/null +++ b/gcc/gcc.go @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: 2025 The Pion community +// SPDX-License-Identifier: MIT + +// Package gcc implements a congestion controller based on +// https://datatracker.ietf.org/doc/html/draft-ietf-rmcat-gcc-02. +package gcc diff --git a/gcc/loss_rate_controller.go b/gcc/loss_rate_controller.go new file mode 100644 index 0000000..cbb22a6 --- /dev/null +++ b/gcc/loss_rate_controller.go @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: 2025 The Pion community +// SPDX-License-Identifier: MIT + +package gcc + +type lossRateController struct { + bitrate int + min, max float64 + + packetsSinceLastUpdate int + arrivedSinceLastUpdate int + lostSinceLastUpdate int +} + +func newLossRateController(initialRate, minRate, maxRate int) *lossRateController { + return &lossRateController{ + bitrate: initialRate, + min: float64(minRate), + max: float64(maxRate), + packetsSinceLastUpdate: 0, + arrivedSinceLastUpdate: 0, + lostSinceLastUpdate: 0, + } +} + +func (l *lossRateController) onPacketAcked() { + l.packetsSinceLastUpdate++ + l.arrivedSinceLastUpdate++ +} + +func (l *lossRateController) onPacketLost() { + l.packetsSinceLastUpdate++ + l.lostSinceLastUpdate++ +} + +func (l *lossRateController) update(lastDeliveryRate int) int { + lossRate := float64(l.lostSinceLastUpdate) / float64(l.packetsSinceLastUpdate) + var target float64 + if lossRate > 0.1 { + target = float64(l.bitrate) * (1 - 0.5*lossRate) + target = max(target, l.min) + } else if lossRate < 0.02 { + target = float64(l.bitrate) * 1.05 + // Cap at 1.5 times the previously delivered rate to ensure we don't + // increase the target rate indefinitely, while being application + // limited. + target = min(target, 1.5*float64(lastDeliveryRate)) + // Cap at previous target rate. In case lastDeliveryRate was much lower + // than our target, we don't want to decrease the target rate. + target = max(target, float64(l.bitrate)) + // Cap at configured max bitrate. + target = min(target, l.max) + } + if target != 0 { + l.bitrate = int(target) + } + + l.packetsSinceLastUpdate = 0 + l.arrivedSinceLastUpdate = 0 + l.lostSinceLastUpdate = 0 + + return l.bitrate +} diff --git a/gcc/loss_rate_controller_test.go b/gcc/loss_rate_controller_test.go new file mode 100644 index 0000000..ad0a425 --- /dev/null +++ b/gcc/loss_rate_controller_test.go @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: 2025 The Pion community +// SPDX-License-Identifier: MIT + +package gcc + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestLossRateController(t *testing.T) { + cases := []struct { + init, min, max int + acked int + lost int + deliveredRate int + expectedRate int + }{ + {}, // all zeros + { + init: 100_000, + min: 100_000, + max: 1_000_000, + acked: 0, + lost: 0, + deliveredRate: 0, + expectedRate: 100_000, + }, + { + init: 100_000, + min: 100_000, + max: 1_000_000, + acked: 99, + lost: 1, + deliveredRate: 100_000, + expectedRate: 105_000, + }, + { + init: 100_000, + min: 100_000, + max: 1_000_000, + acked: 99, + lost: 1, + deliveredRate: 90_000, + expectedRate: 105_000, + }, + { + init: 100_000, + min: 100_000, + max: 1_000_000, + acked: 95, + lost: 5, + deliveredRate: 99_000, + expectedRate: 100_000, + }, + { + init: 100_000, + min: 50_000, + max: 1_000_000, + acked: 89, + lost: 11, + deliveredRate: 90_000, + expectedRate: 94_500, + }, + { + init: 100_000, + min: 100_000, + max: 1_000_000, + acked: 89, + lost: 11, + deliveredRate: 90_000, + expectedRate: 100_000, + }, + } + for i, tc := range cases { + t.Run(fmt.Sprintf("%v", i), func(t *testing.T) { + lrc := newLossRateController(tc.init, tc.min, tc.max) + for i := 0; i < tc.acked; i++ { + lrc.onPacketAcked() + } + for i := 0; i < tc.lost; i++ { + lrc.onPacketLost() + } + assert.Equal(t, tc.expectedRate, lrc.update(tc.deliveredRate)) + }) + } +}