github.com/go-graphite/carbonapi@v0.17.0/expr/helper/align.go (about) 1 package helper 2 3 import ( 4 "math" 5 "time" 6 7 "github.com/go-graphite/carbonapi/expr/types" 8 "github.com/go-graphite/carbonapi/pkg/parser" 9 ) 10 11 // GCD returns greatest common divisor calculated via Euclidean algorithm 12 func GCD(a, b int64) int64 { 13 for b != 0 { 14 t := b 15 b = a % b 16 a = t 17 } 18 return a 19 } 20 21 // LCM returns the least common multiple of 2 or more integers via GDB 22 func LCM(args ...int64) int64 { 23 if len(args) <= 1 { 24 if len(args) == 0 { 25 return 0 26 } 27 return args[0] 28 } 29 lcm := args[0] / GCD(args[0], args[1]) * args[1] 30 31 for i := 2; i < len(args); i++ { 32 lcm = LCM(lcm, args[i]) 33 } 34 return lcm 35 } 36 37 // GetCommonStep returns LCM(steps), changed (bool) for slice of metrics. 38 // If all metrics have the same step, changed == false. 39 func GetCommonStep(args []*types.MetricData) (commonStep int64, changed bool) { 40 steps := make([]int64, 0, 1) 41 stepsIndex := make(map[int64]struct{}) 42 for _, arg := range args { 43 if _, ok := stepsIndex[arg.StepTime]; !ok { 44 stepsIndex[arg.StepTime] = struct{}{} 45 steps = append(steps, arg.StepTime) 46 } 47 } 48 if len(steps) == 1 { 49 return steps[0], false 50 } 51 commonStep = LCM(steps...) 52 return commonStep, true 53 } 54 55 // GetStepRange returns min(steps), changed (bool) for slice of metrics. 56 // If all metrics have the same step, changed == false. 57 func GetStepRange(args []*types.MetricData) (minStep, maxStep int64, needScale bool) { 58 minStep = args[0].StepTime 59 maxStep = args[0].StepTime 60 for _, arg := range args { 61 if minStep > arg.StepTime { 62 minStep = arg.StepTime 63 } 64 if maxStep < arg.StepTime { 65 maxStep = arg.StepTime 66 } 67 } 68 needScale = minStep != maxStep 69 70 return 71 } 72 73 // ScaleToCommonStep returns the metrics, aligned LCM of all metrics steps. 74 // If commonStep == 0, then it will be calculated automatically 75 // It respects xFilesFactor and fills gaps in the begin and end with NaNs if needed. 76 func ScaleToCommonStep(args []*types.MetricData, commonStep int64) []*types.MetricData { 77 if commonStep < 0 || len(args) == 0 { 78 // This doesn't make sense 79 return args 80 } 81 82 // If it's invoked with commonStep other than 0, changes are applied by default 83 if commonStep == 0 { 84 commonStep, _ = GetCommonStep(args) 85 } 86 87 minStart := args[0].StartTime 88 for _, arg := range args { 89 if minStart > arg.StartTime { 90 minStart = arg.StartTime 91 } 92 } 93 minStart -= (minStart % commonStep) // align StartTime against step 94 95 maxVals := 0 96 97 for _, arg := range args { 98 if arg.StepTime == commonStep { 99 if minStart < arg.StartTime { 100 valCnt := (arg.StartTime - minStart) / arg.StepTime 101 newVals := genNaNs(int(valCnt)) 102 arg.Values = append(newVals, arg.Values...) 103 } 104 arg.StartTime = minStart 105 106 if len(arg.Values) > maxVals { 107 maxVals = len(arg.Values) 108 } 109 } else { 110 // arg = arg.Copy(true) 111 // args[a] = arg 112 stepFactor := commonStep / arg.StepTime 113 // newStart := minStart - (arg.StartTime % commonStep) 114 if arg.StartTime > minStart { 115 // Fill with NaNs from newStart to arg.StartTime 116 valCnt := (arg.StartTime - minStart) / arg.StepTime 117 nans := genNaNs(int(valCnt)) 118 arg.Values = append(nans, arg.Values...) 119 arg.StartTime = minStart 120 } 121 122 newValsLen := 1 + int64(len(arg.Values)-1)/stepFactor 123 newStop := arg.StartTime + newValsLen*commonStep 124 newVals := make([]float64, 0, newValsLen) 125 126 if len(arg.Values) != int(stepFactor*newValsLen) { 127 // Fill the last step with NaNs from newStart to (newStart + commonStep - arg.StepTime) 128 valCnt := int(stepFactor*newValsLen) - len(arg.Values) 129 nans := genNaNs(valCnt) 130 arg.Values = append(arg.Values, nans...) 131 } 132 arg.StopTime = newStop 133 for i := 0; i < len(arg.Values); i += int(stepFactor) { 134 aggregatedBatch := aggregateBatch(arg.Values[i:i+int(stepFactor)], arg) 135 newVals = append(newVals, aggregatedBatch) 136 } 137 arg.StepTime = commonStep 138 arg.Values = newVals 139 140 if len(arg.Values) > maxVals { 141 maxVals = len(arg.Values) 142 } 143 } 144 } 145 146 for _, arg := range args { 147 if maxVals > len(arg.Values) { 148 valCnt := maxVals - len(arg.Values) 149 newVals := genNaNs(valCnt) 150 arg.Values = append(arg.Values, newVals...) 151 } 152 arg.RecalcStopTime() 153 } 154 155 return args 156 } 157 158 // GetInterval returns minStartTime, maxStartTime for slice of metrics. 159 func GetInterval(args []*types.MetricData) (minStartTime, maxStopTime int64) { 160 minStartTime = args[0].StartTime 161 maxStopTime = args[0].StopTime 162 for _, arg := range args { 163 if minStartTime > arg.StartTime { 164 minStartTime = arg.StartTime 165 } 166 167 arg.FixStopTime() 168 if maxStopTime < arg.StopTime { 169 maxStopTime = arg.StopTime 170 } 171 } 172 173 return 174 } 175 176 func aggregateBatch(vals []float64, arg *types.MetricData) float64 { 177 if arg.XFilesFactor != 0 { 178 notNans := 0 179 for _, i := range vals { 180 if !math.IsNaN(i) { 181 notNans++ 182 } 183 } 184 if float32(notNans)/float32(len(vals)) < arg.XFilesFactor { 185 return math.NaN() 186 } 187 } 188 return arg.GetAggregateFunction()(vals) 189 } 190 191 // ScaleValuesToCommonStep returns map[parser.MetricRequest][]*types.MetricData. If any element of []*types.MetricData is changed, it doesn't change original 192 // metric, but creates the new one to avoid cache spoiling. 193 func ScaleValuesToCommonStep(values map[parser.MetricRequest][]*types.MetricData) map[parser.MetricRequest][]*types.MetricData { 194 // Calculate global commonStep 195 var args []*types.MetricData 196 for _, metrics := range values { 197 args = append(args, metrics...) 198 } 199 200 commonStep, changed := GetCommonStep(args) 201 if !changed { 202 return values 203 } 204 205 for m, metrics := range values { 206 values[m] = ScaleToCommonStep(metrics, commonStep) 207 } 208 209 return values 210 } 211 212 // GetBuckets returns amount buckets for timeSeries (defined with startTime, stopTime and step (bucket) size. 213 func GetBuckets(start, stop, bucketSize int64) int64 { 214 return int64(math.Ceil(float64(stop-start) / float64(bucketSize))) 215 } 216 217 // AlignStartToInterval aligns start of serie to interval 218 func AlignStartToInterval(start, stop, bucketSize int64) int64 { 219 for _, v := range []int64{86400, 3600, 60} { 220 if bucketSize >= v { 221 start -= start % v 222 break 223 } 224 } 225 226 return start 227 } 228 229 // AlignToBucketSize aligns start and stop of serie to specified bucket (step) size 230 func AlignToBucketSize(start, stop, bucketSize int64) (int64, int64) { 231 start = time.Unix(start, 0).Truncate(time.Duration(bucketSize) * time.Second).Unix() 232 newStop := time.Unix(stop, 0).Truncate(time.Duration(bucketSize) * time.Second).Unix() 233 234 // check if a partial bucket is needed 235 if stop != newStop { 236 newStop += bucketSize 237 } 238 239 return start, newStop 240 } 241 242 // AlignSeries aligns different series together. By default it only prepends and appends NaNs in case of different length, but if ExtrapolatePoints is enabled, it can extrapolate 243 func AlignSeries(args []*types.MetricData) []*types.MetricData { 244 minStart, maxStop := GetInterval(args) 245 246 if ExtrapolatePoints { 247 minStepTime, _, needScale := GetStepRange(args) 248 if needScale { 249 for _, arg := range args { 250 if arg.StepTime > minStepTime { 251 valsCnt := int(math.Ceil(float64(arg.StopTime-arg.StartTime) / float64(minStepTime))) 252 newVals := make([]float64, valsCnt) 253 ts := arg.StartTime 254 nextTs := arg.StartTime + arg.StepTime 255 i := 0 256 j := 0 257 pointsPerInterval := float64(ts-nextTs) / float64(minStepTime) 258 v := arg.Values[0] 259 dv := (arg.Values[0] - arg.Values[1]) / pointsPerInterval 260 for ts < arg.StopTime { 261 newVals[i] = v 262 v += dv 263 if ts > nextTs { 264 j++ 265 nextTs += arg.StepTime 266 v = arg.Values[j] 267 dv = (arg.Values[j-1] - v) / pointsPerInterval 268 } 269 ts += minStepTime 270 i++ 271 } 272 arg.Values = newVals 273 arg.StepTime = minStepTime 274 } 275 } 276 } 277 } 278 279 for _, arg := range args { 280 if minStart < arg.StartTime { 281 valCnt := (arg.StartTime - minStart) / arg.StepTime 282 newVals := genNaNs(int(valCnt)) 283 arg.Values = append(newVals, arg.Values...) 284 } 285 286 arg.StartTime = minStart 287 288 if maxStop > arg.StopTime { 289 valCnt := (maxStop - arg.StopTime) / arg.StepTime 290 newVals := genNaNs(int(valCnt)) 291 arg.Values = append(arg.Values, newVals...) 292 arg.StopTime = maxStop 293 } 294 295 arg.RecalcStopTime() 296 } 297 298 return args 299 } 300 301 // ScaleSeries aligns and scale different series together. By default it only prepends and appends NaNs in case of different length, but if ExtrapolatePoints is enabled, it can extrapolate 302 func ScaleSeries(args []*types.MetricData) []*types.MetricData { 303 minStart, maxStop := GetInterval(args) 304 var commonStep int64 305 var needScale bool 306 307 if ExtrapolatePoints { 308 commonStep, _, needScale = GetStepRange(args) 309 if needScale { 310 for _, arg := range args { 311 if arg.StepTime > commonStep { 312 valsCnt := int(math.Ceil(float64(arg.StopTime-arg.StartTime) / float64(commonStep))) 313 newVals := make([]float64, valsCnt) 314 ts := arg.StartTime 315 nextTs := arg.StartTime + arg.StepTime 316 i := 0 317 j := 0 318 pointsPerInterval := float64(ts-nextTs) / float64(commonStep) 319 v := arg.Values[0] 320 dv := (arg.Values[0] - arg.Values[1]) / pointsPerInterval 321 for ts < arg.StopTime { 322 newVals[i] = v 323 v += dv 324 if ts > nextTs { 325 j++ 326 nextTs += arg.StepTime 327 v = arg.Values[j] 328 dv = (arg.Values[j-1] - v) / pointsPerInterval 329 } 330 ts += commonStep 331 i++ 332 } 333 arg.Values = newVals 334 arg.StepTime = commonStep 335 } 336 } 337 needScale = false 338 } 339 } else { 340 commonStep, needScale = GetCommonStep(args) 341 } 342 343 if needScale { 344 ScaleToCommonStep(args, commonStep) 345 } else { 346 maxVals := 0 347 348 for _, arg := range args { 349 if minStart < arg.StartTime { 350 valCnt := (arg.StartTime - minStart) / arg.StepTime 351 newVals := genNaNs(int(valCnt)) 352 arg.Values = append(newVals, arg.Values...) 353 arg.StartTime = minStart 354 } 355 356 if maxStop > arg.StopTime { 357 valCnt := (maxStop - arg.StopTime) / arg.StepTime 358 newVals := genNaNs(int(valCnt)) 359 arg.Values = append(arg.Values, newVals...) 360 arg.StopTime = maxStop 361 } 362 363 if maxVals < len(arg.Values) { 364 maxVals = len(arg.Values) 365 } 366 } 367 368 for _, arg := range args { 369 if maxVals > len(arg.Values) { 370 valCnt := maxVals - len(arg.Values) 371 newVals := genNaNs(valCnt) 372 arg.Values = append(arg.Values, newVals...) 373 } 374 arg.RecalcStopTime() 375 } 376 377 } 378 379 return args 380 } 381 382 func ConsolidateSeriesByStep(numerator, denominator *types.MetricData) (*types.MetricData, *types.MetricData) { 383 pair := []*types.MetricData{numerator, denominator} 384 385 step, changed := GetCommonStep(pair) 386 if changed || len(numerator.Values) != len(denominator.Values) { 387 alignedSeries := ScaleToCommonStep(types.CopyMetricDataSlice(pair), step) 388 numerator = alignedSeries[0] 389 denominator = alignedSeries[1] 390 391 return alignedSeries[0], alignedSeries[1] 392 } 393 394 return numerator, denominator 395 } 396 397 func genNaNs(length int) []float64 { 398 nans := make([]float64, length) 399 for i := range nans { 400 nans[i] = math.NaN() 401 } 402 return nans 403 } 404 405 func Divmod(numerator, denominator int64) (quotient, remainder int64) { 406 quotient = numerator / denominator // integer division, decimals are truncated 407 remainder = numerator % denominator 408 return 409 }