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  }