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  }