mirror of
https://github.com/hanzoai/livekit.git
synced 2026-10-30 18:29:34 +00:00
This fork lived at github.com/hanzoai/livekit but still declared
`module github.com/livekit/livekit-server`, so all 505 self-references pointed
at upstream. A fork that does not own its module path is the exact thing the
owned-module-paths rule exists to prevent: every import line in every consumer
carries someone else's brand, and `go get` resolves to their code, not ours.
module + 505 self-imports github.com/livekit/livekit-server -> github.com/hanzoai/livekit
KV client github.com/redis/go-redis/v9 -> github.com/hanzokv/go/v9
message bus github.com/livekit/psrpc -> github.com/hanzoai/psrpc
psrpc had to be forked (hanzoai/psrpc, Apache-2.0) because its bus is typed
against the KV client — the same shape redsync had for hanzoai/git.
DELIBERATELY NOT FORKED: github.com/livekit/{protocol,mediatransportutil,
mageutil} keep their addresses. Only `protocol` still reaches go-redis, through
a single file (redis/redis.go) out of 305. Forking an 11.5MB core wire-protocol
library that this server must track in lockstep — and that upstream pushed
yesterday — to change one file trades a tidy dependency graph for permanent
divergence on the fastest-moving dependency here. So one indirect
github.com/redis/go-redis entry remains, via livekit/protocol/redis, and that
is the correct place for it to live.
1160 lines
34 KiB
Go
1160 lines
34 KiB
Go
// Copyright 2023 LiveKit, Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package sendsidebwe
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/hanzoai/livekit/pkg/sfu/bwe"
|
|
"github.com/hanzoai/livekit/pkg/sfu/ccutils"
|
|
"github.com/livekit/protocol/logger"
|
|
"github.com/livekit/protocol/utils/mono"
|
|
"github.com/pion/rtcp"
|
|
"go.uber.org/zap/zapcore"
|
|
)
|
|
|
|
// -------------------------------------------------------------------------------
|
|
|
|
type CongestionSignalConfig struct {
|
|
MinNumberOfGroups int `yaml:"min_number_of_groups,omitempty"`
|
|
MinDuration time.Duration `yaml:"min_duration,omitempty"`
|
|
}
|
|
|
|
func (c CongestionSignalConfig) IsTriggered(numGroups int, duration int64) bool {
|
|
return numGroups >= c.MinNumberOfGroups && duration >= c.MinDuration.Microseconds()
|
|
}
|
|
|
|
var (
|
|
defaultQueuingDelayEarlyWarningJQRConfig = CongestionSignalConfig{
|
|
MinNumberOfGroups: 2,
|
|
MinDuration: 200 * time.Millisecond,
|
|
}
|
|
|
|
defaultQueuingDelayEarlyWarningDQRConfig = CongestionSignalConfig{
|
|
MinNumberOfGroups: 3,
|
|
MinDuration: 300 * time.Millisecond,
|
|
}
|
|
|
|
defaultLossEarlyWarningJQRConfig = CongestionSignalConfig{
|
|
MinNumberOfGroups: 3,
|
|
MinDuration: 300 * time.Millisecond,
|
|
}
|
|
|
|
defaultLossEarlyWarningDQRConfig = CongestionSignalConfig{
|
|
MinNumberOfGroups: 4,
|
|
MinDuration: 400 * time.Millisecond,
|
|
}
|
|
|
|
defaultQueuingDelayCongestedJQRConfig = CongestionSignalConfig{
|
|
MinNumberOfGroups: 4,
|
|
MinDuration: 400 * time.Millisecond,
|
|
}
|
|
|
|
defaultQueuingDelayCongestedDQRConfig = CongestionSignalConfig{
|
|
MinNumberOfGroups: 5,
|
|
MinDuration: 500 * time.Millisecond,
|
|
}
|
|
|
|
defaultLossCongestedJQRConfig = CongestionSignalConfig{
|
|
MinNumberOfGroups: 6,
|
|
MinDuration: 600 * time.Millisecond,
|
|
}
|
|
|
|
defaultLossCongestedDQRConfig = CongestionSignalConfig{
|
|
MinNumberOfGroups: 6,
|
|
MinDuration: 600 * time.Millisecond,
|
|
}
|
|
)
|
|
|
|
// -------------------------------------------------------------------------------
|
|
|
|
type ProbeSignalConfig struct {
|
|
MinBytesRatio float64 `yaml:"min_bytes_ratio,omitempty"`
|
|
MinDurationRatio float64 `yaml:"min_duration_ratio,omitempty"`
|
|
|
|
JQRMinDelay time.Duration `yaml:"jqr_min_delay,omitempty"`
|
|
DQRMaxDelay time.Duration `yaml:"dqr_max_delay,omitempty"`
|
|
|
|
WeightedLoss WeightedLossConfig `yaml:"weighted_loss,omitempty"`
|
|
JQRMinWeightedLoss float64 `yaml:"jqr_min_weighted_loss,omitempty"`
|
|
DQRMaxWeightedLoss float64 `yaml:"dqr_max_weighted_loss,omitempty"`
|
|
}
|
|
|
|
func (p ProbeSignalConfig) IsValid(pci ccutils.ProbeClusterInfo) bool {
|
|
return pci.Result.Bytes() > int(p.MinBytesRatio*float64(pci.Goal.DesiredBytes)) && pci.Result.Duration() > time.Duration(p.MinDurationRatio*float64(pci.Goal.Duration))
|
|
}
|
|
|
|
func (p ProbeSignalConfig) ProbeSignal(ppg *probePacketGroup) (ccutils.ProbeSignal, int64) {
|
|
ts := newTrafficStats(trafficStatsParams{
|
|
Config: p.WeightedLoss,
|
|
})
|
|
ts.Merge(ppg.Traffic())
|
|
|
|
pqd := ppg.PropagatedQueuingDelay()
|
|
if pqd > p.JQRMinDelay.Microseconds() || ts.WeightedLoss() > p.JQRMinWeightedLoss {
|
|
return ccutils.ProbeSignalCongesting, ts.AcknowledgedBitrate()
|
|
}
|
|
|
|
if pqd < p.DQRMaxDelay.Microseconds() && ts.WeightedLoss() < p.DQRMaxWeightedLoss {
|
|
return ccutils.ProbeSignalNotCongesting, ts.AcknowledgedBitrate()
|
|
}
|
|
|
|
return ccutils.ProbeSignalInconclusive, ts.AcknowledgedBitrate()
|
|
}
|
|
|
|
var (
|
|
defaultProbeSignalConfig = ProbeSignalConfig{
|
|
MinBytesRatio: 0.5,
|
|
MinDurationRatio: 0.5,
|
|
|
|
JQRMinDelay: 50 * time.Millisecond,
|
|
DQRMaxDelay: 20 * time.Millisecond,
|
|
|
|
WeightedLoss: defaultWeightedLossConfig,
|
|
JQRMinWeightedLoss: 0.25,
|
|
DQRMaxWeightedLoss: 0.1,
|
|
}
|
|
)
|
|
|
|
// -------------------------------------------------------------------------------
|
|
|
|
type queuingRegion int
|
|
|
|
const (
|
|
queuingRegionDQR queuingRegion = iota
|
|
queuingRegionIndeterminate
|
|
queuingRegionJQR
|
|
)
|
|
|
|
func (q queuingRegion) String() string {
|
|
switch q {
|
|
case queuingRegionDQR:
|
|
return "DQR"
|
|
case queuingRegionIndeterminate:
|
|
return "INDETERMINATE"
|
|
case queuingRegionJQR:
|
|
return "JQR"
|
|
default:
|
|
return fmt.Sprintf("%d", int(q))
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------------------
|
|
|
|
type congestionReason int
|
|
|
|
const (
|
|
congestionReasonNone congestionReason = iota
|
|
congestionReasonQueuingDelay
|
|
congestionReasonLoss
|
|
)
|
|
|
|
func (c congestionReason) String() string {
|
|
switch c {
|
|
case congestionReasonNone:
|
|
return "NONE"
|
|
case congestionReasonQueuingDelay:
|
|
return "QUEUING_DELAY"
|
|
case congestionReasonLoss:
|
|
return "LOSS"
|
|
default:
|
|
return fmt.Sprintf("%d", int(c))
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------------------
|
|
|
|
type qdMeasurement struct {
|
|
jqrConfig CongestionSignalConfig
|
|
dqrConfig CongestionSignalConfig
|
|
jqrMinDelay int64
|
|
jqrMinTrendCoefficient float64
|
|
dqrMaxDelay int64
|
|
|
|
numGroups int
|
|
numJQRGroups int
|
|
numDQRGroups int
|
|
minSendTime int64
|
|
maxSendTime int64
|
|
propagatedQueuingDelays []int64
|
|
|
|
isSealed bool
|
|
minGroupIdx int
|
|
maxGroupIdx int
|
|
|
|
queuingRegion queuingRegion
|
|
}
|
|
|
|
func newQDMeasurement(jqrConfig CongestionSignalConfig, dqrConfig CongestionSignalConfig, jqrMinDelay int64, jqrMinTrendCoefficient float64, dqrMaxDelay int64) *qdMeasurement {
|
|
return &qdMeasurement{
|
|
jqrConfig: jqrConfig,
|
|
dqrConfig: dqrConfig,
|
|
jqrMinDelay: jqrMinDelay,
|
|
jqrMinTrendCoefficient: jqrMinTrendCoefficient,
|
|
dqrMaxDelay: dqrMaxDelay,
|
|
queuingRegion: queuingRegionIndeterminate,
|
|
}
|
|
}
|
|
|
|
func (q *qdMeasurement) ProcessPacketGroup(pg *packetGroup, groupIdx int) {
|
|
if q.isSealed {
|
|
return
|
|
}
|
|
|
|
pqd, pqdOk := pg.FinalizedPropagatedQueuingDelay()
|
|
if !pqdOk {
|
|
return
|
|
}
|
|
|
|
q.numGroups++
|
|
if q.minGroupIdx == 0 || q.minGroupIdx > groupIdx {
|
|
q.minGroupIdx = groupIdx
|
|
}
|
|
q.maxGroupIdx = max(q.maxGroupIdx, groupIdx)
|
|
|
|
minSendTime, maxSendTime := pg.SendWindow()
|
|
if q.minSendTime == 0 || minSendTime < q.minSendTime {
|
|
q.minSendTime = minSendTime
|
|
}
|
|
q.maxSendTime = max(q.maxSendTime, maxSendTime)
|
|
|
|
q.propagatedQueuingDelays = append(q.propagatedQueuingDelays, pqd)
|
|
|
|
switch {
|
|
case pqd < q.dqrMaxDelay:
|
|
q.numDQRGroups++
|
|
if q.numJQRGroups > 0 {
|
|
// broken continuity, seal
|
|
q.isSealed = true
|
|
} else if q.dqrConfig.IsTriggered(q.numDQRGroups, q.maxSendTime-q.minSendTime) {
|
|
q.isSealed = true
|
|
q.queuingRegion = queuingRegionDQR
|
|
}
|
|
|
|
case pqd > q.jqrMinDelay:
|
|
q.numJQRGroups++
|
|
if q.numDQRGroups > 0 {
|
|
// broken continuity, seal
|
|
q.isSealed = true
|
|
} else if q.jqrConfig.IsTriggered(q.numJQRGroups, q.maxSendTime-q.minSendTime) && q.trendCoefficient() > q.jqrMinTrendCoefficient {
|
|
q.isSealed = true
|
|
q.queuingRegion = queuingRegionJQR
|
|
}
|
|
|
|
default:
|
|
if q.numDQRGroups > 0 || q.numJQRGroups > 0 {
|
|
// broken continuity, seal
|
|
q.isSealed = true
|
|
}
|
|
}
|
|
}
|
|
|
|
func (q *qdMeasurement) IsSealed() bool {
|
|
return q.isSealed
|
|
}
|
|
|
|
func (q *qdMeasurement) QueuingRegion() queuingRegion {
|
|
return q.queuingRegion
|
|
}
|
|
|
|
func (q *qdMeasurement) GroupRange() (int, int) {
|
|
return max(0, q.minGroupIdx), max(0, q.maxGroupIdx)
|
|
}
|
|
|
|
func (q *qdMeasurement) MarshalLogObject(e zapcore.ObjectEncoder) error {
|
|
if q == nil {
|
|
return nil
|
|
}
|
|
|
|
e.AddInt("numGroups", q.numGroups)
|
|
e.AddInt("numJQRGroups", q.numJQRGroups)
|
|
e.AddInt("numDQRGroups", q.numDQRGroups)
|
|
e.AddInt64("minSendTime", q.minSendTime)
|
|
e.AddInt64("maxSendTime", q.maxSendTime)
|
|
e.AddArray("propagatedQueuingDelays", logger.Int64Slice(q.propagatedQueuingDelays))
|
|
e.AddFloat64("trendCoefficient", q.trendCoefficient())
|
|
e.AddDuration("duration", time.Duration((q.maxSendTime-q.minSendTime)*1000))
|
|
e.AddBool("isSealed", q.isSealed)
|
|
e.AddInt("minGroupIdx", q.minGroupIdx)
|
|
e.AddInt("maxGroupIdx", q.maxGroupIdx)
|
|
e.AddString("queuingRegion", q.queuingRegion.String())
|
|
return nil
|
|
}
|
|
|
|
func (q *qdMeasurement) trendCoefficient() float64 {
|
|
concordantPairs := 0
|
|
discordantPairs := 0
|
|
|
|
// the packet groups are processed from newest to oldest,
|
|
// so a concordant pair is when the value drops,
|
|
// i. e. the propagated queuing delay is increasing if newer (earlier entity in slice) is higher than older (later entity in slice)
|
|
for i := 0; i < len(q.propagatedQueuingDelays)-1; i++ {
|
|
for j := i + 1; j < len(q.propagatedQueuingDelays); j++ {
|
|
if q.propagatedQueuingDelays[i] > q.propagatedQueuingDelays[j] {
|
|
concordantPairs++
|
|
} else if q.propagatedQueuingDelays[i] < q.propagatedQueuingDelays[j] {
|
|
discordantPairs++
|
|
}
|
|
}
|
|
}
|
|
|
|
if (concordantPairs + discordantPairs) == 0 {
|
|
// if the min requirements is only one sample, trend calculation is not possible, declare highest trend value
|
|
if len(q.propagatedQueuingDelays) == 1 {
|
|
return 1.0
|
|
}
|
|
|
|
return 0.0
|
|
}
|
|
|
|
return (float64(concordantPairs) - float64(discordantPairs)) / (float64(concordantPairs) + float64(discordantPairs))
|
|
}
|
|
|
|
// -------------------------------------------------------------------------------
|
|
|
|
type lossMeasurement struct {
|
|
jqrConfig CongestionSignalConfig
|
|
dqrConfig CongestionSignalConfig
|
|
jqrMinLoss float64
|
|
dqrMaxLoss float64
|
|
|
|
numGroups int
|
|
ts *trafficStats
|
|
|
|
isJQRSealed bool
|
|
isDQRSealed bool
|
|
minGroupIdx int
|
|
maxGroupIdx int
|
|
|
|
weightedLoss float64
|
|
|
|
queuingRegion queuingRegion
|
|
}
|
|
|
|
func newLossMeasurement(
|
|
jqrConfig CongestionSignalConfig,
|
|
dqrConfig CongestionSignalConfig,
|
|
weightedLossConfig WeightedLossConfig,
|
|
jqrMinLoss float64,
|
|
dqrMaxLoss float64,
|
|
logger logger.Logger,
|
|
) *lossMeasurement {
|
|
return &lossMeasurement{
|
|
jqrConfig: jqrConfig,
|
|
dqrConfig: dqrConfig,
|
|
jqrMinLoss: jqrMinLoss,
|
|
dqrMaxLoss: dqrMaxLoss,
|
|
ts: newTrafficStats(trafficStatsParams{
|
|
Config: weightedLossConfig,
|
|
Logger: logger,
|
|
}),
|
|
queuingRegion: queuingRegionIndeterminate,
|
|
}
|
|
}
|
|
|
|
func (l *lossMeasurement) ProcessPacketGroup(pg *packetGroup, groupIdx int) {
|
|
if (l.isJQRSealed && l.isDQRSealed) || !pg.IsFinalized() {
|
|
return
|
|
}
|
|
|
|
l.numGroups++
|
|
if l.minGroupIdx == 0 || l.minGroupIdx > groupIdx {
|
|
l.minGroupIdx = groupIdx
|
|
}
|
|
l.maxGroupIdx = max(l.maxGroupIdx, groupIdx)
|
|
|
|
l.ts.Merge(pg.Traffic())
|
|
|
|
if !l.isJQRSealed && l.jqrConfig.IsTriggered(l.numGroups, l.ts.Duration()) {
|
|
l.isJQRSealed = true
|
|
|
|
weightedLoss := l.ts.WeightedLoss()
|
|
if weightedLoss > l.jqrMinLoss {
|
|
l.weightedLoss = weightedLoss
|
|
l.queuingRegion = queuingRegionJQR
|
|
l.isDQRSealed = true // seal DQR also as queuing region has been determined
|
|
return
|
|
}
|
|
}
|
|
|
|
if !l.isDQRSealed && l.dqrConfig.IsTriggered(l.numGroups, l.ts.Duration()) {
|
|
l.isDQRSealed = true
|
|
|
|
weightedLoss := l.ts.WeightedLoss()
|
|
if weightedLoss < l.dqrMaxLoss {
|
|
l.weightedLoss = weightedLoss
|
|
l.queuingRegion = queuingRegionDQR
|
|
l.isJQRSealed = true // seal JQR also as queuing region has been determined
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (l *lossMeasurement) IsSealed() bool {
|
|
return l.isJQRSealed && l.isDQRSealed
|
|
}
|
|
|
|
func (l *lossMeasurement) QueuingRegion() queuingRegion {
|
|
return l.queuingRegion
|
|
}
|
|
|
|
func (l *lossMeasurement) GroupRange() (int, int) {
|
|
return max(0, l.minGroupIdx), max(0, l.maxGroupIdx)
|
|
}
|
|
|
|
func (l *lossMeasurement) MarshalLogObject(e zapcore.ObjectEncoder) error {
|
|
if l == nil {
|
|
return nil
|
|
}
|
|
|
|
e.AddInt("numGroups", l.numGroups)
|
|
e.AddObject("ts", l.ts)
|
|
e.AddBool("isJQRSealed", l.isJQRSealed)
|
|
e.AddBool("isDQRSealed", l.isDQRSealed)
|
|
e.AddInt("minGroupIdx", l.minGroupIdx)
|
|
e.AddInt("maxGroupIdx", l.maxGroupIdx)
|
|
e.AddFloat64("weightedLoss", l.weightedLoss)
|
|
e.AddString("queuingRegion", l.queuingRegion.String())
|
|
return nil
|
|
}
|
|
|
|
// -------------------------------------------------------------------------------
|
|
|
|
type CongestionDetectorConfig struct {
|
|
PacketGroup PacketGroupConfig `yaml:"packet_group,omitempty"`
|
|
PacketGroupMaxAge time.Duration `yaml:"packet_group_max_age,omitempty"`
|
|
|
|
ProbePacketGroup ProbePacketGroupConfig `yaml:"probe_packet_group,omitempty"`
|
|
ProbeRegulator ccutils.ProbeRegulatorConfig `yaml:"probe_regulator,omitempty"`
|
|
ProbeSignal ProbeSignalConfig `yaml:"probe_signal,omitempty"`
|
|
|
|
JQRMinDelay time.Duration `yaml:"jqr_min_delay,omitempty"`
|
|
JQRMinTrendCoefficient float64 `yaml:"jqr_min_trend_coefficient,omitempty"`
|
|
DQRMaxDelay time.Duration `yaml:"dqr_max_delay,omitempty"`
|
|
|
|
WeightedLoss WeightedLossConfig `yaml:"weighted_loss,omitempty"`
|
|
JQRMinWeightedLoss float64 `yaml:"jqr_min_weighted_loss,omitempty"`
|
|
DQRMaxWeightedLoss float64 `yaml:"dqr_max_weighted_loss,omitempty"`
|
|
|
|
QueuingDelayEarlyWarningJQR CongestionSignalConfig `yaml:"queuing_delay_early_warning_jqr,omitempty"`
|
|
QueuingDelayEarlyWarningDQR CongestionSignalConfig `yaml:"queuing_delay_early_warning_dqr,omitempty"`
|
|
LossEarlyWarningJQR CongestionSignalConfig `yaml:"loss_early_warning_jqr,omitempty"`
|
|
LossEarlyWarningDQR CongestionSignalConfig `yaml:"loss_early_warning_dqr,omitempty"`
|
|
|
|
QueuingDelayCongestedJQR CongestionSignalConfig `yaml:"queuing_delay_congested_jqr,omitempty"`
|
|
QueuingDelayCongestedDQR CongestionSignalConfig `yaml:"queuing_delay_congested_dqr,omitempty"`
|
|
LossCongestedJQR CongestionSignalConfig `yaml:"loss_congested_jqr,omitempty"`
|
|
LossCongestedDQR CongestionSignalConfig `yaml:"loss_congested_dqr,omitempty"`
|
|
|
|
CongestedCTRTrend ccutils.TrendDetectorConfig `yaml:"congested_ctr_trend,omitempty"`
|
|
CongestedCTREpsilon float64 `yaml:"congested_ctr_epsilon,omitempty"`
|
|
CongestedPacketGroup PacketGroupConfig `yaml:"congested_packet_group,omitempty"`
|
|
|
|
EstimationWindowDuration time.Duration `yaml:"estimaton_window_duration,omitempty"`
|
|
}
|
|
|
|
var (
|
|
defaultTrendDetectorConfigCongestedCTR = ccutils.TrendDetectorConfig{
|
|
RequiredSamples: 4,
|
|
RequiredSamplesMin: 2,
|
|
DownwardTrendThreshold: -0.5,
|
|
DownwardTrendMaxWait: 2 * time.Second,
|
|
CollapseThreshold: 500 * time.Millisecond,
|
|
ValidityWindow: 10 * time.Second,
|
|
}
|
|
|
|
defaultCongestedPacketGroupConfig = PacketGroupConfig{
|
|
MinPackets: 20,
|
|
MaxWindowDuration: 150 * time.Millisecond,
|
|
}
|
|
|
|
defaultCongestionDetectorConfig = CongestionDetectorConfig{
|
|
PacketGroup: defaultPacketGroupConfig,
|
|
PacketGroupMaxAge: 10 * time.Second,
|
|
|
|
ProbePacketGroup: defaultProbePacketGroupConfig,
|
|
ProbeRegulator: ccutils.DefaultProbeRegulatorConfig,
|
|
ProbeSignal: defaultProbeSignalConfig,
|
|
|
|
JQRMinDelay: 50 * time.Millisecond,
|
|
JQRMinTrendCoefficient: 0.8,
|
|
DQRMaxDelay: 20 * time.Millisecond,
|
|
|
|
WeightedLoss: defaultWeightedLossConfig,
|
|
JQRMinWeightedLoss: 0.25,
|
|
DQRMaxWeightedLoss: 0.1,
|
|
|
|
QueuingDelayEarlyWarningJQR: defaultQueuingDelayEarlyWarningJQRConfig,
|
|
QueuingDelayEarlyWarningDQR: defaultQueuingDelayEarlyWarningDQRConfig,
|
|
LossEarlyWarningJQR: defaultLossEarlyWarningJQRConfig,
|
|
LossEarlyWarningDQR: defaultLossEarlyWarningDQRConfig,
|
|
|
|
QueuingDelayCongestedJQR: defaultQueuingDelayCongestedJQRConfig,
|
|
QueuingDelayCongestedDQR: defaultQueuingDelayCongestedDQRConfig,
|
|
LossCongestedJQR: defaultLossCongestedJQRConfig,
|
|
LossCongestedDQR: defaultLossCongestedDQRConfig,
|
|
|
|
CongestedCTRTrend: defaultTrendDetectorConfigCongestedCTR,
|
|
CongestedCTREpsilon: 0.05,
|
|
CongestedPacketGroup: defaultCongestedPacketGroupConfig,
|
|
|
|
EstimationWindowDuration: time.Second,
|
|
}
|
|
)
|
|
|
|
// -------------------------------------------------------------------------------
|
|
|
|
type congestionDetectorParams struct {
|
|
Config CongestionDetectorConfig
|
|
Logger logger.Logger
|
|
}
|
|
|
|
type congestionDetector struct {
|
|
params congestionDetectorParams
|
|
|
|
lock sync.Mutex
|
|
|
|
rtt float64
|
|
|
|
*packetTracker
|
|
twccFeedback *twccFeedback
|
|
|
|
packetGroups []*packetGroup
|
|
|
|
probePacketGroup *probePacketGroup
|
|
probeRegulator *ccutils.ProbeRegulator
|
|
|
|
estimatedAvailableChannelCapacity int64
|
|
estimateTrafficStats *trafficStats
|
|
|
|
congestionState bwe.CongestionState
|
|
congestionStateSwitchedAt time.Time
|
|
|
|
congestedCTRTrend *ccutils.TrendDetector[float64]
|
|
congestedTrafficStats *trafficStats
|
|
congestedPacketGroup *packetGroup
|
|
|
|
congestionReason congestionReason
|
|
qdMeasurement *qdMeasurement
|
|
lossMeasurement *lossMeasurement
|
|
|
|
bweListener bwe.BWEListener
|
|
}
|
|
|
|
func newCongestionDetector(params congestionDetectorParams) *congestionDetector {
|
|
c := &congestionDetector{
|
|
params: params,
|
|
packetTracker: newPacketTracker(packetTrackerParams{Logger: params.Logger}),
|
|
twccFeedback: newTWCCFeedback(twccFeedbackParams{Logger: params.Logger}),
|
|
}
|
|
c.Reset()
|
|
|
|
return c
|
|
}
|
|
|
|
func (c *congestionDetector) Reset() {
|
|
c.lock.Lock()
|
|
defer c.lock.Unlock()
|
|
|
|
c.rtt = bwe.DefaultRTT
|
|
|
|
c.packetGroups = nil
|
|
|
|
c.probePacketGroup = nil
|
|
c.probeRegulator = ccutils.NewProbeRegulator(ccutils.ProbeRegulatorParams{
|
|
Config: c.params.Config.ProbeRegulator,
|
|
Logger: c.params.Logger,
|
|
})
|
|
|
|
c.estimatedAvailableChannelCapacity = 100_000_000
|
|
c.estimateTrafficStats = nil
|
|
|
|
c.congestionState = bwe.CongestionStateNone
|
|
c.congestionStateSwitchedAt = mono.Now()
|
|
|
|
c.clearCTRTrend()
|
|
|
|
c.congestionReason = congestionReasonNone
|
|
c.qdMeasurement = nil
|
|
c.lossMeasurement = nil
|
|
}
|
|
|
|
func (c *congestionDetector) SetBWEListener(bweListener bwe.BWEListener) {
|
|
c.lock.Lock()
|
|
defer c.lock.Unlock()
|
|
|
|
c.bweListener = bweListener
|
|
}
|
|
|
|
func (c *congestionDetector) getBWEListener() bwe.BWEListener {
|
|
c.lock.Lock()
|
|
defer c.lock.Unlock()
|
|
|
|
return c.bweListener
|
|
}
|
|
|
|
func (c *congestionDetector) HandleTWCCFeedback(report *rtcp.TransportLayerCC) {
|
|
c.lock.Lock()
|
|
recvRefTime, _ := c.twccFeedback.ProcessReport(report, mono.Now())
|
|
|
|
if len(c.packetGroups) == 0 {
|
|
c.packetGroups = append(
|
|
c.packetGroups,
|
|
newPacketGroup(
|
|
packetGroupParams{
|
|
Config: c.params.Config.PacketGroup,
|
|
WeightedLoss: c.params.Config.WeightedLoss,
|
|
Logger: c.params.Logger,
|
|
},
|
|
0,
|
|
),
|
|
)
|
|
}
|
|
|
|
pg := c.packetGroups[len(c.packetGroups)-1]
|
|
trackPacketGroup := func(pi *packetInfo, sendDelta, recvDelta int64, isLost bool) {
|
|
if pi == nil {
|
|
return
|
|
}
|
|
|
|
c.updateCTRTrend(pi, sendDelta, recvDelta, isLost)
|
|
|
|
if c.probePacketGroup != nil {
|
|
c.probePacketGroup.Add(pi, sendDelta, recvDelta, isLost)
|
|
}
|
|
|
|
err := pg.Add(pi, sendDelta, recvDelta, isLost)
|
|
if err == nil {
|
|
return
|
|
}
|
|
|
|
if err == errGroupFinalized {
|
|
// previous group ended, start a new group
|
|
pg = newPacketGroup(
|
|
packetGroupParams{
|
|
Config: c.params.Config.PacketGroup,
|
|
WeightedLoss: c.params.Config.WeightedLoss,
|
|
Logger: c.params.Logger,
|
|
},
|
|
pg.PropagatedQueuingDelay(),
|
|
)
|
|
c.packetGroups = append(c.packetGroups, pg)
|
|
|
|
if err = pg.Add(pi, sendDelta, recvDelta, isLost); err != nil {
|
|
c.params.Logger.Warnw("send side bwe: could not add packet to new packet group", err, "packetInfo", pi, "packetGroup", pg)
|
|
}
|
|
return
|
|
}
|
|
|
|
// try an older group
|
|
for idx := len(c.packetGroups) - 2; idx >= 0; idx-- {
|
|
opg := c.packetGroups[idx]
|
|
if err := opg.Add(pi, sendDelta, recvDelta, isLost); err == nil {
|
|
return
|
|
} else if err == errGroupFinalized {
|
|
c.params.Logger.Infow("send side bwe: unexpected finalized group", "packetInfo", pi, "packetGroup", opg)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 1. go through the TWCC feedback report and record receive time as reported by remote
|
|
// 2. process acknowledged packet and group them
|
|
//
|
|
// losses are not recorded if a feedback report is completely lost.
|
|
// RFC recommends treating lost reports by ignoring packets that would have been in it.
|
|
// -----------------------------------------------------------------------------------
|
|
// | From a congestion control perspective, lost feedback messages are |
|
|
// | handled by ignoring packets which would have been reported as lost or |
|
|
// | received in the lost feedback messages. This behavior is similar to |
|
|
// | how a lost RTCP receiver report is handled. |
|
|
// -----------------------------------------------------------------------------------
|
|
// Reference: https://datatracker.ietf.org/doc/html/draft-holmer-rmcat-transport-wide-cc-extensions-01#page-4
|
|
sequenceNumber := report.BaseSequenceNumber
|
|
endSequenceNumberExclusive := sequenceNumber + report.PacketStatusCount
|
|
deltaIdx := 0
|
|
processSymbol := func(symbol uint16) {
|
|
recvTime := int64(0)
|
|
isLost := false
|
|
if symbol != rtcp.TypeTCCPacketNotReceived {
|
|
recvRefTime += report.RecvDeltas[deltaIdx].Delta
|
|
deltaIdx++
|
|
|
|
recvTime = recvRefTime
|
|
} else {
|
|
isLost = true
|
|
}
|
|
pi, sendDelta, recvDelta := c.packetTracker.RecordPacketIndicationFromRemote(sequenceNumber, recvTime)
|
|
if pi.sendTime != 0 {
|
|
trackPacketGroup(&pi, sendDelta, recvDelta, isLost)
|
|
}
|
|
sequenceNumber++
|
|
}
|
|
for _, chunk := range report.PacketChunks {
|
|
if sequenceNumber == endSequenceNumberExclusive {
|
|
break
|
|
}
|
|
|
|
switch chunk := chunk.(type) {
|
|
case *rtcp.RunLengthChunk:
|
|
for i := uint16(0); i < chunk.RunLength; i++ {
|
|
if sequenceNumber == endSequenceNumberExclusive {
|
|
break
|
|
}
|
|
|
|
processSymbol(chunk.PacketStatusSymbol)
|
|
}
|
|
|
|
case *rtcp.StatusVectorChunk:
|
|
for _, symbol := range chunk.SymbolList {
|
|
if sequenceNumber == endSequenceNumberExclusive {
|
|
break
|
|
}
|
|
|
|
processSymbol(symbol)
|
|
}
|
|
}
|
|
}
|
|
|
|
c.prunePacketGroups()
|
|
shouldNotify, fromState, toState, committedChannelCapacity := c.congestionDetectionStateMachine()
|
|
c.lock.Unlock()
|
|
|
|
if shouldNotify {
|
|
if bweListener := c.getBWEListener(); bweListener != nil {
|
|
bweListener.OnCongestionStateChange(fromState, toState, committedChannelCapacity)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *congestionDetector) UpdateRTT(rtt float64) {
|
|
c.lock.Lock()
|
|
defer c.lock.Unlock()
|
|
|
|
if rtt == 0 {
|
|
c.rtt = bwe.DefaultRTT
|
|
} else {
|
|
if c.rtt == 0 {
|
|
c.rtt = rtt
|
|
} else {
|
|
c.rtt = bwe.RTTSmoothingFactor*rtt + (1.0-bwe.RTTSmoothingFactor)*c.rtt
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *congestionDetector) CongestionState() bwe.CongestionState {
|
|
c.lock.Lock()
|
|
defer c.lock.Unlock()
|
|
|
|
return c.congestionState
|
|
}
|
|
|
|
func (c *congestionDetector) CanProbe() bool {
|
|
c.lock.Lock()
|
|
defer c.lock.Unlock()
|
|
|
|
return c.congestionState == bwe.CongestionStateNone && c.probePacketGroup == nil && c.probeRegulator.CanProbe()
|
|
}
|
|
|
|
func (c *congestionDetector) ProbeDuration() time.Duration {
|
|
c.lock.Lock()
|
|
defer c.lock.Unlock()
|
|
|
|
return c.probeRegulator.ProbeDuration()
|
|
}
|
|
|
|
func (c *congestionDetector) ProbeClusterStarting(pci ccutils.ProbeClusterInfo) {
|
|
c.lock.Lock()
|
|
defer c.lock.Unlock()
|
|
|
|
c.probePacketGroup = newProbePacketGroup(
|
|
probePacketGroupParams{
|
|
Config: c.params.Config.ProbePacketGroup,
|
|
WeightedLoss: c.params.Config.WeightedLoss,
|
|
Logger: c.params.Logger,
|
|
},
|
|
pci,
|
|
)
|
|
|
|
c.packetTracker.ProbeClusterStarting(pci.Id)
|
|
}
|
|
|
|
func (c *congestionDetector) ProbeClusterDone(pci ccutils.ProbeClusterInfo) {
|
|
c.lock.Lock()
|
|
defer c.lock.Unlock()
|
|
|
|
c.packetTracker.ProbeClusterDone(pci.Id)
|
|
if c.probePacketGroup != nil {
|
|
c.probePacketGroup.ProbeClusterDone(pci)
|
|
}
|
|
}
|
|
|
|
func (c *congestionDetector) ProbeClusterIsGoalReached() bool {
|
|
c.lock.Lock()
|
|
defer c.lock.Unlock()
|
|
|
|
if c.probePacketGroup == nil || c.congestionState != bwe.CongestionStateNone {
|
|
return false
|
|
}
|
|
|
|
pci := c.probePacketGroup.ProbeClusterInfo()
|
|
if !c.params.Config.ProbeSignal.IsValid(pci) {
|
|
return false
|
|
}
|
|
|
|
probeSignal, estimatedAvailableChannelCapacity := c.params.Config.ProbeSignal.ProbeSignal(c.probePacketGroup)
|
|
return probeSignal != ccutils.ProbeSignalNotCongesting && estimatedAvailableChannelCapacity > int64(pci.Goal.DesiredBps)
|
|
}
|
|
|
|
func (c *congestionDetector) ProbeClusterFinalize() (ccutils.ProbeSignal, int64, bool) {
|
|
c.lock.Lock()
|
|
defer c.lock.Unlock()
|
|
|
|
if c.probePacketGroup == nil {
|
|
return ccutils.ProbeSignalInconclusive, 0, false
|
|
}
|
|
|
|
pci, isFinalized := c.probePacketGroup.MaybeFinalizeProbe(c.packetTracker.ProbeMaxSequenceNumber(), c.rtt)
|
|
if !isFinalized {
|
|
return ccutils.ProbeSignalInconclusive, 0, isFinalized
|
|
}
|
|
|
|
isSignalValid := c.params.Config.ProbeSignal.IsValid(pci)
|
|
c.params.Logger.Infow(
|
|
"send side bwe: probe finalized",
|
|
"isSignalValid", isSignalValid,
|
|
"probeClusterInfo", pci,
|
|
"probePacketGroup", c.probePacketGroup,
|
|
"congestionState", c.congestionState,
|
|
"rtt", c.rtt,
|
|
)
|
|
|
|
// if congestion signal changed during probe, defer to that signal
|
|
if c.congestionState != bwe.CongestionStateNone {
|
|
probeSignal := ccutils.ProbeSignalCongesting
|
|
c.probeRegulator.ProbeSignal(probeSignal, pci.CreatedAt)
|
|
c.probePacketGroup = nil
|
|
return probeSignal, c.estimatedAvailableChannelCapacity, true
|
|
}
|
|
|
|
probeSignal, estimatedAvailableChannelCapacity := c.params.Config.ProbeSignal.ProbeSignal(c.probePacketGroup)
|
|
if probeSignal == ccutils.ProbeSignalNotCongesting && estimatedAvailableChannelCapacity > c.estimatedAvailableChannelCapacity {
|
|
c.estimatedAvailableChannelCapacity = estimatedAvailableChannelCapacity
|
|
}
|
|
|
|
c.probeRegulator.ProbeSignal(probeSignal, pci.CreatedAt)
|
|
c.probePacketGroup = nil
|
|
return probeSignal, c.estimatedAvailableChannelCapacity, true
|
|
}
|
|
|
|
func (c *congestionDetector) prunePacketGroups() {
|
|
if len(c.packetGroups) == 0 {
|
|
return
|
|
}
|
|
|
|
threshold, ok := c.packetTracker.BaseSendTimeThreshold(c.params.Config.PacketGroupMaxAge.Microseconds())
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
for idx, pg := range c.packetGroups {
|
|
if mst := pg.MinSendTime(); mst > threshold {
|
|
c.packetGroups = c.packetGroups[idx:]
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *congestionDetector) updateCongestionSignal(
|
|
qdJQRConfig CongestionSignalConfig,
|
|
qdDQRConfig CongestionSignalConfig,
|
|
lossJQRConfig CongestionSignalConfig,
|
|
lossDQRConfig CongestionSignalConfig,
|
|
) queuingRegion {
|
|
c.qdMeasurement = newQDMeasurement(
|
|
qdJQRConfig,
|
|
qdDQRConfig,
|
|
c.params.Config.JQRMinDelay.Microseconds(),
|
|
c.params.Config.JQRMinTrendCoefficient,
|
|
c.params.Config.DQRMaxDelay.Microseconds(),
|
|
)
|
|
c.lossMeasurement = newLossMeasurement(
|
|
lossJQRConfig,
|
|
lossDQRConfig,
|
|
c.params.Config.WeightedLoss,
|
|
c.params.Config.JQRMinWeightedLoss,
|
|
c.params.Config.DQRMaxWeightedLoss,
|
|
c.params.Logger,
|
|
)
|
|
|
|
var idx int
|
|
for idx = len(c.packetGroups) - 1; idx >= 0; idx-- {
|
|
pg := c.packetGroups[idx]
|
|
c.qdMeasurement.ProcessPacketGroup(pg, idx)
|
|
c.lossMeasurement.ProcessPacketGroup(pg, idx)
|
|
|
|
// if both measurements have enough data to make a decision, stop processing groups
|
|
if c.qdMeasurement.IsSealed() && c.lossMeasurement.IsSealed() {
|
|
break
|
|
}
|
|
}
|
|
|
|
qr := queuingRegionIndeterminate
|
|
qdQueuingRegion := c.qdMeasurement.QueuingRegion()
|
|
lossQueuingRegion := c.lossMeasurement.QueuingRegion()
|
|
switch {
|
|
case qdQueuingRegion == queuingRegionJQR:
|
|
qr = queuingRegionJQR
|
|
c.congestionReason = congestionReasonQueuingDelay
|
|
case lossQueuingRegion == queuingRegionJQR:
|
|
qr = queuingRegionJQR
|
|
c.congestionReason = congestionReasonLoss
|
|
case qdQueuingRegion == queuingRegionDQR && lossQueuingRegion == queuingRegionDQR:
|
|
qr = queuingRegionDQR
|
|
c.congestionReason = congestionReasonNone
|
|
}
|
|
|
|
return qr
|
|
}
|
|
|
|
func (c *congestionDetector) updateEarlyWarningSignal() queuingRegion {
|
|
return c.updateCongestionSignal(
|
|
c.params.Config.QueuingDelayEarlyWarningJQR,
|
|
c.params.Config.QueuingDelayEarlyWarningDQR,
|
|
c.params.Config.LossEarlyWarningJQR,
|
|
c.params.Config.LossEarlyWarningDQR,
|
|
)
|
|
}
|
|
|
|
func (c *congestionDetector) updateCongestedSignal() queuingRegion {
|
|
return c.updateCongestionSignal(
|
|
c.params.Config.QueuingDelayCongestedJQR,
|
|
c.params.Config.QueuingDelayCongestedDQR,
|
|
c.params.Config.LossCongestedJQR,
|
|
c.params.Config.LossCongestedDQR,
|
|
)
|
|
}
|
|
|
|
func (c *congestionDetector) congestionDetectionStateMachine() (bool, bwe.CongestionState, bwe.CongestionState, int64) {
|
|
fromState := c.congestionState
|
|
toState := c.congestionState
|
|
|
|
switch fromState {
|
|
case bwe.CongestionStateNone:
|
|
if c.updateEarlyWarningSignal() == queuingRegionJQR {
|
|
toState = bwe.CongestionStateEarlyWarning
|
|
}
|
|
|
|
case bwe.CongestionStateEarlyWarning:
|
|
if c.updateCongestedSignal() == queuingRegionJQR {
|
|
toState = bwe.CongestionStateCongested
|
|
} else if c.updateEarlyWarningSignal() == queuingRegionDQR {
|
|
toState = bwe.CongestionStateNone
|
|
}
|
|
|
|
case bwe.CongestionStateCongested:
|
|
if c.updateCongestedSignal() == queuingRegionDQR {
|
|
toState = bwe.CongestionStateNone
|
|
}
|
|
}
|
|
|
|
shouldNotify := false
|
|
if toState != fromState {
|
|
c.estimateAvailableChannelCapacity()
|
|
fromState, toState = c.updateCongestionState(toState)
|
|
shouldNotify = true
|
|
}
|
|
|
|
if c.congestedCTRTrend != nil && c.congestedCTRTrend.GetDirection() == ccutils.TrendDirectionDownward {
|
|
congestedAckedBitrate := c.congestedTrafficStats.AcknowledgedBitrate()
|
|
if congestedAckedBitrate < c.estimatedAvailableChannelCapacity {
|
|
c.estimatedAvailableChannelCapacity = congestedAckedBitrate
|
|
|
|
c.params.Logger.Infow(
|
|
"send side bwe: captured traffic ratio is trending downward",
|
|
"channel", c.congestedCTRTrend,
|
|
"trafficStats", c.congestedTrafficStats,
|
|
"estimatedAvailableChannelCapacity", c.estimatedAvailableChannelCapacity,
|
|
)
|
|
|
|
shouldNotify = true
|
|
}
|
|
|
|
// reset to get new set of samples for next trend
|
|
c.resetCTRTrend()
|
|
}
|
|
|
|
return shouldNotify, fromState, toState, c.estimatedAvailableChannelCapacity
|
|
}
|
|
|
|
func (c *congestionDetector) createCTRTrend() {
|
|
c.resetCTRTrend()
|
|
c.congestedPacketGroup = nil
|
|
}
|
|
|
|
func (c *congestionDetector) resetCTRTrend() {
|
|
c.congestedCTRTrend = ccutils.NewTrendDetector[float64](ccutils.TrendDetectorParams{
|
|
Name: "ssbwe-ctr",
|
|
Logger: c.params.Logger,
|
|
Config: c.params.Config.CongestedCTRTrend,
|
|
})
|
|
c.congestedTrafficStats = newTrafficStats(trafficStatsParams{
|
|
Config: c.params.Config.WeightedLoss,
|
|
Logger: c.params.Logger,
|
|
})
|
|
}
|
|
|
|
func (c *congestionDetector) clearCTRTrend() {
|
|
c.congestedCTRTrend = nil
|
|
c.congestedTrafficStats = nil
|
|
c.congestedPacketGroup = nil
|
|
}
|
|
|
|
func (c *congestionDetector) updateCTRTrend(pi *packetInfo, sendDelta, recvDelta int64, isLost bool) {
|
|
if c.congestedCTRTrend == nil {
|
|
return
|
|
}
|
|
|
|
if c.congestedPacketGroup == nil {
|
|
c.congestedPacketGroup = newPacketGroup(
|
|
packetGroupParams{
|
|
Config: c.params.Config.CongestedPacketGroup,
|
|
WeightedLoss: c.params.Config.WeightedLoss,
|
|
Logger: c.params.Logger,
|
|
},
|
|
0,
|
|
)
|
|
}
|
|
|
|
if err := c.congestedPacketGroup.Add(pi, sendDelta, recvDelta, isLost); err == errGroupFinalized {
|
|
// progressively keep increasing the window and make measurements over longer windows,
|
|
// if congestion is not relieving, CTR will trend down
|
|
c.congestedTrafficStats.Merge(c.congestedPacketGroup.Traffic())
|
|
|
|
ts := newTrafficStats(trafficStatsParams{
|
|
Config: c.params.Config.WeightedLoss,
|
|
Logger: c.params.Logger,
|
|
})
|
|
ts.Merge(c.congestedPacketGroup.Traffic())
|
|
ctr := ts.CapturedTrafficRatio()
|
|
|
|
// quantise CTR to filter out small changes
|
|
c.congestedCTRTrend.AddValue(float64(int((ctr+(c.params.Config.CongestedCTREpsilon/2))/c.params.Config.CongestedCTREpsilon)) * c.params.Config.CongestedCTREpsilon)
|
|
|
|
c.congestedPacketGroup = newPacketGroup(
|
|
packetGroupParams{
|
|
Config: c.params.Config.CongestedPacketGroup,
|
|
WeightedLoss: c.params.Config.WeightedLoss,
|
|
Logger: c.params.Logger,
|
|
},
|
|
c.congestedPacketGroup.PropagatedQueuingDelay(),
|
|
)
|
|
}
|
|
}
|
|
|
|
func (c *congestionDetector) estimateAvailableChannelCapacity() {
|
|
c.estimateTrafficStats = nil
|
|
if len(c.packetGroups) == 0 || c.probePacketGroup != nil {
|
|
return
|
|
}
|
|
|
|
// when congested, use contributing groups,
|
|
// else use a time windowed measurement
|
|
useWindow := false
|
|
isAggValid := true
|
|
minGroupIdx := 0
|
|
maxGroupIdx := len(c.packetGroups) - 1
|
|
switch c.congestionReason {
|
|
case congestionReasonQueuingDelay:
|
|
minGroupIdx, maxGroupIdx = c.qdMeasurement.GroupRange()
|
|
case congestionReasonLoss:
|
|
minGroupIdx, maxGroupIdx = c.lossMeasurement.GroupRange()
|
|
default:
|
|
useWindow = true
|
|
isAggValid = false
|
|
}
|
|
|
|
agg := newTrafficStats(trafficStatsParams{
|
|
Config: c.params.Config.WeightedLoss,
|
|
Logger: c.params.Logger,
|
|
})
|
|
for idx := maxGroupIdx; idx >= minGroupIdx; idx-- {
|
|
pg := c.packetGroups[idx]
|
|
if !pg.IsFinalized() {
|
|
continue
|
|
}
|
|
|
|
agg.Merge(pg.Traffic())
|
|
if useWindow && agg.Duration() > c.params.Config.EstimationWindowDuration.Microseconds() {
|
|
isAggValid = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if isAggValid {
|
|
c.estimatedAvailableChannelCapacity = agg.AcknowledgedBitrate()
|
|
c.estimateTrafficStats = agg
|
|
}
|
|
}
|
|
|
|
func (c *congestionDetector) updateCongestionState(state bwe.CongestionState) (bwe.CongestionState, bwe.CongestionState) {
|
|
loggingFields := []any{
|
|
"from", c.congestionState,
|
|
"to", state,
|
|
"congestionReason", c.congestionReason,
|
|
"qdMeasurement", c.qdMeasurement,
|
|
"lossMeasurement", c.lossMeasurement,
|
|
"numPacketGroups", len(c.packetGroups),
|
|
"estimatedAvailableChannelCapacity", c.estimatedAvailableChannelCapacity,
|
|
"estimateTrafficStats", c.estimateTrafficStats,
|
|
}
|
|
if c.congestionReason != congestionReasonNone {
|
|
var minGroupIdx, maxGroupIdx int
|
|
switch c.congestionReason {
|
|
case congestionReasonQueuingDelay:
|
|
minGroupIdx, maxGroupIdx = c.qdMeasurement.GroupRange()
|
|
case congestionReasonLoss:
|
|
minGroupIdx, maxGroupIdx = c.lossMeasurement.GroupRange()
|
|
}
|
|
loggingFields = append(
|
|
loggingFields,
|
|
"contributingGroups", logger.ObjectSlice(c.packetGroups[minGroupIdx:maxGroupIdx+1]),
|
|
)
|
|
}
|
|
c.params.Logger.Infow("send side bwe: congestion state change", loggingFields...)
|
|
|
|
if state != c.congestionState {
|
|
c.congestionStateSwitchedAt = mono.Now()
|
|
}
|
|
|
|
fromState := c.congestionState
|
|
c.congestionState = state
|
|
|
|
// when in congested state, monitor changes in captured traffic ratio (CTR)
|
|
// to ensure allocations are in line with latest estimates, it is possible that
|
|
// the estimate is incorrect when congestion starts and the allocation may be
|
|
// sub-optimal and not enough to reduce/relieve congestion, by monitoring CTR
|
|
// on a continuous basis allocations can be adjusted in the direction of
|
|
// reducing/relieving congestion
|
|
if state == bwe.CongestionStateCongested && fromState != bwe.CongestionStateCongested {
|
|
c.createCTRTrend()
|
|
} else if state != bwe.CongestionStateCongested {
|
|
c.clearCTRTrend()
|
|
}
|
|
|
|
return fromState, c.congestionState
|
|
}
|