mirror of
				https://github.com/tailscale/tailscale.git
				synced 2025-10-31 08:11:32 +01:00 
			
		
		
		
	In order to improve latency tracking, we will use an exponentially weighted moving average that will smooth change over time and suppress large outlier values. Updates tailscale/corp#26649 Signed-off-by: James Tucker <james@tailscale.com>
		
			
				
	
	
		
			179 lines
		
	
	
		
			5.0 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			179 lines
		
	
	
		
			5.0 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright (c) Tailscale Inc & AUTHORS
 | |
| // SPDX-License-Identifier: BSD-3-Clause
 | |
| 
 | |
| package maths
 | |
| 
 | |
| import (
 | |
| 	"slices"
 | |
| 	"testing"
 | |
| 	"time"
 | |
| )
 | |
| 
 | |
| // some real world latency samples.
 | |
| var (
 | |
| 	latencyHistory1 = []int{
 | |
| 		14, 12, 15, 6, 19, 12, 13, 13, 13, 16, 17, 11, 17, 11, 14, 15, 14, 15,
 | |
| 		16, 16, 17, 14, 12, 16, 18, 14, 14, 11, 15, 15, 25, 11, 15, 14, 12, 15,
 | |
| 		13, 12, 13, 15, 11, 13, 15, 14, 14, 15, 12, 15, 18, 12, 15, 22, 12, 13,
 | |
| 		10, 14, 16, 15, 16, 11, 14, 17, 18, 20, 16, 11, 16, 14, 5, 15, 17, 12,
 | |
| 		15, 11, 15, 20, 12, 17, 12, 17, 15, 12, 12, 11, 14, 15, 11, 20, 14, 13,
 | |
| 		11, 12, 13, 13, 11, 13, 11, 15, 13, 13, 14, 12, 11, 12, 12, 14, 11, 13,
 | |
| 		12, 12, 12, 19, 14, 13, 13, 14, 11, 12, 10, 11, 15, 12, 14, 11, 11, 14,
 | |
| 		14, 12, 12, 11, 14, 12, 11, 12, 14, 11, 12, 15, 12, 14, 12, 12, 21, 16,
 | |
| 		21, 12, 16, 9, 11, 16, 14, 13, 14, 12, 13, 16,
 | |
| 	}
 | |
| 	latencyHistory2 = []int{
 | |
| 		18, 20, 21, 21, 20, 23, 18, 18, 20, 21, 20, 19, 22, 18, 20, 20, 19, 21,
 | |
| 		21, 22, 22, 19, 18, 22, 22, 19, 20, 17, 16, 11, 25, 16, 18, 21, 17, 22,
 | |
| 		19, 18, 22, 21, 20, 18, 22, 17, 17, 20, 19, 10, 19, 16, 19, 25, 17, 18,
 | |
| 		15, 20, 21, 20, 23, 22, 22, 22, 19, 22, 22, 17, 22, 20, 20, 19, 21, 22,
 | |
| 		20, 19, 17, 22, 16, 16, 20, 22, 17, 19, 21, 16, 20, 22, 19, 21, 20, 19,
 | |
| 		13, 14, 23, 19, 16, 10, 19, 15, 15, 17, 16, 18, 14, 16, 18, 22, 20, 18,
 | |
| 		18, 21, 15, 19, 18, 19, 18, 20, 17, 19, 21, 19, 20, 19, 20, 20, 17, 14,
 | |
| 		17, 17, 18, 21, 20, 18, 18, 17, 16, 17, 17, 20, 22, 19, 20, 21, 21, 20,
 | |
| 		21, 24, 20, 18, 12, 17, 18, 17, 19, 19, 19,
 | |
| 	}
 | |
| )
 | |
| 
 | |
| func TestEWMALatencyHistory(t *testing.T) {
 | |
| 	type result struct {
 | |
| 		t time.Time
 | |
| 		v float64
 | |
| 		s int
 | |
| 	}
 | |
| 
 | |
| 	for _, latencyHistory := range [][]int{latencyHistory1, latencyHistory2} {
 | |
| 		startTime := time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC)
 | |
| 		halfLife := 30.0
 | |
| 
 | |
| 		ewma := NewEWMA(halfLife)
 | |
| 
 | |
| 		var results []result
 | |
| 		sum := 0.0
 | |
| 		for i, latency := range latencyHistory {
 | |
| 			t := startTime.Add(time.Duration(i) * time.Second)
 | |
| 			ewma.Update(float64(latency), t)
 | |
| 			sum += float64(latency)
 | |
| 
 | |
| 			results = append(results, result{t, ewma.Get(), latency})
 | |
| 		}
 | |
| 		mean := sum / float64(len(latencyHistory))
 | |
| 		min := float64(slices.Min(latencyHistory))
 | |
| 		max := float64(slices.Max(latencyHistory))
 | |
| 
 | |
| 		t.Logf("EWMA Latency History (half-life: %.1f seconds):", halfLife)
 | |
| 		t.Logf("Mean latency: %.2f ms", mean)
 | |
| 		t.Logf("Range: [%.1f, %.1f]", min, max)
 | |
| 
 | |
| 		t.Log("Samples: ")
 | |
| 		sparkline := []rune("▁▂▃▄▅▆▇█")
 | |
| 		var sampleLine []rune
 | |
| 		for _, r := range results {
 | |
| 			idx := int(((float64(r.s) - min) / (max - min)) * float64(len(sparkline)-1))
 | |
| 			if idx >= len(sparkline) {
 | |
| 				idx = len(sparkline) - 1
 | |
| 			}
 | |
| 			sampleLine = append(sampleLine, sparkline[idx])
 | |
| 		}
 | |
| 		t.Log(string(sampleLine))
 | |
| 
 | |
| 		t.Log("EWMA:    ")
 | |
| 		var ewmaLine []rune
 | |
| 		for _, r := range results {
 | |
| 			idx := int(((r.v - min) / (max - min)) * float64(len(sparkline)-1))
 | |
| 			if idx >= len(sparkline) {
 | |
| 				idx = len(sparkline) - 1
 | |
| 			}
 | |
| 			ewmaLine = append(ewmaLine, sparkline[idx])
 | |
| 		}
 | |
| 		t.Log(string(ewmaLine))
 | |
| 		t.Log("")
 | |
| 
 | |
| 		t.Logf("Time       | Sample | Value  | Value - Sample")
 | |
| 		t.Logf("")
 | |
| 
 | |
| 		for _, result := range results {
 | |
| 			t.Logf("%10s | % 6d | % 5.2f | % 5.2f", result.t.Format("15:04:05"), result.s, result.v, result.v-float64(result.s))
 | |
| 		}
 | |
| 
 | |
| 		// check that all results are greater than the min, and less than the max of the input,
 | |
| 		// and they're all close to the mean.
 | |
| 		for _, result := range results {
 | |
| 			if result.v < float64(min) || result.v > float64(max) {
 | |
| 				t.Errorf("result %f out of range [%f, %f]", result.v, min, max)
 | |
| 			}
 | |
| 
 | |
| 			if result.v < mean*0.9 || result.v > mean*1.1 {
 | |
| 				t.Errorf("result %f not close to mean %f", result.v, mean)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestHalfLife(t *testing.T) {
 | |
| 	start := time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC)
 | |
| 
 | |
| 	ewma := NewEWMA(30.0)
 | |
| 	ewma.Update(10, start)
 | |
| 	ewma.Update(0, start.Add(30*time.Second))
 | |
| 
 | |
| 	if ewma.Get() != 5 {
 | |
| 		t.Errorf("expected 5, got %f", ewma.Get())
 | |
| 	}
 | |
| 
 | |
| 	ewma.Update(10, start.Add(60*time.Second))
 | |
| 	if ewma.Get() != 7.5 {
 | |
| 		t.Errorf("expected 7.5, got %f", ewma.Get())
 | |
| 	}
 | |
| 
 | |
| 	ewma.Update(10, start.Add(90*time.Second))
 | |
| 	if ewma.Get() != 8.75 {
 | |
| 		t.Errorf("expected 8.75, got %f", ewma.Get())
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestZeroValue(t *testing.T) {
 | |
| 	start := time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC)
 | |
| 
 | |
| 	var ewma EWMA
 | |
| 	ewma.Update(10, start)
 | |
| 	ewma.Update(0, start.Add(time.Second))
 | |
| 
 | |
| 	if ewma.Get() != 5 {
 | |
| 		t.Errorf("expected 5, got %f", ewma.Get())
 | |
| 	}
 | |
| 
 | |
| 	ewma.Update(10, start.Add(2*time.Second))
 | |
| 	if ewma.Get() != 7.5 {
 | |
| 		t.Errorf("expected 7.5, got %f", ewma.Get())
 | |
| 	}
 | |
| 
 | |
| 	ewma.Update(10, start.Add(3*time.Second))
 | |
| 	if ewma.Get() != 8.75 {
 | |
| 		t.Errorf("expected 8.75, got %f", ewma.Get())
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestReset(t *testing.T) {
 | |
| 	start := time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC)
 | |
| 
 | |
| 	ewma := NewEWMA(30.0)
 | |
| 	ewma.Update(10, start)
 | |
| 	ewma.Update(0, start.Add(30*time.Second))
 | |
| 
 | |
| 	if ewma.Get() != 5 {
 | |
| 		t.Errorf("expected 5, got %f", ewma.Get())
 | |
| 	}
 | |
| 
 | |
| 	ewma.Reset()
 | |
| 
 | |
| 	if ewma.Get() != 0 {
 | |
| 		t.Errorf("expected 0, got %f", ewma.Get())
 | |
| 	}
 | |
| 
 | |
| 	ewma.Update(10, start.Add(90*time.Second))
 | |
| 	if ewma.Get() != 10 {
 | |
| 		t.Errorf("expected 10, got %f", ewma.Get())
 | |
| 	}
 | |
| }
 |