Squashed 'livekit-server/' content from commit 154b4d26
git-subtree-dir: livekit-server git-subtree-split: 154b4d26b769c68a03c096124094b97bf61a996f
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
// 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 bwe
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/sfu/ccutils"
|
||||
"github.com/pion/rtcp"
|
||||
)
|
||||
|
||||
// ------------------------------------------------
|
||||
|
||||
const (
|
||||
DefaultRTT = float64(0.070) // 70 ms
|
||||
RTTSmoothingFactor = float64(0.5)
|
||||
)
|
||||
|
||||
// ------------------------------------------------
|
||||
|
||||
type CongestionState int
|
||||
|
||||
const (
|
||||
CongestionStateNone CongestionState = iota
|
||||
CongestionStateEarlyWarning
|
||||
CongestionStateCongested
|
||||
)
|
||||
|
||||
func (c CongestionState) String() string {
|
||||
switch c {
|
||||
case CongestionStateNone:
|
||||
return "NONE"
|
||||
case CongestionStateEarlyWarning:
|
||||
return "EARLY_WARNING"
|
||||
case CongestionStateCongested:
|
||||
return "CONGESTED"
|
||||
default:
|
||||
return fmt.Sprintf("%d", int(c))
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------
|
||||
|
||||
type BWE interface {
|
||||
SetBWEListener(bweListner BWEListener)
|
||||
|
||||
Reset()
|
||||
|
||||
HandleREMB(
|
||||
receivedEstimate int64,
|
||||
expectedBandwidthUsage int64,
|
||||
sentPackets uint32,
|
||||
repeatedNacks uint32,
|
||||
)
|
||||
|
||||
// TWCC sequence number
|
||||
RecordPacketSendAndGetSequenceNumber(
|
||||
atMicro int64,
|
||||
size int,
|
||||
isRTX bool,
|
||||
probeClusterId ccutils.ProbeClusterId,
|
||||
isProbe bool,
|
||||
) uint16
|
||||
|
||||
HandleTWCCFeedback(report *rtcp.TransportLayerCC)
|
||||
|
||||
UpdateRTT(rtt float64)
|
||||
|
||||
CongestionState() CongestionState
|
||||
|
||||
CanProbe() bool
|
||||
ProbeDuration() time.Duration
|
||||
ProbeClusterStarting(pci ccutils.ProbeClusterInfo)
|
||||
ProbeClusterDone(pci ccutils.ProbeClusterInfo)
|
||||
ProbeClusterIsGoalReached() bool
|
||||
ProbeClusterFinalize() (ccutils.ProbeSignal, int64, bool)
|
||||
}
|
||||
|
||||
// ------------------------------------------------
|
||||
|
||||
type BWEListener interface {
|
||||
OnCongestionStateChange(fromState CongestionState, toState CongestionState, estimatedAvailableChannelCapacity int64)
|
||||
}
|
||||
|
||||
// ------------------------------------------------
|
||||
@@ -0,0 +1,77 @@
|
||||
// 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 bwe
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/sfu/ccutils"
|
||||
"github.com/pion/rtcp"
|
||||
)
|
||||
|
||||
type NullBWE struct {
|
||||
}
|
||||
|
||||
func (n *NullBWE) SetBWEListener(_bweListener BWEListener) {}
|
||||
|
||||
func (n *NullBWE) Reset() {}
|
||||
|
||||
func (n *NullBWE) RecordPacketSendAndGetSequenceNumber(
|
||||
_atMicro int64,
|
||||
_size int,
|
||||
_isRTX bool,
|
||||
_probeClusterId ccutils.ProbeClusterId,
|
||||
_isProbe bool,
|
||||
) uint16 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (n *NullBWE) HandleREMB(
|
||||
_receivedEstimate int64,
|
||||
_expectedBandwidthUsage int64,
|
||||
_sentPackets uint32,
|
||||
_repeatedNacks uint32,
|
||||
) {
|
||||
}
|
||||
|
||||
func (n *NullBWE) HandleTWCCFeedback(_report *rtcp.TransportLayerCC) {}
|
||||
|
||||
func (n *NullBWE) UpdateRTT(rtt float64) {}
|
||||
|
||||
func (n *NullBWE) CongestionState() CongestionState {
|
||||
return CongestionStateNone
|
||||
}
|
||||
|
||||
func (n *NullBWE) CanProbe() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (n *NullBWE) ProbeDuration() time.Duration {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (n *NullBWE) ProbeClusterStarting(_pci ccutils.ProbeClusterInfo) {}
|
||||
|
||||
func (n *NullBWE) ProbeClusterDone(_pci ccutils.ProbeClusterInfo) {}
|
||||
|
||||
func (n *NullBWE) ProbeClusterIsGoalReached() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (n *NullBWE) ProbeClusterFinalize() (ccutils.ProbeSignal, int64, bool) {
|
||||
return ccutils.ProbeSignalInconclusive, 0, false
|
||||
}
|
||||
|
||||
// ------------------------------------------------
|
||||
@@ -0,0 +1,202 @@
|
||||
// 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 remotebwe
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap/zapcore"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/sfu/ccutils"
|
||||
"github.com/livekit/protocol/logger"
|
||||
)
|
||||
|
||||
// ------------------------------------------------
|
||||
|
||||
type channelTrend int
|
||||
|
||||
const (
|
||||
channelTrendInconclusive channelTrend = iota
|
||||
channelTrendClearing
|
||||
channelTrendCongesting
|
||||
)
|
||||
|
||||
func (c channelTrend) String() string {
|
||||
switch c {
|
||||
case channelTrendInconclusive:
|
||||
return "INCONCLUSIVE"
|
||||
case channelTrendClearing:
|
||||
return "CLEARING"
|
||||
case channelTrendCongesting:
|
||||
return "CONGESTING"
|
||||
default:
|
||||
return fmt.Sprintf("%d", int(c))
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------
|
||||
|
||||
type channelCongestionReason int
|
||||
|
||||
const (
|
||||
channelCongestionReasonNone channelCongestionReason = iota
|
||||
channelCongestionReasonEstimate
|
||||
channelCongestionReasonLoss
|
||||
)
|
||||
|
||||
func (c channelCongestionReason) String() string {
|
||||
switch c {
|
||||
case channelCongestionReasonNone:
|
||||
return "NONE"
|
||||
case channelCongestionReasonEstimate:
|
||||
return "ESTIMATE"
|
||||
case channelCongestionReasonLoss:
|
||||
return "LOSS"
|
||||
default:
|
||||
return fmt.Sprintf("%d", int(c))
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------
|
||||
|
||||
type ChannelObserverConfig struct {
|
||||
Estimate ccutils.TrendDetectorConfig `yaml:"estimate,omitempty"`
|
||||
Nack NackTrackerConfig `yaml:"nack,omitempty"`
|
||||
}
|
||||
|
||||
var (
|
||||
defaultTrendDetectorConfigProbe = ccutils.TrendDetectorConfig{
|
||||
RequiredSamples: 3,
|
||||
RequiredSamplesMin: 3,
|
||||
DownwardTrendThreshold: 0.0,
|
||||
DownwardTrendMaxWait: 5 * time.Second,
|
||||
CollapseThreshold: 0,
|
||||
ValidityWindow: 10 * time.Second,
|
||||
}
|
||||
|
||||
defaultChannelObserverConfigProbe = ChannelObserverConfig{
|
||||
Estimate: defaultTrendDetectorConfigProbe,
|
||||
Nack: defaultNackTrackerConfigProbe,
|
||||
}
|
||||
|
||||
defaultTrendDetectorConfigNonProbe = ccutils.TrendDetectorConfig{
|
||||
RequiredSamples: 12,
|
||||
RequiredSamplesMin: 8,
|
||||
DownwardTrendThreshold: -0.6,
|
||||
DownwardTrendMaxWait: 5 * time.Second,
|
||||
CollapseThreshold: 500 * time.Millisecond,
|
||||
ValidityWindow: 10 * time.Second,
|
||||
}
|
||||
|
||||
defaultChannelObserverConfigNonProbe = ChannelObserverConfig{
|
||||
Estimate: defaultTrendDetectorConfigNonProbe,
|
||||
Nack: defaultNackTrackerConfigNonProbe,
|
||||
}
|
||||
)
|
||||
|
||||
// ------------------------------------------------
|
||||
|
||||
type channelObserverParams struct {
|
||||
Name string
|
||||
Config ChannelObserverConfig
|
||||
}
|
||||
|
||||
type channelObserver struct {
|
||||
params channelObserverParams
|
||||
logger logger.Logger
|
||||
|
||||
estimateTrend *ccutils.TrendDetector[int64]
|
||||
nackTracker *nackTracker
|
||||
}
|
||||
|
||||
func newChannelObserver(params channelObserverParams, logger logger.Logger) *channelObserver {
|
||||
return &channelObserver{
|
||||
params: params,
|
||||
logger: logger,
|
||||
estimateTrend: ccutils.NewTrendDetector[int64](ccutils.TrendDetectorParams{
|
||||
Name: params.Name + "-estimate",
|
||||
Logger: logger,
|
||||
Config: params.Config.Estimate,
|
||||
}),
|
||||
nackTracker: newNackTracker(nackTrackerParams{
|
||||
Name: params.Name + "-nack",
|
||||
Logger: logger,
|
||||
Config: params.Config.Nack,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *channelObserver) SeedEstimate(estimate int64) {
|
||||
c.estimateTrend.Seed(estimate)
|
||||
}
|
||||
|
||||
func (c *channelObserver) AddEstimate(estimate int64) {
|
||||
c.estimateTrend.AddValue(estimate)
|
||||
}
|
||||
|
||||
func (c *channelObserver) AddNack(packets uint32, repeatedNacks uint32) {
|
||||
c.nackTracker.Add(packets, repeatedNacks)
|
||||
}
|
||||
|
||||
func (c *channelObserver) GetLowestEstimate() int64 {
|
||||
return c.estimateTrend.GetLowest()
|
||||
}
|
||||
|
||||
func (c *channelObserver) GetHighestEstimate() int64 {
|
||||
return c.estimateTrend.GetHighest()
|
||||
}
|
||||
|
||||
func (c *channelObserver) HasEnoughEstimateSamples() bool {
|
||||
return c.estimateTrend.HasEnoughSamples()
|
||||
}
|
||||
|
||||
func (c *channelObserver) GetNackRatio() float64 {
|
||||
return c.nackTracker.GetRatio()
|
||||
}
|
||||
|
||||
func (c *channelObserver) GetTrend() (channelTrend, channelCongestionReason) {
|
||||
estimateDirection := c.estimateTrend.GetDirection()
|
||||
|
||||
switch {
|
||||
case estimateDirection == ccutils.TrendDirectionDownward:
|
||||
return channelTrendCongesting, channelCongestionReasonEstimate
|
||||
|
||||
case c.nackTracker.IsTriggered():
|
||||
return channelTrendCongesting, channelCongestionReasonLoss
|
||||
|
||||
case estimateDirection == ccutils.TrendDirectionUpward:
|
||||
return channelTrendClearing, channelCongestionReasonNone
|
||||
}
|
||||
|
||||
return channelTrendInconclusive, channelCongestionReasonNone
|
||||
}
|
||||
|
||||
func (c *channelObserver) MarshalLogObject(e zapcore.ObjectEncoder) error {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
e.AddString("name", c.params.Name)
|
||||
e.AddObject("estimate", c.estimateTrend)
|
||||
e.AddObject("nack", c.nackTracker)
|
||||
|
||||
channelTrend, channelCongestionReason := c.GetTrend()
|
||||
e.AddString("channelTrend", channelTrend.String())
|
||||
e.AddString("channelCongestionReason", channelCongestionReason.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
// ------------------------------------------------
|
||||
@@ -0,0 +1,130 @@
|
||||
// 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 remotebwe
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap/zapcore"
|
||||
|
||||
"github.com/livekit/protocol/logger"
|
||||
)
|
||||
|
||||
// ------------------------------------------------
|
||||
|
||||
type NackTrackerConfig struct {
|
||||
WindowMinDuration time.Duration `yaml:"window_min_duration,omitempty"`
|
||||
WindowMaxDuration time.Duration `yaml:"window_max_duration,omitempty"`
|
||||
RatioThreshold float64 `yaml:"ratio_threshold,omitempty"`
|
||||
}
|
||||
|
||||
var (
|
||||
defaultNackTrackerConfigProbe = NackTrackerConfig{
|
||||
WindowMinDuration: 500 * time.Millisecond,
|
||||
WindowMaxDuration: 1 * time.Second,
|
||||
RatioThreshold: 0.04,
|
||||
}
|
||||
|
||||
defaultNackTrackerConfigNonProbe = NackTrackerConfig{
|
||||
WindowMinDuration: 2 * time.Second,
|
||||
WindowMaxDuration: 3 * time.Second,
|
||||
RatioThreshold: 0.08,
|
||||
}
|
||||
)
|
||||
|
||||
// ------------------------------------------------
|
||||
|
||||
type nackTrackerParams struct {
|
||||
Name string
|
||||
Logger logger.Logger
|
||||
Config NackTrackerConfig
|
||||
}
|
||||
|
||||
type nackTracker struct {
|
||||
params nackTrackerParams
|
||||
|
||||
windowStartTime time.Time
|
||||
packets uint32
|
||||
repeatedNacks uint32
|
||||
}
|
||||
|
||||
func newNackTracker(params nackTrackerParams) *nackTracker {
|
||||
return &nackTracker{
|
||||
params: params,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *nackTracker) Add(packets uint32, repeatedNacks uint32) {
|
||||
if n.params.Config.WindowMaxDuration != 0 && !n.windowStartTime.IsZero() && time.Since(n.windowStartTime) > n.params.Config.WindowMaxDuration {
|
||||
n.windowStartTime = time.Time{}
|
||||
n.packets = 0
|
||||
n.repeatedNacks = 0
|
||||
}
|
||||
|
||||
//
|
||||
// Start NACK monitoring window only when a repeated NACK happens.
|
||||
// This allows locking tightly to when NACKs start happening and
|
||||
// check if the NACKs keep adding up (potentially a sign of congestion)
|
||||
// or isolated losses
|
||||
//
|
||||
if n.repeatedNacks == 0 && repeatedNacks != 0 {
|
||||
n.windowStartTime = time.Now()
|
||||
}
|
||||
|
||||
if !n.windowStartTime.IsZero() {
|
||||
n.packets += packets
|
||||
n.repeatedNacks += repeatedNacks
|
||||
}
|
||||
}
|
||||
|
||||
func (n *nackTracker) GetRatio() float64 {
|
||||
ratio := 0.0
|
||||
if n.packets != 0 {
|
||||
ratio = float64(n.repeatedNacks) / float64(n.packets)
|
||||
if ratio > 1.0 {
|
||||
ratio = 1.0
|
||||
}
|
||||
}
|
||||
|
||||
return ratio
|
||||
}
|
||||
|
||||
func (n *nackTracker) IsTriggered() bool {
|
||||
if n.params.Config.WindowMinDuration != 0 && !n.windowStartTime.IsZero() && time.Since(n.windowStartTime) > n.params.Config.WindowMinDuration {
|
||||
return n.GetRatio() > n.params.Config.RatioThreshold
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (n *nackTracker) MarshalLogObject(e zapcore.ObjectEncoder) error {
|
||||
if n == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
e.AddString("name", n.params.Name)
|
||||
if n.windowStartTime.IsZero() {
|
||||
e.AddString("window", "inactive")
|
||||
} else {
|
||||
e.AddTime("windowStartTime", n.windowStartTime)
|
||||
e.AddDuration("windowDuration", time.Since(n.windowStartTime))
|
||||
e.AddUint32("packets", n.packets)
|
||||
e.AddUint32("repeatedNacks", n.repeatedNacks)
|
||||
e.AddFloat64("nackRatio", n.GetRatio())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ------------------------------------------------
|
||||
@@ -0,0 +1,184 @@
|
||||
// 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 remotebwe
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/sfu/bwe"
|
||||
"github.com/livekit/livekit-server/pkg/sfu/ccutils"
|
||||
"github.com/livekit/protocol/logger"
|
||||
"github.com/livekit/protocol/utils/mono"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type probeControllerState int
|
||||
|
||||
const (
|
||||
probeControllerStateNone probeControllerState = iota
|
||||
probeControllerStateProbing
|
||||
probeControllerStateHangover
|
||||
)
|
||||
|
||||
func (p probeControllerState) String() string {
|
||||
switch p {
|
||||
case probeControllerStateNone:
|
||||
return "NONE"
|
||||
case probeControllerStateProbing:
|
||||
return "PROBING"
|
||||
case probeControllerStateHangover:
|
||||
return "HANGOVER"
|
||||
default:
|
||||
return fmt.Sprintf("%d", int(p))
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------
|
||||
|
||||
type ProbeControllerConfig struct {
|
||||
ProbeRegulator ccutils.ProbeRegulatorConfig `yaml:"probe_regulator,omitempty"`
|
||||
|
||||
SettleWaitNumRTT uint32 `yaml:"settle_wait_num_rtt,omitempty"`
|
||||
SettleWaitMin time.Duration `yaml:"settle_wait_min,omitempty"`
|
||||
SettleWaitMax time.Duration `yaml:"settle_wait_max,omitempty"`
|
||||
}
|
||||
|
||||
var (
|
||||
DefaultProbeControllerConfig = ProbeControllerConfig{
|
||||
ProbeRegulator: ccutils.DefaultProbeRegulatorConfig,
|
||||
|
||||
SettleWaitNumRTT: 5,
|
||||
SettleWaitMin: 250 * time.Millisecond,
|
||||
SettleWaitMax: 5 * time.Second,
|
||||
}
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type probeControllerParams struct {
|
||||
Config ProbeControllerConfig
|
||||
Logger logger.Logger
|
||||
}
|
||||
|
||||
type probeController struct {
|
||||
params probeControllerParams
|
||||
|
||||
state probeControllerState
|
||||
stateSwitchedAt time.Time
|
||||
|
||||
pci ccutils.ProbeClusterInfo
|
||||
rtt float64
|
||||
|
||||
*ccutils.ProbeRegulator
|
||||
}
|
||||
|
||||
func newProbeController(params probeControllerParams) *probeController {
|
||||
return &probeController{
|
||||
params: params,
|
||||
state: probeControllerStateNone,
|
||||
stateSwitchedAt: mono.Now(),
|
||||
pci: ccutils.ProbeClusterInfoInvalid,
|
||||
rtt: bwe.DefaultRTT,
|
||||
ProbeRegulator: ccutils.NewProbeRegulator(
|
||||
ccutils.ProbeRegulatorParams{
|
||||
Config: params.Config.ProbeRegulator,
|
||||
Logger: params.Logger,
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *probeController) UpdateRTT(rtt float64) {
|
||||
if rtt == 0 {
|
||||
p.rtt = bwe.DefaultRTT
|
||||
} else {
|
||||
if p.rtt == 0 {
|
||||
p.rtt = rtt
|
||||
} else {
|
||||
p.rtt = bwe.RTTSmoothingFactor*rtt + (1.0-bwe.RTTSmoothingFactor)*p.rtt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *probeController) GetRTT() float64 {
|
||||
return p.rtt
|
||||
}
|
||||
|
||||
func (p *probeController) CanProbe() bool {
|
||||
return p.state == probeControllerStateNone && p.ProbeRegulator.CanProbe()
|
||||
}
|
||||
|
||||
func (p *probeController) IsInProbe() bool {
|
||||
return p.state != probeControllerStateNone
|
||||
}
|
||||
|
||||
func (p *probeController) ProbeClusterStarting(pci ccutils.ProbeClusterInfo) {
|
||||
if p.state != probeControllerStateNone {
|
||||
p.params.Logger.Warnw("unexpected probe controller state", nil, "state", p.state)
|
||||
}
|
||||
|
||||
p.setState(probeControllerStateProbing)
|
||||
p.pci = pci
|
||||
}
|
||||
|
||||
func (p *probeController) ProbeClusterDone(pci ccutils.ProbeClusterInfo) {
|
||||
if p.pci.Id != pci.Id {
|
||||
return
|
||||
}
|
||||
|
||||
p.pci.Result = pci.Result
|
||||
p.setState(probeControllerStateHangover)
|
||||
}
|
||||
|
||||
func (p *probeController) ProbeClusterIsGoalReached(estimate int64) bool {
|
||||
if p.pci.Id == ccutils.ProbeClusterIdInvalid {
|
||||
return false
|
||||
}
|
||||
|
||||
return estimate > int64(p.pci.Goal.DesiredBps)
|
||||
}
|
||||
|
||||
func (p *probeController) MaybeFinalizeProbe() (ccutils.ProbeClusterInfo, bool) {
|
||||
if p.state != probeControllerStateHangover {
|
||||
return ccutils.ProbeClusterInfoInvalid, false
|
||||
}
|
||||
|
||||
settleWait := time.Duration(float64(p.params.Config.SettleWaitNumRTT) * p.rtt * float64(time.Second))
|
||||
if settleWait < p.params.Config.SettleWaitMin {
|
||||
settleWait = p.params.Config.SettleWaitMin
|
||||
}
|
||||
if settleWait > p.params.Config.SettleWaitMax {
|
||||
settleWait = p.params.Config.SettleWaitMax
|
||||
}
|
||||
if time.Since(p.stateSwitchedAt) < settleWait {
|
||||
return ccutils.ProbeClusterInfoInvalid, false
|
||||
}
|
||||
|
||||
p.setState(probeControllerStateNone)
|
||||
return p.pci, true
|
||||
}
|
||||
|
||||
func (p *probeController) setState(state probeControllerState) {
|
||||
if state == p.state {
|
||||
return
|
||||
}
|
||||
|
||||
p.state = state
|
||||
p.stateSwitchedAt = mono.Now()
|
||||
}
|
||||
|
||||
// ------------------------------------------------
|
||||
@@ -0,0 +1,353 @@
|
||||
// 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 remotebwe
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/sfu/bwe"
|
||||
"github.com/livekit/livekit-server/pkg/sfu/ccutils"
|
||||
"github.com/livekit/protocol/logger"
|
||||
"github.com/livekit/protocol/utils/mono"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type RemoteBWEConfig struct {
|
||||
NackRatioAttenuator float64 `yaml:"nack_ratio_attenuator,omitempty"`
|
||||
ExpectedUsageThreshold float64 `yaml:"expected_usage_threshold,omitempty"`
|
||||
ChannelObserverProbe ChannelObserverConfig `yaml:"channel_observer_probe,omitempty"`
|
||||
ChannelObserverNonProbe ChannelObserverConfig `yaml:"channel_observer_non_probe,omitempty"`
|
||||
ProbeController ProbeControllerConfig `yaml:"probe_controller,omitempty"`
|
||||
}
|
||||
|
||||
var (
|
||||
DefaultRemoteBWEConfig = RemoteBWEConfig{
|
||||
NackRatioAttenuator: 0.4,
|
||||
ExpectedUsageThreshold: 0.95,
|
||||
ChannelObserverProbe: defaultChannelObserverConfigProbe,
|
||||
ChannelObserverNonProbe: defaultChannelObserverConfigNonProbe,
|
||||
ProbeController: DefaultProbeControllerConfig,
|
||||
}
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type RemoteBWEParams struct {
|
||||
Config RemoteBWEConfig
|
||||
Logger logger.Logger
|
||||
}
|
||||
|
||||
type RemoteBWE struct {
|
||||
bwe.NullBWE
|
||||
|
||||
params RemoteBWEParams
|
||||
|
||||
lock sync.RWMutex
|
||||
|
||||
lastReceivedEstimate int64
|
||||
lastExpectedBandwidthUsage int64
|
||||
committedChannelCapacity int64
|
||||
|
||||
probeController *probeController
|
||||
|
||||
channelObserver *channelObserver
|
||||
|
||||
congestionState bwe.CongestionState
|
||||
congestionStateSwitchedAt time.Time
|
||||
|
||||
bweListener bwe.BWEListener
|
||||
}
|
||||
|
||||
func NewRemoteBWE(params RemoteBWEParams) *RemoteBWE {
|
||||
r := &RemoteBWE{
|
||||
params: params,
|
||||
}
|
||||
|
||||
r.Reset()
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *RemoteBWE) SetBWEListener(bweListener bwe.BWEListener) {
|
||||
r.lock.Lock()
|
||||
defer r.lock.Unlock()
|
||||
|
||||
r.bweListener = bweListener
|
||||
}
|
||||
|
||||
func (r *RemoteBWE) getBWEListener() bwe.BWEListener {
|
||||
r.lock.RLock()
|
||||
defer r.lock.RUnlock()
|
||||
|
||||
return r.bweListener
|
||||
}
|
||||
|
||||
func (r *RemoteBWE) Reset() {
|
||||
r.lock.Lock()
|
||||
defer r.lock.Unlock()
|
||||
|
||||
r.lastReceivedEstimate = 0
|
||||
r.lastExpectedBandwidthUsage = 0
|
||||
r.committedChannelCapacity = 100_000_000
|
||||
|
||||
r.congestionState = bwe.CongestionStateNone
|
||||
r.congestionStateSwitchedAt = mono.Now()
|
||||
|
||||
r.probeController = newProbeController(probeControllerParams{
|
||||
Config: r.params.Config.ProbeController,
|
||||
Logger: r.params.Logger,
|
||||
})
|
||||
|
||||
r.newChannelObserver()
|
||||
}
|
||||
|
||||
func (r *RemoteBWE) HandleREMB(
|
||||
receivedEstimate int64,
|
||||
expectedBandwidthUsage int64,
|
||||
sentPackets uint32,
|
||||
repeatedNacks uint32,
|
||||
) {
|
||||
r.lock.Lock()
|
||||
r.lastReceivedEstimate = receivedEstimate
|
||||
r.lastExpectedBandwidthUsage = expectedBandwidthUsage
|
||||
|
||||
// in probe, freeze channel observer state if probe causes congestion till the probe is done,
|
||||
// this is to ensure that probe result is not marked a success,
|
||||
// an unsuccessful probe will not up allocate any tracks
|
||||
if r.congestionState != bwe.CongestionStateNone && r.probeController.IsInProbe() {
|
||||
r.lock.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
r.channelObserver.AddEstimate(r.lastReceivedEstimate)
|
||||
r.channelObserver.AddNack(sentPackets, repeatedNacks)
|
||||
|
||||
shouldNotify, fromState, toState, committedChannelCapacity := r.congestionDetectionStateMachine()
|
||||
r.lock.Unlock()
|
||||
|
||||
if shouldNotify {
|
||||
if bweListener := r.getBWEListener(); bweListener != nil {
|
||||
bweListener.OnCongestionStateChange(fromState, toState, committedChannelCapacity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RemoteBWE) UpdateRTT(rtt float64) {
|
||||
r.lock.Lock()
|
||||
defer r.lock.Unlock()
|
||||
|
||||
r.probeController.UpdateRTT(rtt)
|
||||
}
|
||||
|
||||
func (r *RemoteBWE) congestionDetectionStateMachine() (bool, bwe.CongestionState, bwe.CongestionState, int64) {
|
||||
fromState := r.congestionState
|
||||
toState := r.congestionState
|
||||
update := false
|
||||
trend, reason := r.channelObserver.GetTrend()
|
||||
|
||||
switch fromState {
|
||||
case bwe.CongestionStateNone:
|
||||
if trend == channelTrendCongesting {
|
||||
if r.probeController.IsInProbe() || r.estimateAvailableChannelCapacity(reason) {
|
||||
// when in probe, if congested, stays there till probe is done,
|
||||
// the estimate stays at pre-probe level
|
||||
toState = bwe.CongestionStateCongested
|
||||
}
|
||||
}
|
||||
|
||||
case bwe.CongestionStateCongested:
|
||||
if trend == channelTrendCongesting {
|
||||
if r.estimateAvailableChannelCapacity(reason) {
|
||||
// update state as this needs to reset switch time to wait for congestion min duration again
|
||||
update = true
|
||||
}
|
||||
} else {
|
||||
toState = bwe.CongestionStateNone
|
||||
}
|
||||
}
|
||||
|
||||
shouldNotify := false
|
||||
if toState != fromState || update {
|
||||
fromState, toState = r.updateCongestionState(toState, reason)
|
||||
shouldNotify = true
|
||||
}
|
||||
|
||||
return shouldNotify, fromState, toState, r.committedChannelCapacity
|
||||
}
|
||||
|
||||
func (r *RemoteBWE) estimateAvailableChannelCapacity(reason channelCongestionReason) bool {
|
||||
var estimateToCommit int64
|
||||
switch reason {
|
||||
case channelCongestionReasonLoss:
|
||||
estimateToCommit = int64(float64(r.lastExpectedBandwidthUsage) * (1.0 - r.params.Config.NackRatioAttenuator*r.channelObserver.GetNackRatio()))
|
||||
default:
|
||||
estimateToCommit = r.lastReceivedEstimate
|
||||
}
|
||||
if estimateToCommit > r.lastReceivedEstimate {
|
||||
estimateToCommit = r.lastReceivedEstimate
|
||||
}
|
||||
|
||||
commitThreshold := int64(r.params.Config.ExpectedUsageThreshold * float64(r.lastExpectedBandwidthUsage))
|
||||
if estimateToCommit > commitThreshold || r.committedChannelCapacity == estimateToCommit {
|
||||
return false
|
||||
}
|
||||
|
||||
r.params.Logger.Infow(
|
||||
"remote bwe: channel congestion detected, applying channel capacity update",
|
||||
"reason", reason,
|
||||
"old(bps)", r.committedChannelCapacity,
|
||||
"new(bps)", estimateToCommit,
|
||||
"lastReceived(bps)", r.lastReceivedEstimate,
|
||||
"expectedUsage(bps)", r.lastExpectedBandwidthUsage,
|
||||
"commitThreshold(bps)", commitThreshold,
|
||||
"channel", r.channelObserver,
|
||||
)
|
||||
r.committedChannelCapacity = estimateToCommit
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *RemoteBWE) updateCongestionState(state bwe.CongestionState, reason channelCongestionReason) (bwe.CongestionState, bwe.CongestionState) {
|
||||
r.params.Logger.Debugw(
|
||||
"remote bwe: congestion state change",
|
||||
"from", r.congestionState,
|
||||
"to", state,
|
||||
"reason", reason,
|
||||
"committedChannelCapacity", r.committedChannelCapacity,
|
||||
)
|
||||
|
||||
fromState := r.congestionState
|
||||
r.congestionState = state
|
||||
r.congestionStateSwitchedAt = mono.Now()
|
||||
return fromState, r.congestionState
|
||||
}
|
||||
|
||||
func (r *RemoteBWE) CongestionState() bwe.CongestionState {
|
||||
r.lock.Lock()
|
||||
defer r.lock.Unlock()
|
||||
|
||||
return r.congestionState
|
||||
}
|
||||
|
||||
func (r *RemoteBWE) CanProbe() bool {
|
||||
r.lock.Lock()
|
||||
defer r.lock.Unlock()
|
||||
|
||||
return r.congestionState == bwe.CongestionStateNone && r.probeController.CanProbe()
|
||||
}
|
||||
|
||||
func (r *RemoteBWE) ProbeDuration() time.Duration {
|
||||
r.lock.Lock()
|
||||
defer r.lock.Unlock()
|
||||
|
||||
return r.probeController.ProbeDuration()
|
||||
}
|
||||
|
||||
func (r *RemoteBWE) ProbeClusterStarting(pci ccutils.ProbeClusterInfo) {
|
||||
r.lock.Lock()
|
||||
defer r.lock.Unlock()
|
||||
|
||||
r.lastExpectedBandwidthUsage = int64(pci.Goal.ExpectedUsageBps)
|
||||
|
||||
r.params.Logger.Debugw(
|
||||
"remote bwe: starting probe",
|
||||
"lastReceived", r.lastReceivedEstimate,
|
||||
"expectedBandwidthUsage", r.lastExpectedBandwidthUsage,
|
||||
"channel", r.channelObserver,
|
||||
)
|
||||
|
||||
r.probeController.ProbeClusterStarting(pci)
|
||||
r.newChannelObserver()
|
||||
}
|
||||
|
||||
func (r *RemoteBWE) ProbeClusterDone(pci ccutils.ProbeClusterInfo) {
|
||||
r.lock.Lock()
|
||||
defer r.lock.Unlock()
|
||||
|
||||
r.probeController.ProbeClusterDone(pci)
|
||||
}
|
||||
|
||||
func (r *RemoteBWE) ProbeClusterIsGoalReached() bool {
|
||||
r.lock.Lock()
|
||||
defer r.lock.Unlock()
|
||||
|
||||
if !r.probeController.IsInProbe() ||
|
||||
r.congestionState != bwe.CongestionStateNone ||
|
||||
!r.channelObserver.HasEnoughEstimateSamples() {
|
||||
return false
|
||||
}
|
||||
|
||||
return r.probeController.ProbeClusterIsGoalReached(r.channelObserver.GetHighestEstimate())
|
||||
}
|
||||
|
||||
func (r *RemoteBWE) ProbeClusterFinalize() (ccutils.ProbeSignal, int64, bool) {
|
||||
r.lock.Lock()
|
||||
defer r.lock.Unlock()
|
||||
|
||||
pci, isFinalized := r.probeController.MaybeFinalizeProbe()
|
||||
if !isFinalized {
|
||||
return ccutils.ProbeSignalInconclusive, 0, isFinalized
|
||||
}
|
||||
|
||||
// switch to a non-probe channel observer on probe end,
|
||||
// reset congestion state to get a fresh trend
|
||||
pco := r.channelObserver
|
||||
probeCongestionState := r.congestionState
|
||||
|
||||
r.congestionState = bwe.CongestionStateNone
|
||||
r.newChannelObserver()
|
||||
|
||||
r.params.Logger.Infow(
|
||||
"remote bwe: probe finalized",
|
||||
"lastReceived", r.lastReceivedEstimate,
|
||||
"expectedBandwidthUsage", r.lastExpectedBandwidthUsage,
|
||||
"channel", pco,
|
||||
"isSignalValid", pco.HasEnoughEstimateSamples(),
|
||||
"probeClusterInfo", pci,
|
||||
"rtt", r.probeController.GetRTT(),
|
||||
)
|
||||
|
||||
probeSignal := ccutils.ProbeSignalNotCongesting
|
||||
if probeCongestionState != bwe.CongestionStateNone {
|
||||
probeSignal = ccutils.ProbeSignalCongesting
|
||||
} else if !pco.HasEnoughEstimateSamples() {
|
||||
probeSignal = ccutils.ProbeSignalInconclusive
|
||||
} else {
|
||||
highestEstimate := pco.GetHighestEstimate()
|
||||
if highestEstimate > r.committedChannelCapacity {
|
||||
r.committedChannelCapacity = highestEstimate
|
||||
}
|
||||
}
|
||||
|
||||
r.probeController.ProbeSignal(probeSignal, pci.CreatedAt)
|
||||
return probeSignal, r.committedChannelCapacity, true
|
||||
}
|
||||
|
||||
func (r *RemoteBWE) newChannelObserver() {
|
||||
var params channelObserverParams
|
||||
if r.probeController.IsInProbe() {
|
||||
params = channelObserverParams{
|
||||
Name: "probe",
|
||||
Config: r.params.Config.ChannelObserverProbe,
|
||||
}
|
||||
} else {
|
||||
params = channelObserverParams{
|
||||
Name: "non-probe",
|
||||
Config: r.params.Config.ChannelObserverNonProbe,
|
||||
}
|
||||
}
|
||||
|
||||
r.channelObserver = newChannelObserver(params, r.params.Logger)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,310 @@
|
||||
// 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 (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/livekit/protocol/logger"
|
||||
"github.com/livekit/protocol/utils"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
// -------------------------------------------------------------
|
||||
|
||||
var (
|
||||
errGroupFinalized = errors.New("packet group is finalized")
|
||||
errOldPacket = errors.New("packet is older than packet group start")
|
||||
)
|
||||
|
||||
// -------------------------------------------------------------
|
||||
|
||||
type PacketGroupConfig struct {
|
||||
MinPackets int `yaml:"min_packets,omitempty"`
|
||||
MaxWindowDuration time.Duration `yaml:"max_window_duration,omitempty"`
|
||||
}
|
||||
|
||||
var (
|
||||
defaultPacketGroupConfig = PacketGroupConfig{
|
||||
MinPackets: 30,
|
||||
MaxWindowDuration: 500 * time.Millisecond,
|
||||
}
|
||||
)
|
||||
|
||||
// -------------------------------------------------------------
|
||||
|
||||
type stat struct {
|
||||
numPackets int
|
||||
numBytes int
|
||||
}
|
||||
|
||||
func (s *stat) add(size int) {
|
||||
s.numPackets++
|
||||
s.numBytes += size
|
||||
}
|
||||
|
||||
func (s *stat) remove(size int) {
|
||||
s.numPackets--
|
||||
s.numBytes -= size
|
||||
}
|
||||
|
||||
func (s *stat) getNumPackets() int {
|
||||
return s.numPackets
|
||||
}
|
||||
|
||||
func (s *stat) getNumBytes() int {
|
||||
return s.numBytes
|
||||
}
|
||||
|
||||
func (s stat) MarshalLogObject(e zapcore.ObjectEncoder) error {
|
||||
e.AddInt("numPackets", s.numPackets)
|
||||
e.AddInt("numBytes", s.numBytes)
|
||||
return nil
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
|
||||
type classStat struct {
|
||||
primary stat
|
||||
rtx stat
|
||||
probe stat
|
||||
}
|
||||
|
||||
func (c *classStat) add(size int, isRTX bool, isProbe bool) {
|
||||
if isRTX {
|
||||
c.rtx.add(size)
|
||||
} else if isProbe {
|
||||
c.probe.add(size)
|
||||
} else {
|
||||
c.primary.add(size)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *classStat) remove(size int, isRTX bool, isProbe bool) {
|
||||
if isRTX {
|
||||
c.rtx.remove(size)
|
||||
} else if isProbe {
|
||||
c.probe.remove(size)
|
||||
} else {
|
||||
c.primary.remove(size)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *classStat) numPackets() int {
|
||||
return c.primary.getNumPackets() + c.rtx.getNumPackets() + c.probe.getNumPackets()
|
||||
}
|
||||
|
||||
func (c *classStat) numBytes() int {
|
||||
return c.primary.getNumBytes() + c.rtx.getNumBytes() + c.probe.getNumBytes()
|
||||
}
|
||||
|
||||
func (c classStat) MarshalLogObject(e zapcore.ObjectEncoder) error {
|
||||
e.AddObject("primary", c.primary)
|
||||
e.AddObject("rtx", c.rtx)
|
||||
e.AddObject("probe", c.probe)
|
||||
return nil
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
|
||||
type packetGroupParams struct {
|
||||
Config PacketGroupConfig
|
||||
WeightedLoss WeightedLossConfig
|
||||
Logger logger.Logger
|
||||
}
|
||||
|
||||
type packetGroup struct {
|
||||
params packetGroupParams
|
||||
|
||||
minSequenceNumber uint64
|
||||
maxSequenceNumber uint64
|
||||
|
||||
minSendTime int64
|
||||
maxSendTime int64
|
||||
|
||||
minRecvTime int64 // for information only
|
||||
maxRecvTime int64 // for information only
|
||||
|
||||
acked classStat
|
||||
lost classStat
|
||||
snBitmap *utils.Bitmap[uint64]
|
||||
|
||||
aggregateSendDelta int64
|
||||
aggregateRecvDelta int64
|
||||
inheritedQueuingDelay int64
|
||||
|
||||
isFinalized bool
|
||||
}
|
||||
|
||||
func newPacketGroup(params packetGroupParams, inheritedQueuingDelay int64) *packetGroup {
|
||||
return &packetGroup{
|
||||
params: params,
|
||||
inheritedQueuingDelay: inheritedQueuingDelay,
|
||||
snBitmap: utils.NewBitmap[uint64](params.Config.MinPackets),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *packetGroup) Add(pi *packetInfo, sendDelta, recvDelta int64, isLost bool) error {
|
||||
if isLost {
|
||||
return p.lostPacket(pi)
|
||||
}
|
||||
|
||||
if err := p.inGroup(pi.sequenceNumber); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if p.minSequenceNumber == 0 || pi.sequenceNumber < p.minSequenceNumber {
|
||||
p.minSequenceNumber = pi.sequenceNumber
|
||||
}
|
||||
p.maxSequenceNumber = max(p.maxSequenceNumber, pi.sequenceNumber)
|
||||
|
||||
if p.minSendTime == 0 || (pi.sendTime-sendDelta) < p.minSendTime {
|
||||
p.minSendTime = pi.sendTime - sendDelta
|
||||
}
|
||||
p.maxSendTime = max(p.maxSendTime, pi.sendTime)
|
||||
|
||||
if p.minRecvTime == 0 || (pi.recvTime-recvDelta) < p.minRecvTime {
|
||||
p.minRecvTime = pi.recvTime - recvDelta
|
||||
}
|
||||
p.maxRecvTime = max(p.maxRecvTime, pi.recvTime)
|
||||
|
||||
p.acked.add(int(pi.size), pi.isRTX, pi.isProbe)
|
||||
if int(pi.sequenceNumber-p.minSequenceNumber) < p.snBitmap.Len() && p.snBitmap.IsSet(pi.sequenceNumber-p.minSequenceNumber) {
|
||||
// an earlier packet reported as lost has been received
|
||||
p.snBitmap.Clear(pi.sequenceNumber - p.minSequenceNumber)
|
||||
p.lost.remove(int(pi.size), pi.isRTX, pi.isProbe)
|
||||
}
|
||||
|
||||
// note that out-of-order deliveries will amplify the queueing delay.
|
||||
// for e.g. a, b, c getting delivered as a, c, b.
|
||||
// let us say packets are delivered with interval of `x`
|
||||
// send delta aggregate will go up by x((a, c) = 2x + (c, b) -1x)
|
||||
// recv delta aggregate will go up by 3x((a, c) = 2x + (c, b) 1x)
|
||||
p.aggregateSendDelta += sendDelta
|
||||
p.aggregateRecvDelta += recvDelta
|
||||
|
||||
if p.acked.numPackets() == p.params.Config.MinPackets || (pi.sendTime-p.minSendTime) > p.params.Config.MaxWindowDuration.Microseconds() {
|
||||
p.isFinalized = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *packetGroup) lostPacket(pi *packetInfo) error {
|
||||
if pi.recvTime != 0 {
|
||||
// previously received packet, so not lost
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := p.inGroup(pi.sequenceNumber); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if p.minSequenceNumber == 0 || pi.sequenceNumber < p.minSequenceNumber {
|
||||
p.minSequenceNumber = pi.sequenceNumber
|
||||
}
|
||||
p.maxSequenceNumber = max(p.maxSequenceNumber, pi.sequenceNumber)
|
||||
p.snBitmap.Set(pi.sequenceNumber - p.minSequenceNumber)
|
||||
|
||||
p.lost.add(int(pi.size), pi.isRTX, pi.isProbe)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *packetGroup) MinSendTime() int64 {
|
||||
return p.minSendTime
|
||||
}
|
||||
|
||||
func (p *packetGroup) SendWindow() (int64, int64) {
|
||||
return p.minSendTime, p.maxSendTime
|
||||
}
|
||||
|
||||
func (p *packetGroup) PropagatedQueuingDelay() int64 {
|
||||
if p.inheritedQueuingDelay+p.aggregateRecvDelta-p.aggregateSendDelta > 0 {
|
||||
return p.inheritedQueuingDelay + p.aggregateRecvDelta - p.aggregateSendDelta
|
||||
}
|
||||
|
||||
return max(0, p.aggregateRecvDelta-p.aggregateSendDelta)
|
||||
}
|
||||
|
||||
func (p *packetGroup) FinalizedPropagatedQueuingDelay() (int64, bool) {
|
||||
if !p.isFinalized {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return p.PropagatedQueuingDelay(), true
|
||||
}
|
||||
|
||||
func (p *packetGroup) IsFinalized() bool {
|
||||
return p.isFinalized
|
||||
}
|
||||
|
||||
func (p *packetGroup) Traffic() *trafficStats {
|
||||
return &trafficStats{
|
||||
minSendTime: p.minSendTime,
|
||||
maxSendTime: p.maxSendTime,
|
||||
sendDelta: p.aggregateSendDelta,
|
||||
recvDelta: p.aggregateRecvDelta,
|
||||
ackedPackets: p.acked.numPackets(),
|
||||
ackedBytes: p.acked.numBytes(),
|
||||
lostPackets: p.lost.numPackets(),
|
||||
lostBytes: p.lost.numBytes(),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *packetGroup) MarshalLogObject(e zapcore.ObjectEncoder) error {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
e.AddUint64("minSequenceNumber", p.minSequenceNumber)
|
||||
e.AddUint64("maxSequenceNumber", p.maxSequenceNumber)
|
||||
e.AddObject("acked", p.acked)
|
||||
e.AddObject("lost", p.lost)
|
||||
|
||||
e.AddInt64("minRecvTime", p.minRecvTime)
|
||||
e.AddInt64("maxRecvTime", p.maxRecvTime)
|
||||
recvDuration := time.Duration((p.maxRecvTime - p.minRecvTime) * 1000)
|
||||
e.AddDuration("recvDuration", recvDuration)
|
||||
|
||||
recvBitrate := float64(0)
|
||||
if recvDuration != 0 {
|
||||
recvBitrate = float64(p.acked.numBytes()*8) / recvDuration.Seconds()
|
||||
e.AddFloat64("recvBitrate", recvBitrate)
|
||||
}
|
||||
|
||||
ts := newTrafficStats(trafficStatsParams{
|
||||
Config: p.params.WeightedLoss,
|
||||
Logger: p.params.Logger,
|
||||
})
|
||||
ts.Merge(p.Traffic())
|
||||
e.AddObject("trafficStats", ts)
|
||||
e.AddInt64("inheritedQueuingDelay", p.inheritedQueuingDelay)
|
||||
e.AddInt64("propagatedQueuingDelay", p.PropagatedQueuingDelay())
|
||||
|
||||
e.AddBool("isFinalized", p.isFinalized)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *packetGroup) inGroup(sequenceNumber uint64) error {
|
||||
if p.isFinalized && sequenceNumber > p.maxSequenceNumber {
|
||||
return errGroupFinalized
|
||||
}
|
||||
|
||||
if sequenceNumber < p.minSequenceNumber {
|
||||
return errOldPacket
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// 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 (
|
||||
"github.com/livekit/livekit-server/pkg/sfu/ccutils"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
type packetInfo struct {
|
||||
sequenceNumber uint64
|
||||
sendTime int64
|
||||
recvTime int64
|
||||
probeClusterId ccutils.ProbeClusterId
|
||||
size uint16
|
||||
isRTX bool
|
||||
isProbe bool
|
||||
}
|
||||
|
||||
func (pi *packetInfo) MarshalLogObject(e zapcore.ObjectEncoder) error {
|
||||
if pi == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
e.AddUint64("sequenceNumber", pi.sequenceNumber)
|
||||
e.AddInt64("sendTime", pi.sendTime)
|
||||
e.AddInt64("recvTime", pi.recvTime)
|
||||
e.AddUint32("probeClusterId", uint32(pi.probeClusterId))
|
||||
e.AddUint16("size", pi.size)
|
||||
e.AddBool("isRTX", pi.isRTX)
|
||||
e.AddBool("isProbe", pi.isProbe)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
// 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 (
|
||||
"math/rand"
|
||||
"sync"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/sfu/ccutils"
|
||||
"github.com/livekit/protocol/logger"
|
||||
"github.com/livekit/protocol/utils/mono"
|
||||
)
|
||||
|
||||
// -------------------------------------------------------------------------------
|
||||
|
||||
type packetTrackerParams struct {
|
||||
Logger logger.Logger
|
||||
}
|
||||
|
||||
type packetTracker struct {
|
||||
params packetTrackerParams
|
||||
|
||||
lock sync.Mutex
|
||||
|
||||
sequenceNumber uint64
|
||||
|
||||
baseSendTime int64
|
||||
packetInfos [2048]packetInfo
|
||||
|
||||
baseRecvTime int64
|
||||
piLastRecv *packetInfo
|
||||
|
||||
probeClusterId ccutils.ProbeClusterId
|
||||
probeMaxSequenceNumber uint64
|
||||
}
|
||||
|
||||
func newPacketTracker(params packetTrackerParams) *packetTracker {
|
||||
return &packetTracker{
|
||||
params: params,
|
||||
sequenceNumber: uint64(rand.Intn(1<<14)) + uint64(1<<15), // a random number in third quartile of sequence number space
|
||||
}
|
||||
}
|
||||
|
||||
func (p *packetTracker) RecordPacketSendAndGetSequenceNumber(
|
||||
atMicro int64,
|
||||
size int,
|
||||
isRTX bool,
|
||||
probeClusterId ccutils.ProbeClusterId,
|
||||
isProbe bool,
|
||||
) uint16 {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
|
||||
if p.baseSendTime == 0 {
|
||||
p.baseSendTime = atMicro
|
||||
}
|
||||
|
||||
pi := p.getPacketInfo(uint16(p.sequenceNumber))
|
||||
*pi = packetInfo{
|
||||
sequenceNumber: p.sequenceNumber,
|
||||
sendTime: atMicro - p.baseSendTime,
|
||||
size: uint16(size),
|
||||
isRTX: isRTX,
|
||||
probeClusterId: probeClusterId,
|
||||
isProbe: isProbe,
|
||||
}
|
||||
|
||||
p.sequenceNumber++
|
||||
|
||||
// extreme case of wrap around before receiving any feedback
|
||||
if pi == p.piLastRecv {
|
||||
p.piLastRecv = nil
|
||||
}
|
||||
|
||||
if p.probeClusterId != ccutils.ProbeClusterIdInvalid && p.probeClusterId == pi.probeClusterId && pi.sequenceNumber > p.probeMaxSequenceNumber {
|
||||
p.probeMaxSequenceNumber = pi.sequenceNumber
|
||||
}
|
||||
|
||||
return uint16(pi.sequenceNumber)
|
||||
}
|
||||
|
||||
func (p *packetTracker) BaseSendTimeThreshold(threshold int64) (int64, bool) {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
|
||||
if p.baseSendTime == 0 {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return mono.UnixMicro() - p.baseSendTime - threshold, true
|
||||
}
|
||||
|
||||
func (p *packetTracker) RecordPacketIndicationFromRemote(sn uint16, recvTime int64) (piRecv packetInfo, sendDelta, recvDelta int64) {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
|
||||
pi := p.getPacketInfoExisting(sn)
|
||||
if pi == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if recvTime == 0 {
|
||||
// maybe lost OR already received but reported lost in a later report
|
||||
piRecv = *pi
|
||||
return
|
||||
}
|
||||
|
||||
if p.baseRecvTime == 0 {
|
||||
p.baseRecvTime = recvTime
|
||||
p.piLastRecv = pi
|
||||
}
|
||||
|
||||
pi.recvTime = recvTime - p.baseRecvTime
|
||||
piRecv = *pi
|
||||
if p.piLastRecv != nil {
|
||||
sendDelta, recvDelta = pi.sendTime-p.piLastRecv.sendTime, pi.recvTime-p.piLastRecv.recvTime
|
||||
}
|
||||
p.piLastRecv = pi
|
||||
return
|
||||
}
|
||||
|
||||
func (p *packetTracker) getPacketInfo(sn uint16) *packetInfo {
|
||||
return &p.packetInfos[int(sn)%len(p.packetInfos)]
|
||||
}
|
||||
|
||||
func (p *packetTracker) getPacketInfoExisting(sn uint16) *packetInfo {
|
||||
pi := &p.packetInfos[int(sn)%len(p.packetInfos)]
|
||||
if uint16(pi.sequenceNumber) == sn {
|
||||
return pi
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *packetTracker) ProbeClusterStarting(probeClusterId ccutils.ProbeClusterId) {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
|
||||
p.probeClusterId = probeClusterId
|
||||
}
|
||||
|
||||
func (p *packetTracker) ProbeClusterDone(probeClusterId ccutils.ProbeClusterId) {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
|
||||
if p.probeClusterId == probeClusterId {
|
||||
p.probeClusterId = ccutils.ProbeClusterIdInvalid
|
||||
}
|
||||
}
|
||||
|
||||
func (p *packetTracker) ProbeMaxSequenceNumber() uint64 {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
|
||||
return p.probeMaxSequenceNumber
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
// 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 (
|
||||
"time"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/sfu/ccutils"
|
||||
"github.com/livekit/protocol/logger"
|
||||
"github.com/livekit/protocol/utils/mono"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
// -------------------------------------------------------------
|
||||
|
||||
type ProbePacketGroupConfig struct {
|
||||
PacketGroup PacketGroupConfig `yaml:"packet_group,omitempty"`
|
||||
|
||||
SettleWaitNumRTT uint32 `yaml:"settle_wait_num_rtt,omitempty"`
|
||||
SettleWaitMin time.Duration `yaml:"settle_wait_min,omitempty"`
|
||||
SettleWaitMax time.Duration `yaml:"settle_wait_max,omitempty"`
|
||||
}
|
||||
|
||||
var (
|
||||
// large numbers to treat a probe packet group as one
|
||||
defaultProbePacketGroupConfig = ProbePacketGroupConfig{
|
||||
PacketGroup: PacketGroupConfig{
|
||||
MinPackets: 16384,
|
||||
MaxWindowDuration: time.Minute,
|
||||
},
|
||||
|
||||
SettleWaitNumRTT: 5,
|
||||
SettleWaitMin: 250 * time.Millisecond,
|
||||
SettleWaitMax: 5 * time.Second,
|
||||
}
|
||||
)
|
||||
|
||||
// -------------------------------------------------------------
|
||||
|
||||
type probePacketGroupParams struct {
|
||||
Config ProbePacketGroupConfig
|
||||
WeightedLoss WeightedLossConfig
|
||||
Logger logger.Logger
|
||||
}
|
||||
|
||||
type probePacketGroup struct {
|
||||
params probePacketGroupParams
|
||||
pci ccutils.ProbeClusterInfo
|
||||
*packetGroup
|
||||
maxSequenceNumber uint64
|
||||
doneAt time.Time
|
||||
}
|
||||
|
||||
func newProbePacketGroup(params probePacketGroupParams, pci ccutils.ProbeClusterInfo) *probePacketGroup {
|
||||
return &probePacketGroup{
|
||||
params: params,
|
||||
pci: pci,
|
||||
packetGroup: newPacketGroup(
|
||||
packetGroupParams{
|
||||
Config: params.Config.PacketGroup,
|
||||
WeightedLoss: params.WeightedLoss,
|
||||
Logger: params.Logger,
|
||||
},
|
||||
0,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *probePacketGroup) ProbeClusterDone(pci ccutils.ProbeClusterInfo) {
|
||||
if p.pci.Id != pci.Id {
|
||||
return
|
||||
}
|
||||
|
||||
p.pci.Result = pci.Result
|
||||
p.doneAt = mono.Now()
|
||||
}
|
||||
|
||||
func (p *probePacketGroup) ProbeClusterInfo() ccutils.ProbeClusterInfo {
|
||||
return p.pci
|
||||
}
|
||||
|
||||
func (p *probePacketGroup) MaybeFinalizeProbe(maxSequenceNumber uint64, rtt float64) (ccutils.ProbeClusterInfo, bool) {
|
||||
if p.doneAt.IsZero() {
|
||||
return ccutils.ProbeClusterInfoInvalid, false
|
||||
}
|
||||
|
||||
if maxSequenceNumber != 0 && p.maxSequenceNumber >= maxSequenceNumber {
|
||||
return p.pci, true
|
||||
}
|
||||
|
||||
settleWait := time.Duration(float64(p.params.Config.SettleWaitNumRTT) * rtt * float64(time.Second))
|
||||
if settleWait < p.params.Config.SettleWaitMin {
|
||||
settleWait = p.params.Config.SettleWaitMin
|
||||
}
|
||||
if settleWait > p.params.Config.SettleWaitMax {
|
||||
settleWait = p.params.Config.SettleWaitMax
|
||||
}
|
||||
if time.Since(p.doneAt) < settleWait {
|
||||
return ccutils.ProbeClusterInfoInvalid, false
|
||||
}
|
||||
|
||||
return p.pci, true
|
||||
}
|
||||
|
||||
func (p *probePacketGroup) Add(pi *packetInfo, sendDelta, recvDelta int64, isLost bool) error {
|
||||
if pi.probeClusterId != p.pci.Id {
|
||||
return nil
|
||||
}
|
||||
|
||||
p.maxSequenceNumber = max(p.maxSequenceNumber, pi.sequenceNumber)
|
||||
|
||||
return p.packetGroup.Add(pi, sendDelta, recvDelta, isLost)
|
||||
}
|
||||
|
||||
func (p *probePacketGroup) MarshalLogObject(e zapcore.ObjectEncoder) error {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
e.AddObject("pci", p.pci)
|
||||
e.AddObject("packetGroup", p.packetGroup)
|
||||
e.AddUint64("maxSequenceNumber", p.maxSequenceNumber)
|
||||
e.AddTime("doneAt", p.doneAt)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
// 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 (
|
||||
"time"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/sfu/bwe"
|
||||
"github.com/livekit/livekit-server/pkg/sfu/ccutils"
|
||||
"github.com/livekit/protocol/logger"
|
||||
"github.com/pion/rtcp"
|
||||
)
|
||||
|
||||
//
|
||||
// Based on a simplified/modified version of JitterPath paper
|
||||
// (https://homepage.iis.sinica.edu.tw/papers/lcs/2114-F.pdf)
|
||||
//
|
||||
// TWCC feedback is uesed to calcualte delta one-way-delay.
|
||||
// It is accumulated/propagated to determine in which region
|
||||
// groups of packets are operating in.
|
||||
//
|
||||
// In simplified terms,
|
||||
// o JQR (Join Queuing Region) is when channel is congested.
|
||||
// o DQR (Disjoint Queuing Region) is when channel is not.
|
||||
//
|
||||
// Packets are grouped and thresholds applied to smooth over
|
||||
// small variations. For example, in the paper,
|
||||
// if propagated_queuing_delay + delta_one_way_delay > 0 {
|
||||
// possibly_operating_in_jqr
|
||||
// }
|
||||
// But, in this implementation it is checked at packet group level,
|
||||
// i. e. using queuing delay and aggreated delta one-way-delay of
|
||||
// the group and a minimum value threshold is applied before declaring
|
||||
// that a group is in JQR.
|
||||
//
|
||||
// There is also hysteresis to make transisitons smoother, i.e. if the
|
||||
// metric is above a certain threshold, it is JQR and it is DQR only if it
|
||||
// is below a certain value and the gap in between those two thresholds
|
||||
// are treated as interdeterminate groups.
|
||||
//
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type SendSideBWEConfig struct {
|
||||
CongestionDetector CongestionDetectorConfig `yaml:"congestion_detector,omitempty"`
|
||||
}
|
||||
|
||||
var (
|
||||
DefaultSendSideBWEConfig = SendSideBWEConfig{
|
||||
CongestionDetector: defaultCongestionDetectorConfig,
|
||||
}
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type SendSideBWEParams struct {
|
||||
Config SendSideBWEConfig
|
||||
Logger logger.Logger
|
||||
}
|
||||
|
||||
type SendSideBWE struct {
|
||||
bwe.NullBWE
|
||||
|
||||
params SendSideBWEParams
|
||||
|
||||
*congestionDetector
|
||||
}
|
||||
|
||||
func NewSendSideBWE(params SendSideBWEParams) *SendSideBWE {
|
||||
return &SendSideBWE{
|
||||
params: params,
|
||||
congestionDetector: newCongestionDetector(congestionDetectorParams{
|
||||
Config: params.Config.CongestionDetector,
|
||||
Logger: params.Logger,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SendSideBWE) SetBWEListener(bweListener bwe.BWEListener) {
|
||||
s.congestionDetector.SetBWEListener(bweListener)
|
||||
}
|
||||
|
||||
func (s *SendSideBWE) Reset() {
|
||||
s.congestionDetector.Reset()
|
||||
}
|
||||
|
||||
func (s *SendSideBWE) RecordPacketSendAndGetSequenceNumber(
|
||||
atMicro int64,
|
||||
size int,
|
||||
isRTX bool,
|
||||
probeClusterId ccutils.ProbeClusterId,
|
||||
isProbe bool,
|
||||
) uint16 {
|
||||
return s.congestionDetector.RecordPacketSendAndGetSequenceNumber(atMicro, size, isRTX, probeClusterId, isProbe)
|
||||
}
|
||||
|
||||
func (s *SendSideBWE) HandleTWCCFeedback(report *rtcp.TransportLayerCC) {
|
||||
s.congestionDetector.HandleTWCCFeedback(report)
|
||||
}
|
||||
|
||||
func (s *SendSideBWE) UpdateRTT(rtt float64) {
|
||||
s.congestionDetector.UpdateRTT(rtt)
|
||||
}
|
||||
|
||||
func (s *SendSideBWE) CongestionState() bwe.CongestionState {
|
||||
return s.congestionDetector.CongestionState()
|
||||
}
|
||||
|
||||
func (s *SendSideBWE) CanProbe() bool {
|
||||
return s.congestionDetector.CanProbe()
|
||||
}
|
||||
|
||||
func (s *SendSideBWE) ProbeDuration() time.Duration {
|
||||
return s.congestionDetector.ProbeDuration()
|
||||
}
|
||||
|
||||
func (s *SendSideBWE) ProbeClusterStarting(pci ccutils.ProbeClusterInfo) {
|
||||
s.congestionDetector.ProbeClusterStarting(pci)
|
||||
}
|
||||
|
||||
func (s *SendSideBWE) ProbeClusterDone(pci ccutils.ProbeClusterInfo) {
|
||||
s.congestionDetector.ProbeClusterDone(pci)
|
||||
}
|
||||
|
||||
func (s *SendSideBWE) ProbeClusterIsGoalReached() bool {
|
||||
return s.congestionDetector.ProbeClusterIsGoalReached()
|
||||
}
|
||||
|
||||
func (s *SendSideBWE) ProbeClusterFinalize() (ccutils.ProbeSignal, int64, bool) {
|
||||
return s.congestionDetector.ProbeClusterFinalize()
|
||||
}
|
||||
|
||||
// ------------------------------------------------
|
||||
@@ -0,0 +1,191 @@
|
||||
// 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 (
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/livekit/protocol/logger"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
// -----------------------------------------------------------
|
||||
|
||||
type WeightedLossConfig struct {
|
||||
MinDurationForLossValidity time.Duration `yaml:"min_duration_for_loss_validity,omitempty"`
|
||||
BaseDuration time.Duration `yaml:"base_duration,omitempty"`
|
||||
BasePPS int `yaml:"base_pps,omitempty"`
|
||||
LossPenaltyFactor float64 `yaml:"loss_penalty_factor,omitempty"`
|
||||
}
|
||||
|
||||
var (
|
||||
defaultWeightedLossConfig = WeightedLossConfig{
|
||||
MinDurationForLossValidity: 100 * time.Millisecond,
|
||||
BaseDuration: 500 * time.Millisecond,
|
||||
BasePPS: 30,
|
||||
LossPenaltyFactor: 0.25,
|
||||
}
|
||||
)
|
||||
|
||||
// -----------------------------------------------------------
|
||||
|
||||
type trafficStatsParams struct {
|
||||
Config WeightedLossConfig
|
||||
Logger logger.Logger
|
||||
}
|
||||
|
||||
type trafficStats struct {
|
||||
params trafficStatsParams
|
||||
|
||||
minSendTime int64
|
||||
maxSendTime int64
|
||||
sendDelta int64
|
||||
recvDelta int64
|
||||
ackedPackets int
|
||||
ackedBytes int
|
||||
lostPackets int
|
||||
lostBytes int
|
||||
}
|
||||
|
||||
func newTrafficStats(params trafficStatsParams) *trafficStats {
|
||||
return &trafficStats{
|
||||
params: params,
|
||||
}
|
||||
}
|
||||
|
||||
func (ts *trafficStats) Merge(rhs *trafficStats) {
|
||||
if ts.minSendTime == 0 || rhs.minSendTime < ts.minSendTime {
|
||||
ts.minSendTime = rhs.minSendTime
|
||||
}
|
||||
if rhs.maxSendTime > ts.maxSendTime {
|
||||
ts.maxSendTime = rhs.maxSendTime
|
||||
}
|
||||
ts.sendDelta += rhs.sendDelta
|
||||
ts.recvDelta += rhs.recvDelta
|
||||
ts.ackedPackets += rhs.ackedPackets
|
||||
ts.ackedBytes += rhs.ackedBytes
|
||||
ts.lostPackets += rhs.lostPackets
|
||||
ts.lostBytes += rhs.lostBytes
|
||||
}
|
||||
|
||||
func (ts *trafficStats) NumBytes() int {
|
||||
return ts.ackedBytes + ts.lostBytes
|
||||
}
|
||||
|
||||
func (ts *trafficStats) Duration() int64 {
|
||||
return ts.maxSendTime - ts.minSendTime
|
||||
}
|
||||
|
||||
func (ts *trafficStats) AcknowledgedBitrate() int64 {
|
||||
duration := ts.Duration()
|
||||
if duration == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
ackedBitrate := float64(ts.ackedBytes) * 8 * 1e6 / float64(ts.Duration())
|
||||
return int64(ackedBitrate * ts.CapturedTrafficRatio())
|
||||
}
|
||||
|
||||
func (ts *trafficStats) CapturedTrafficRatio() float64 {
|
||||
if ts.recvDelta == 0 {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
// apply a penalty for lost packets,
|
||||
// the rationale being packet dropping is a strategy to relieve congestion
|
||||
// and if they were not dropped, they would have increased queuing delay,
|
||||
// as it is not possible to know the reason for the losses,
|
||||
// apply a small penalty to receive delta aggregate to simulate those packets
|
||||
// building up queuing delay.
|
||||
return min(1.0, float64(ts.sendDelta)/float64(ts.recvDelta+ts.lossPenalty()))
|
||||
}
|
||||
|
||||
func (ts *trafficStats) WeightedLoss() float64 {
|
||||
durationMicro := ts.Duration()
|
||||
if time.Duration(durationMicro*1000) < ts.params.Config.MinDurationForLossValidity {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
totalPackets := float64(ts.lostPackets + ts.ackedPackets)
|
||||
pps := totalPackets * 1e6 / float64(durationMicro)
|
||||
|
||||
// longer duration, i. e. more time resolution, lower pps is acceptable as the measurement is more stable
|
||||
deltaDuration := time.Duration(durationMicro*1000) - ts.params.Config.BaseDuration
|
||||
if deltaDuration < 0 {
|
||||
deltaDuration = 0
|
||||
}
|
||||
threshold := math.Exp(-deltaDuration.Seconds()) * float64(ts.params.Config.BasePPS)
|
||||
if pps < threshold {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
lossRatio := float64(0.0)
|
||||
if totalPackets != 0 {
|
||||
lossRatio = float64(ts.lostPackets) / totalPackets
|
||||
}
|
||||
|
||||
// Log10 is used to give higher weight for the same loss ratio at higher packet rates,
|
||||
// for e.g.
|
||||
// - 10% loss at 20 pps = 0.1 * log10(20) = 0.130
|
||||
// - 10% loss at 100 pps = 0.1 * log10(100) = 0.2
|
||||
// - 10% loss at 1000 pps = 0.1 * log10(1000) = 0.3
|
||||
return lossRatio * math.Log10(pps)
|
||||
}
|
||||
|
||||
func (ts *trafficStats) lossPenalty() int64 {
|
||||
return int64(float64(ts.recvDelta) * ts.WeightedLoss() * ts.params.Config.LossPenaltyFactor)
|
||||
}
|
||||
|
||||
func (ts *trafficStats) MarshalLogObject(e zapcore.ObjectEncoder) error {
|
||||
if ts == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
e.AddInt64("minSendTime", ts.minSendTime)
|
||||
e.AddInt64("maxSendTime", ts.maxSendTime)
|
||||
duration := time.Duration(ts.Duration() * 1000)
|
||||
e.AddDuration("duration", duration)
|
||||
|
||||
e.AddInt("ackedPackets", ts.ackedPackets)
|
||||
e.AddInt("ackedBytes", ts.ackedBytes)
|
||||
e.AddInt("lostPackets", ts.lostPackets)
|
||||
e.AddInt("lostBytes", ts.lostBytes)
|
||||
|
||||
bitrate := float64(0)
|
||||
if duration != 0 {
|
||||
bitrate = float64(ts.ackedBytes*8) / duration.Seconds()
|
||||
e.AddFloat64("bitrate", bitrate)
|
||||
}
|
||||
|
||||
e.AddInt64("sendDelta", ts.sendDelta)
|
||||
e.AddInt64("recvDelta", ts.recvDelta)
|
||||
e.AddInt64("groupDelay", ts.recvDelta-ts.sendDelta)
|
||||
|
||||
totalPackets := ts.lostPackets + ts.ackedPackets
|
||||
if duration != 0 {
|
||||
e.AddFloat64("pps", float64(totalPackets)/duration.Seconds())
|
||||
}
|
||||
if (totalPackets) != 0 {
|
||||
e.AddFloat64("rawLoss", float64(ts.lostPackets)/float64(totalPackets))
|
||||
}
|
||||
e.AddFloat64("weightedLoss", ts.WeightedLoss())
|
||||
e.AddInt64("lossPenalty", ts.lossPenalty())
|
||||
|
||||
capturedTrafficRatio := ts.CapturedTrafficRatio()
|
||||
e.AddFloat64("capturedTrafficRatio", capturedTrafficRatio)
|
||||
e.AddFloat64("estimatedAvailableChannelCapacity", bitrate*capturedTrafficRatio)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
// 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 (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/livekit/protocol/logger"
|
||||
"github.com/pion/rtcp"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
// ------------------------------------------------------
|
||||
|
||||
const (
|
||||
cOutlierReportFactor = 3
|
||||
cEstimatedFeedbackIntervalAlpha = float64(0.9)
|
||||
|
||||
cReferenceTimeMask = (1 << 24) - 1
|
||||
cReferenceTimeResolution = 64 // 64 ms
|
||||
)
|
||||
|
||||
// ------------------------------------------------------
|
||||
|
||||
var (
|
||||
errFeedbackReportOutOfOrder = errors.New("feedback report out-of-order")
|
||||
)
|
||||
|
||||
// ------------------------------------------------------
|
||||
|
||||
type twccFeedbackParams struct {
|
||||
Logger logger.Logger
|
||||
}
|
||||
|
||||
type twccFeedback struct {
|
||||
params twccFeedbackParams
|
||||
|
||||
lastFeedbackTime time.Time
|
||||
estimatedFeedbackInterval time.Duration
|
||||
numReports int
|
||||
numReportsOutOfOrder int
|
||||
|
||||
highestFeedbackCount uint8
|
||||
|
||||
cycles int64
|
||||
highestReferenceTime uint32
|
||||
}
|
||||
|
||||
func newTWCCFeedback(params twccFeedbackParams) *twccFeedback {
|
||||
return &twccFeedback{
|
||||
params: params,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *twccFeedback) ProcessReport(report *rtcp.TransportLayerCC, at time.Time) (int64, bool) {
|
||||
t.numReports++
|
||||
if t.lastFeedbackTime.IsZero() {
|
||||
t.lastFeedbackTime = at
|
||||
t.highestReferenceTime = report.ReferenceTime
|
||||
t.highestFeedbackCount = report.FbPktCount
|
||||
return (t.cycles + int64(report.ReferenceTime)) * cReferenceTimeResolution * 1000, false
|
||||
}
|
||||
|
||||
isOutOfOrder := false
|
||||
if (report.FbPktCount - t.highestFeedbackCount) > (1 << 7) {
|
||||
t.numReportsOutOfOrder++
|
||||
isOutOfOrder = true
|
||||
}
|
||||
|
||||
// reference time wrap around handling
|
||||
var referenceTime int64
|
||||
if (report.ReferenceTime-t.highestReferenceTime)&cReferenceTimeMask < (1 << 23) {
|
||||
if report.ReferenceTime < t.highestReferenceTime {
|
||||
t.cycles += (1 << 24)
|
||||
}
|
||||
t.highestReferenceTime = report.ReferenceTime
|
||||
referenceTime = t.cycles + int64(report.ReferenceTime)
|
||||
} else {
|
||||
cycles := t.cycles
|
||||
if report.ReferenceTime > t.highestReferenceTime && cycles >= (1<<24) {
|
||||
cycles -= (1 << 24)
|
||||
}
|
||||
referenceTime = cycles + int64(report.ReferenceTime)
|
||||
}
|
||||
|
||||
if !isOutOfOrder {
|
||||
sinceLast := at.Sub(t.lastFeedbackTime)
|
||||
if t.estimatedFeedbackInterval == 0 {
|
||||
t.estimatedFeedbackInterval = sinceLast
|
||||
} else {
|
||||
// filter out outliers from estimate
|
||||
if sinceLast > t.estimatedFeedbackInterval/cOutlierReportFactor && sinceLast < cOutlierReportFactor*t.estimatedFeedbackInterval {
|
||||
// smoothed version of inter feedback interval
|
||||
t.estimatedFeedbackInterval = time.Duration(cEstimatedFeedbackIntervalAlpha*float64(t.estimatedFeedbackInterval) + (1.0-cEstimatedFeedbackIntervalAlpha)*float64(sinceLast))
|
||||
}
|
||||
}
|
||||
t.lastFeedbackTime = at
|
||||
t.highestFeedbackCount = report.FbPktCount
|
||||
}
|
||||
|
||||
return referenceTime * cReferenceTimeResolution * 1000, isOutOfOrder
|
||||
}
|
||||
|
||||
func (t *twccFeedback) MarshalLogObject(e zapcore.ObjectEncoder) error {
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
e.AddTime("lastFeedbackTime", t.lastFeedbackTime)
|
||||
e.AddDuration("estimatedFeedbackInterval", t.estimatedFeedbackInterval)
|
||||
e.AddInt("numReports", t.numReports)
|
||||
e.AddInt("numReportsOutOfOrder", t.numReportsOutOfOrder)
|
||||
e.AddInt64("cycles", t.cycles/(1<<24))
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user