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  }