github.com/theQRL/go-zond@v0.1.1/metrics/ewma.go (about) 1 package metrics 2 3 import ( 4 "math" 5 "sync" 6 "sync/atomic" 7 "time" 8 ) 9 10 type EWMASnapshot interface { 11 Rate() float64 12 } 13 14 // EWMAs continuously calculate an exponentially-weighted moving average 15 // based on an outside source of clock ticks. 16 type EWMA interface { 17 Snapshot() EWMASnapshot 18 Tick() 19 Update(int64) 20 } 21 22 // NewEWMA constructs a new EWMA with the given alpha. 23 func NewEWMA(alpha float64) EWMA { 24 return &StandardEWMA{alpha: alpha} 25 } 26 27 // NewEWMA1 constructs a new EWMA for a one-minute moving average. 28 func NewEWMA1() EWMA { 29 return NewEWMA(1 - math.Exp(-5.0/60.0/1)) 30 } 31 32 // NewEWMA5 constructs a new EWMA for a five-minute moving average. 33 func NewEWMA5() EWMA { 34 return NewEWMA(1 - math.Exp(-5.0/60.0/5)) 35 } 36 37 // NewEWMA15 constructs a new EWMA for a fifteen-minute moving average. 38 func NewEWMA15() EWMA { 39 return NewEWMA(1 - math.Exp(-5.0/60.0/15)) 40 } 41 42 // ewmaSnapshot is a read-only copy of another EWMA. 43 type ewmaSnapshot float64 44 45 // Rate returns the rate of events per second at the time the snapshot was 46 // taken. 47 func (a ewmaSnapshot) Rate() float64 { return float64(a) } 48 49 // NilEWMA is a no-op EWMA. 50 type NilEWMA struct{} 51 52 func (NilEWMA) Snapshot() EWMASnapshot { return (*emptySnapshot)(nil) } 53 func (NilEWMA) Tick() {} 54 func (NilEWMA) Update(n int64) {} 55 56 // StandardEWMA is the standard implementation of an EWMA and tracks the number 57 // of uncounted events and processes them on each tick. It uses the 58 // sync/atomic package to manage uncounted events. 59 type StandardEWMA struct { 60 uncounted atomic.Int64 61 alpha float64 62 rate atomic.Uint64 63 init atomic.Bool 64 mutex sync.Mutex 65 } 66 67 // Snapshot returns a read-only copy of the EWMA. 68 func (a *StandardEWMA) Snapshot() EWMASnapshot { 69 r := math.Float64frombits(a.rate.Load()) * float64(time.Second) 70 return ewmaSnapshot(r) 71 } 72 73 // Tick ticks the clock to update the moving average. It assumes it is called 74 // every five seconds. 75 func (a *StandardEWMA) Tick() { 76 // Optimization to avoid mutex locking in the hot-path. 77 if a.init.Load() { 78 a.updateRate(a.fetchInstantRate()) 79 return 80 } 81 // Slow-path: this is only needed on the first Tick() and preserves transactional updating 82 // of init and rate in the else block. The first conditional is needed below because 83 // a different thread could have set a.init = 1 between the time of the first atomic load and when 84 // the lock was acquired. 85 a.mutex.Lock() 86 if a.init.Load() { 87 // The fetchInstantRate() uses atomic loading, which is unnecessary in this critical section 88 // but again, this section is only invoked on the first successful Tick() operation. 89 a.updateRate(a.fetchInstantRate()) 90 } else { 91 a.init.Store(true) 92 a.rate.Store(math.Float64bits(a.fetchInstantRate())) 93 } 94 a.mutex.Unlock() 95 } 96 97 func (a *StandardEWMA) fetchInstantRate() float64 { 98 count := a.uncounted.Swap(0) 99 return float64(count) / float64(5*time.Second) 100 } 101 102 func (a *StandardEWMA) updateRate(instantRate float64) { 103 currentRate := math.Float64frombits(a.rate.Load()) 104 currentRate += a.alpha * (instantRate - currentRate) 105 a.rate.Store(math.Float64bits(currentRate)) 106 } 107 108 // Update adds n uncounted events. 109 func (a *StandardEWMA) Update(n int64) { 110 a.uncounted.Add(n) 111 }