github.com/haraldrudell/parl@v0.4.176/counter/datapoint.go (about) 1 /* 2 © 2022–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/) 3 ISC License 4 */ 5 6 package counter 7 8 import ( 9 "sync" 10 "time" 11 12 "github.com/haraldrudell/parl" 13 "github.com/haraldrudell/parl/perrors" 14 "github.com/haraldrudell/parl/ptime" 15 ) 16 17 // Datapoint tracks a fluctuating value with average. Thread-safe. 18 type Datapoint struct { 19 period time.Duration 20 21 lock sync.Mutex 22 value uint64 23 max uint64 24 min uint64 25 // period in in progress 26 periodStart time.Time 27 isFullPeriod bool 28 nPeriod uint64 29 aggregate float64 30 // valid values 31 average float64 32 n uint64 33 } 34 35 var _ parl.DatapointValue = &Datapoint{} 36 37 func newDatapoint(period time.Duration) (datapoint parl.Datapoint) { 38 if period <= 0 { 39 panic(perrors.ErrorfPF("period must be positive: %s", ptime.Duration(period))) 40 } 41 return &Datapoint{period: period} 42 } 43 44 // SetValue records a new datapoint value 45 func (dt *Datapoint) SetValue(value uint64) (datapoint parl.Datapoint) { 46 tNow := time.Now() // best possible tNow 47 datapoint = dt 48 dt.lock.Lock() 49 defer dt.lock.Unlock() 50 51 // update value max min 52 dt.value = value 53 isFirst := dt.periodStart.IsZero() 54 if isFirst || value > dt.max { 55 dt.max = value 56 } 57 if isFirst || value < dt.min { 58 dt.min = value 59 } 60 61 // determine period 62 thisPeriodStart, isEnd := dt.isEnd(tNow) 63 64 // first SetValue invocation: initialize periodStart 65 if isFirst { 66 dt.periodStart = thisPeriodStart 67 return // first period started return 68 } 69 70 // check for end of a period 71 if !isEnd { 72 if !dt.isFullPeriod { 73 // not at end of a period and current period is the first, partial, period 74 return // in the first, non-full, period return: no averaging data updates 75 } 76 // not at end of a full period 77 } else { 78 // beyond the end of the recording period: handle period change 79 dt.updatePeriod(thisPeriodStart) 80 } 81 82 // update aggregate 83 dt.nPeriod++ 84 dt.aggregate += float64(value) 85 86 return 87 } 88 89 func (dt *Datapoint) CloneDatapoint() (datapoint parl.Datapoint) { 90 return dt.cloneDatapoint(false) 91 } 92 func (dt *Datapoint) CloneDatapointReset() (datapoint parl.Datapoint) { 93 return dt.cloneDatapoint(true) 94 } 95 func (dt *Datapoint) cloneDatapoint(reset bool) (datapoint parl.Datapoint) { 96 tNow := time.Now() 97 dt.lock.Lock() 98 defer dt.lock.Unlock() 99 100 // handle period end 101 if thisPeriodStart, isEnd := dt.isEnd(tNow); isEnd { 102 dt.updatePeriod(thisPeriodStart) 103 } 104 105 datapoint = &Datapoint{ 106 period: dt.period, 107 value: dt.value, 108 max: dt.max, 109 min: dt.min, 110 periodStart: dt.periodStart, 111 isFullPeriod: dt.isFullPeriod, 112 nPeriod: dt.nPeriod, 113 aggregate: dt.aggregate, 114 average: dt.average, 115 n: dt.n, 116 } 117 118 if !reset { 119 return 120 } 121 122 dt.value = 0 123 dt.max = 0 124 dt.min = 0 125 dt.periodStart = time.Time{} 126 dt.isFullPeriod = false 127 dt.nPeriod = 0 128 dt.aggregate = 0 129 dt.average = 0 130 dt.n = 0 131 132 return 133 } 134 135 func (dt *Datapoint) GetDatapoint() (value, max, min uint64, isValid bool, average float64, n uint64) { 136 tNow := time.Now() // best possible tNow 137 dt.lock.Lock() 138 defer dt.lock.Unlock() 139 140 // handle period end 141 if thisPeriodStart, isEnd := dt.isEnd(tNow); isEnd { 142 dt.updatePeriod(thisPeriodStart) 143 } 144 145 value = dt.value 146 max = dt.max 147 min = dt.min 148 isValid = !dt.periodStart.IsZero() 149 average = dt.average 150 n = dt.n 151 152 return 153 } 154 155 func (dt *Datapoint) DatapointValue() (value uint64) { 156 value, _, _, _, _, _ = dt.GetDatapoint() 157 return 158 } 159 func (dt *Datapoint) DatapointMax() (max uint64) { 160 _, max, _, _, _, _ = dt.GetDatapoint() 161 return 162 } 163 func (dt *Datapoint) DatapointMin() (min uint64) { 164 _, _, min, _, _, _ = dt.GetDatapoint() 165 return 166 } 167 168 // isEnd determines if the current period has ended. 169 // isEnd returns the start of the current period. 170 func (dt *Datapoint) isEnd(tNow time.Time) (thisPeriodStart time.Time, isEnd bool) { 171 thisPeriodStart = tNow.Truncate(dt.period) 172 173 // if the first SetValue has not happened, it cannot be isEnd 174 if dt.periodStart.IsZero() { 175 return 176 } 177 178 isEnd = !dt.periodStart.Equal(thisPeriodStart) 179 return 180 } 181 182 // updatePeriod carries otu a period change. 183 // updatePeriod must only be executed when isEnd returns true. 184 func (dt *Datapoint) updatePeriod(thisPeriodStart time.Time) { 185 if !dt.isFullPeriod { 186 // the ending period was not a full period 187 // no avergaing was done for that non-full period 188 dt.isFullPeriod = true // first full period return 189 return 190 } 191 192 // save valid average 193 if dt.nPeriod > 0 { 194 dt.average = dt.aggregate / float64(dt.nPeriod) 195 dt.n = dt.nPeriod 196 } 197 198 // restart period averaging 199 dt.periodStart = thisPeriodStart 200 dt.aggregate = 0 201 dt.nPeriod = 0 202 }