Files
netbird/client/internal/peer/worker_ice_test.go
T

169 lines
6.4 KiB
Go

package peer
import (
"testing"
"time"
"github.com/pion/ice/v4"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func pairStat(local, remote string, rtt time.Duration) ice.CandidatePairStats {
return ice.CandidatePairStats{
LocalCandidateID: local,
RemoteCandidateID: remote,
CurrentRoundTripTime: rtt.Seconds(),
}
}
func TestLatencySamplerObserve(t *testing.T) {
t.Run("first sample is taken as is", func(t *testing.T) {
var s latencySampler
got := s.observe(pairStat("local", "remote", 40*time.Millisecond))
assert.Equal(t, 40*time.Millisecond, got.Latency, "the first sample has nothing to average with")
})
t.Run("zero round trip time is not recorded", func(t *testing.T) {
var s latencySampler
require.Zero(t, s.observe(pairStat("local", "remote", 0)).Latency, "a pair without an RTT sample yields no latency")
// the first real sample still seeds the average
assert.Equal(t, 20*time.Millisecond, s.observe(pairStat("local", "remote", 20*time.Millisecond)).Latency)
})
t.Run("single spike is damped", func(t *testing.T) {
var s latencySampler
s.observe(pairStat("local", "remote", 20*time.Millisecond))
spiked := s.observe(pairStat("local", "remote", 220*time.Millisecond)).Latency
// a single sample only contributes its weight of the 200ms delta
assert.Equal(t, 50*time.Millisecond, spiked, "a single spike must not be reported in full")
// and it decays again once the path is back to normal
for range 10 {
s.observe(pairStat("local", "remote", 20*time.Millisecond))
}
assert.Less(t, s.observe(pairStat("local", "remote", 20*time.Millisecond)).Latency, 30*time.Millisecond,
"the average should return towards the steady state")
})
t.Run("sustained change is followed", func(t *testing.T) {
var s latencySampler
s.observe(pairStat("local", "remote", 20*time.Millisecond))
// the route selection switch margin, see routemanager/client
const switchMargin = 20 * time.Millisecond
// at a 4s sample interval, 3 samples are about 12 seconds
var latency time.Duration
for range 3 {
latency = s.observe(pairStat("local", "remote", 80*time.Millisecond)).Latency
}
assert.Greater(t, latency-20*time.Millisecond, switchMargin,
"a sustained 60ms degradation must clear the switch margin within seconds")
// and 10 more samples, about 40 seconds in total, get most of the way there
for range 10 {
latency = s.observe(pairStat("local", "remote", 80*time.Millisecond)).Latency
}
assert.Greater(t, latency, 70*time.Millisecond, "a sustained degradation must be tracked within tens of seconds")
})
// Smoothing narrows the scatter of a jittery path but leaves a residue, and
// that residue is what route selection has to compare its margins against.
// The reported noise has to describe it, so it has to grow with the jitter
// and cover the movement of the smoothed value.
t.Run("reported noise reflects the jitter of the path", func(t *testing.T) {
// 100ms +/- 40ms, the same shape the netem jitter test uses
jittery := []int{
104, 138, 61, 96, 141, 72, 118, 65, 133, 88,
59, 127, 92, 136, 68, 111, 143, 74, 99, 121,
}
steady := []int{
100, 101, 99, 100, 102, 99, 100, 101, 100, 99, 101, 100, 99, 100, 101, 100, 99, 101, 100, 100,
101, 99, 100, 102, 99, 100, 101, 100, 99, 101, 100, 99, 100, 101, 100, 99, 101, 100, 100, 101,
}
spreadAndNoise := func(raw []int) (spread, noise time.Duration) {
var s latencySampler
var minSmoothed, maxSmoothed time.Duration
for i, ms := range raw {
got := s.observe(pairStat("local", "remote", time.Duration(ms)*time.Millisecond))
noise = got.Noise
// skip the warmup, the first sample is taken as is
if i < 5 {
continue
}
if minSmoothed == 0 || got.Latency < minSmoothed {
minSmoothed = got.Latency
}
if got.Latency > maxSmoothed {
maxSmoothed = got.Latency
}
}
return maxSmoothed - minSmoothed, noise
}
jitterySpread, jitteryNoise := spreadAndNoise(jittery)
steadySpread, steadyNoise := spreadAndNoise(steady)
assert.Greater(t, jitteryNoise, steadyNoise, "a jittery path must report more noise than a steady one")
assert.Less(t, steadyNoise, time.Millisecond, "a steady path must report almost no noise, so the fixed margins decide")
// the noise is a deviation, so a few of them have to cover the movement
// of the smoothed value, which is what the switch margin relies on
assert.Greater(t, 4*jitteryNoise, jitterySpread,
"reported noise %v must account for the smoothed spread %v", jitteryNoise, jitterySpread)
assert.Greater(t, 4*steadyNoise+time.Millisecond, steadySpread,
"reported noise %v must account for the smoothed spread %v", steadyNoise, steadySpread)
})
t.Run("new candidate pair discards the previous average", func(t *testing.T) {
var s latencySampler
s.observe(pairStat("local", "remote", 200*time.Millisecond))
got := s.observe(pairStat("local2", "remote2", 20*time.Millisecond)).Latency
assert.Equal(t, 20*time.Millisecond, got, "samples of the previous path must not be carried over")
})
// A single raw sample is when the estimate deserves the least trust: with
// zero reported noise, a lucky outlier could clear the switch margins and
// move a route on its own.
t.Run("a fresh path reports conservative noise", func(t *testing.T) {
var s latencySampler
first := s.observe(pairStat("local", "remote", 100*time.Millisecond))
assert.Greater(t, first.Noise, 10*time.Millisecond, "a single-sample estimate must not be reported as noise-free")
// steady samples shrink the uncertainty again
var got LatencySample
for range 40 {
got = s.observe(pairStat("local", "remote", 100*time.Millisecond))
}
assert.Less(t, got.Noise, 2*time.Millisecond, "steady samples should earn the estimate its trust back")
})
t.Run("a pair change keeps the larger deviation of a jittery link", func(t *testing.T) {
var s latencySampler
// build up a deviation well above the reseed floor of the new path
for _, ms := range []int{100, 180, 60, 170, 50, 190, 70, 160} {
s.observe(pairStat("local", "remote", time.Duration(ms)*time.Millisecond))
}
before := s.deviation
require.Greater(t, before, 25*time.Millisecond, "the jittery path should have accumulated deviation")
s.observe(pairStat("local2", "remote2", 50*time.Millisecond))
assert.GreaterOrEqual(t, s.deviation, before, "a pair change must not shrink the known jitter of the link")
})
}