github.com/network-quality/goresponsiveness@v0.0.0-20240129151524-343954285090/stabilizer/algorithm.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 package stabilizer 16 17 import ( 18 "fmt" 19 "sync" 20 21 "github.com/network-quality/goresponsiveness/debug" 22 "github.com/network-quality/goresponsiveness/series" 23 "github.com/network-quality/goresponsiveness/utilities" 24 "golang.org/x/exp/constraints" 25 ) 26 27 type MeasurementStablizer[Data constraints.Float | constraints.Integer, Bucket utilities.Number] struct { 28 // The number of instantaneous measurements in the current interval could be infinite (Forever). 29 instantaneousses series.WindowSeries[Data, Bucket] 30 // There are a fixed, finite number of aggregates (WindowOnly). 31 aggregates series.WindowSeries[series.WindowSeries[Data, Bucket], int] 32 stabilityStandardDeviation float64 33 trimmingLevel uint 34 m sync.Mutex 35 dbgLevel debug.DebugLevel 36 dbgConfig *debug.DebugWithPrefix 37 units string 38 currentInterval int 39 } 40 41 // Stabilizer parameters: 42 // 1. MAD: An all-purpose value that determines the hysteresis of various calculations 43 // that will affect saturation (of either throughput or responsiveness). 44 // 2: SDT: The standard deviation cutoff used to determine stability among the K preceding 45 // moving averages of a measurement. 46 // 3: TMP: The percentage by which to trim the values before calculating the standard deviation 47 // to determine whether the value is within acceptable range for stability (SDT). 48 49 // Stabilizer Algorithm: 50 // Throughput stabilization is achieved when the standard deviation of the MAD number of the most 51 // recent moving averages of instantaneous measurements is within an upper bound. 52 // 53 // Yes, that *is* a little confusing: 54 // The user will deliver us a steady diet of measurements of the number of bytes transmitted during the immediately 55 // previous interval. We will keep the MAD most recent of those measurements. Every time that we get a new 56 // measurement, we will recalculate the moving average of the MAD most instantaneous measurements. We will call that 57 // the moving average aggregate throughput at interval p. We keep the MAD most recent of those values. 58 // If the calculated standard deviation of *those* values is less than SDT, we declare 59 // stability. 60 61 func NewStabilizer[Data constraints.Float | constraints.Integer, Bucket utilities.Number]( 62 mad int, 63 sdt float64, 64 trimmingLevel uint, 65 units string, 66 debugLevel debug.DebugLevel, 67 debug *debug.DebugWithPrefix, 68 ) MeasurementStablizer[Data, Bucket] { 69 return MeasurementStablizer[Data, Bucket]{ 70 instantaneousses: series.NewWindowSeries[Data, Bucket](series.Forever, 0), 71 aggregates: series.NewWindowSeries[ 72 series.WindowSeries[Data, Bucket], int](series.WindowOnly, mad), 73 stabilityStandardDeviation: sdt, 74 trimmingLevel: trimmingLevel, 75 units: units, 76 currentInterval: 0, 77 dbgConfig: debug, 78 dbgLevel: debugLevel, 79 } 80 } 81 82 func (r3 *MeasurementStablizer[Data, Bucket]) Reserve(bucket Bucket) { 83 r3.m.Lock() 84 defer r3.m.Unlock() 85 r3.instantaneousses.Reserve(bucket) 86 } 87 88 func (r3 *MeasurementStablizer[Data, Bucket]) AddMeasurement(bucket Bucket, measurement Data) { 89 r3.m.Lock() 90 defer r3.m.Unlock() 91 92 // Fill in the bucket in the current interval. 93 if err := r3.instantaneousses.Fill(bucket, measurement); err != nil { 94 if debug.IsDebug(r3.dbgLevel) { 95 fmt.Printf("%s: A bucket (with id %v) does not exist in the isntantaneousses.\n", 96 r3.dbgConfig.String(), 97 bucket) 98 } 99 } 100 101 // The result may have been retired from the current interval. Look in the older series 102 // to fill it in there if it is. 103 r3.aggregates.ForEach(func(b int, md *utilities.Optional[series.WindowSeries[Data, Bucket]]) { 104 if utilities.IsSome[series.WindowSeries[Data, Bucket]](*md) { 105 md := utilities.GetSome[series.WindowSeries[Data, Bucket]](*md) 106 if err := md.Fill(bucket, measurement); err != nil { 107 if debug.IsDebug(r3.dbgLevel) { 108 fmt.Printf("%s: A bucket (with id %v) does not exist in a historical window.\n", 109 r3.dbgConfig.String(), 110 bucket) 111 } 112 } else { 113 if debug.IsDebug(r3.dbgLevel) { 114 fmt.Printf("%s: A bucket (with id %v) does exist in a historical window.\n", 115 r3.dbgConfig.String(), 116 bucket) 117 } 118 } 119 } 120 }) 121 122 /* 123 // Add this instantaneous measurement to the mix of the MAD previous instantaneous measurements. 124 r3.instantaneousses.Fill(bucket, measurement) 125 // Calculate the moving average of the MAD previous instantaneous measurements (what the 126 // algorithm calls moving average aggregate throughput at interval p) and add it to 127 // the mix of MAD previous moving averages. 128 129 r3.aggregates.AutoFill(r3.instantaneousses.CalculateAverage()) 130 131 if debug.IsDebug(r3.dbgLevel) { 132 fmt.Printf( 133 "%s: MA: %f Mbps (previous %d intervals).\n", 134 r3.dbgConfig.String(), 135 r3.aggregates.CalculateAverage(), 136 r3.aggregates.Len(), 137 ) 138 } 139 */ 140 } 141 142 func (r3 *MeasurementStablizer[Data, Bucket]) Interval() { 143 r3.m.Lock() 144 defer r3.m.Unlock() 145 146 if debug.IsDebug(r3.dbgLevel) { 147 fmt.Printf( 148 "%s: stability interval marked (transitioning from %d to %d).\n", 149 r3.dbgConfig.String(), 150 r3.currentInterval, 151 r3.currentInterval+1, 152 ) 153 } 154 155 // At the interval boundary, move the instantaneous series to 156 // the aggregates and start a new instantaneous series. 157 r3.aggregates.Reserve(r3.currentInterval) 158 r3.aggregates.Fill(r3.currentInterval, r3.instantaneousses) 159 160 r3.instantaneousses = series.NewWindowSeries[Data, Bucket](series.Forever, 0) 161 r3.currentInterval++ 162 } 163 164 func (r3 *MeasurementStablizer[Data, Bucket]) IsStable() bool { 165 r3.m.Lock() 166 defer r3.m.Unlock() 167 168 if debug.IsDebug(r3.dbgLevel) { 169 fmt.Printf( 170 "%s: Determining stability in the %d th interval.\n", 171 r3.dbgConfig.String(), 172 r3.currentInterval, 173 ) 174 } 175 // Determine if 176 // a) All the aggregates have values, 177 // b) All the aggregates are complete. 178 allComplete := true 179 r3.aggregates.ForEach(func(b int, md *utilities.Optional[series.WindowSeries[Data, Bucket]]) { 180 if utilities.IsSome[series.WindowSeries[Data, Bucket]](*md) { 181 md := utilities.GetSome[series.WindowSeries[Data, Bucket]](*md) 182 allComplete = allComplete && md.Complete() 183 if debug.IsDebug(r3.dbgLevel) { 184 fmt.Printf("%s\n", md.String()) 185 } 186 } else { 187 allComplete = false 188 } 189 if debug.IsDebug(r3.dbgLevel) { 190 fmt.Printf( 191 "%s: The aggregate for the %d th interval was %s.\n", 192 r3.dbgConfig.String(), 193 b, 194 utilities.Conditional(allComplete, "complete", "incomplete"), 195 ) 196 } 197 }) 198 199 if !allComplete { 200 return false 201 } 202 203 // Calculate the averages of each of the aggregates. 204 averages := make([]float64, 0) 205 r3.aggregates.ForEach(func(b int, md *utilities.Optional[series.WindowSeries[Data, Bucket]]) { 206 if utilities.IsSome[series.WindowSeries[Data, Bucket]](*md) { 207 md := utilities.GetSome[series.WindowSeries[Data, Bucket]](*md) 208 209 _, average := series.CalculateAverage(md) 210 averages = append(averages, average) 211 } 212 }) 213 214 if debug.IsDebug(r3.dbgLevel) { 215 r3.aggregates.ForEach(func(b int, md *utilities.Optional[series.WindowSeries[Data, Bucket]]) { 216 if utilities.IsSome[series.WindowSeries[Data, Bucket]](*md) { 217 md := utilities.GetSome[series.WindowSeries[Data, Bucket]](*md) 218 219 filled, unfilled := md.Count() 220 fmt.Printf("An aggregate has %v filled and %v unfilled buckets.\n", filled, unfilled) 221 } 222 }) 223 } 224 225 // Calculate the standard deviation of the averages of the aggregates. 226 sd := utilities.CalculateStandardDeviation(averages) 227 228 // Take a percentage of the average of the averages of the aggregates ... 229 stabilityCutoff := utilities.CalculateAverage(averages) * (r3.stabilityStandardDeviation / 100.0) 230 // and compare that to the standard deviation to determine stability. 231 isStable := sd <= stabilityCutoff 232 233 return isStable 234 } 235 236 func (r3 *MeasurementStablizer[Data, Bucket]) GetBounds() (Bucket, Bucket) { 237 r3.m.Lock() 238 defer r3.m.Unlock() 239 240 haveMinimum := false 241 242 lowerBound := Bucket(0) 243 upperBound := Bucket(0) 244 245 r3.aggregates.ForEach(func(b int, md *utilities.Optional[series.WindowSeries[Data, Bucket]]) { 246 if utilities.IsSome[series.WindowSeries[Data, Bucket]](*md) { 247 md := utilities.GetSome[series.WindowSeries[Data, Bucket]](*md) 248 currentAggregateLowerBound, currentAggregateUpperBound := md.GetBucketBounds() 249 250 if !haveMinimum { 251 lowerBound = currentAggregateLowerBound 252 haveMinimum = true 253 } else { 254 lowerBound = min(lowerBound, currentAggregateLowerBound) 255 } 256 upperBound = max(upperBound, currentAggregateUpperBound) 257 } 258 }) 259 260 return lowerBound, upperBound 261 }