mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-30 03:21:29 +02:00
234 lines
6.0 KiB
Go
234 lines
6.0 KiB
Go
//go:build !js && !ios && !android
|
|
|
|
package server
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"net"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
)
|
|
|
|
// SessionTick is one sampling slice of a VNC session's wire activity.
|
|
// BytesOut / Writes / FBUs are deltas observed during this tick;
|
|
// Max* fields are the high-water marks observed during this tick (reset
|
|
// at the start of the next). Period is the wall-clock duration covered
|
|
// (typically sessionTickInterval, shorter for the final flush).
|
|
type SessionTick struct {
|
|
Period time.Duration
|
|
BytesOut uint64
|
|
Writes uint64
|
|
FBUs uint64
|
|
MaxFBUBytes uint64
|
|
MaxFBURects uint64
|
|
MaxWriteBytes uint64
|
|
WriteNanos uint64
|
|
}
|
|
|
|
// sessionTickInterval is how often metricsConn emits a SessionTick. One
|
|
// second covers roughly one FBU round-trip at typical client request
|
|
// cadences during steady-state activity.
|
|
const sessionTickInterval = time.Second
|
|
|
|
// metricsConn wraps a net.Conn and tracks per-session byte / write / FBU
|
|
// counters. Updates are atomic so the cost is a few atomic ops per Write
|
|
// (well under 100 ns), negligible against the syscall itself, so the wrap
|
|
// is always installed. A goroutine emits a SessionTick to the recorder
|
|
// every sessionTickInterval (only when the tick has activity to report);
|
|
// a final partial-tick flush runs on Close.
|
|
type metricsConn struct {
|
|
net.Conn
|
|
|
|
recorder func(SessionTick)
|
|
|
|
bytesOut atomic.Uint64
|
|
writes atomic.Uint64
|
|
writeNanos atomic.Uint64
|
|
largestPkt atomic.Uint64
|
|
fbus atomic.Uint64
|
|
fbuBytes atomic.Uint64
|
|
fbuRects atomic.Uint64
|
|
maxFBUBytes atomic.Uint64
|
|
maxFBURects atomic.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{}
|
|
}
|
|
|
|
func newMetricsConn(c net.Conn, recorder func(SessionTick)) net.Conn {
|
|
m := &metricsConn{
|
|
Conn: c,
|
|
recorder: recorder,
|
|
tickStart: time.Now(),
|
|
done: make(chan struct{}),
|
|
}
|
|
if recorder != nil {
|
|
go m.tickLoop()
|
|
}
|
|
return m
|
|
}
|
|
|
|
// tickLoop emits a SessionTick every sessionTickInterval until done.
|
|
// Empty ticks (no writes since the last tick) are skipped.
|
|
func (m *metricsConn) tickLoop() {
|
|
t := time.NewTicker(sessionTickInterval)
|
|
defer t.Stop()
|
|
for {
|
|
select {
|
|
case <-m.done:
|
|
return
|
|
case <-t.C:
|
|
m.flushTick(false)
|
|
}
|
|
}
|
|
}
|
|
|
|
// flushTick computes deltas since the last tick, resets the per-tick max
|
|
// trackers, and emits a SessionTick to the recorder. final=true forces
|
|
// emission even if no writes happened (used at session close to record
|
|
// the trailing partial period).
|
|
func (m *metricsConn) flushTick(final bool) {
|
|
m.tickMu.Lock()
|
|
defer m.tickMu.Unlock()
|
|
|
|
b := m.bytesOut.Load()
|
|
w := m.writes.Load()
|
|
f := m.fbus.Load()
|
|
ns := m.writeNanos.Load()
|
|
|
|
db := b - m.tickPrevB
|
|
dw := w - m.tickPrevW
|
|
df := f - m.tickPrevF
|
|
dns := ns - m.tickPrevNS
|
|
m.tickPrevB, m.tickPrevW, m.tickPrevF, m.tickPrevNS = b, w, f, ns
|
|
|
|
maxFBU := m.maxFBUBytes.Swap(0)
|
|
maxRects := m.maxFBURects.Swap(0)
|
|
maxPkt := m.largestPkt.Swap(0)
|
|
|
|
period := time.Since(m.tickStart)
|
|
m.tickStart = time.Now()
|
|
|
|
if dw == 0 && !final {
|
|
return
|
|
}
|
|
m.recorder(SessionTick{
|
|
Period: period,
|
|
BytesOut: db,
|
|
Writes: dw,
|
|
FBUs: df,
|
|
MaxFBUBytes: maxFBU,
|
|
MaxFBURects: maxRects,
|
|
MaxWriteBytes: maxPkt,
|
|
WriteNanos: dns,
|
|
})
|
|
}
|
|
|
|
// 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 := m.writeNanos.Load()
|
|
|
|
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
|
|
}
|
|
|
|
// startsFBU reports whether the Write payload begins a FramebufferUpdate
|
|
// message (message type byte 0). This holds both for the standalone 4-byte
|
|
// header that sendDirtyAndMoves writes before its rect bodies and for the
|
|
// single framed Write that sendFullUpdate / sendEmptyUpdate use to emit a
|
|
// whole FBU (header plus body) at once. Either way the FBU boundary lines
|
|
// up with this Write boundary.
|
|
func startsFBU(p []byte) bool {
|
|
return len(p) >= 1 && p[0] == serverFramebufferUpdate
|
|
}
|
|
|
|
func (m *metricsConn) Write(p []byte) (int, error) {
|
|
fbuStart := startsFBU(p)
|
|
if fbuStart {
|
|
m.flushFBUMax()
|
|
m.fbus.Add(1)
|
|
}
|
|
|
|
t0 := time.Now()
|
|
n, err := m.Conn.Write(p)
|
|
m.writeNanos.Add(uint64(time.Since(t0).Nanoseconds()))
|
|
m.bytesOut.Add(uint64(n))
|
|
m.writes.Add(1)
|
|
|
|
m.fbuBytes.Add(uint64(n))
|
|
if fbuStart {
|
|
// Rect count is carried in bytes 2:3 of the FBU header. A standalone
|
|
// header records it here; the rect bodies that follow only add bytes.
|
|
if len(p) >= 4 {
|
|
m.fbuRects.Add(uint64(binary.BigEndian.Uint16(p[2:4])))
|
|
}
|
|
}
|
|
|
|
if uint64(n) > m.largestPkt.Load() {
|
|
m.largestPkt.Store(uint64(n))
|
|
}
|
|
return n, err
|
|
}
|
|
|
|
// flushFBUMax folds the bytes and rects accumulated for the FBU that just
|
|
// ended into the per-tick high-water marks, then resets the accumulators
|
|
// for the next FBU.
|
|
func (m *metricsConn) flushFBUMax() {
|
|
if b := m.fbuBytes.Swap(0); b > m.maxFBUBytes.Load() {
|
|
m.maxFBUBytes.Store(b)
|
|
}
|
|
if r := m.fbuRects.Swap(0); r > m.maxFBURects.Load() {
|
|
m.maxFBURects.Store(r)
|
|
}
|
|
}
|
|
|
|
func (m *metricsConn) Close() error {
|
|
m.closeOnce.Do(func() {
|
|
close(m.done)
|
|
if m.recorder == nil {
|
|
return
|
|
}
|
|
m.flushFBUMax()
|
|
m.flushTick(true)
|
|
})
|
|
return m.Conn.Close()
|
|
}
|