mirror of
https://github.com/livekit/livekit.git
synced 2026-09-17 03:44:48 +00:00
* fix: don't undercount data channel bitrate over idle gaps
BitrateCalculator divided drained bytes by wall-clock time, including
idle gaps between sparse writes. A fast channel fed sparsely (e.g. 1 KB
every second, acked in a few ms) therefore reported its low offered load
(~8 kbps) instead of its drain capacity (~800 kbps). In the unreliable
writer that shrinks targetLatencyLimit and drops bursts on a channel
that is actually keeping up.
Measure the rate over backlogged ("busy") time only, so idle time no
longer dilutes the estimate. Backlog is detected from the buffer
occupancy just before a write (bufferedAmount - bytesWritten): since no
bytes are added between writes the buffer only drains, so a non-zero
pre-write level means it never emptied and the interval was genuinely
busy. Gating on the post-write buffered amount would be wrong -- it
always includes the just-enqueued (not yet SACKed) bytes and is ~never
zero, collapsing the estimate back to wall-clock. When no backlog is
ever observed the calculator reports no estimate (ok=false) instead of a
confidently-low number.
Introduce BitrateMode:
- BusyOnly (writer default): busy denominator, all drained bytes counted.
- ExcludeIdleDrain: also drops bytes drained across non-backlogged
intervals for a clean drain-capacity estimate free of idle
contamination.
- WallClock: elapsed-time denominator, all bytes; for the test-client
reader which has no send buffer to observe.
Call sites keep the conservative behaviour (writers BusyOnly, reader
WallClock). Tests exercise both writer modes and the mixed idle-then-
backlog window where they diverge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Trim comments in data channel bitrate calculator
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Make BitrateMode.String a method on the enum
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
176 lines
4.3 KiB
Go
176 lines
4.3 KiB
Go
package datachannel
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/gammazero/deque"
|
|
|
|
"github.com/livekit/protocol/utils/mono"
|
|
)
|
|
|
|
const (
|
|
BitrateDuration = 2 * time.Second
|
|
BitrateWindow = 100 * time.Millisecond
|
|
)
|
|
|
|
// BitrateMode selects how the sliding-window rate is computed.
|
|
type BitrateMode int
|
|
|
|
const (
|
|
// BitrateModeBusyOnly divides drained bytes by backlogged ("busy") time
|
|
// only; idle gaps are excluded from the denominator. All drained bytes count.
|
|
BitrateModeBusyOnly BitrateMode = iota
|
|
|
|
// BitrateModeExcludeIdleDrain is like BitrateModeBusyOnly but also excludes
|
|
// bytes drained across a non-backlogged interval from the numerator.
|
|
BitrateModeExcludeIdleDrain
|
|
|
|
// BitrateModeWallClock divides all bytes by elapsed wall-clock time. For
|
|
// consumers with no send buffer, e.g. a receiver measuring its incoming rate.
|
|
BitrateModeWallClock
|
|
)
|
|
|
|
func (m BitrateMode) String() string {
|
|
switch m {
|
|
case BitrateModeBusyOnly:
|
|
return "busyOnly"
|
|
case BitrateModeExcludeIdleDrain:
|
|
return "excludeIdleDrain"
|
|
case BitrateModeWallClock:
|
|
return "wallClock"
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|
|
|
|
// BitrateCalculator calculates bitrate over sliding window
|
|
type BitrateCalculator struct {
|
|
lock sync.Mutex
|
|
windowDuration time.Duration
|
|
duration time.Duration
|
|
|
|
windows deque.Deque[bitrateWindow]
|
|
active bitrateWindow
|
|
|
|
bytes int
|
|
busy time.Duration
|
|
lastBufferedAmount int
|
|
last time.Time
|
|
start time.Time
|
|
mode BitrateMode
|
|
}
|
|
|
|
// NewBitrateCalculator creates a calculator. See BitrateMode for how mode
|
|
// governs the numerator and denominator.
|
|
func NewBitrateCalculator(duration time.Duration, window time.Duration, mode BitrateMode) *BitrateCalculator {
|
|
windowCnt := int((duration + (window - 1)) / window)
|
|
if windowCnt == 0 {
|
|
windowCnt = 1
|
|
}
|
|
now := mono.Now()
|
|
c := &BitrateCalculator{
|
|
duration: duration,
|
|
windowDuration: window,
|
|
start: now,
|
|
active: bitrateWindow{start: now},
|
|
mode: mode,
|
|
}
|
|
c.windows.SetBaseCap(windowCnt + 1)
|
|
|
|
return c
|
|
}
|
|
|
|
func (c *BitrateCalculator) AddBytes(bytes int, bufferedAmout int, ts time.Time) {
|
|
c.lock.Lock()
|
|
defer c.lock.Unlock()
|
|
|
|
// bufferedAmout is sampled right after the write, so it includes the bytes
|
|
// just enqueued; subtracting them gives the buffer occupancy before the
|
|
// write. Between writes the buffer only drains, so a non-zero pre-write
|
|
// level means it stayed backlogged for the whole interval.
|
|
backlogged := bufferedAmout-bytes > 0
|
|
var busy time.Duration
|
|
if !c.last.IsZero() && backlogged {
|
|
if busy = ts.Sub(c.last); busy < 0 {
|
|
busy = 0
|
|
}
|
|
}
|
|
c.last = ts
|
|
|
|
// bytes that actually drained since the previous sample
|
|
bytes -= bufferedAmout - c.lastBufferedAmount
|
|
if bytes < 0 {
|
|
// it is possible that internal buffering (non-data like DCEP packet from webrtc) caused bytes to be negative
|
|
bytes = 0
|
|
}
|
|
c.lastBufferedAmount = bufferedAmout
|
|
|
|
if c.mode == BitrateModeExcludeIdleDrain && !backlogged {
|
|
// drain across a non-backlogged interval finished at an unknown point,
|
|
// so it is not a reliable rate sample
|
|
bytes = 0
|
|
}
|
|
|
|
if ts.Sub(c.active.start) >= c.windowDuration {
|
|
c.windows.PushBack(c.active)
|
|
c.active.start = ts
|
|
c.active.bytes = 0
|
|
c.active.busy = 0
|
|
|
|
for c.windows.Len() > 0 {
|
|
// pop expired windows
|
|
if w := c.windows.Front(); ts.Sub(w.start) > (c.duration + c.windowDuration) {
|
|
c.bytes -= w.bytes
|
|
c.busy -= w.busy
|
|
c.windows.PopFront()
|
|
} else {
|
|
c.start = w.start
|
|
break
|
|
}
|
|
}
|
|
if c.windows.Len() == 0 {
|
|
c.start = ts
|
|
c.bytes = 0
|
|
c.busy = 0
|
|
}
|
|
}
|
|
c.bytes += bytes
|
|
c.active.bytes += bytes
|
|
c.busy += busy
|
|
c.active.busy += busy
|
|
}
|
|
|
|
func (c *BitrateCalculator) Bitrate(ts time.Time) (int, bool) {
|
|
return c.bitrate(ts, false)
|
|
}
|
|
|
|
func (c *BitrateCalculator) ForceBitrate(ts time.Time) (int, bool) {
|
|
return c.bitrate(ts, true)
|
|
}
|
|
|
|
func (c *BitrateCalculator) bitrate(ts time.Time, force bool) (int, bool) {
|
|
c.lock.Lock()
|
|
defer c.lock.Unlock()
|
|
// busy modes divide by backlogged time; wall-clock mode by elapsed time
|
|
denom := c.busy
|
|
if c.mode == BitrateModeWallClock {
|
|
denom = ts.Sub(c.start)
|
|
}
|
|
if denom < c.windowDuration {
|
|
if force {
|
|
denom = c.windowDuration
|
|
} else {
|
|
return 0, false
|
|
}
|
|
}
|
|
|
|
return c.bytes * 8 * 1000 / int(denom.Milliseconds()), true
|
|
}
|
|
|
|
type bitrateWindow struct {
|
|
start time.Time
|
|
bytes int
|
|
busy time.Duration
|
|
}
|