-
Notifications
You must be signed in to change notification settings - Fork 454
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[query] Add quantile_over_time #1367
Conversation
Codecov Report
@@ Coverage Diff @@
## master #1367 +/- ##
========================================
- Coverage 70.6% 63% -7.6%
========================================
Files 823 817 -6
Lines 70369 70004 -365
========================================
- Hits 49692 44169 -5523
- Misses 17455 22797 +5342
+ Partials 3222 3038 -184
Continue to review full report at Codecov.
|
Codecov Report
@@ Coverage Diff @@
## master #1367 +/- ##
========================================
+ Coverage 70.6% 70.6% +<.1%
========================================
Files 823 823
Lines 70369 70421 +52
========================================
+ Hits 49692 49746 +54
+ Misses 17455 17448 -7
- Partials 3222 3227 +5
Continue to review full report at Codecov.
|
@@ -85,28 +92,42 @@ func NewAggOp(args []interface{}, optype string) (transform.Params, error) { | |||
aggFunc: aggregationFunc, | |||
} | |||
|
|||
if optype == QuantileType { | |||
if len(args) != 2 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is pretty messy; do a similar approach to what's there for holt winters instead
src/query/parser/promql/types.go
Outdated
@@ -204,7 +204,7 @@ func NewFunctionExpr( | |||
|
|||
case temporal.AvgType, temporal.CountType, temporal.MinType, | |||
temporal.MaxType, temporal.SumType, temporal.StdDevType, | |||
temporal.StdVarType: | |||
temporal.StdVarType, temporal.QuantileType: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add to parse test
op baseOp | ||
controller *transform.Controller | ||
aggFunc func([]float64, float64) float64 | ||
quantileScalar float64 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we have to add this quantileScalar at the generic agg func layer? Doesn't seem scalable given how many different types of agg funcs we can have. Can we just do the args checking and parsing once we get to the functions that need them?
} | ||
|
||
func avgOverTime(values []float64) float64 { | ||
func avgOverTime(values []float64, _ float64) float64 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not great that we modify all the func signatures for a var that isn't used in most of these functions. If it needs to be this way, can we wrap it in an options or something at least so the function signature doesn't have to ever change again? (But only if you have to pass this all the way down, see comment above).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or consider putting this in some sort of context object that you propagate instead of a specific options for agg functions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1. Would it make sense to pass in scalar
as a constructor argument to the quantile aggFunc
? That way we separate out the "configuration" of the aggFunc
from it's actual execution. A note on how that could work:
- We can switch
aggFunc
to be an interface (functions can implement interfaces, so this is actually less work than it seems), to allow forquantile
to hold configuration (scalar
) - Pass
scalar
toquantile
when we initialize the node instead (threading throughNewAggOp
andtemporal.baseOp.Node
)
@@ -192,3 +213,60 @@ func sumAndCount(values []float64) (float64, float64) { | |||
|
|||
return sum, count | |||
} | |||
|
|||
func quantileOverTime(values []float64, scalar float64) float64 { | |||
valuesSlice := make(valsSlice, 0, len(values)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a comment about why copying the array is necessary?
return func(values []float64) float64 { | ||
valuesSlice := make(valsSlice, 0, len(values)) | ||
for _, v := range values { | ||
valuesSlice = append(values, v) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is this loop trying to achieve? This seems to be overwriting valuesSlice on each iteration with the whole of values + the current value from values. You end up with just the values slice + the very last value in values in valuesSlice. Is that what you want?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1--looks like it should be valuesSlice
? You could probably also use copy
for this (assuming that's the goal)
src/query/functions/temporal/base.go
Outdated
if operatorType != QuantileType { | ||
durArg = args[0] | ||
} else { | ||
durArg = args[1] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know you check outside of this function, but is it worth checking len args in this func just in case? There's some check above for len(args) != 1, but does that cover everything?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Eh I think it's fine (we do the same thing in the Holt-Winters function) + newBaseOp() is a private function
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just expose a new function NewQuantileOp
? Spreading this kind of casing across multiple functions confuses the code imo.
@@ -78,6 +82,30 @@ func (a aggProcessor) Init(op baseOp, controller *transform.Controller, opts tra | |||
} | |||
} | |||
|
|||
// NewQuantileOp create a new base temporal transform for quantile_over_time func |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: .
at end of comment
@@ -78,6 +82,30 @@ func (a aggProcessor) Init(op baseOp, controller *transform.Controller, opts tra | |||
} | |||
} | |||
|
|||
// NewQuantileOp create a new base temporal transform for quantile_over_time func | |||
func NewQuantileOp(args []interface{}, optype string) (transform.Params, error) { | |||
if optype == QuantileType { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Flip the conditional to reduce nesting
|
||
scalar, ok := args[0].(float64) | ||
if !ok { | ||
return emptyOp, fmt.Errorf("unable to cast to scalar argument: %v for %s", args[1], QuantileType) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This prints args[1] but you're checking args[0]; do we ever use args[1]? Does it need two args?
if len(values) == 0 { | ||
return math.NaN() | ||
} | ||
if q < 0 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: newlines
// If 'values' has zero elements, NaN is returned. | ||
// If q<0, -Inf is returned. | ||
// If q>1, +Inf is returned. | ||
func quantile(q float64, values valsSlice) float64 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: keep same name for the q
variable throughout rather than having it be scalar
when you parse it
return emptyOp, fmt.Errorf("unable to cast to scalar argument: %v for %s", args[1], QuantileType) | ||
} | ||
|
||
aggregationFunc := makeQuantileOverTimeFn(scalar) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can add an optimization here is scalar < 0 || scalar > 1
by instead having a function that looks like:
func makeInvalidQuantileFn(inf float64) aggFunc {
return func(_ []float64) float64 {
return inf
}
}
And passing the correctly signed infinity in
src/query/functions/temporal/base.go
Outdated
@@ -49,15 +49,27 @@ type baseOp struct { | |||
// skipping lint check for a single operator type since we will be adding more | |||
// nolint : unparam | |||
func newBaseOp(args []interface{}, operatorType string, processorFn MakeProcessor) (baseOp, error) { | |||
if operatorType != HoltWintersType && operatorType != PredictLinearType { | |||
if operatorType != HoltWintersType && operatorType != PredictLinearType && operatorType != QuantileType { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seconding +arnikola's earlier comment here; why not handle this in the same way that we do HoltWinters
and PredictLinear
, and handle this casing in NewFunctionExpr
? Handling the argument extraction in 2 places is unnecessarily confusing imo.
name: "quantile_over_time", | ||
opType: QuantileType, | ||
afterBlockOne: [][]float64{ | ||
{math.NaN(), math.NaN(), math.NaN(), math.NaN(), math.NaN()}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we have some tests that don't include NaN
's? Might be a useful simple case to cover.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah we should add tests for all of the temporal functions with some NaNs - ill do this in a separate PR.
@@ -78,6 +82,30 @@ func (a aggProcessor) Init(op baseOp, controller *transform.Controller, opts tra | |||
} | |||
} | |||
|
|||
// NewQuantileOp create a new base temporal transform for quantile_over_time func | |||
func NewQuantileOp(args []interface{}, optype string) (transform.Params, error) { | |||
if optype == QuantileType { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this is creating a QuantileOp
, why does it need optype
as input? Anything other than QuantileType
is an error, so it doesn't seem worth having the argument (fwiw, HoltWinters
doesn't take one).
@@ -192,3 +220,62 @@ func sumAndCount(values []float64) (float64, float64) { | |||
|
|||
return sum, count | |||
} | |||
|
|||
func makeQuantileOverTimeFn(scalar float64) aggFunc { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for this refactor, much cleaner this way.
if q > 1 { | ||
return math.Inf(+1) | ||
} | ||
sort.Sort(values) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There can be a problem with NaNs actually; we don't want them to pollute the output since they don't exist in Prom's implementation. Instead of just doing a sort
here, it would be better to put the through a sanitize function that will remove NaNs from the values, then sort.
On the bright side, this means we can use the inbuilt int slice sorter, and not have to worry about the valsSlice
type
714c745
to
bad88d2
Compare
src/query/functions/temporal/base.go
Outdated
if operatorType != QuantileType { | ||
durArg = args[0] | ||
} else { | ||
durArg = args[1] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just expose a new function NewQuantileOp
? Spreading this kind of casing across multiple functions confuses the code imo.
|
||
return newBaseOp(duration, QuantileType, a) | ||
} | ||
|
||
// NewAggOp creates a new base temporal transform with a specified node. | ||
func NewAggOp(args []interface{}, optype string) (transform.Params, error) { | ||
if aggregationFunc, ok := aggFuncs[optype]; ok { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: flip the conditional, since the if
is a lot larger now.
return emptyOp, fmt.Errorf("unable to cast to scalar argument: %v for %s", args[0], operatorType) | ||
} | ||
|
||
func newBaseOp(duration time.Duration, operatorType string, processorFn MakeProcessor) (baseOp, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
|
||
duration, ok := args[1].(time.Duration) | ||
if !ok { | ||
return emptyOp, fmt.Errorf("unable to cast to scalar argument: %v for %s", args[1], QuantileType) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we push all of these parsing functions up into promql/types.go
, similar to how this does it?
A small gotcha is that any numeric expression is valid in Prom, i.e. quantile_over_time( (1*2)/3, up[5m] )
is a valid query but would fail here and in many of the other temporal functions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really don't wanna change this anymore. Also this query works in ours:
quantile_over_time((1*2)/3, service_writeTaggedBatchRaw_latency{instance=~"$instance",quantile="0.99"}[1m])
No description provided.