mirror of
https://github.com/livekit/livekit.git
synced 2026-04-01 13:05:42 +00:00
173 lines
5.9 KiB
Go
173 lines
5.9 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 streamallocator
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/livekit/protocol/utils/timeseries"
|
|
)
|
|
|
|
// ------------------------------------------------
|
|
|
|
const (
|
|
rateMonitorWindow = 10 * time.Second
|
|
queueMonitorWindow = 2 * time.Second
|
|
)
|
|
|
|
// ------------------------------------------------
|
|
|
|
type RateMonitor struct {
|
|
bitrateEstimate *timeseries.TimeSeries[int64]
|
|
managedBytesSent *timeseries.TimeSeries[uint32]
|
|
managedBytesRetransmitted *timeseries.TimeSeries[uint32]
|
|
unmanagedBytesSent *timeseries.TimeSeries[uint32]
|
|
unmanagedBytesRetransmitted *timeseries.TimeSeries[uint32]
|
|
|
|
// STREAM-ALLOCATOR-EXPERIMENTAL-TODO: remove after experimental
|
|
history []string
|
|
}
|
|
|
|
func NewRateMonitor() *RateMonitor {
|
|
return &RateMonitor{
|
|
bitrateEstimate: timeseries.NewTimeSeries[int64](timeseries.TimeSeriesParams{
|
|
UpdateOp: timeseries.TimeSeriesUpdateOpLatest,
|
|
Window: rateMonitorWindow,
|
|
}),
|
|
managedBytesSent: timeseries.NewTimeSeries[uint32](timeseries.TimeSeriesParams{
|
|
UpdateOp: timeseries.TimeSeriesUpdateOpAdd,
|
|
Window: rateMonitorWindow,
|
|
}),
|
|
managedBytesRetransmitted: timeseries.NewTimeSeries[uint32](timeseries.TimeSeriesParams{
|
|
UpdateOp: timeseries.TimeSeriesUpdateOpAdd,
|
|
Window: rateMonitorWindow,
|
|
}),
|
|
unmanagedBytesSent: timeseries.NewTimeSeries[uint32](timeseries.TimeSeriesParams{
|
|
UpdateOp: timeseries.TimeSeriesUpdateOpAdd,
|
|
Window: rateMonitorWindow,
|
|
}),
|
|
unmanagedBytesRetransmitted: timeseries.NewTimeSeries[uint32](timeseries.TimeSeriesParams{
|
|
UpdateOp: timeseries.TimeSeriesUpdateOpAdd,
|
|
Window: rateMonitorWindow,
|
|
}),
|
|
}
|
|
}
|
|
|
|
func (r *RateMonitor) Update(estimate int64, managedBytesSent uint32, managedBytesRetransmitted uint32, unmanagedBytesSent uint32, unmanagedBytesRetransmitted uint32) {
|
|
now := time.Now()
|
|
r.bitrateEstimate.AddSampleAt(estimate, now)
|
|
r.managedBytesSent.AddSampleAt(managedBytesSent, now)
|
|
r.managedBytesRetransmitted.AddSampleAt(managedBytesRetransmitted, now)
|
|
r.unmanagedBytesSent.AddSampleAt(unmanagedBytesSent, now)
|
|
r.unmanagedBytesRetransmitted.AddSampleAt(unmanagedBytesRetransmitted, now)
|
|
|
|
r.updateHistory()
|
|
}
|
|
|
|
// STREAM-ALLOCATOR-TODO:
|
|
// This should be updated periodically to flush any pending.
|
|
// Reason is that the estimate could be higher than the actual rate by a significant amount.
|
|
// So, updating periodically to flush out samples that will not contribute to queueing would be good.
|
|
func (r *RateMonitor) GetQueuingGuess() float64 {
|
|
_, _, _, _, _, qd := r.getRates(queueMonitorWindow)
|
|
return qd
|
|
}
|
|
|
|
func (r *RateMonitor) getRates(monitorDuration time.Duration) (float64, float64, float64, float64, float64, float64) {
|
|
threshold := time.Now().Add(-monitorDuration)
|
|
bitrateEstimateSamples := r.bitrateEstimate.GetSamplesAfter(threshold)
|
|
managedBytesSentSamples := r.managedBytesSent.GetSamplesAfter(threshold)
|
|
managedBytesRetransmittedSamples := r.managedBytesRetransmitted.GetSamplesAfter(threshold)
|
|
unmanagedBytesSentSamples := r.unmanagedBytesSent.GetSamplesAfter(threshold)
|
|
unmanagedBytesRetransmittedSamples := r.unmanagedBytesRetransmitted.GetSamplesAfter(threshold)
|
|
|
|
if len(bitrateEstimateSamples) == 0 || (len(managedBytesSentSamples)+len(managedBytesRetransmittedSamples)+len(unmanagedBytesSentSamples)+len(unmanagedBytesRetransmittedSamples)) == 0 {
|
|
return 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
|
|
}
|
|
|
|
totalBitrateEstimate := getTimeWeightedSum(bitrateEstimateSamples)
|
|
totalManagedSent := getRate(managedBytesSentSamples) * 8
|
|
totalManagedRetransmitted := getRate(managedBytesRetransmittedSamples) * 8
|
|
totalUnmanagedSent := getRate(unmanagedBytesSentSamples) * 8
|
|
totalUnmanagedRetransmitted := getRate(unmanagedBytesRetransmittedSamples) * 8
|
|
totalBits := totalManagedSent + totalManagedRetransmitted + totalUnmanagedSent + totalUnmanagedRetransmitted
|
|
|
|
queuingDelay := float64(0.0)
|
|
if totalBits > totalBitrateEstimate {
|
|
latestBitrateEstimate := bitrateEstimateSamples[len(bitrateEstimateSamples)-1].Value
|
|
excessBits := totalBits - totalBitrateEstimate
|
|
queuingDelay = excessBits / float64(latestBitrateEstimate)
|
|
}
|
|
return totalBitrateEstimate, totalManagedSent, totalManagedRetransmitted, totalUnmanagedSent, totalUnmanagedRetransmitted, queuingDelay
|
|
}
|
|
|
|
func (r *RateMonitor) updateHistory() {
|
|
if len(r.history) >= 10 {
|
|
r.history = r.history[1:]
|
|
}
|
|
|
|
e, m, mr, um, umr, qd := r.getRates(time.Second)
|
|
if e == 0.0 {
|
|
return
|
|
}
|
|
|
|
r.history = append(
|
|
r.history,
|
|
fmt.Sprintf("t: %+v, e: %.2f, m: %.2f/%.2f, um: %.2f/%.2f, qd: %.2f", time.Now().UnixMilli(), e, m, mr, um, umr, qd),
|
|
)
|
|
}
|
|
|
|
func (r *RateMonitor) GetHistory() []string {
|
|
return r.history
|
|
}
|
|
|
|
// ------------------------------------------------
|
|
|
|
func getTimeWeightedSum[T int64 | uint32](samples []timeseries.TimeSeriesSample[T]) float64 {
|
|
if len(samples) < 2 {
|
|
return 0.0
|
|
}
|
|
|
|
sum := 0.0
|
|
for i := 1; i < len(samples); i++ {
|
|
diff := samples[i].At.Sub(samples[i-1].At).Seconds()
|
|
sum += diff * float64(samples[i-1].Value)
|
|
}
|
|
|
|
diff := time.Now().Sub(samples[len(samples)-1].At).Seconds()
|
|
sum += diff * float64(samples[len(samples)-1].Value)
|
|
return sum
|
|
}
|
|
|
|
func getRate[T int64 | uint32](samples []timeseries.TimeSeriesSample[T]) float64 {
|
|
if len(samples) < 2 {
|
|
return 0.0
|
|
}
|
|
|
|
sum := 0.0
|
|
// start at 1 as the first sample duration is not available
|
|
for i := 1; i < len(samples); i++ {
|
|
sum += float64(samples[i].Value)
|
|
}
|
|
|
|
duration := samples[len(samples)-1].At.Sub(samples[0].At)
|
|
if duration == 0 {
|
|
return 0.0
|
|
}
|
|
|
|
return sum / duration.Seconds()
|
|
}
|