Skip to content
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

Add scalar block and binary functions #822

Merged
merged 27 commits into from
Aug 16, 2018
Merged

Add scalar block and binary functions #822

merged 27 commits into from
Aug 16, 2018

Conversation

arnikola
Copy link
Collaborator

@arnikola arnikola commented Aug 4, 2018

Added scalar block type for parsing prom constant values

Arithmetic functions added (+, -, *, /, ^, %) for scalar-scalar,
scalar-series, series-scalar, series-series left and right node values

Comparison functions added (==, !=, >, <, >=, <=) for scalar-scalar,
scalar-series, series-scalar, series-series left and right node values �

Added unit tests for all permutations of comparison and arithmetic functions

Still TODO: consolidate binary node with logical node, as a lot of behaviour
is shared.

@@ -0,0 +1,150 @@
// Copyright (c) 2018 Uber Technologies, Inc.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

call it scalar.go since its already in block package?

}, nil
}

func (b *scalarBlock) SeriesIter() (SeriesIter, error) { return nil, nil }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dont you want to return scalarSeriesIter?

// StepIter returns a StepIterator
func (b *scalarBlock) StepIter() (StepIter, error) {
bounds := b.meta.Bounds
steps := bounds.End.Sub(bounds.Start) / bounds.StepSize
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you define Steps() on Bounds struct itself ? A multiple places in code will need it

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turns out Bounds.Steps() exists already :) using that instead

@@ -0,0 +1,232 @@
// Copyright (c) 2018 Uber Technologies, Inc.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

node.go?

@arnikola arnikola changed the base branch from arnikola/orOp to master August 14, 2018 19:45
@codecov
Copy link

codecov bot commented Aug 14, 2018

Codecov Report

Merging #822 into master will decrease coverage by 0.09%.
The diff coverage is 77.41%.

Impacted file tree graph

@@            Coverage Diff            @@
##           master     #822     +/-   ##
=========================================
- Coverage   78.42%   78.33%   -0.1%     
=========================================
  Files         375      379      +4     
  Lines       32247    32595    +348     
=========================================
+ Hits        25289    25532    +243     
- Misses       5239     5319     +80     
- Partials     1719     1744     +25
Flag Coverage Δ
#dbnode 81.34% <ø> (-0.12%) ⬇️
#m3ninx 71.99% <ø> (ø) ⬆️
#query 66.34% <77.41%> (+0.86%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 1eb2dd7...d3640fe. Read the comment docs.


// ScalarBlock is a block containing a single value
// over a certain bound
type ScalarBlock struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

call it Scalar since its already in block package. Also do you really need to expose it outside ? Seems like you outside functions should just work on the Block interface

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cast to Scalar block sometimes as an optimization to pull out the value for the entire block, rather than having to step through each value one by one. Will rename


// NewScalarBlockResult creates a Result consisting
// of scalar blocks over the bound
func NewScalarBlockResult(val float64, bounds Bounds) *Result {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why does result need to know about scalar block ?

Copy link
Collaborator Author

@arnikola arnikola Aug 15, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was mirroring how fetch performed executions in NewScalarOp; changed it a bit to only build a single block and use that instead.

"github.com/m3db/m3/src/query/models"
)

// ScalarBlock is a block containing a single value
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add a comment as to when this would be useful ?

}

func (it *scalarStepIter) Current() (Step, error) {
if it.idx >= it.numVals || it.idx < 0 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we haven't traditionally been checking this in Current() usually rely on people to call Next() before Current

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree in general, but since this doesn't do any real "bounds" checking and just returns a value, the alternative is that this panics in this check, as it really shouldn't be returning anything if Next() is false.

I prefer to error than to panic here, but if you have particular preference towards panicing I can change to that

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

other places have panics instead of errors since its a way to avoid more codepaths :( I guess we can do either here since its returning error but a lot of times Current() only returns data and panic is the only option.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, if it weren't already able to return an error I would've paniced, but figured might as well make use of the capability here so we don't kill the node :)

}

func (it *scalarSeriesIter) Current() (Series, error) {
if it.idx != 0 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we haven't traditionally been checking this in Current() usually rely on people to call Next() before Current

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree in general, but since this doesn't do any real "bounds" checking and just returns a value, the alternative is that this panics in this check, as it really shouldn't be returning anything if Next() is false.

I prefer to error than to panic here, but if you have particular preference towards panicing I can change to that

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same reply as above

}

// computeOrCache figures out if both lhs and rhs are available, if not then it caches the incoming block
func (n *binaryNode) computeOrCache(ID parser.NodeID, b block.Block) (block.Block, block.Block, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this the same as the other place ? If yes, then think about refactor

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this falls under:
TODO: consolidate binary node with logical node, as a lot of behaviour
is shared.

Might do that in a separate commit once this is in. No other binary functions are planned in the near future so shouldn't affect any upcoming commits

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cool, was just checking


// intersect returns the slice of lhs indices that are shared with rhs,
// the indices of the corresponding rhs values, and the metas for taken indices
func intersect(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this the same as the other place ? If yes, then think about refactor

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's slightly different in that this one is designed to be able to drop series which do not match, consistent with prom behaviour here.


// If returnBool is false and op is a comparison function,
// the function should return the scalar value rather than 1 or 0
func actualScalarFunc(fn singleScalarFunc, op string, returnBool bool) singleScalarFunc {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

naming :(

// If returnBool is false and op is a comparison function,
// the function should return the scalar value of lhs if the comparison
// is true; otherwise, it should return 1 if true, and 0 if false
func actualFunc(fn mathFunc, op string, returnBool bool) mathFunc {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

naming :(

}

if info.RIsScalar {
scalarR, ok := rhs.(*block.ScalarBlock)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some duplication here:

maybe create a function which takes two block.Block, assumes first can be casted into scalar and other can't be. Then you can use the same function for this case and one of the previous cases

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure that would reduce duplication much, and may make it a bit more confusing since I'd need to use some kind of scalarLeft bool; it also introduces a couple of extra bool checks.

All I'd be saving by using a function for this would be the cast to *block.ScalarBlock, since the singleScalarFunction would be different for either branch

)

// Scalar is a block containing a single value
// over a certain bound
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: add this to end of first line

type scalarStepIter struct {
meta Metadata
step scalarStep
numVals int
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: numVals, idx int

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks a bit uglier but ok :p

var _ SeriesIter = (*scalarSeriesIter)(nil)

func (it *scalarSeriesIter) Close() { /* No-op*/ }
func (it *scalarSeriesIter) SeriesCount() int { return 1 }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is kind of weird- wouldn't this always be 1 in all SeriesIters?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a SeriesIter is created from a block with multiple series, SeriesCount would be the number of the series in that block; in the case of a scalar block, there's only one "series" which has val as the value at each step.


var (
start = time.Time{}
val = 13.37
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make this like 100 or something simple?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wanted to make it explicitly a float with decimal places

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

math.Pi is also an option


const (
// *******************************************
// * Arithmetic functions
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's going on with the *s?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just visual delineation

return n.processSingleBlock(
rhs,
func(x float64) float64 {
return n.op.fn(lVal, x)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a comment here?

lIter, rIter block.StepIter,
) (block.Block, error) {
lMeta, rMeta := lIter.Meta(), rIter.Meta()

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check to make sure bounds are equal

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it we're doing the check, should that be in process() ?

return nil, err
}

lValues := lStep.Values()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Get the values right before you use them below instead of here

return nil, err
}

rValues := rStep.Values()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one's already right before it's used

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah lol

@@ -176,6 +179,18 @@ func (p *parseState) walk(node pql.Node) error {
p.transforms = append(p.transforms, opTransform)
return nil

case *pql.NumberLiteral:
op := NewScalarOperator(n)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: remove newline

func TestDAGWithAbsentOp(t *testing.T) {
q := "absent(http_requests_total{method=\"GET\"})"
p, err := Parse(q)
var linearParseTests = []struct {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!!

@@ -178,6 +239,10 @@ func promVectorCardinalityToM3(card promql.VectorMatchCardinality) logical.Vecto
}

func promMatchingToM3(vectorMatching *promql.VectorMatching) *logical.VectorMatching {
// vectorMatching can be nil iff at least one of the sides is a scalar
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: s/iff/if

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}

// assert that Scalar implements Block
var _ Block = (*Scalar)(nil)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you don't need it since NewScalar returns Block

}

// assert that scalarStepIter implements StepIter
var _ StepIter = (*scalarStepIter)(nil)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you don't need it since StepIter() returns & scalarStepIter

}

func (it *scalarStepIter) Current() (Step, error) {
if it.idx >= it.numVals || it.idx < 0 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

other places have panics instead of errors since its a way to avoid more codepaths :( I guess we can do either here since its returning error but a lot of times Current() only returns data and panic is the only option.

}

// assert that scalarStepIter implements StepIter
var _ Step = (*scalarStep)(nil)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't think you need this

}

// assert that scalarStepIter implements StepIter
var _ SeriesIter = (*scalarSeriesIter)(nil)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above

options transform.Options,
) (parser.Source, *transform.Controller) {
controller := &transform.Controller{ID: ID}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extra line ?

@@ -47,6 +47,15 @@ type TimeSpec struct {
Step time.Duration
}

// ToBounds transforms a timespec to bounds
func (ts TimeSpec) ToBounds() block.Bounds {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bounds() ?

lIter, rIter block.StepIter,
) (block.Block, error) {
lMeta, rMeta := lIter.Meta(), rIter.Meta()

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it we're doing the check, should that be in process() ?

}

// computeOrCache figures out if both lhs and rhs are available, if not then it caches the incoming block
func (n *binaryNode) computeOrCache(ID parser.NodeID, b block.Block) (block.Block, block.Block, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cool, was just checking

n.cache.Remove(n.op.params.RNode)
}

const initIndexSliceLength = 10
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense to move it to the top ? We might want to put some of these things in the config later.

@arnikola arnikola merged commit b63563e into master Aug 16, 2018
@arnikola arnikola deleted the arnikola/binOps branch August 16, 2018 22:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants