summaryrefslogtreecommitdiff
path: root/qualityattenuation/qualityattenuation.go
blob: 279959eae887f637033ddd19847b7efd98ba524f (plain)
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
}