From 1dc9c660ca8829bd386688a56b7047644789eb9a Mon Sep 17 00:00:00 2001 From: Bjørn Ivar Teigen Date: Fri, 19 May 2023 13:38:52 +0200 Subject: Implemented basic Quality Attenuation --- go.mod | 4 + go.sum | 7 ++ networkQuality.go | 37 +++++++++ qualityattenuation/qualityattenuation.go | 133 +++++++++++++++++++++++++++++++ 4 files changed, 181 insertions(+) create mode 100644 qualityattenuation/qualityattenuation.go diff --git a/go.mod b/go.mod index a448c75..49590e5 100644 --- a/go.mod +++ b/go.mod @@ -11,3 +11,7 @@ require ( golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb golang.org/x/text v0.7.0 // indirect ) + +require ( + github.com/influxdata/tdigest v0.0.1 +) \ No newline at end of file diff --git a/go.sum b/go.sum index 3a4ddff..13f172f 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/influxdata/tdigest v0.0.1 h1:XpFptwYmnEKUqmkcDjrzffswZ3nvNeevbUSLPP/ZzIY= +github.com/influxdata/tdigest v0.0.1/go.mod h1:Z0kXnxzbTC2qrx4NaIzYkE1k66+6oEDQTvL95hQFh5Y= +golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb h1:PaBZQdo+iSDyHT053FjUCgZQ/9uqVwPOcl7KSWhKn6w= golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= @@ -6,3 +10,6 @@ golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gonum.org/v1/gonum v0.0.0-20181121035319-3f7ecaa7e8ca/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= +gonum.org/v1/netlib v0.0.0-20181029234149-ec6d1f5cefe6/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= diff --git a/networkQuality.go b/networkQuality.go index 309aa94..676d98d 100644 --- a/networkQuality.go +++ b/networkQuality.go @@ -33,6 +33,7 @@ import ( "github.com/network-quality/goresponsiveness/lgc" "github.com/network-quality/goresponsiveness/ms" "github.com/network-quality/goresponsiveness/probe" + "github.com/network-quality/goresponsiveness/qualityattenuation" "github.com/network-quality/goresponsiveness/rpm" "github.com/network-quality/goresponsiveness/stabilizer" "github.com/network-quality/goresponsiveness/timeoutat" @@ -86,6 +87,11 @@ var ( false, "Enable the collection and display of extended statistics -- may not be available on certain platforms.", ) + printQualityAttenuation = flag.Bool( + "quality-attenuation", + false, + "Print quality attenuation information.", + ) dataLoggerBaseFileName = flag.String( "logger-filename", "", @@ -469,6 +475,7 @@ func main() { probeStabilizer := stabilizer.NewProbeStabilizer(probeI, K, S, probeStabilizerDebugLevel, probeStabilizerDebugConfig) selfRtts := ms.NewInfiniteMathematicalSeries[float64]() + selfRttsQualityAttenuation := qualityattenuation.NewSimpleQualityAttenuation() foreignRtts := ms.NewInfiniteMathematicalSeries[float64]() // For later debugging output, record the last throughputs on load-generating connectings @@ -543,6 +550,7 @@ timeout: } } else if probeMeasurement.Type == probe.SelfDown || probeMeasurement.Type == probe.SelfUp { selfRtts.AddElement(probeMeasurement.Duration.Seconds()) + selfRttsQualityAttenuation.AddSample(probeMeasurement.Duration.Seconds()) } if probeMeasurement.Type == probe.Foreign { @@ -670,6 +678,35 @@ Trimmed Mean Foreign RTT: %f ) } + if *printQualityAttenuation { + fmt.Println("Quality Attenuation Statistics:") + fmt.Printf( + `Number of losses: %d +Number of samples: %d +Loss: %f +Min: %.6f +Max: %.6f +Mean: %.6f +Variance: %.6f +Standard Deviation: %.6f +PDV(90): %.6f +PDV(99): %.6f +P(90): %.6f +P(99): %.6f +`, selfRttsQualityAttenuation.GetNumberOfLosses(), + selfRttsQualityAttenuation.GetNumberOfSamples(), + selfRttsQualityAttenuation.GetLossPercentage(), + selfRttsQualityAttenuation.GetMinimum(), + selfRttsQualityAttenuation.GetMaximum(), + selfRttsQualityAttenuation.GetAverage(), + selfRttsQualityAttenuation.GetVariance(), + selfRttsQualityAttenuation.GetStandardDeviation(), + selfRttsQualityAttenuation.GetPDV(90), + selfRttsQualityAttenuation.GetPDV(99), + selfRttsQualityAttenuation.GetPercentile(90), + selfRttsQualityAttenuation.GetPercentile(99)) + } + if !testRanToStability { fmt.Printf("Test did not run to stability, these results are estimates:\n") } diff --git a/qualityattenuation/qualityattenuation.go b/qualityattenuation/qualityattenuation.go new file mode 100644 index 0000000..67143a7 --- /dev/null +++ b/qualityattenuation/qualityattenuation.go @@ -0,0 +1,133 @@ +// Implements a data structure for quality attenuation. + +package qualityattenuation + +import ( + "math" + + "github.com/influxdata/tdigest" +) + +type SimpleQualityAttenuation struct { + EmpiricalDistribution *tdigest.TDigest + Offset float64 + Offset_sum float64 + Offset_sum_of_squares float64 + Number_of_samples int64 + Number_of_losses int64 + Latency_eq_loss_threshold float64 + Minimum_latency float64 + Maximum_latency float64 +} + +func NewSimpleQualityAttenuation() *SimpleQualityAttenuation { + return &SimpleQualityAttenuation{ + EmpiricalDistribution: tdigest.NewWithCompression(50), + Offset: 0.1, + Offset_sum: 0.0, + Offset_sum_of_squares: 0.0, + Number_of_samples: 0, + Number_of_losses: 0, + Latency_eq_loss_threshold: 15.0, // Count latency greater than this value as a loss. + Minimum_latency: 0.0, + Maximum_latency: 0.0, + } +} + +func (qa *SimpleQualityAttenuation) AddSample(sample float64) { + if sample <= 0.0 { + // Ignore zero or negative samples because they cannot be valid. + // TODO: This should raise a warning and/or trigger error handling. + return + } + qa.Number_of_samples++ + if sample > qa.Latency_eq_loss_threshold { + qa.Number_of_losses++ + return + } else { + if qa.Minimum_latency == 0.0 || sample < qa.Minimum_latency { + qa.Minimum_latency = sample + } + if qa.Maximum_latency == 0.0 || sample > qa.Maximum_latency { + qa.Maximum_latency = sample + } + qa.EmpiricalDistribution.Add(sample, 1) + qa.Offset_sum += sample - qa.Offset + qa.Offset_sum_of_squares += (sample - qa.Offset) * (sample - qa.Offset) + } +} + +func (qa *SimpleQualityAttenuation) GetNumberOfLosses() int64 { + return qa.Number_of_losses +} + +func (qa *SimpleQualityAttenuation) GetNumberOfSamples() int64 { + return qa.Number_of_samples +} + +func (qa *SimpleQualityAttenuation) GetPercentile(percentile float64) float64 { + return qa.EmpiricalDistribution.Quantile(percentile / 100) +} + +func (qa *SimpleQualityAttenuation) GetAverage() float64 { + return qa.Offset_sum/float64(qa.Number_of_samples) + qa.Offset +} + +func (qa *SimpleQualityAttenuation) GetVariance() float64 { + number_of_latency_samples := float64(qa.Number_of_samples) - float64(qa.Number_of_losses) + return (qa.Offset_sum_of_squares - (qa.Offset_sum * qa.Offset_sum / number_of_latency_samples)) / (number_of_latency_samples - 1) +} + +func (qa *SimpleQualityAttenuation) GetStandardDeviation() float64 { + return math.Sqrt(qa.GetVariance()) +} + +func (qa *SimpleQualityAttenuation) GetMinimum() float64 { + return qa.Minimum_latency +} + +func (qa *SimpleQualityAttenuation) GetMaximum() float64 { + return qa.Maximum_latency +} + +func (qa *SimpleQualityAttenuation) GetMedian() float64 { + return qa.GetPercentile(50.0) +} + +func (qa *SimpleQualityAttenuation) GetLossPercentage() float64 { + return float64(qa.Number_of_losses) / float64(qa.Number_of_samples) +} + +func (qa *SimpleQualityAttenuation) GetRPM() float64 { + return 60 / qa.GetAverage() +} + +func (qa *SimpleQualityAttenuation) GetPDV(percentile float64) float64 { + return qa.GetPercentile(percentile) - qa.GetMinimum() +} + +// Merge two quality attenuation values. This operation assumes the two samples have the same offset and latency_eq_loss_threshold, and +// will return an error if they do not. +// It also assumes that the two quality attenuation values are measurements of the same thing (path, outcome, etc.). +func (qa *SimpleQualityAttenuation) Merge(other *SimpleQualityAttenuation) { + // Check that offsets are the same + if qa.Offset != other.Offset || qa.Latency_eq_loss_threshold != other.Latency_eq_loss_threshold { + //"Cannot merge quality attenuation values with different offset or latency_eq_loss_threshold" + + } + for _, centroid := range other.EmpiricalDistribution.Centroids() { + mean := centroid.Mean + weight := centroid.Weight + qa.EmpiricalDistribution.Add(mean, weight) + } + qa.Offset_sum += other.Offset_sum + qa.Offset_sum_of_squares += other.Offset_sum_of_squares + qa.Number_of_samples += other.Number_of_samples + qa.Number_of_losses += other.Number_of_losses + if other.Minimum_latency < qa.Minimum_latency { + qa.Minimum_latency = other.Minimum_latency + } + if other.Maximum_latency > qa.Maximum_latency { + qa.Maximum_latency = other.Maximum_latency + } +} -- cgit v1.2.3 From 372640ea973d318443427c2468b00b54e116dfb0 Mon Sep 17 00:00:00 2001 From: Bjørn Ivar Teigen Date: Fri, 19 May 2023 21:08:12 +0200 Subject: Added basic tests of qualityattenuation --- Makefile | 2 +- go.mod | 9 +++- go.sum | 9 ++++ qualityattenuation/qualityattenuation.go | 8 ++-- qualityattenuation/qualityattenuation_test.go | 65 +++++++++++++++++++++++++++ 5 files changed, 87 insertions(+), 6 deletions(-) create mode 100644 qualityattenuation/qualityattenuation_test.go diff --git a/Makefile b/Makefile index 564f87b..a024bc2 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ all: build test build: go build $(LDFLAGS) networkQuality.go test: - go test ./timeoutat/ ./traceable/ ./ms/ ./utilities/ ./lgc + go test ./timeoutat/ ./traceable/ ./ms/ ./utilities/ ./lgc ./qualityattenuation golines: find . -name '*.go' -exec ~/go/bin/golines -w {} \; clean: diff --git a/go.mod b/go.mod index 49590e5..bc9bdf0 100644 --- a/go.mod +++ b/go.mod @@ -14,4 +14,11 @@ require ( require ( github.com/influxdata/tdigest v0.0.1 -) \ No newline at end of file + github.com/stretchr/testify v1.8.3 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum index 13f172f..34c604e 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,12 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/influxdata/tdigest v0.0.1 h1:XpFptwYmnEKUqmkcDjrzffswZ3nvNeevbUSLPP/ZzIY= github.com/influxdata/tdigest v0.0.1/go.mod h1:Z0kXnxzbTC2qrx4NaIzYkE1k66+6oEDQTvL95hQFh5Y= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb h1:PaBZQdo+iSDyHT053FjUCgZQ/9uqVwPOcl7KSWhKn6w= golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= @@ -13,3 +19,6 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gonum.org/v1/gonum v0.0.0-20181121035319-3f7ecaa7e8ca/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/netlib v0.0.0-20181029234149-ec6d1f5cefe6/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/qualityattenuation/qualityattenuation.go b/qualityattenuation/qualityattenuation.go index 67143a7..9782925 100644 --- a/qualityattenuation/qualityattenuation.go +++ b/qualityattenuation/qualityattenuation.go @@ -70,12 +70,12 @@ func (qa *SimpleQualityAttenuation) GetPercentile(percentile float64) float64 { } func (qa *SimpleQualityAttenuation) GetAverage() float64 { - return qa.Offset_sum/float64(qa.Number_of_samples) + qa.Offset + return qa.Offset_sum/float64(qa.Number_of_samples-qa.Number_of_losses) + qa.Offset } func (qa *SimpleQualityAttenuation) GetVariance() float64 { number_of_latency_samples := float64(qa.Number_of_samples) - float64(qa.Number_of_losses) - return (qa.Offset_sum_of_squares - (qa.Offset_sum * qa.Offset_sum / number_of_latency_samples)) / (number_of_latency_samples - 1) + return (qa.Offset_sum_of_squares - (qa.Offset_sum * qa.Offset_sum / number_of_latency_samples)) / (number_of_latency_samples) } func (qa *SimpleQualityAttenuation) GetStandardDeviation() float64 { @@ -95,11 +95,11 @@ func (qa *SimpleQualityAttenuation) GetMedian() float64 { } func (qa *SimpleQualityAttenuation) GetLossPercentage() float64 { - return float64(qa.Number_of_losses) / float64(qa.Number_of_samples) + return 100 * float64(qa.Number_of_losses) / float64(qa.Number_of_samples) } func (qa *SimpleQualityAttenuation) GetRPM() float64 { - return 60 / qa.GetAverage() + return 60.0 / qa.GetAverage() } func (qa *SimpleQualityAttenuation) GetPDV(percentile float64) float64 { diff --git a/qualityattenuation/qualityattenuation_test.go b/qualityattenuation/qualityattenuation_test.go new file mode 100644 index 0000000..862dc28 --- /dev/null +++ b/qualityattenuation/qualityattenuation_test.go @@ -0,0 +1,65 @@ +package qualityattenuation + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestBasicSimpleQualityAttenuation(t *testing.T) { + qa := NewSimpleQualityAttenuation() + qa.AddSample(1.0) + qa.AddSample(2.0) + qa.AddSample(3.0) + assert.Equal(t, qa.Number_of_samples, int64(3)) + assert.Equal(t, qa.Number_of_losses, int64(0)) + assert.InEpsilon(t, 1.0, qa.Minimum_latency, 0.000001) + assert.InEpsilon(t, 3.0, qa.Maximum_latency, 0.000001) + assert.InEpsilon(t, 5.7, qa.Offset_sum, 0.000001) + assert.InEpsilon(t, 12.83, qa.Offset_sum_of_squares, 0.000001) + assert.InEpsilon(t, 1.0, qa.EmpiricalDistribution.Quantile(0.1), 0.000001) + assert.InEpsilon(t, 2.0, qa.EmpiricalDistribution.Quantile(0.5), 0.000001) + assert.InEpsilon(t, 3.0, qa.EmpiricalDistribution.Quantile(0.9), 0.000001) + //Test the get functions + assert.Equal(t, qa.GetNumberOfSamples(), int64(3)) + assert.Equal(t, qa.GetNumberOfLosses(), int64(0)) + assert.InEpsilon(t, 1.0, qa.GetMinimum(), 0.000001) + assert.InEpsilon(t, 3.0, qa.GetMaximum(), 0.000001) + assert.InEpsilon(t, 2.0, qa.GetAverage(), 0.000001) + assert.InEpsilon(t, 1.0, qa.GetVariance(), 0.000001) + assert.InEpsilon(t, 1.0, qa.GetStandardDeviation(), 0.000001) + assert.InEpsilon(t, 2.0, qa.GetMedian(), 0.000001) + assert.InEpsilon(t, 1.0, qa.GetLossPercentage()+1.000000000, 0.000001) + assert.InEpsilon(t, 30, qa.GetRPM(), 0.000001) + assert.InEpsilon(t, 1.0, qa.GetPercentile(10.0), 0.000001) + assert.InEpsilon(t, 2.0, qa.GetPercentile(50.0), 0.000001) + assert.InEpsilon(t, 3.0, qa.GetPercentile(90.0), 0.000001) + assert.InEpsilon(t, 2.0, qa.GetPDV(90), 0.000001) +} + +func TestManySamples(t *testing.T) { + qa := NewSimpleQualityAttenuation() + for i := 1; i < 160000; i++ { + qa.AddSample(float64(i) / 10000.0) //Linear ramp from 0.0001 to 16.0 + } + assert.Equal(t, qa.Number_of_samples, int64(160000-1)) + assert.Equal(t, qa.Number_of_losses, int64(10000-1)) //Samples from 15.0001 to 16.0 are lost + assert.InEpsilon(t, 0.0001, qa.Minimum_latency, 0.000001) + assert.InEpsilon(t, 15.0000, qa.Maximum_latency, 0.000001) + assert.InEpsilon(t, 1110007.5, qa.Offset_sum, 0.000001) + assert.InEpsilon(t, 11026611.00024998, qa.Offset_sum_of_squares, 0.000001) + assert.InEpsilon(t, 1.50005, qa.EmpiricalDistribution.Quantile(0.1), 0.000001) + assert.InEpsilon(t, 7.500049, qa.EmpiricalDistribution.Quantile(0.5), 0.000001) + assert.InEpsilon(t, 13.50005, qa.EmpiricalDistribution.Quantile(0.9), 0.000001) + //Test the get functions + assert.Equal(t, qa.GetNumberOfSamples(), int64(160000-1)) + assert.Equal(t, qa.GetNumberOfLosses(), int64(10000-1)) + assert.InEpsilon(t, 0.0001, qa.GetMinimum(), 0.000001) + assert.InEpsilon(t, 15.0000, qa.GetMaximum(), 0.000001) + assert.InEpsilon(t, 7.500049, qa.GetAverage(), 0.000001) + assert.InEpsilon(t, 18.749750, qa.GetVariance(), 0.001) + assert.InEpsilon(t, 4.330098, qa.GetStandardDeviation(), 0.001) + assert.InEpsilon(t, 7.500049, qa.GetMedian(), 0.000001) + assert.InEpsilon(t, 6.249414, qa.GetLossPercentage(), 0.000001) + assert.InEpsilon(t, 7.999947, qa.GetRPM(), 0.000001) +} -- cgit v1.2.3 From 47b2c2f9f695f9bb5d9f0aa2c2860d4f7f28016a Mon Sep 17 00:00:00 2001 From: Bjørn Ivar Teigen Date: Sat, 20 May 2023 00:24:57 +0200 Subject: Hotfix of broken test --- qualityattenuation/qualityattenuation.go | 3 ++- qualityattenuation/qualityattenuation_test.go | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/qualityattenuation/qualityattenuation.go b/qualityattenuation/qualityattenuation.go index 9782925..980253f 100644 --- a/qualityattenuation/qualityattenuation.go +++ b/qualityattenuation/qualityattenuation.go @@ -111,7 +111,8 @@ func (qa *SimpleQualityAttenuation) GetPDV(percentile float64) float64 { // It also assumes that the two quality attenuation values are measurements of the same thing (path, outcome, etc.). func (qa *SimpleQualityAttenuation) Merge(other *SimpleQualityAttenuation) { // Check that offsets are the same - if qa.Offset != other.Offset || qa.Latency_eq_loss_threshold != other.Latency_eq_loss_threshold { + if qa.Offset != other.Offset || + qa.Latency_eq_loss_threshold != other.Latency_eq_loss_threshold { //"Cannot merge quality attenuation values with different offset or latency_eq_loss_threshold" } diff --git a/qualityattenuation/qualityattenuation_test.go b/qualityattenuation/qualityattenuation_test.go index 862dc28..975cbf3 100644 --- a/qualityattenuation/qualityattenuation_test.go +++ b/qualityattenuation/qualityattenuation_test.go @@ -26,8 +26,8 @@ func TestBasicSimpleQualityAttenuation(t *testing.T) { assert.InEpsilon(t, 1.0, qa.GetMinimum(), 0.000001) assert.InEpsilon(t, 3.0, qa.GetMaximum(), 0.000001) assert.InEpsilon(t, 2.0, qa.GetAverage(), 0.000001) - assert.InEpsilon(t, 1.0, qa.GetVariance(), 0.000001) - assert.InEpsilon(t, 1.0, qa.GetStandardDeviation(), 0.000001) + assert.InEpsilon(t, 0.666667, qa.GetVariance(), 0.000001) + assert.InEpsilon(t, 0.816496, qa.GetStandardDeviation(), 0.000001) assert.InEpsilon(t, 2.0, qa.GetMedian(), 0.000001) assert.InEpsilon(t, 1.0, qa.GetLossPercentage()+1.000000000, 0.000001) assert.InEpsilon(t, 30, qa.GetRPM(), 0.000001) -- cgit v1.2.3 From b522f9e23f7aae9d6a6b0f2de74f6df860e5ee09 Mon Sep 17 00:00:00 2001 From: Bjørn Ivar Teigen Date: Tue, 23 May 2023 09:10:54 +0200 Subject: Adressed pull request comments. Fixed silly error in calculating sample variance --- networkQuality.go | 4 +- qualityattenuation/qualityattenuation.go | 112 +++++++++++++------------- qualityattenuation/qualityattenuation_test.go | 44 +++++----- 3 files changed, 82 insertions(+), 78 deletions(-) diff --git a/networkQuality.go b/networkQuality.go index 676d98d..10ee8d3 100644 --- a/networkQuality.go +++ b/networkQuality.go @@ -550,7 +550,9 @@ timeout: } } else if probeMeasurement.Type == probe.SelfDown || probeMeasurement.Type == probe.SelfUp { selfRtts.AddElement(probeMeasurement.Duration.Seconds()) - selfRttsQualityAttenuation.AddSample(probeMeasurement.Duration.Seconds()) + if *printQualityAttenuation { + selfRttsQualityAttenuation.AddSample(probeMeasurement.Duration.Seconds()) + } } if probeMeasurement.Type == probe.Foreign { diff --git a/qualityattenuation/qualityattenuation.go b/qualityattenuation/qualityattenuation.go index 980253f..279959e 100644 --- a/qualityattenuation/qualityattenuation.go +++ b/qualityattenuation/qualityattenuation.go @@ -3,79 +3,81 @@ package qualityattenuation import ( + "fmt" "math" "github.com/influxdata/tdigest" ) type SimpleQualityAttenuation struct { - EmpiricalDistribution *tdigest.TDigest - Offset float64 - Offset_sum float64 - Offset_sum_of_squares float64 - Number_of_samples int64 - Number_of_losses int64 - Latency_eq_loss_threshold float64 - Minimum_latency float64 - Maximum_latency float64 + empiricalDistribution *tdigest.TDigest + offset float64 + offsetSum float64 + offsetSumOfSquares float64 + numberOfSamples int64 + numberOfLosses int64 + latencyEqLossThreshold float64 + minimumLatency float64 + maximumLatency float64 } func NewSimpleQualityAttenuation() *SimpleQualityAttenuation { return &SimpleQualityAttenuation{ - EmpiricalDistribution: tdigest.NewWithCompression(50), - Offset: 0.1, - Offset_sum: 0.0, - Offset_sum_of_squares: 0.0, - Number_of_samples: 0, - Number_of_losses: 0, - Latency_eq_loss_threshold: 15.0, // Count latency greater than this value as a loss. - Minimum_latency: 0.0, - Maximum_latency: 0.0, + empiricalDistribution: tdigest.NewWithCompression(50), + offset: 0.1, + offsetSum: 0.0, + offsetSumOfSquares: 0.0, + numberOfSamples: 0, + numberOfLosses: 0, + latencyEqLossThreshold: 15.0, // Count latency greater than this value as a loss. + minimumLatency: 0.0, + maximumLatency: 0.0, } } -func (qa *SimpleQualityAttenuation) AddSample(sample float64) { +func (qa *SimpleQualityAttenuation) AddSample(sample float64) error { if sample <= 0.0 { // Ignore zero or negative samples because they cannot be valid. // TODO: This should raise a warning and/or trigger error handling. - return + return fmt.Errorf("sample is zero or negative") } - qa.Number_of_samples++ - if sample > qa.Latency_eq_loss_threshold { - qa.Number_of_losses++ - return + qa.numberOfSamples++ + if sample > qa.latencyEqLossThreshold { + qa.numberOfLosses++ + return nil } else { - if qa.Minimum_latency == 0.0 || sample < qa.Minimum_latency { - qa.Minimum_latency = sample + if qa.minimumLatency == 0.0 || sample < qa.minimumLatency { + qa.minimumLatency = sample } - if qa.Maximum_latency == 0.0 || sample > qa.Maximum_latency { - qa.Maximum_latency = sample + if qa.maximumLatency == 0.0 || sample > qa.maximumLatency { + qa.maximumLatency = sample } - qa.EmpiricalDistribution.Add(sample, 1) - qa.Offset_sum += sample - qa.Offset - qa.Offset_sum_of_squares += (sample - qa.Offset) * (sample - qa.Offset) + qa.empiricalDistribution.Add(sample, 1) + qa.offsetSum += sample - qa.offset + qa.offsetSumOfSquares += (sample - qa.offset) * (sample - qa.offset) } + return nil } func (qa *SimpleQualityAttenuation) GetNumberOfLosses() int64 { - return qa.Number_of_losses + return qa.numberOfLosses } func (qa *SimpleQualityAttenuation) GetNumberOfSamples() int64 { - return qa.Number_of_samples + return qa.numberOfSamples } func (qa *SimpleQualityAttenuation) GetPercentile(percentile float64) float64 { - return qa.EmpiricalDistribution.Quantile(percentile / 100) + return qa.empiricalDistribution.Quantile(percentile / 100) } func (qa *SimpleQualityAttenuation) GetAverage() float64 { - return qa.Offset_sum/float64(qa.Number_of_samples-qa.Number_of_losses) + qa.Offset + return qa.offsetSum/float64(qa.numberOfSamples-qa.numberOfLosses) + qa.offset } func (qa *SimpleQualityAttenuation) GetVariance() float64 { - number_of_latency_samples := float64(qa.Number_of_samples) - float64(qa.Number_of_losses) - return (qa.Offset_sum_of_squares - (qa.Offset_sum * qa.Offset_sum / number_of_latency_samples)) / (number_of_latency_samples) + number_of_latency_samples := float64(qa.numberOfSamples) - float64(qa.numberOfLosses) + return (qa.offsetSumOfSquares - (qa.offsetSum * qa.offsetSum / number_of_latency_samples)) / (number_of_latency_samples - 1) } func (qa *SimpleQualityAttenuation) GetStandardDeviation() float64 { @@ -83,11 +85,11 @@ func (qa *SimpleQualityAttenuation) GetStandardDeviation() float64 { } func (qa *SimpleQualityAttenuation) GetMinimum() float64 { - return qa.Minimum_latency + return qa.minimumLatency } func (qa *SimpleQualityAttenuation) GetMaximum() float64 { - return qa.Maximum_latency + return qa.maximumLatency } func (qa *SimpleQualityAttenuation) GetMedian() float64 { @@ -95,7 +97,7 @@ func (qa *SimpleQualityAttenuation) GetMedian() float64 { } func (qa *SimpleQualityAttenuation) GetLossPercentage() float64 { - return 100 * float64(qa.Number_of_losses) / float64(qa.Number_of_samples) + return 100 * float64(qa.numberOfLosses) / float64(qa.numberOfSamples) } func (qa *SimpleQualityAttenuation) GetRPM() float64 { @@ -109,26 +111,26 @@ func (qa *SimpleQualityAttenuation) GetPDV(percentile float64) float64 { // Merge two quality attenuation values. This operation assumes the two samples have the same offset and latency_eq_loss_threshold, and // will return an error if they do not. // It also assumes that the two quality attenuation values are measurements of the same thing (path, outcome, etc.). -func (qa *SimpleQualityAttenuation) Merge(other *SimpleQualityAttenuation) { +func (qa *SimpleQualityAttenuation) Merge(other *SimpleQualityAttenuation) error { // Check that offsets are the same - if qa.Offset != other.Offset || - qa.Latency_eq_loss_threshold != other.Latency_eq_loss_threshold { - //"Cannot merge quality attenuation values with different offset or latency_eq_loss_threshold" - + if qa.offset != other.offset || + qa.latencyEqLossThreshold != other.latencyEqLossThreshold { + return fmt.Errorf("merge quality attenuation values with different offset or latency_eq_loss_threshold") } - for _, centroid := range other.EmpiricalDistribution.Centroids() { + for _, centroid := range other.empiricalDistribution.Centroids() { mean := centroid.Mean weight := centroid.Weight - qa.EmpiricalDistribution.Add(mean, weight) + qa.empiricalDistribution.Add(mean, weight) } - qa.Offset_sum += other.Offset_sum - qa.Offset_sum_of_squares += other.Offset_sum_of_squares - qa.Number_of_samples += other.Number_of_samples - qa.Number_of_losses += other.Number_of_losses - if other.Minimum_latency < qa.Minimum_latency { - qa.Minimum_latency = other.Minimum_latency + qa.offsetSum += other.offsetSum + qa.offsetSumOfSquares += other.offsetSumOfSquares + qa.numberOfSamples += other.numberOfSamples + qa.numberOfLosses += other.numberOfLosses + if other.minimumLatency < qa.minimumLatency { + qa.minimumLatency = other.minimumLatency } - if other.Maximum_latency > qa.Maximum_latency { - qa.Maximum_latency = other.Maximum_latency + if other.maximumLatency > qa.maximumLatency { + qa.maximumLatency = other.maximumLatency } + return nil } diff --git a/qualityattenuation/qualityattenuation_test.go b/qualityattenuation/qualityattenuation_test.go index 975cbf3..4746364 100644 --- a/qualityattenuation/qualityattenuation_test.go +++ b/qualityattenuation/qualityattenuation_test.go @@ -11,23 +11,23 @@ func TestBasicSimpleQualityAttenuation(t *testing.T) { qa.AddSample(1.0) qa.AddSample(2.0) qa.AddSample(3.0) - assert.Equal(t, qa.Number_of_samples, int64(3)) - assert.Equal(t, qa.Number_of_losses, int64(0)) - assert.InEpsilon(t, 1.0, qa.Minimum_latency, 0.000001) - assert.InEpsilon(t, 3.0, qa.Maximum_latency, 0.000001) - assert.InEpsilon(t, 5.7, qa.Offset_sum, 0.000001) - assert.InEpsilon(t, 12.83, qa.Offset_sum_of_squares, 0.000001) - assert.InEpsilon(t, 1.0, qa.EmpiricalDistribution.Quantile(0.1), 0.000001) - assert.InEpsilon(t, 2.0, qa.EmpiricalDistribution.Quantile(0.5), 0.000001) - assert.InEpsilon(t, 3.0, qa.EmpiricalDistribution.Quantile(0.9), 0.000001) + assert.Equal(t, qa.numberOfSamples, int64(3)) + assert.Equal(t, qa.numberOfLosses, int64(0)) + assert.InEpsilon(t, 1.0, qa.minimumLatency, 0.000001) + assert.InEpsilon(t, 3.0, qa.maximumLatency, 0.000001) + assert.InEpsilon(t, 5.7, qa.offsetSum, 0.000001) + assert.InEpsilon(t, 12.83, qa.offsetSumOfSquares, 0.000001) + assert.InEpsilon(t, 1.0, qa.empiricalDistribution.Quantile(0.1), 0.000001) + assert.InEpsilon(t, 2.0, qa.empiricalDistribution.Quantile(0.5), 0.000001) + assert.InEpsilon(t, 3.0, qa.empiricalDistribution.Quantile(0.9), 0.000001) //Test the get functions assert.Equal(t, qa.GetNumberOfSamples(), int64(3)) assert.Equal(t, qa.GetNumberOfLosses(), int64(0)) assert.InEpsilon(t, 1.0, qa.GetMinimum(), 0.000001) assert.InEpsilon(t, 3.0, qa.GetMaximum(), 0.000001) assert.InEpsilon(t, 2.0, qa.GetAverage(), 0.000001) - assert.InEpsilon(t, 0.666667, qa.GetVariance(), 0.000001) - assert.InEpsilon(t, 0.816496, qa.GetStandardDeviation(), 0.000001) + assert.InEpsilon(t, 1.000000, qa.GetVariance(), 0.000001) + assert.InEpsilon(t, 1.000000, qa.GetStandardDeviation(), 0.000001) assert.InEpsilon(t, 2.0, qa.GetMedian(), 0.000001) assert.InEpsilon(t, 1.0, qa.GetLossPercentage()+1.000000000, 0.000001) assert.InEpsilon(t, 30, qa.GetRPM(), 0.000001) @@ -42,23 +42,23 @@ func TestManySamples(t *testing.T) { for i := 1; i < 160000; i++ { qa.AddSample(float64(i) / 10000.0) //Linear ramp from 0.0001 to 16.0 } - assert.Equal(t, qa.Number_of_samples, int64(160000-1)) - assert.Equal(t, qa.Number_of_losses, int64(10000-1)) //Samples from 15.0001 to 16.0 are lost - assert.InEpsilon(t, 0.0001, qa.Minimum_latency, 0.000001) - assert.InEpsilon(t, 15.0000, qa.Maximum_latency, 0.000001) - assert.InEpsilon(t, 1110007.5, qa.Offset_sum, 0.000001) - assert.InEpsilon(t, 11026611.00024998, qa.Offset_sum_of_squares, 0.000001) - assert.InEpsilon(t, 1.50005, qa.EmpiricalDistribution.Quantile(0.1), 0.000001) - assert.InEpsilon(t, 7.500049, qa.EmpiricalDistribution.Quantile(0.5), 0.000001) - assert.InEpsilon(t, 13.50005, qa.EmpiricalDistribution.Quantile(0.9), 0.000001) + assert.Equal(t, qa.numberOfSamples, int64(160000-1)) + assert.Equal(t, qa.numberOfLosses, int64(10000-1)) //Samples from 15.0001 to 16.0 are lost + assert.InEpsilon(t, 0.0001, qa.minimumLatency, 0.000001) + assert.InEpsilon(t, 15.0000, qa.maximumLatency, 0.000001) + assert.InEpsilon(t, 1110007.5, qa.offsetSum, 0.000001) + assert.InEpsilon(t, 11026611.00024998, qa.offsetSumOfSquares, 0.000001) + assert.InEpsilon(t, 1.50005, qa.empiricalDistribution.Quantile(0.1), 0.000001) + assert.InEpsilon(t, 7.500049, qa.empiricalDistribution.Quantile(0.5), 0.000001) + assert.InEpsilon(t, 13.50005, qa.empiricalDistribution.Quantile(0.9), 0.000001) //Test the get functions assert.Equal(t, qa.GetNumberOfSamples(), int64(160000-1)) assert.Equal(t, qa.GetNumberOfLosses(), int64(10000-1)) assert.InEpsilon(t, 0.0001, qa.GetMinimum(), 0.000001) assert.InEpsilon(t, 15.0000, qa.GetMaximum(), 0.000001) assert.InEpsilon(t, 7.500049, qa.GetAverage(), 0.000001) - assert.InEpsilon(t, 18.749750, qa.GetVariance(), 0.001) - assert.InEpsilon(t, 4.330098, qa.GetStandardDeviation(), 0.001) + assert.InEpsilon(t, 18.750120, qa.GetVariance(), 0.000001) + assert.InEpsilon(t, 4.330141, qa.GetStandardDeviation(), 0.000001) assert.InEpsilon(t, 7.500049, qa.GetMedian(), 0.000001) assert.InEpsilon(t, 6.249414, qa.GetLossPercentage(), 0.000001) assert.InEpsilon(t, 7.999947, qa.GetRPM(), 0.000001) -- cgit v1.2.3