mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-20 23:59:55 +00:00
Throttle VNC encoder JPEG quality and skip frames under write backpressure
This commit is contained in:
@@ -51,12 +51,18 @@ type metricsConn struct {
|
||||
maxFBUBytes uint64
|
||||
maxFBURects uint64
|
||||
|
||||
tickMu sync.Mutex
|
||||
tickStart time.Time
|
||||
tickPrevB uint64
|
||||
tickPrevW uint64
|
||||
tickPrevF uint64
|
||||
tickPrevNS uint64
|
||||
tickMu sync.Mutex
|
||||
tickStart time.Time
|
||||
tickPrevB uint64
|
||||
tickPrevW uint64
|
||||
tickPrevF uint64
|
||||
tickPrevNS uint64
|
||||
|
||||
// busyMu guards the sliding window used by BusyFraction.
|
||||
busyMu sync.Mutex
|
||||
busyLastTime time.Time
|
||||
busyLastNanos uint64
|
||||
busyFraction float64
|
||||
|
||||
closeOnce sync.Once
|
||||
done chan struct{}
|
||||
@@ -131,6 +137,38 @@ func (m *metricsConn) flushTick(final bool) {
|
||||
})
|
||||
}
|
||||
|
||||
// BusyFraction reports the fraction of recent wall time that Write spent
|
||||
// blocked in the underlying socket, as an exponentially smoothed value in
|
||||
// [0, 1]. Approximates downstream backpressure: persistent values near 1
|
||||
// mean the socket cannot keep up with the encoder's output. Callers can
|
||||
// throttle JPEG quality or skip frames in response.
|
||||
func (m *metricsConn) BusyFraction() float64 {
|
||||
now := time.Now()
|
||||
ns := atomic.LoadUint64(&m.writeNanos)
|
||||
|
||||
m.busyMu.Lock()
|
||||
defer m.busyMu.Unlock()
|
||||
if m.busyLastTime.IsZero() {
|
||||
m.busyLastTime = now
|
||||
m.busyLastNanos = ns
|
||||
return 0
|
||||
}
|
||||
period := now.Sub(m.busyLastTime)
|
||||
if period < 50*time.Millisecond {
|
||||
return m.busyFraction
|
||||
}
|
||||
delta := ns - m.busyLastNanos
|
||||
sample := float64(delta) / float64(period.Nanoseconds())
|
||||
if sample > 1 {
|
||||
sample = 1
|
||||
}
|
||||
const alpha = 0.4
|
||||
m.busyFraction = alpha*sample + (1-alpha)*m.busyFraction
|
||||
m.busyLastTime = now
|
||||
m.busyLastNanos = ns
|
||||
return m.busyFraction
|
||||
}
|
||||
|
||||
// isFBUHeader reports whether the given Write payload is the 4-byte
|
||||
// FramebufferUpdate header (message type 0, padding 0, rect-count high
|
||||
// byte). Rect bodies are written separately by sendDirtyAndMoves, so the
|
||||
|
||||
@@ -37,6 +37,11 @@ func (s *session) processFBRequest(req fbRequest) error {
|
||||
return err
|
||||
}
|
||||
|
||||
busy := s.applyBackpressure()
|
||||
if busy >= backpressureSkipThreshold {
|
||||
return s.sendEmptyUpdate()
|
||||
}
|
||||
|
||||
img, err := s.captureFrame()
|
||||
if errors.Is(err, errFrameUnchanged) {
|
||||
// macOS hashes the raw capture bytes and short-circuits when the
|
||||
@@ -123,6 +128,60 @@ func (s *session) processIncremental(img *image.RGBA) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// backpressureSkipThreshold is the BusyFraction at and above which we drop
|
||||
// the next encode entirely and respond with an empty FramebufferUpdate.
|
||||
// Above this level the encoder would only stack more bytes behind a socket
|
||||
// that is already write-blocked, raising end-to-end latency.
|
||||
const backpressureSkipThreshold = 0.65
|
||||
|
||||
// backpressureRampStart is where adaptive quality begins clipping. Below
|
||||
// this fraction the honoured client quality is used as-is.
|
||||
const backpressureRampStart = 0.2
|
||||
|
||||
// backpressureMinQuality is the floor JPEG quality picked when the socket
|
||||
// is fully saturated short of the skip threshold.
|
||||
const backpressureMinQuality = 25
|
||||
|
||||
// applyBackpressure samples the socket BusyFraction (if available) and, if
|
||||
// Tight is in use, ramps the active JPEG quality from the client-honoured
|
||||
// value down to backpressureMinQuality as the fraction climbs from
|
||||
// backpressureRampStart toward backpressureSkipThreshold. Returns the
|
||||
// observed fraction so the caller can decide whether to skip the frame.
|
||||
func (s *session) applyBackpressure() float64 {
|
||||
type busyReporter interface{ BusyFraction() float64 }
|
||||
bs, ok := s.conn.(busyReporter)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
frac := bs.BusyFraction()
|
||||
|
||||
s.encMu.RLock()
|
||||
tight := s.tight
|
||||
s.encMu.RUnlock()
|
||||
if tight == nil {
|
||||
return frac
|
||||
}
|
||||
|
||||
base := jpegQualityForLevel(tight.qualityLevel)
|
||||
if base == 0 {
|
||||
base = tightJPEGQuality
|
||||
}
|
||||
q := base
|
||||
if frac > backpressureRampStart {
|
||||
span := backpressureSkipThreshold - backpressureRampStart
|
||||
t := (frac - backpressureRampStart) / span
|
||||
if t > 1 {
|
||||
t = 1
|
||||
}
|
||||
q = base - int(float64(base-backpressureMinQuality)*t)
|
||||
if q < backpressureMinQuality {
|
||||
q = backpressureMinQuality
|
||||
}
|
||||
}
|
||||
tight.jpegQualityOverride = q
|
||||
return frac
|
||||
}
|
||||
|
||||
// captureErrorLog emits one log line on the first failure after success,
|
||||
// then at most once every captureErrThrottle while the capturer keeps
|
||||
// failing. The "recovered" transition is logged once when err is nil and
|
||||
|
||||
Reference in New Issue
Block a user