From 51b4345eeb87b5f5f66ea42d7b107632adbd8d1c Mon Sep 17 00:00:00 2001 From: Mathis Engelbart Date: Wed, 17 Jan 2024 22:11:56 +0100 Subject: [PATCH 1/9] Increase should never decrease When the controller decides to increase the rate, it should not decrease, even if the measured transmission rate was low. The rate could be lower because there was not enough data to send. --- pkg/gcc/rate_controller.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/gcc/rate_controller.go b/pkg/gcc/rate_controller.go index c19a617b..8dd35b53 100644 --- a/pkg/gcc/rate_controller.go +++ b/pkg/gcc/rate_controller.go @@ -104,7 +104,7 @@ func (c *rateController) onDelayStats(ds DelayStats) { case stateHold: // should never occur due to check above, but makes the linter happy case stateIncrease: - c.target = clampInt(c.increase(now), c.minBitrate, c.maxBitrate) + c.target = clampInt(c.increase(now), c.target, c.maxBitrate) next = DelayStats{ Measurement: c.delayStats.Measurement, Estimate: c.delayStats.Estimate, From 3b36e9e614c78ec69c0f18c64933c40920e0878c Mon Sep 17 00:00:00 2001 From: Alex Pokotilo Date: Wed, 24 Jan 2024 13:34:24 +0600 Subject: [PATCH 2/9] Fix variance calculation Variance calculation for exponentialMovingAverage was incorrect --- pkg/gcc/rate_controller.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/gcc/rate_controller.go b/pkg/gcc/rate_controller.go index 8dd35b53..31717837 100644 --- a/pkg/gcc/rate_controller.go +++ b/pkg/gcc/rate_controller.go @@ -45,7 +45,7 @@ func (a *exponentialMovingAverage) update(value float64) { } else { x := value - a.average a.average += decreaseEMAAlpha * x - a.variance = (1 - decreaseEMAAlpha) * (a.variance + decreaseEMAAlpha*x*x) + a.variance = decreaseEMAAlpha*x*x + (1-decreaseEMAAlpha)*a.variance a.stdDeviation = math.Sqrt(a.variance) } } From 1dcaf85f7b9710bea2950226b01cd33f941e2617 Mon Sep 17 00:00:00 2001 From: Alex Pokotilo Date: Wed, 24 Jan 2024 14:04:51 +0600 Subject: [PATCH 3/9] Reset exponentialMovingAverage added Reset exponentialMovingAverage if last decrease was 1 minute ago or If R_hat(i) increases above three standard deviations of the average max bitrate --- pkg/gcc/rate_controller.go | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/pkg/gcc/rate_controller.go b/pkg/gcc/rate_controller.go index 31717837..d19ba3a8 100644 --- a/pkg/gcc/rate_controller.go +++ b/pkg/gcc/rate_controller.go @@ -34,20 +34,35 @@ type rateController struct { } type exponentialMovingAverage struct { + init bool average float64 variance float64 stdDeviation float64 + lastUpdate time.Time +} + +func (a *exponentialMovingAverage) reset() { + a.init = false + a.average = 0 + a.variance = 0 + a.stdDeviation = 0 } func (a *exponentialMovingAverage) update(value float64) { - if a.average == 0.0 { + if !a.init { a.average = value + a.init = true } else { x := value - a.average a.average += decreaseEMAAlpha * x a.variance = decreaseEMAAlpha*x*x + (1-decreaseEMAAlpha)*a.variance a.stdDeviation = math.Sqrt(a.variance) } + a.lastUpdate = time.Now() +} + +func (a *exponentialMovingAverage) expired(now time.Time) bool { + return a.init && now.Sub(a.lastUpdate) > time.Minute } func newRateController(now now, initialTargetBitrate, minBitrate, maxBitrate int, dsw func(DelayStats)) *rateController { @@ -134,7 +149,16 @@ func (c *rateController) onDelayStats(ds DelayStats) { } func (c *rateController) increase(now time.Time) int { - if c.latestDecreaseRate.average > 0 && float64(c.latestReceivedRate) > c.latestDecreaseRate.average-3*c.latestDecreaseRate.stdDeviation && + if c.latestDecreaseRate.init && + float64(c.latestReceivedRate) > c.latestDecreaseRate.average+3*c.latestDecreaseRate.stdDeviation { + c.latestDecreaseRate.reset() + } + + if c.latestDecreaseRate.expired(now) { + c.latestDecreaseRate.reset() + } + + if c.latestDecreaseRate.init && float64(c.latestReceivedRate) > c.latestDecreaseRate.average-3*c.latestDecreaseRate.stdDeviation && float64(c.latestReceivedRate) < c.latestDecreaseRate.average+3*c.latestDecreaseRate.stdDeviation { bitsPerFrame := float64(c.target) / 30.0 packetsPerFrame := math.Ceil(bitsPerFrame / (1200 * 8)) From eb972e3d37e5efa4ff1bba78d1d1c4932199454e Mon Sep 17 00:00:00 2001 From: Alex Pokotilo Date: Wed, 24 Jan 2024 15:56:29 +0600 Subject: [PATCH 4/9] Don't double update uncertainty Standard says we need to use either 3*sqrt(var_v_hat) or z(i) --- pkg/gcc/kalman.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/gcc/kalman.go b/pkg/gcc/kalman.go index 1eb76a88..ca366e13 100644 --- a/pkg/gcc/kalman.go +++ b/pkg/gcc/kalman.go @@ -80,8 +80,9 @@ func (k *kalman) updateEstimate(measurement time.Duration) time.Duration { root3 := 3 * root if zms > root3 { k.measurementUncertainty = math.Max(alpha*k.measurementUncertainty+(1-alpha)*root3*root3, 1) + } else { + k.measurementUncertainty = math.Max(alpha*k.measurementUncertainty+(1-alpha)*zms*zms, 1) } - k.measurementUncertainty = math.Max(alpha*k.measurementUncertainty+(1-alpha)*zms*zms, 1) } estimateUncertainty := k.estimateError + k.processUncertainty From 3e56393a13b5eba1fe43e14147fbe70b42ac41a2 Mon Sep 17 00:00:00 2001 From: Alex Pokotilo Date: Thu, 25 Jan 2024 15:48:07 +0600 Subject: [PATCH 5/9] Set alpha to 0.95 as proposed in [1] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [1] https://c3lab.poliba.it/images/6/65/Gcc-analysis.pdf [2] https://datatracker.ietf.org/doc/html/draft-ietf-rmcat-gcc-02 Now alpha is almost 1, since (1−0,001)^(30÷(1000×5×1000000)) == 1 And this makes var_v_hat(i) always have initial value and k.gain to be a constant after convergence and hence current version of Kalman filter works as exponential moving average with A ~= 0.3. To make it more Kalman we need to calculate var_v_hat(i) corretly. Maybe we need to calculate alpha in different way, but not just set it to 1 for sure. See [1] and [2](section 5.3. Arrival-time filter) for more details --- pkg/gcc/kalman.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/gcc/kalman.go b/pkg/gcc/kalman.go index ca366e13..259f3094 100644 --- a/pkg/gcc/kalman.go +++ b/pkg/gcc/kalman.go @@ -9,7 +9,8 @@ import ( ) const ( - chi = 0.001 + chi = 0.001 + alpha = 0.95 ) type kalmanOption func(*kalman) @@ -75,7 +76,6 @@ func (k *kalman) updateEstimate(measurement time.Duration) time.Duration { zms := float64(z.Microseconds()) / 1000.0 if !k.disableMeasurementUncertaintyUpdates { - alpha := math.Pow((1 - chi), 30.0/(1000.0*5*float64(time.Millisecond))) root := math.Sqrt(k.measurementUncertainty) root3 := 3 * root if zms > root3 { From c3af4c7b39b6915228984726d72f29b00aa4baf4 Mon Sep 17 00:00:00 2001 From: Alex Pokotilo Date: Thu, 25 Jan 2024 17:18:56 +0600 Subject: [PATCH 6/9] Don't multiply estimate from Kalman filter I was not able to find a reason why current implementation multiplied output from Kalman filter to up to 60. --- pkg/gcc/adaptive_threshold.go | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/pkg/gcc/adaptive_threshold.go b/pkg/gcc/adaptive_threshold.go index 61d34d8f..7f25eb00 100644 --- a/pkg/gcc/adaptive_threshold.go +++ b/pkg/gcc/adaptive_threshold.go @@ -8,10 +8,6 @@ import ( "time" ) -const ( - maxDeltas = 60 -) - type adaptiveThresholdOption func(*adaptiveThreshold) func setInitialThreshold(t time.Duration) adaptiveThresholdOption { @@ -65,16 +61,16 @@ func (a *adaptiveThreshold) compare(estimate, _ time.Duration) (usage, time.Dura if a.numDeltas < 2 { return usageNormal, estimate, a.max } - t := time.Duration(minInt(a.numDeltas, maxDeltas)) * estimate + use := usageNormal - if t > a.thresh { + if estimate > a.thresh { use = usageOver - } else if t < -a.thresh { + } else if estimate < -a.thresh { use = usageUnder } thresh := a.thresh - a.update(t) - return use, t, thresh + a.update(estimate) + return use, estimate, thresh } func (a *adaptiveThreshold) update(estimate time.Duration) { From 72379b9fe9bccb4538ceedcfbff8e57076773bb1 Mon Sep 17 00:00:00 2001 From: Alex Pokotilo Date: Thu, 25 Jan 2024 20:36:05 +0600 Subject: [PATCH 7/9] Calculate fmax according to the standard Standard states fmax should highest bitrate among K packets. I set K to 10 and use interarrival delta from slope filter as T(i)-T(i-1) --- pkg/gcc/kalman.go | 34 +++++++++++++++++++++++++++++++--- pkg/gcc/slope_estimator.go | 10 ++-------- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/pkg/gcc/kalman.go b/pkg/gcc/kalman.go index 259f3094..03d0108d 100644 --- a/pkg/gcc/kalman.go +++ b/pkg/gcc/kalman.go @@ -10,7 +10,7 @@ import ( const ( chi = 0.001 - alpha = 0.95 + Kcount = 10 ) type kalmanOption func(*kalman) @@ -21,7 +21,9 @@ type kalman struct { processUncertainty float64 // Q_i estimateError float64 measurementUncertainty float64 - + K [Kcount] time.Duration + Kmin time.Duration + kIndex int disableMeasurementUncertaintyUpdates bool } @@ -70,12 +72,36 @@ func newKalman(opts ...kalmanOption) *kalman { return k } -func (k *kalman) updateEstimate(measurement time.Duration) time.Duration { +func (k *kalman) updateEstimate(measurement, lastReceiveDelta time.Duration) time.Duration { z := measurement - k.estimate zms := float64(z.Microseconds()) / 1000.0 if !k.disableMeasurementUncertaintyUpdates { + index:= k.kIndex % Kcount + + + if k.kIndex == 0 { + k.Kmin = lastReceiveDelta + } else if lastReceiveDelta < k.Kmin { + k.Kmin = lastReceiveDelta + } else if k.kIndex >= Kcount && k.K[index] == k.Kmin { + k.Kmin = lastReceiveDelta + + for i:= 0; i < k.kIndex && i < Kcount; i++ { + if i != index && k.Kmin > k.K[i] { + k.Kmin = k.K[i] + } + } + } + + k.K[index] = lastReceiveDelta + + kMinms := float64(k.Kmin.Microseconds()) / 1000.0 + + fmax:= 1 / kMinms + + alpha := math.Pow((1 - chi), 30.0/(1000.0 * fmax)) root := math.Sqrt(k.measurementUncertainty) root3 := 3 * root if zms > root3 { @@ -83,6 +109,8 @@ func (k *kalman) updateEstimate(measurement time.Duration) time.Duration { } else { k.measurementUncertainty = math.Max(alpha*k.measurementUncertainty+(1-alpha)*zms*zms, 1) } + + k.kIndex++ } estimateUncertainty := k.estimateError + k.processUncertainty diff --git a/pkg/gcc/slope_estimator.go b/pkg/gcc/slope_estimator.go index 2b57f4da..1f19bcda 100644 --- a/pkg/gcc/slope_estimator.go +++ b/pkg/gcc/slope_estimator.go @@ -8,13 +8,7 @@ import ( ) type estimator interface { - updateEstimate(measurement time.Duration) time.Duration -} - -type estimatorFunc func(time.Duration) time.Duration - -func (f estimatorFunc) updateEstimate(d time.Duration) time.Duration { - return f(d) + updateEstimate(measurement, lastReceiveDelta time.Duration) time.Duration } type slopeEstimator struct { @@ -42,7 +36,7 @@ func (e *slopeEstimator) onArrivalGroup(ag arrivalGroup) { e.group = ag e.delayStatsWriter(DelayStats{ Measurement: measurement, - Estimate: e.updateEstimate(measurement), + Estimate: e.updateEstimate(measurement, delta), Threshold: 0, LastReceiveDelta: delta, Usage: 0, From 6c00b797e12ff58f07f0fd96c22b83448fa61dae Mon Sep 17 00:00:00 2001 From: Alex Pokotilo Date: Thu, 25 Jan 2024 22:01:52 +0600 Subject: [PATCH 8/9] Tune 'chi', Kcount and set min Kalman gain 0.01 This these params Kalmain gain recovered from bandwidth decreases faster that with lower chi. Set Kcount to track longer frames queue to calculate fmax --- pkg/gcc/kalman.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/gcc/kalman.go b/pkg/gcc/kalman.go index 03d0108d..43b0c117 100644 --- a/pkg/gcc/kalman.go +++ b/pkg/gcc/kalman.go @@ -9,8 +9,8 @@ import ( ) const ( - chi = 0.001 - Kcount = 10 + chi = 0.01 + Kcount = 25 ) type kalmanOption func(*kalman) @@ -114,7 +114,7 @@ func (k *kalman) updateEstimate(measurement, lastReceiveDelta time.Duration) tim } estimateUncertainty := k.estimateError + k.processUncertainty - k.gain = estimateUncertainty / (estimateUncertainty + k.measurementUncertainty) + k.gain = math.Max(estimateUncertainty / (estimateUncertainty + k.measurementUncertainty), 0.01) k.estimate += time.Duration(k.gain * zms * float64(time.Millisecond)) From b903f0f66ae7ef091f5088ffb583cb44065efe2c Mon Sep 17 00:00:00 2001 From: Alex Pokotilo Date: Fri, 26 Jan 2024 16:50:19 +0600 Subject: [PATCH 9/9] Fix golint and test issues Golint and test cases fixed according to new filter values --- pkg/gcc/adaptive_threshold_test.go | 2 +- pkg/gcc/kalman.go | 41 +++++++++++++++--------------- pkg/gcc/kalman_test.go | 2 +- pkg/gcc/slope_estimator.go | 6 +++++ pkg/gcc/slope_estimator_test.go | 2 +- 5 files changed, 29 insertions(+), 24 deletions(-) diff --git a/pkg/gcc/adaptive_threshold_test.go b/pkg/gcc/adaptive_threshold_test.go index 968ae0a5..084ab141 100644 --- a/pkg/gcc/adaptive_threshold_test.go +++ b/pkg/gcc/adaptive_threshold_test.go @@ -104,7 +104,7 @@ func TestAdaptiveThreshold(t *testing.T) { }, expected: []usage{usageNormal, usageOver, usageNormal}, options: []adaptiveThresholdOption{ - setInitialThreshold(40 * time.Millisecond), + setInitialThreshold(20 * time.Millisecond), }, }, { diff --git a/pkg/gcc/kalman.go b/pkg/gcc/kalman.go index 43b0c117..f385e3c4 100644 --- a/pkg/gcc/kalman.go +++ b/pkg/gcc/kalman.go @@ -9,21 +9,21 @@ import ( ) const ( - chi = 0.01 - Kcount = 25 + chi = 0.01 + kCount = 25 ) type kalmanOption func(*kalman) type kalman struct { - gain float64 - estimate time.Duration - processUncertainty float64 // Q_i - estimateError float64 - measurementUncertainty float64 - K [Kcount] time.Duration - Kmin time.Duration - kIndex int + gain float64 + estimate time.Duration + processUncertainty float64 // Q_i + estimateError float64 + measurementUncertainty float64 + K [kCount]time.Duration + Kmin time.Duration + kIndex int disableMeasurementUncertaintyUpdates bool } @@ -78,30 +78,29 @@ func (k *kalman) updateEstimate(measurement, lastReceiveDelta time.Duration) tim zms := float64(z.Microseconds()) / 1000.0 if !k.disableMeasurementUncertaintyUpdates { - index:= k.kIndex % Kcount + index := k.kIndex % kCount - - if k.kIndex == 0 { + switch { + case k.kIndex == 0: k.Kmin = lastReceiveDelta - } else if lastReceiveDelta < k.Kmin { + case lastReceiveDelta < k.Kmin: k.Kmin = lastReceiveDelta - } else if k.kIndex >= Kcount && k.K[index] == k.Kmin { + case k.kIndex >= kCount && k.K[index] == k.Kmin: k.Kmin = lastReceiveDelta - for i:= 0; i < k.kIndex && i < Kcount; i++ { + for i := 0; i < k.kIndex && i < kCount; i++ { if i != index && k.Kmin > k.K[i] { k.Kmin = k.K[i] } } + default: } k.K[index] = lastReceiveDelta - kMinms := float64(k.Kmin.Microseconds()) / 1000.0 + fmax := 1 / kMinms - fmax:= 1 / kMinms - - alpha := math.Pow((1 - chi), 30.0/(1000.0 * fmax)) + alpha := math.Pow((1 - chi), 30.0/(1000.0*fmax)) root := math.Sqrt(k.measurementUncertainty) root3 := 3 * root if zms > root3 { @@ -114,7 +113,7 @@ func (k *kalman) updateEstimate(measurement, lastReceiveDelta time.Duration) tim } estimateUncertainty := k.estimateError + k.processUncertainty - k.gain = math.Max(estimateUncertainty / (estimateUncertainty + k.measurementUncertainty), 0.01) + k.gain = math.Max(estimateUncertainty/(estimateUncertainty+k.measurementUncertainty), 0.01) k.estimate += time.Duration(k.gain * zms * float64(time.Millisecond)) diff --git a/pkg/gcc/kalman_test.go b/pkg/gcc/kalman_test.go index f3b90056..312408ca 100644 --- a/pkg/gcc/kalman_test.go +++ b/pkg/gcc/kalman_test.go @@ -63,7 +63,7 @@ func TestKalman(t *testing.T) { k := newKalman(append(tc.opts, setDisableMeasurementUncertaintyUpdates(true))...) estimates := []time.Duration{} for _, m := range tc.measurements { - estimates = append(estimates, k.updateEstimate(m)) + estimates = append(estimates, k.updateEstimate(m, 5*time.Millisecond)) } assert.Equal(t, tc.expected, estimates, "%v != %v", tc.expected, estimates) }) diff --git a/pkg/gcc/slope_estimator.go b/pkg/gcc/slope_estimator.go index 1f19bcda..47ee0eb2 100644 --- a/pkg/gcc/slope_estimator.go +++ b/pkg/gcc/slope_estimator.go @@ -11,6 +11,12 @@ type estimator interface { updateEstimate(measurement, lastReceiveDelta time.Duration) time.Duration } +type estimatorFunc func(time.Duration, time.Duration) time.Duration + +func (f estimatorFunc) updateEstimate(d, c time.Duration) time.Duration { + return f(d, c) +} + type slopeEstimator struct { estimator init bool diff --git a/pkg/gcc/slope_estimator_test.go b/pkg/gcc/slope_estimator_test.go index ba7bfc59..564d5e87 100644 --- a/pkg/gcc/slope_estimator_test.go +++ b/pkg/gcc/slope_estimator_test.go @@ -10,7 +10,7 @@ import ( "github.com/stretchr/testify/assert" ) -func identity(d time.Duration) time.Duration { +func identity(d, _ time.Duration) time.Duration { return d }