1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
|
// Implements a data structure for quality attenuation.
package qualityattenuation
import (
"fmt"
"math"
"github.com/influxdata/tdigest"
)
type SimpleQualityAttenuation struct {
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,
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) 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 fmt.Errorf("sample is zero or negative")
}
qa.numberOfSamples++
if sample > qa.latencyEqLossThreshold {
qa.numberOfLosses++
return nil
} else {
if qa.minimumLatency == 0.0 || sample < qa.minimumLatency {
qa.minimumLatency = sample
}
if qa.maximumLatency == 0.0 || sample > qa.maximumLatency {
qa.maximumLatency = sample
}
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.numberOfLosses
}
func (qa *SimpleQualityAttenuation) GetNumberOfSamples() int64 {
return qa.numberOfSamples
}
func (qa *SimpleQualityAttenuation) GetPercentile(percentile float64) float64 {
return qa.empiricalDistribution.Quantile(percentile / 100)
}
func (qa *SimpleQualityAttenuation) GetAverage() float64 {
return qa.offsetSum/float64(qa.numberOfSamples-qa.numberOfLosses) + qa.offset
}
func (qa *SimpleQualityAttenuation) GetVariance() float64 {
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 {
return math.Sqrt(qa.GetVariance())
}
func (qa *SimpleQualityAttenuation) GetMinimum() float64 {
return qa.minimumLatency
}
func (qa *SimpleQualityAttenuation) GetMaximum() float64 {
return qa.maximumLatency
}
func (qa *SimpleQualityAttenuation) GetMedian() float64 {
return qa.GetPercentile(50.0)
}
func (qa *SimpleQualityAttenuation) GetLossPercentage() float64 {
return 100 * float64(qa.numberOfLosses) / float64(qa.numberOfSamples)
}
func (qa *SimpleQualityAttenuation) GetRPM() float64 {
return 60.0 / 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) error {
// Check that offsets are the same
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() {
mean := centroid.Mean
weight := centroid.Weight
qa.empiricalDistribution.Add(mean, weight)
}
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.maximumLatency > qa.maximumLatency {
qa.maximumLatency = other.maximumLatency
}
return nil
}
|