github.com/m3db/m3@v1.5.0/src/query/functions/temporal/rate.go (about) 1 // Copyright (c) 2018 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 package temporal 22 23 import ( 24 "fmt" 25 "math" 26 "time" 27 28 "github.com/m3db/m3/src/query/executor/transform" 29 "github.com/m3db/m3/src/query/ts" 30 xtime "github.com/m3db/m3/src/x/time" 31 ) 32 33 const ( 34 // IRateType calculates the per-second rate of increase of the time series 35 // across the specified time range. This is based on the last two data points. 36 IRateType = "irate" 37 38 // IDeltaType calculates the difference between the last two values in the time series. 39 // IDeltaTemporalType should only be used with gauges. 40 IDeltaType = "idelta" 41 42 // RateType calculates the per-second average rate of increase of the time series. 43 RateType = "rate" 44 45 // DeltaType calculates the difference between the first and last value of each time series. 46 DeltaType = "delta" 47 48 // IncreaseType calculates the increase in the time series. 49 IncreaseType = "increase" 50 ) 51 52 // RateProcessor is a structure containing details about the rate. 53 type RateProcessor struct { 54 IsRate, IsCounter bool 55 RateFn RateFn 56 } 57 58 func (r RateProcessor) initialize( 59 duration time.Duration, 60 _ transform.Options, 61 ) processor { 62 return &rateNode{ 63 isRate: r.IsRate, 64 isCounter: r.IsCounter, 65 rateFn: r.RateFn, 66 duration: duration, 67 } 68 } 69 70 // NewRateOpWithProcessor creates a new base temporal transform for 71 // the given rate processor. 72 func NewRateOpWithProcessor( 73 args []interface{}, 74 opType string, 75 rateProcessor RateProcessor, 76 ) (transform.Params, error) { 77 if len(args) != 1 { 78 return emptyOp, 79 fmt.Errorf("invalid number of args for %s: %d", opType, len(args)) 80 } 81 82 duration, ok := args[0].(time.Duration) 83 if !ok { 84 return emptyOp, 85 fmt.Errorf("unable to cast to scalar argument: %v for %s", args[0], opType) 86 } 87 88 return newBaseOp(duration, opType, rateProcessor) 89 } 90 91 // NewRateOp creates a new base temporal transform for rate functions. 92 func NewRateOp(args []interface{}, opType string) (transform.Params, error) { 93 var ( 94 isRate, isCounter bool 95 rateFn = standardRateFunc 96 ) 97 98 switch opType { 99 case IRateType: 100 isRate = true 101 rateFn = irateFunc 102 case IDeltaType: 103 rateFn = irateFunc 104 case RateType: 105 isRate = true 106 isCounter = true 107 case IncreaseType: 108 isCounter = true 109 case DeltaType: 110 default: 111 return nil, fmt.Errorf("unknown rate type: %s", opType) 112 } 113 114 r := RateProcessor{ 115 IsRate: isRate, 116 IsCounter: isCounter, 117 RateFn: rateFn, 118 } 119 120 return NewRateOpWithProcessor(args, opType, r) 121 } 122 123 // RateFn is a function that calculates rate over the given set of datapoints. 124 type RateFn func( 125 datapoints ts.Datapoints, 126 isRate bool, 127 isCounter bool, 128 rangeStart xtime.UnixNano, 129 rangeEnd xtime.UnixNano, 130 duration time.Duration, 131 ) float64 132 133 type rateNode struct { 134 isRate, isCounter bool 135 duration time.Duration 136 rateFn RateFn 137 } 138 139 func (r *rateNode) process(datapoints ts.Datapoints, bounds iterationBounds) float64 { 140 return r.rateFn( 141 datapoints, 142 r.isRate, 143 r.isCounter, 144 bounds.start, 145 bounds.end, 146 r.duration, 147 ) 148 } 149 150 func standardRateFunc( 151 datapoints ts.Datapoints, 152 isRate bool, 153 isCounter bool, 154 rangeStart xtime.UnixNano, 155 rangeEnd xtime.UnixNano, 156 timeWindow time.Duration, 157 ) float64 { 158 if len(datapoints) < 2 { 159 return math.NaN() 160 } 161 162 var ( 163 counterCorrection float64 164 firstVal, lastValue float64 165 firstIdx, lastIdx int 166 firstTS, lastTS xtime.UnixNano 167 foundFirst bool 168 ) 169 170 for i, dp := range datapoints { 171 if math.IsNaN(dp.Value) { 172 continue 173 } 174 175 if !foundFirst { 176 firstVal = dp.Value 177 firstTS = dp.Timestamp 178 firstIdx = i 179 foundFirst = true 180 } 181 182 if isCounter && dp.Value < lastValue { 183 counterCorrection += lastValue 184 } 185 186 lastValue = dp.Value 187 lastTS = dp.Timestamp 188 lastIdx = i 189 } 190 191 if firstIdx == lastIdx { 192 return math.NaN() 193 } 194 195 durationToStart := subSeconds(firstTS, rangeStart) 196 durationToEnd := subSeconds(rangeEnd, lastTS) 197 sampledInterval := subSeconds(lastTS, firstTS) 198 averageDurationBetweenSamples := sampledInterval / float64(lastIdx-firstIdx) 199 200 resultValue := lastValue - firstVal + counterCorrection 201 if isCounter && resultValue > 0 && firstVal >= 0 { 202 // Counters cannot be negative. If we have any slope at 203 // all (i.e. resultValue went up), we can extrapolate 204 // the zero point of the counter. If the duration to the 205 // zero point is shorter than the durationToStart, we 206 // take the zero point as the start of the series, 207 // thereby avoiding extrapolation to negative counter 208 // values. 209 durationToZero := sampledInterval * (firstVal / resultValue) 210 if durationToZero < durationToStart { 211 durationToStart = durationToZero 212 } 213 } 214 215 // If the first/last samples are close to the boundaries of the range, 216 // extrapolate the result. This is as we expect that another sample 217 // will exist given the spacing between samples we've seen thus far, 218 // with an allowance for noise. 219 extrapolationThreshold := averageDurationBetweenSamples * 1.1 220 extrapolateToInterval := sampledInterval 221 222 if durationToStart < extrapolationThreshold { 223 extrapolateToInterval += durationToStart 224 } else { 225 extrapolateToInterval += averageDurationBetweenSamples / 2 226 } 227 228 if durationToEnd < extrapolationThreshold { 229 extrapolateToInterval += durationToEnd 230 } else { 231 extrapolateToInterval += averageDurationBetweenSamples / 2 232 } 233 234 resultValue = resultValue * (extrapolateToInterval / sampledInterval) 235 if isRate { 236 resultValue /= timeWindow.Seconds() 237 } 238 239 return resultValue 240 } 241 242 func irateFunc( 243 datapoints ts.Datapoints, 244 isRate bool, 245 _ bool, _ xtime.UnixNano, _ xtime.UnixNano, _ time.Duration, 246 ) float64 { 247 dpsLen := len(datapoints) 248 if dpsLen < 2 { 249 return math.NaN() 250 } 251 252 nonNanIdx := dpsLen - 1 253 // find idx for last non-NaN value 254 indexLast := findNonNanIdx(datapoints, nonNanIdx) 255 // if indexLast is 0 then you only have one value and should return a NaN 256 if indexLast < 1 { 257 return math.NaN() 258 } 259 260 nonNanIdx = findNonNanIdx(datapoints, indexLast-1) 261 if nonNanIdx == -1 { 262 return math.NaN() 263 } 264 265 previousSample := datapoints[nonNanIdx] 266 lastSample := datapoints[indexLast] 267 268 var resultValue float64 269 if isRate && lastSample.Value < previousSample.Value { 270 // Counter reset. 271 resultValue = lastSample.Value 272 } else { 273 resultValue = lastSample.Value - previousSample.Value 274 } 275 276 if isRate { 277 sampledInterval := lastSample.Timestamp.Sub(previousSample.Timestamp) 278 if sampledInterval == 0 { 279 return math.NaN() 280 } 281 282 resultValue /= sampledInterval.Seconds() 283 } 284 285 return resultValue 286 } 287 288 // findNonNanIdx iterates over the values backwards until we find a non-NaN 289 // value, then returns its index. 290 func findNonNanIdx(dps ts.Datapoints, startingIdx int) int { 291 for i := startingIdx; i >= 0; i-- { 292 if !math.IsNaN(dps[i].Value) { 293 return i 294 } 295 } 296 297 return -1 298 }