github.com/haraldrudell/parl@v0.4.176/ptime/averager.go (about) 1 /* 2 © 2023–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/) 3 ISC License 4 */ 5 6 package ptime 7 8 import ( 9 "sync" 10 "time" 11 12 "github.com/haraldrudell/parl/perrors" 13 "github.com/haraldrudell/parl/pslices" 14 "golang.org/x/exp/constraints" 15 ) 16 17 const ( 18 averagerDefaultPeriod = time.Second 19 averagerDefaultCount = 10 20 ) 21 22 // Averager is a container for averaging integer values providing perioding and history. 23 // - maxCount is the maximum interval over which average is calculated 24 // - average is the average value of provided datapoints during all periods 25 // - because averaging is over 10 or many periods, the average will change slowly 26 type Averager[T constraints.Integer] struct { 27 // period provides linear interval-numbering from a time.Time timestamp 28 period Period 29 maxCount int 30 31 sliceLock sync.RWMutex 32 // each pointer has a period index, datapoint count and datapoint aggregate 33 // - may be nil 34 // - if no value was provided during a particular period, that pointer is nil 35 // - if length >0, first and last intervals are not nil 36 // - thread-safe behind sliceLock 37 intervals []*AverageInterval 38 } 39 40 // NewAverager returns an object that calculates average over a number of interval periods. 41 // - interval-length is 1 s 42 // - averaging over 10 intervals 43 func NewAverager[T constraints.Integer]() (averager *Averager[T]) { 44 return &Averager[T]{ 45 period: *NewPeriod(averagerDefaultPeriod), 46 maxCount: averagerDefaultCount, 47 } 48 } 49 50 // NewAverager2 returns an object that calculates average over a number of interval periods. 51 // - period 0 means default 1 s interval-length 52 // - periodCount 0 means averaging over 10 intervals 53 func NewAverager2[T constraints.Integer](period time.Duration, periodCount int) (averager *Averager[T]) { 54 if period == 0 { 55 period = averagerDefaultPeriod 56 } else if period < 1 { 57 perrors.ErrorfPF("period cannot be less than 1 ns: %s", period) 58 } 59 if periodCount == 0 { 60 periodCount = averagerDefaultCount 61 } else if periodCount < 2 { 62 perrors.ErrorfPF("periodCount cannot be less than 2: %d", periodCount) 63 } 64 return &Averager[T]{ 65 period: *NewPeriod(period), 66 maxCount: periodCount, 67 } 68 } 69 70 // Add adds a new value basis for averaging 71 // - value is the sample value 72 // - t is the sample time, default time.Now() 73 func (a *Averager[T]) Add(value T, t ...time.Time) { 74 if interval := a.getCurrent(a.period.Index(t...)); interval != nil { 75 interval.Add(float64(value)) 76 } 77 } 78 79 // Average returns the average 80 // - t is time, default time.Now() 81 // - if sample count aggregated across the slice zero, average is zero 82 // - count is number of values making up the average 83 func (a *Averager[T]) Average(t ...time.Time) (average float64, count uint64) { 84 85 // invoking getCurrent ensure periods are updated 86 // - if no recent datapoints were provided, the slice may contain old values 87 a.getCurrent(a.period.Index(t...)) 88 a.sliceLock.RLock() 89 defer a.sliceLock.RUnlock() 90 91 // calculate the average 92 for _, aPeriod := range a.intervals { 93 if aPeriod == nil { 94 continue // nil average pointers is allowed 95 } 96 aPeriod.Aggregate(&count, &average) 97 } 98 if count > 0 { 99 average = average / float64(count) 100 } 101 102 return 103 } 104 105 // TODO 230726 Delete 106 // func (a *Averager[T]) Index(t ...time.Time) (index PeriodIndex) { 107 // return a.period.Index(t...) 108 // } 109 110 // getCurrent ensures an interval for the current period exists and returns it 111 // - interval is nil if periodIndex is too far in the past 112 // - the slice is updated to include periodIndex if it is after the last entry 113 func (a *Averager[T]) getCurrent(periodIndex PeriodIndex) (interval *AverageInterval) { 114 if interval = a.getLastInterval(); interval != nil && interval.index == periodIndex { 115 return // last existing period is the right one return 116 } 117 118 // ensure the slice is up-to-date and 119 // return the requested period if it is not out of range 120 a.sliceLock.Lock() 121 defer a.sliceLock.Unlock() 122 123 // 1. determine at what period the slice should end 124 125 // the latest known period 126 // - either the requested value or 127 // - the last existing in slice 128 var lastPeriod = periodIndex 129 if interval != nil && interval.index > lastPeriod { 130 lastPeriod = interval.index 131 } 132 // firstInterval is the lowest allowed period number considering: 133 // - maxCount and period0 134 var firstInterval = a.period.Sub(lastPeriod+1, a.maxCount) 135 // length is the number of entries in the slice 136 var length = len(a.intervals) 137 138 // 2. remove any stale slice entries at start of slice 139 if length > 0 { 140 var firstIndex PeriodIndex 141 if firstIndex = a.intervals[0].Index(); firstIndex < firstInterval { 142 pslices.TrimLeft(&a.intervals, a.period.Since(firstInterval, firstIndex)) 143 length = len(a.intervals) 144 } 145 } 146 // numberOfIntervals is the length the slice should have to include periodIndex 147 var numberOfIntervals = a.period.Available(periodIndex, a.maxCount) 148 149 // 3. extend the slice as necessary 150 if length < numberOfIntervals { 151 pslices.SetLength(&a.intervals, numberOfIntervals) 152 length = numberOfIntervals 153 } 154 155 // ensure first interval exists 156 if a.intervals[0] == nil { 157 a.intervals[0] = NewAverageInterval(firstInterval) 158 } 159 160 // ensure last interval exists 161 var lastIndex = length - 1 162 if interval = a.intervals[lastIndex]; interval == nil { 163 interval = NewAverageInterval(periodIndex) 164 a.intervals[lastIndex] = interval 165 } 166 167 if periodIndex < firstInterval { 168 interval = nil 169 return // periodIndex too far in the past return 170 } 171 172 interval = a.intervals[periodIndex-firstInterval] 173 174 return 175 } 176 177 // getLastInterval returns the last period or nil if no period exists 178 // - the last period provide the current end of the value series 179 // - if no recent datapoints were provided, 180 // the last period may be stale 181 func (a *Averager[T]) getLastInterval() (interval *AverageInterval) { 182 a.sliceLock.RLock() 183 defer a.sliceLock.RUnlock() 184 185 if length := len(a.intervals); length > 0 { 186 interval = a.intervals[length-1] 187 } 188 189 return 190 }