github.com/rudderlabs/rudder-go-kit@v0.30.0/stats/metric/ewma.go (about)

     1  package metric
     2  
     3  import "sync"
     4  
     5  const (
     6  	// AVG_METRIC_AGE By default, we average over a one-minute period, which means the average
     7  	// age of the metrics in the period is 30 seconds.
     8  	AVG_METRIC_AGE float64 = 30.0
     9  
    10  	// DECAY The formula for computing the decay factor from the average age comes
    11  	// from "Production and Operations Analysis" by Steven Nahmias.
    12  	DECAY = 2 / (AVG_METRIC_AGE + 1)
    13  
    14  	// WARMUP_SAMPLES For best results, the moving average should not be initialized to the
    15  	// samples it sees immediately. The book "Production and Operations
    16  	// Analysis" by Steven Nahmias suggests initializing the moving average to
    17  	// the mean of the first 10 samples. Until the VariableEwma has seen this
    18  	// many samples, it is not "ready" to be queried for the value of the
    19  	// moving average. This adds some memory cost.
    20  	WARMUP_SAMPLES uint8 = 10
    21  )
    22  
    23  var threadSafeMutex sync.RWMutex
    24  
    25  // MovingAverage is the interface that computes a moving average over a time-
    26  // series stream of numbers. The average may be over a window or exponentially
    27  // decaying.
    28  type MovingAverage interface {
    29  	Add(float64)
    30  	Value() float64
    31  	Set(float64)
    32  }
    33  
    34  // NewMovingAverage constructs a MovingAverage that computes an average with the
    35  // desired characteristics in the moving window or exponential decay. If no
    36  // age is given, it constructs a default exponentially weighted implementation
    37  // that consumes minimal memory. The age is related to the decay factor alpha
    38  // by the formula given for the DECAY constant. It signifies the average age
    39  // of the samples as time goes to infinity.
    40  func NewMovingAverage(age ...float64) MovingAverage {
    41  	if len(age) == 0 {
    42  		return new(SimpleEWMA)
    43  	}
    44  	return &VariableEWMA{
    45  		decay: 2 / (age[0] + 1),
    46  	}
    47  }
    48  
    49  // A SimpleEWMA represents the exponentially weighted moving average of a
    50  // series of numbers. It WILL have different behavior than the VariableEWMA
    51  // for multiple reasons. It has no warm-up period and it uses a constant
    52  // decay.  These properties let it use less memory.  It will also behave
    53  // differently when it's equal to zero, which is assumed to mean
    54  // uninitialized, so if a value is likely to actually become zero over time,
    55  // then any non-zero value will cause a sharp jump instead of a small change.
    56  // However, note that this takes a long time, and the value may just
    57  // decays to a stable value that's close to zero, but which won't be mistaken
    58  // for uninitialized. See http://play.golang.org/p/litxBDr_RC for example.
    59  type SimpleEWMA struct {
    60  	// The current value of the average. After adding with Add(), this is
    61  	// updated to reflect the average of all values seen thus far.
    62  	value float64
    63  }
    64  
    65  // Add adds a value to the series and updates the moving average.
    66  func (e *SimpleEWMA) Add(value float64) {
    67  	threadSafeMutex.Lock()
    68  	defer threadSafeMutex.Unlock()
    69  	if e.value == 0 { // this is a proxy for "uninitialized"
    70  		e.value = value
    71  	} else {
    72  		e.value = (value * DECAY) + (e.value * (1 - DECAY))
    73  	}
    74  }
    75  
    76  // Value returns the current value of the moving average.
    77  func (e *SimpleEWMA) Value() float64 {
    78  	threadSafeMutex.RLock()
    79  	defer threadSafeMutex.RUnlock()
    80  	return e.value
    81  }
    82  
    83  // Set sets the EWMA's value.
    84  func (e *SimpleEWMA) Set(value float64) {
    85  	threadSafeMutex.Lock()
    86  	defer threadSafeMutex.Unlock()
    87  	e.value = value
    88  }
    89  
    90  // VariableEWMA represents the exponentially weighted moving average of a series of
    91  // numbers. Unlike SimpleEWMA, it supports a custom age, and thus uses more memory.
    92  type VariableEWMA struct {
    93  	// The multiplier factor by which the previous samples decay.
    94  	decay float64
    95  	// The current value of the average.
    96  	value float64
    97  	// The number of samples added to this instance.
    98  	count uint8
    99  }
   100  
   101  // Add adds a value to the series and updates the moving average.
   102  func (e *VariableEWMA) Add(value float64) {
   103  	threadSafeMutex.Lock()
   104  	defer threadSafeMutex.Unlock()
   105  	switch {
   106  	case e.count < WARMUP_SAMPLES:
   107  		e.count++
   108  		e.value += value
   109  	case e.count == WARMUP_SAMPLES:
   110  		e.count++
   111  		e.value = e.value / float64(WARMUP_SAMPLES)
   112  		e.value = (value * e.decay) + (e.value * (1 - e.decay))
   113  	default:
   114  		e.value = (value * e.decay) + (e.value * (1 - e.decay))
   115  	}
   116  }
   117  
   118  // Value returns the current value of the average, or 0.0 if the series hasn't
   119  // warmed up yet.
   120  func (e *VariableEWMA) Value() float64 {
   121  	threadSafeMutex.RLock()
   122  	defer threadSafeMutex.RUnlock()
   123  	if e.count <= WARMUP_SAMPLES {
   124  		return 0.0
   125  	}
   126  
   127  	return e.value
   128  }
   129  
   130  // Set sets the EWMA's value.
   131  func (e *VariableEWMA) Set(value float64) {
   132  	threadSafeMutex.Lock()
   133  	defer threadSafeMutex.Unlock()
   134  	e.value = value
   135  	if e.count <= WARMUP_SAMPLES {
   136  		e.count = WARMUP_SAMPLES + 1
   137  	}
   138  }