github.com/network-quality/goresponsiveness@v0.0.0-20240129151524-343954285090/qualityattenuation/qualityattenuation.go (about) 1 /* 2 * This file is part of Go Responsiveness. 3 * 4 * Go Responsiveness is free software: you can redistribute it and/or modify it under 5 * the terms of the GNU General Public License as published by the Free Software Foundation, 6 * either version 2 of the License, or (at your option) any later version. 7 * Go Responsiveness is distributed in the hope that it will be useful, but WITHOUT ANY 8 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 9 * PARTICULAR PURPOSE. See the GNU General Public License for more details. 10 * 11 * You should have received a copy of the GNU General Public License along 12 * with Go Responsiveness. If not, see <https://www.gnu.org/licenses/>. 13 */ 14 15 // Implements a data structure for quality attenuation. 16 17 package qualityattenuation 18 19 import ( 20 "fmt" 21 "math" 22 23 "github.com/influxdata/tdigest" 24 ) 25 26 type cablelabsHist struct { 27 hist [256]float64 28 } 29 30 func (h *cablelabsHist) GetHistogram() [256]float64 { 31 return h.hist 32 } 33 34 func (h *cablelabsHist) AddSample(sample float64) error { 35 bin := 0 36 if sample < 0.050 { 37 // Round down 38 bin = int(sample / 0.0005) 39 h.hist[bin]++ 40 } else if sample < 0.150 { 41 bin = int((sample - 0.050) / 0.001) 42 h.hist[100+bin]++ 43 } else if sample < 1.150 { 44 bin = int((sample - 0.150) / 0.020) 45 h.hist[200+bin]++ 46 } else if sample < 1.400 { 47 bin = 250 48 h.hist[bin]++ 49 } else if sample < 3.000 { 50 bin = int((sample - 1.400) / 0.400) 51 h.hist[251+bin]++ 52 } else { 53 bin = 255 54 h.hist[bin]++ 55 } 56 return nil 57 } 58 59 type SimpleQualityAttenuation struct { 60 empiricalDistribution *tdigest.TDigest 61 offset float64 62 offsetSum float64 63 offsetSumOfSquares float64 64 numberOfSamples int64 65 numberOfLosses int64 66 latencyEqLossThreshold float64 67 minimumLatency float64 68 maximumLatency float64 69 } 70 71 type percentileLatencyPair struct { 72 percentile float64 73 perfectLatency float64 74 uselessLatency float64 75 } 76 77 type qualityRequirement struct { 78 latencyRequirements []percentileLatencyPair 79 } 80 81 func NewSimpleQualityAttenuation() *SimpleQualityAttenuation { 82 return &SimpleQualityAttenuation{ 83 empiricalDistribution: tdigest.NewWithCompression(50), 84 offset: 0.1, 85 offsetSum: 0.0, 86 offsetSumOfSquares: 0.0, 87 numberOfSamples: 0, 88 numberOfLosses: 0, 89 latencyEqLossThreshold: 15.0, // Count latency greater than this value as a loss. 90 minimumLatency: 0.0, 91 maximumLatency: 0.0, 92 } 93 } 94 95 func (qa *SimpleQualityAttenuation) AddSample(sample float64) error { 96 if sample <= 0.0 { 97 // Ignore zero or negative samples because they cannot be valid. 98 // TODO: This should raise a warning and/or trigger error handling. 99 return fmt.Errorf("sample is zero or negative") 100 } 101 qa.numberOfSamples++ 102 if sample > qa.latencyEqLossThreshold { 103 qa.numberOfLosses++ 104 return nil 105 } else { 106 if qa.minimumLatency == 0.0 || sample < qa.minimumLatency { 107 qa.minimumLatency = sample 108 } 109 if qa.maximumLatency == 0.0 || sample > qa.maximumLatency { 110 qa.maximumLatency = sample 111 } 112 qa.empiricalDistribution.Add(sample, 1) 113 qa.offsetSum += sample - qa.offset 114 qa.offsetSumOfSquares += (sample - qa.offset) * (sample - qa.offset) 115 } 116 return nil 117 } 118 119 func (qa *SimpleQualityAttenuation) GetNumberOfLosses() int64 { 120 return qa.numberOfLosses 121 } 122 123 func (qa *SimpleQualityAttenuation) GetNumberOfSamples() int64 { 124 return qa.numberOfSamples 125 } 126 127 func (qa *SimpleQualityAttenuation) GetPercentile(percentile float64) float64 { 128 return qa.empiricalDistribution.Quantile(percentile / 100) 129 } 130 131 func (qa *SimpleQualityAttenuation) GetAverage() float64 { 132 return qa.offsetSum/float64(qa.numberOfSamples-qa.numberOfLosses) + qa.offset 133 } 134 135 func (qa *SimpleQualityAttenuation) GetVariance() float64 { 136 number_of_latency_samples := float64(qa.numberOfSamples) - float64(qa.numberOfLosses) 137 return (qa.offsetSumOfSquares - (qa.offsetSum * qa.offsetSum / number_of_latency_samples)) / (number_of_latency_samples - 1) 138 } 139 140 func (qa *SimpleQualityAttenuation) GetStandardDeviation() float64 { 141 return math.Sqrt(qa.GetVariance()) 142 } 143 144 func (qa *SimpleQualityAttenuation) GetMinimum() float64 { 145 return qa.minimumLatency 146 } 147 148 func (qa *SimpleQualityAttenuation) GetMaximum() float64 { 149 return qa.maximumLatency 150 } 151 152 func (qa *SimpleQualityAttenuation) GetMedian() float64 { 153 return qa.GetPercentile(50.0) 154 } 155 156 func (qa *SimpleQualityAttenuation) GetLossPercentage() float64 { 157 return 100 * float64(qa.numberOfLosses) / float64(qa.numberOfSamples) 158 } 159 160 func (qa *SimpleQualityAttenuation) GetRPM() float64 { 161 return 60.0 / qa.GetAverage() 162 } 163 164 func (qa *SimpleQualityAttenuation) GetPDV(percentile float64) float64 { 165 return qa.GetPercentile(percentile) - qa.GetMinimum() 166 } 167 168 func (qa *SimpleQualityAttenuation) PrintCablelabsStatisticsSummary() string { 169 // Prints a digest based on Cablelabs Latency Measurements Metrics and Architeture, CL-TR-LM-Arch-V01-221123, https://www.cablelabs.com/specifications/CL-TR-LM-Arch 170 // The recommendation is to report the following percentiles: 0, 10, 25, 50, 75, 90, 95, 99, 99.9 and 100 171 return fmt.Sprintf("Cablelabs Statistics Summary:\n"+ 172 "0th Percentile: %f\n"+ 173 "10th Percentile: %f\n"+ 174 "25th Percentile: %f\n"+ 175 "50th Percentile: %f\n"+ 176 "75th Percentile: %f\n"+ 177 "90th Percentile: %f\n"+ 178 "95th Percentile: %f\n"+ 179 "99th Percentile: %f\n"+ 180 "99.9th Percentile: %f\n"+ 181 "100th Percentile: %f\n", 182 qa.GetPercentile(0.0), 183 qa.GetPercentile(10.0), 184 qa.GetPercentile(25.0), 185 qa.GetPercentile(50.0), 186 qa.GetPercentile(75.0), 187 qa.GetPercentile(90.0), 188 qa.GetPercentile(95.0), 189 qa.GetPercentile(99.0), 190 qa.GetPercentile(99.9), 191 qa.GetPercentile(100.0)) 192 } 193 194 // Merge two quality attenuation values. This operation assumes the two samples have the same offset and latency_eq_loss_threshold, and 195 // will return an error if they do not. 196 // It also assumes that the two quality attenuation values are measurements of the same thing (path, outcome, etc.). 197 func (qa *SimpleQualityAttenuation) Merge(other *SimpleQualityAttenuation) error { 198 // Check that offsets are the same 199 if qa.offset != other.offset || 200 qa.latencyEqLossThreshold != other.latencyEqLossThreshold { 201 return fmt.Errorf("merge quality attenuation values with different offset or latency_eq_loss_threshold") 202 } 203 for _, centroid := range other.empiricalDistribution.Centroids() { 204 mean := centroid.Mean 205 weight := centroid.Weight 206 qa.empiricalDistribution.Add(mean, weight) 207 } 208 qa.offsetSum += other.offsetSum 209 qa.offsetSumOfSquares += other.offsetSumOfSquares 210 qa.numberOfSamples += other.numberOfSamples 211 qa.numberOfLosses += other.numberOfLosses 212 if other.minimumLatency < qa.minimumLatency { 213 qa.minimumLatency = other.minimumLatency 214 } 215 if other.maximumLatency > qa.maximumLatency { 216 qa.maximumLatency = other.maximumLatency 217 } 218 return nil 219 } 220 221 func (qa *SimpleQualityAttenuation) EmpiricalDistributionHistogram() []float64 { 222 // Convert the tdigest to a histogram on the format defined by CableLabs, with the following bucket edges: 223 // 100 bins from 0 to 50 ms, each 0.5 ms wide 224 // 100 bins from 50 to 100 ms, each 1 ms wide 225 // 50 bins from 150 to 1150 ms, each 20 ms wide 226 // 1 bin from 1150 to 1400 ms, 250 ms wide 227 // 4 bins from 1400 to 3000 ms, each 400 ms wide 228 hist := make([]float64, 256) 229 for i := 0; i < 100; i++ { 230 hist[i] = float64(qa.numberOfSamples) * (qa.empiricalDistribution.CDF(float64(i+1)*0.0005) - 231 qa.empiricalDistribution.CDF(float64(i)*0.0005)) 232 } 233 for i := 100; i < 200; i++ { 234 hist[i] = float64(qa.numberOfSamples) * (qa.empiricalDistribution.CDF(0.050+float64(i-99)*0.001) - 235 qa.empiricalDistribution.CDF(0.050+float64(i-100)*0.001)) 236 } 237 for i := 200; i < 250; i++ { 238 hist[i] = float64(qa.numberOfSamples) * (qa.empiricalDistribution.CDF(0.150+float64(i-199)*0.020) - 239 qa.empiricalDistribution.CDF(0.150+float64(i-200)*0.020)) 240 } 241 for i := 250; i < 251; i++ { 242 hist[i] = float64(qa.numberOfSamples) * (qa.empiricalDistribution.CDF(1.150+0.250) - qa.empiricalDistribution.CDF(1.150)) 243 } 244 for i := 251; i < 255; i++ { 245 hist[i] = float64(qa.numberOfSamples) * (qa.empiricalDistribution.CDF(1.400+float64(i-250)*0.400) - 246 qa.empiricalDistribution.CDF(1.400+float64(i-251)*0.400)) 247 } 248 hist[255] = float64(qa.numberOfSamples) * (1 - qa.empiricalDistribution.CDF(3.000)) 249 return hist 250 } 251 252 // Compute the Quality of Outcome (QoO) for a given quality requirement. 253 // The details and motivation for the QoO metric are described in the following internet draft: 254 // https://datatracker.ietf.org/doc/draft-olden-ippm-qoo/ 255 func (qa *SimpleQualityAttenuation) QoO(requirement qualityRequirement) float64 { 256 QoO := 100.0 257 for _, percentileLatencyPair := range requirement.latencyRequirements { 258 score := 0.0 259 percentile := percentileLatencyPair.percentile 260 perfectLatency := percentileLatencyPair.perfectLatency 261 uselessLatency := percentileLatencyPair.uselessLatency 262 latency := qa.GetPercentile(percentile) 263 if latency >= uselessLatency { 264 score = 0.0 265 } else if latency <= perfectLatency { 266 score = 100.0 267 } else { 268 score = 100 * ((uselessLatency - latency) / (uselessLatency - perfectLatency)) 269 } 270 if score < QoO { 271 QoO = score 272 } 273 } 274 return QoO 275 } 276 277 func (qa *SimpleQualityAttenuation) GetGamingQoO() float64 { 278 qualReq := qualityRequirement{} 279 qualReq.latencyRequirements = []percentileLatencyPair{} 280 qualReq.latencyRequirements = append(qualReq.latencyRequirements, percentileLatencyPair{ 281 percentile: 50.0, perfectLatency: 0.030, uselessLatency: 0.150, 282 }) 283 qualReq.latencyRequirements = append(qualReq.latencyRequirements, percentileLatencyPair{ 284 percentile: 95.0, perfectLatency: 0.065, uselessLatency: 0.200, 285 }) 286 qualReq.latencyRequirements = append(qualReq.latencyRequirements, percentileLatencyPair{ 287 percentile: 99.0, perfectLatency: 0.100, uselessLatency: 0.250, 288 }) 289 return qa.QoO(qualReq) 290 }