mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-13 17:09:07 +02:00
169 lines
6.4 KiB
Go
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")
|
|
})
|
|
}
|