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 }