Skip to content

Commit

Permalink
Speicalized Stateful Expression
Browse files Browse the repository at this point in the history
Creating specialized stateful expression in order to improve performance
and code clarity.
Speicalized stateful expression eliminates all heap allocations and most
of interface{} conversions.

The idea behind "specialized stateful expression" is to implement (very
very simple) "JIT
Compiler" for stateful expression.

For example: given this expression ``"value" > 8.0``, at runtime we
will try to find the type of "value", once we find the type (for this
example, let's say it's float64) we will comparison functions which
accepts float64 both on the right and left side.

Contiuing with our example, let's say that on runtime after evaluating
multiple times - "value" changed is type to int64, if that happens - we
will have "type guard" that will raise an error and say this is not
"float64 > float64" it's "int64 > float64" and we will adjust the
evaluation function.

There are more changes, those are the bottom line:
* Tests - lots of tests added, at the time of writing this commit we have
77.8% coverage for "stateful" package.
* Compile time errors - simple compile time errors that will stop the
tasks starting if there is some simple mistakes.

Finally, benchmarks:
* Compared against the interepeted one - "NewStatefulExpr"
* Ran with count=5 on Macbook Pro 13" Late 2011 (i5, 8GB RAM, 120GB SSD)
name
me
old time/op    new time/op    delta
_EvalBool_OneOperator_UnaryNode_BoolNode-4
252ns ± 2%      16ns ± 3%   -93.53%  (p=0.008 n=5+5)
_EvalBool_OneOperator_NumberFloat64_NumberFloat64-4
540ns ± 2%      40ns ± 2%   -92.58%  (p=0.008 n=5+5)
_EvalBool_OneOperator_NumberFloat64_NumberInt64-4
550ns ± 3%      40ns ± 3%   -92.68%  (p=0.008 n=5+5)
_EvalBool_OneOperator_NumberInt64_NumberInt64-4
539ns ± 2%      39ns ± 4%   -92.77%  (p=0.008 n=5+5)
_EvalBool_OneOperator_ReferenceNodeFloat64_NumberFloat64-4
524ns ± 3%      74ns ± 5%   -85.93%  (p=0.008 n=5+5)
_EvalBool_OneOperator_ReferenceNodeFloat64_NumberInt64-4
526ns ± 1%      76ns ± 3%   -85.51%  (p=0.008 n=5+5)
_EvalBool_OneOperator_ReferenceNodeFloat64_ReferenceNodeFloat64-4
495ns ± 3%     116ns ± 4%   -76.48%  (p=0.008 n=5+5)
_EvalBool_OneOperatorWith11ScopeItem_ReferenceNodeFloat64_NumberFloat64-4
534ns ± 3%      90ns ± 4%   -83.11%  (p=0.008 n=5+5)
_EvalBool_OneOperatorValueChanges_ReferenceNodeFloat64_NumberFloat64-4
2.98µs ± 1%    1.24µs ± 3%   -58.44%  (p=0.008 n=5+5)
_EvalBool_OneOperator_ReferenceNodeInt64_ReferenceNodeInt64-4
503ns ± 3%     119ns ± 7%   -76.25%  (p=0.008 n=5+5)
_EvalBool_OneOperatorWith11ScopeItem_ReferenceNodeInt64_NumberInt64-4
533ns ± 1%      87ns ± 3%   -83.69%  (p=0.008 n=5+5)
_EvalBool_OneOperatorValueChanges_ReferenceNodeInt64_NumberInt64-4
3.08µs ± 4%    1.26µs ± 3%   -59.22%  (p=0.008 n=5+5)

name
old alloc/op   new alloc/op   delta
_EvalBool_OneOperator_UnaryNode_BoolNode-4
18.0B ± 0%     0.0B ±NaN%  -100.00%  (p=0.008 n=5+5)
_EvalBool_OneOperator_NumberFloat64_NumberFloat64-4
72.0B ± 0%     0.0B ±NaN%  -100.00%  (p=0.008 n=5+5)
_EvalBool_OneOperator_NumberFloat64_NumberInt64-4
72.0B ± 0%     0.0B ±NaN%  -100.00%  (p=0.008 n=5+5)
_EvalBool_OneOperator_NumberInt64_NumberInt64-4
72.0B ± 0%     0.0B ±NaN%  -100.00%  (p=0.008 n=5+5)
_EvalBool_OneOperator_ReferenceNodeFloat64_NumberFloat64-4
64.0B ± 0%     0.0B ±NaN%  -100.00%  (p=0.008 n=5+5)
_EvalBool_OneOperator_ReferenceNodeFloat64_NumberInt64-4
64.0B ± 0%     0.0B ±NaN%  -100.00%  (p=0.008 n=5+5)
_EvalBool_OneOperator_ReferenceNodeFloat64_ReferenceNodeFloat64-4
49.0B ± 0%     0.0B ±NaN%  -100.00%  (p=0.008 n=5+5)
_EvalBool_OneOperatorWith11ScopeItem_ReferenceNodeFloat64_NumberFloat64-4
64.0B ± 0%     0.0B ±NaN%  -100.00%  (p=0.008 n=5+5)
_EvalBool_OneOperatorValueChanges_ReferenceNodeFloat64_NumberFloat64-4
64.0B ± 0%     0.0B ±NaN%  -100.00%  (p=0.008 n=5+5)
_EvalBool_OneOperator_ReferenceNodeInt64_ReferenceNodeInt64-4
49.0B ± 0%     0.0B ±NaN%  -100.00%  (p=0.008 n=5+5)
_EvalBool_OneOperatorWith11ScopeItem_ReferenceNodeInt64_NumberInt64-4
64.0B ± 0%     0.0B ±NaN%  -100.00%  (p=0.008 n=5+5)
_EvalBool_OneOperatorValueChanges_ReferenceNodeInt64_NumberInt64-4
64.0B ± 0%     0.0B ±NaN%  -100.00%  (p=0.008 n=5+5)

name
old allocs/op  new allocs/op  delta
_EvalBool_OneOperator_UnaryNode_BoolNode-4
3.00 ± 0%     0.00 ±NaN%  -100.00%  (p=0.008 n=5+5)
_EvalBool_OneOperator_NumberFloat64_NumberFloat64-4
5.00 ± 0%     0.00 ±NaN%  -100.00%  (p=0.008 n=5+5)
_EvalBool_OneOperator_NumberFloat64_NumberInt64-4
5.00 ± 0%     0.00 ±NaN%  -100.00%  (p=0.008 n=5+5)
_EvalBool_OneOperator_NumberInt64_NumberInt64-4
5.00 ± 0%     0.00 ±NaN%  -100.00%  (p=0.008 n=5+5)
_EvalBool_OneOperator_ReferenceNodeFloat64_NumberFloat64-4
4.00 ± 0%     0.00 ±NaN%  -100.00%  (p=0.008 n=5+5)
_EvalBool_OneOperator_ReferenceNodeFloat64_NumberInt64-4
4.00 ± 0%     0.00 ±NaN%  -100.00%  (p=0.008 n=5+5)
_EvalBool_OneOperator_ReferenceNodeFloat64_ReferenceNodeFloat64-4
3.00 ± 0%     0.00 ±NaN%  -100.00%  (p=0.008 n=5+5)
_EvalBool_OneOperatorWith11ScopeItem_ReferenceNodeFloat64_NumberFloat64-4
4.00 ± 0%     0.00 ±NaN%  -100.00%  (p=0.008 n=5+5)
_EvalBool_OneOperatorValueChanges_ReferenceNodeFloat64_NumberFloat64-4
4.00 ± 0%     0.00 ±NaN%  -100.00%  (p=0.008 n=5+5)
_EvalBool_OneOperator_ReferenceNodeInt64_ReferenceNodeInt64-4
3.00 ± 0%     0.00 ±NaN%  -100.00%  (p=0.008 n=5+5)
_EvalBool_OneOperatorWith11ScopeItem_ReferenceNodeInt64_NumberInt64-4
4.00 ± 0%     0.00 ±NaN%  -100.00%  (p=0.008 n=5+5)
_EvalBool_OneOperatorValueChanges_ReferenceNodeInt64_NumberInt64-4
4.00 ± 0%     0.00 ±NaN%  -100.00%  (p=0.008 n=5+5)
  • Loading branch information
yosiat committed Apr 29, 2016
1 parent f1a188b commit 107aa66
Show file tree
Hide file tree
Showing 29 changed files with 3,438 additions and 547 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ For example, let's say we want to store all data that triggered an alert in Infl
- [#388](https://github.com/influxdata/kapacitor/issues/388): The duration of an alert is now tracked and exposed as part of the alert data as well as can be set as a field via `.durationField('duration')`.
- [#486](https://github.com/influxdata/kapacitor/pull/486): Default config file location.
- [#461](https://github.com/influxdata/kapacitor/pull/461): Make Alerta `event` property configurable.
- [#491](https://github.com/influxdata/kapacitor/pull/491): BREAKING: Rewriting stateful expression in order to improve performance, the only breaking change is: short circuit evaluation for booleans - for example: ``lambda: "bool_value" && (count() > 100)`` if "bool_value" is false, we won't evaluate "count".

### Bugfixes

Expand Down
28 changes: 22 additions & 6 deletions alert.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (
imodels "github.com/influxdata/influxdb/models"
"github.com/influxdata/kapacitor/models"
"github.com/influxdata/kapacitor/pipeline"
"github.com/influxdata/kapacitor/tick"
"github.com/influxdata/kapacitor/tick/stateful"
)

const (
Expand Down Expand Up @@ -97,7 +97,7 @@ type AlertNode struct {
a *pipeline.AlertNode
endpoint string
handlers []AlertHandler
levels []*tick.StatefulExpr
levels []stateful.Expression
states map[models.GroupID]*alertState
idTmpl *text.Template
messageTmpl *text.Template
Expand Down Expand Up @@ -273,16 +273,32 @@ func newAlertNode(et *ExecutingTask, n *pipeline.AlertNode, l *log.Logger) (an *
}

// Parse level expressions
an.levels = make([]*tick.StatefulExpr, CritAlert+1)
an.levels = make([]stateful.Expression, CritAlert+1)

if n.Info != nil {
an.levels[InfoAlert] = tick.NewStatefulExpr(n.Info)
statefulExpression, expressionCompileError := stateful.NewExpression(n.Info)
if expressionCompileError != nil {
return nil, fmt.Errorf("Failed to compile stateful expression for info: %s", expressionCompileError)
}
an.levels[InfoAlert] = statefulExpression
}

if n.Warn != nil {
an.levels[WarnAlert] = tick.NewStatefulExpr(n.Warn)
statefulExpression, expressionCompileError := stateful.NewExpression(n.Warn)
if expressionCompileError != nil {
return nil, fmt.Errorf("Failed to compile stateful expression for warn: %s", expressionCompileError)
}
an.levels[WarnAlert] = statefulExpression
}

if n.Crit != nil {
an.levels[CritAlert] = tick.NewStatefulExpr(n.Crit)
statefulExpression, expressionCompileError := stateful.NewExpression(n.Crit)
if expressionCompileError != nil {
return nil, fmt.Errorf("Failed to compile stateful expression for crit: %s", expressionCompileError)
}
an.levels[CritAlert] = statefulExpression
}

// Setup states
if n.History < 2 {
n.History = 2
Expand Down
14 changes: 10 additions & 4 deletions eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ package kapacitor

import (
"errors"
"fmt"
"log"
"time"

"github.com/influxdata/kapacitor/expvar"
"github.com/influxdata/kapacitor/models"
"github.com/influxdata/kapacitor/pipeline"
"github.com/influxdata/kapacitor/tick"
"github.com/influxdata/kapacitor/tick/stateful"
)

const (
Expand All @@ -18,7 +19,7 @@ const (
type EvalNode struct {
node
e *pipeline.EvalNode
expressions []*tick.StatefulExpr
expressions []stateful.Expression
evalErrors *expvar.Int
}

Expand All @@ -32,9 +33,14 @@ func newEvalNode(et *ExecutingTask, n *pipeline.EvalNode, l *log.Logger) (*EvalN
e: n,
}
// Create stateful expressions
en.expressions = make([]*tick.StatefulExpr, len(n.Expressions))
en.expressions = make([]stateful.Expression, len(n.Expressions))
for i, expr := range n.Expressions {
en.expressions[i] = tick.NewStatefulExpr(expr)
statefulExpr, err := stateful.NewExpression(expr)
if err != nil {
return nil, fmt.Errorf("Failed to compile %v expression: %v", i, err)
}

en.expressions[i] = statefulExpr
}

en.node.runF = en.runEval
Expand Down
5 changes: 3 additions & 2 deletions expr.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import (

"github.com/influxdata/kapacitor/models"
"github.com/influxdata/kapacitor/tick"
"github.com/influxdata/kapacitor/tick/stateful"
)

// Evaluate a given expression as a boolean predicate against a set of fields and tags
func EvalPredicate(se *tick.StatefulExpr, now time.Time, fields models.Fields, tags models.Tags) (bool, error) {
// EvalPredicate - Evaluate a given expression as a boolean predicate against a set of fields and tags
func EvalPredicate(se stateful.Expression, now time.Time, fields models.Fields, tags models.Tags) (bool, error) {
vars, err := mergeFieldsAndTags(now, fields, tags)
if err != nil {
return false, err
Expand Down
12 changes: 9 additions & 3 deletions stream.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package kapacitor

import (
"fmt"
"log"

"github.com/influxdata/kapacitor/models"
"github.com/influxdata/kapacitor/pipeline"
"github.com/influxdata/kapacitor/tick"
"github.com/influxdata/kapacitor/tick/stateful"
)

type StreamNode struct {
Expand Down Expand Up @@ -38,7 +39,7 @@ func (s *StreamNode) runSourceStream([]byte) error {
type FromNode struct {
node
s *pipeline.FromNode
expression *tick.StatefulExpr
expression stateful.Expression
dimensions []string
allDimensions bool
db string
Expand All @@ -59,7 +60,12 @@ func newFromNode(et *ExecutingTask, n *pipeline.FromNode, l *log.Logger) (*FromN
sn.allDimensions, sn.dimensions = determineDimensions(n.Dimensions)

if n.Expression != nil {
sn.expression = tick.NewStatefulExpr(n.Expression)
expr, err := stateful.NewExpression(n.Expression)
if err != nil {
return nil, fmt.Errorf("Failed to compile from expression: %v", err)
}

sn.expression = expr
}

return sn, nil
Expand Down
8 changes: 4 additions & 4 deletions tick/lex.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,24 +161,24 @@ func (t TokenType) String() string {
return "TRUE"
case t == TokenFalse:
return "FALSE"
case isExprOperator(t):
case IsExprOperator(t):
return operatorStr[t]
}
return fmt.Sprintf("%d", t)
}

// True if token type is an operator used in mathematical or boolean expressions.
func isExprOperator(typ TokenType) bool {
func IsExprOperator(typ TokenType) bool {
return typ > begin_tok_operator && typ < end_tok_operator
}

// True if token type is an operator used in mathematical expressions.
func isMathOperator(typ TokenType) bool {
func IsMathOperator(typ TokenType) bool {
return typ > begin_tok_operator_math && typ < end_tok_operator_math
}

// True if token type is an operator used in comparisons.
func isCompOperator(typ TokenType) bool {
func IsCompOperator(typ TokenType) bool {
return typ > begin_tok_operator_comp && typ < end_tok_operator_comp
}

Expand Down
6 changes: 3 additions & 3 deletions tick/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -342,12 +342,12 @@ var precedence = [...]int{
// https://en.wikipedia.org/wiki/Operator-precedence_parser#Pseudo-code
func (p *parser) precedence(lhs Node, minP int) Node {
look := p.peek()
for isExprOperator(look.typ) && precedence[look.typ] >= minP {
for IsExprOperator(look.typ) && precedence[look.typ] >= minP {
op := p.next()
rhs := p.primary(nil)
look = p.peek()
// left-associative
for isExprOperator(look.typ) && precedence[look.typ] > precedence[op.typ] {
for IsExprOperator(look.typ) && precedence[look.typ] > precedence[op.typ] {
rhs = p.precedence(rhs, precedence[look.typ])
look = p.peek()
}
Expand Down Expand Up @@ -389,7 +389,7 @@ func (p *parser) lparameters() (args []Node) {

func (p *parser) lparameter() (n Node) {
n = p.primary(nil)
if isExprOperator(p.peek().typ) {
if IsExprOperator(p.peek().typ) {
n = p.precedence(n, 0)
}
return
Expand Down
Loading

0 comments on commit 107aa66

Please sign in to comment.