Reduce allocations and improve perf over 50%

This commit is contained in:
Vikram Rangnekar
2019-06-01 02:03:09 -04:00
parent 58408eadc1
commit 7c704637e8
18 changed files with 1728 additions and 1273 deletions

7
qcode/bench.0 Normal file
View File

@ -0,0 +1,7 @@
goos: darwin
goarch: amd64
pkg: github.com/dosco/super-graph/qcode
BenchmarkQCompile-8 100000 18528 ns/op 9208 B/op 107 allocs/op
BenchmarkQCompileP-8 300000 5952 ns/op 9208 B/op 107 allocs/op
PASS
ok github.com/dosco/super-graph/qcode 3.893s

7
qcode/bench.1 Normal file
View File

@ -0,0 +1,7 @@
goos: darwin
goarch: amd64
pkg: github.com/dosco/super-graph/qcode
BenchmarkQCompile-8 100000 18240 ns/op 7454 B/op 88 allocs/op
BenchmarkQCompileP-8 300000 5788 ns/op 7494 B/op 88 allocs/op
PASS
ok github.com/dosco/super-graph/qcode 3.813s

7
qcode/bench.2 Normal file
View File

@ -0,0 +1,7 @@
goos: darwin
goarch: amd64
pkg: github.com/dosco/super-graph/qcode
BenchmarkQCompile-8 100000 17231 ns/op 3352 B/op 87 allocs/op
BenchmarkQCompileP-8 300000 5023 ns/op 3387 B/op 87 allocs/op
PASS
ok github.com/dosco/super-graph/qcode 3.462s

View File

@ -103,13 +103,20 @@ type stateFn func(*lexer) stateFn
// lexer holds the state of the scanner.
type lexer struct {
name string // the name of the input; used only for error reports
input string // the string being scanned
pos Pos // current position in the input
start Pos // start position of this item
width Pos // width of last rune read from input
items []item // array of scanned items
line int // 1+number of newlines seen
name string // the name of the input; used only for error reports
input string // the string being scanned
pos Pos // current position in the input
start Pos // start position of this item
width Pos // width of last rune read from input
items []item // array of scanned items
itemsA [100]item
line int // 1+number of newlines seen
}
var zeroLex = lexer{}
func (l *lexer) Reset() {
*l = zeroLex
}
// next returns the next rune in the input.
@ -207,21 +214,19 @@ func (l *lexer) errorf(format string, args ...interface{}) stateFn {
}
// lex creates a new scanner for the input string.
func lex(input string) (*lexer, error) {
func lex(l *lexer, input string) error {
if len(input) == 0 {
return nil, errors.New("empty query")
}
l := &lexer{
input: input,
items: make([]item, 0, 100),
line: 1,
return errors.New("empty query")
}
l.input = input
l.line = 1
l.items = l.itemsA[:0]
l.run()
if last := l.items[len(l.items)-1]; last.typ == itemError {
return nil, fmt.Errorf(last.val)
return fmt.Errorf(last.val)
}
return l, nil
return nil
}
// run runs the state machine for the lexer.

View File

@ -3,6 +3,7 @@ package qcode
import (
"errors"
"fmt"
"sync"
"github.com/dosco/super-graph/util"
)
@ -15,6 +16,7 @@ type parserType int16
const (
maxFields = 100
maxArgs = 10
parserError parserType = iota
parserEOF
@ -30,52 +32,30 @@ const (
nodeVar
)
func (t parserType) String() string {
var v string
switch t {
case parserEOF:
v = "EOF"
case parserError:
v = "error"
case opQuery:
v = "query"
case opMutate:
v = "mutation"
case opSub:
v = "subscription"
case nodeStr:
v = "node-string"
case nodeInt:
v = "node-int"
case nodeFloat:
v = "node-float"
case nodeBool:
v = "node-bool"
case nodeVar:
v = "node-var"
case nodeObj:
v = "node-obj"
case nodeList:
v = "node-list"
}
return fmt.Sprintf("<%s>", v)
type Operation struct {
Type parserType
Name string
Args []Arg
argsA [10]Arg
Fields []Field
fieldsA [10]Field
}
type Operation struct {
Type parserType
Name string
Args []*Arg
Fields []Field
var zeroOperation = Operation{}
func (o *Operation) Reset() {
*o = zeroOperation
}
type Field struct {
ID uint16
Name string
Alias string
Args []*Arg
ParentID uint16
Children []uint16
ID uint16
Name string
Alias string
Args []Arg
argsA [10]Arg
ParentID uint16
Children []uint16
childrenA [10]uint16
}
type Arg struct {
@ -91,6 +71,12 @@ type Node struct {
Children []*Node
}
var zeroNode = Node{}
func (n *Node) Reset() {
*n = zeroNode
}
type Parser struct {
pos int
items []item
@ -98,20 +84,36 @@ type Parser struct {
err error
}
var nodePool = sync.Pool{
New: func() interface{} { return new(Node) },
}
var opPool = sync.Pool{
New: func() interface{} { return new(Operation) },
}
var lexPool = sync.Pool{
New: func() interface{} { return new(lexer) },
}
func Parse(gql string) (*Operation, error) {
if len(gql) == 0 {
return nil, errors.New("blank query")
}
l := lexPool.Get().(*lexer)
l.Reset()
l, err := lex(gql)
if err != nil {
if err := lex(l, gql); err != nil {
return nil, err
}
p := &Parser{
pos: -1,
items: l.items,
}
return p.parseOp()
op, err := p.parseOp()
lexPool.Put(l)
return op, err
}
func ParseQuery(gql string) (*Operation, error) {
@ -119,28 +121,37 @@ func ParseQuery(gql string) (*Operation, error) {
}
func ParseArgValue(argVal string) (*Node, error) {
l, err := lex(argVal)
if err != nil {
l := lexPool.Get().(*lexer)
l.Reset()
if err := lex(l, argVal); err != nil {
return nil, err
}
p := &Parser{
pos: -1,
items: l.items,
}
op, err := p.parseValue()
lexPool.Put(l)
return p.parseValue()
return op, err
}
func parseByType(gql string, ty parserType) (*Operation, error) {
l, err := lex(gql)
if err != nil {
l := lexPool.Get().(*lexer)
l.Reset()
if err := lex(l, gql); err != nil {
return nil, err
}
p := &Parser{
pos: -1,
items: l.items,
}
return p.parseOpByType(ty)
op, err := p.parseOpByType(ty)
lexPool.Put(l)
return op, err
}
func (p *Parser) next() item {
@ -188,7 +199,13 @@ func (p *Parser) peek(types ...itemType) bool {
}
func (p *Parser) parseOpByType(ty parserType) (*Operation, error) {
op := &Operation{Type: ty}
op := opPool.Get().(*Operation)
op.Reset()
op.Type = ty
op.Fields = op.fieldsA[:0]
op.Args = op.argsA[:0]
var err error
if p.peek(itemName) {
@ -197,7 +214,7 @@ func (p *Parser) parseOpByType(ty parserType) (*Operation, error) {
if p.peek(itemArgsOpen) {
p.ignore()
op.Args, err = p.parseArgs()
op.Args, err = p.parseArgs(op.Args)
if err != nil {
return nil, err
}
@ -205,7 +222,7 @@ func (p *Parser) parseOpByType(ty parserType) (*Operation, error) {
if p.peek(itemObjOpen) {
p.ignore()
op.Fields, err = p.parseFields()
op.Fields, err = p.parseFields(op.Fields)
if err != nil {
return nil, err
}
@ -238,15 +255,12 @@ func (p *Parser) parseOp() (*Operation, error) {
return nil, errors.New("unknown operation type")
}
func (p *Parser) parseFields() ([]Field, error) {
var id uint16
fields := make([]Field, 0, 5)
func (p *Parser) parseFields(fields []Field) ([]Field, error) {
st := util.NewStack()
for {
if id >= maxFields {
return nil, fmt.Errorf("field limit reached (%d)", maxFields)
if len(fields) >= maxFields {
return nil, fmt.Errorf("too many fields (max %d)", maxFields)
}
if p.peek(itemObjClose) {
@ -263,9 +277,12 @@ func (p *Parser) parseFields() ([]Field, error) {
return nil, errors.New("expecting an alias or field name")
}
f := Field{ID: id}
fields = append(fields, Field{ID: uint16(len(fields))})
f := &fields[(len(fields) - 1)]
f.Args = f.argsA[:0]
f.Children = f.childrenA[:0]
if err := p.parseField(&f); err != nil {
if err := p.parseField(f); err != nil {
return nil, err
}
@ -281,9 +298,6 @@ func (p *Parser) parseFields() ([]Field, error) {
fields[pid].Children = append(fields[pid].Children, f.ID)
}
fields = append(fields, f)
id++
if p.peek(itemObjOpen) {
p.ignore()
st.Push(f.ID)
@ -310,7 +324,7 @@ func (p *Parser) parseField(f *Field) error {
if p.peek(itemArgsOpen) {
p.ignore()
if f.Args, err = p.parseArgs(); err != nil {
if f.Args, err = p.parseArgs(f.Args); err != nil {
return err
}
}
@ -318,11 +332,14 @@ func (p *Parser) parseField(f *Field) error {
return nil
}
func (p *Parser) parseArgs() ([]*Arg, error) {
var args []*Arg
func (p *Parser) parseArgs(args []Arg) ([]Arg, error) {
var err error
for {
if len(args) >= maxArgs {
return nil, fmt.Errorf("too many args (max %d)", maxArgs)
}
if p.peek(itemArgsClose) {
p.ignore()
break
@ -330,7 +347,8 @@ func (p *Parser) parseArgs() ([]*Arg, error) {
if p.peek(itemName) == false {
return nil, errors.New("expecting an argument name")
}
arg := &Arg{Name: p.next().val}
args = append(args, Arg{Name: p.next().val})
arg := &args[(len(args) - 1)]
if p.peek(itemColon) == false {
return nil, errors.New("missing ':' after argument name")
@ -341,16 +359,17 @@ func (p *Parser) parseArgs() ([]*Arg, error) {
if err != nil {
return nil, err
}
args = append(args, arg)
}
return args, nil
}
func (p *Parser) parseList() (*Node, error) {
parent := &Node{}
var nodes []*Node
var ty parserType
nodes := []*Node{}
parent := nodePool.Get().(*Node)
parent.Reset()
var ty parserType
for {
if p.peek(itemListClose) {
p.ignore()
@ -381,8 +400,10 @@ func (p *Parser) parseList() (*Node, error) {
}
func (p *Parser) parseObj() (*Node, error) {
parent := &Node{}
var nodes []*Node
nodes := []*Node{}
parent := nodePool.Get().(*Node)
parent.Reset()
for {
if p.peek(itemObjClose) {
@ -427,7 +448,8 @@ func (p *Parser) parseValue() (*Node, error) {
}
item := p.next()
node := &Node{}
node := nodePool.Get().(*Node)
node.Reset()
switch item.typ {
case itemIntVal:
@ -449,3 +471,39 @@ func (p *Parser) parseValue() (*Node, error) {
return node, nil
}
func (t parserType) String() string {
var v string
switch t {
case parserEOF:
v = "EOF"
case parserError:
v = "error"
case opQuery:
v = "query"
case opMutate:
v = "mutation"
case opSub:
v = "subscription"
case nodeStr:
v = "node-string"
case nodeInt:
v = "node-int"
case nodeFloat:
v = "node-float"
case nodeBool:
v = "node-bool"
case nodeVar:
v = "node-var"
case nodeObj:
v = "node-obj"
case nodeList:
v = "node-list"
}
return fmt.Sprintf("<%s>", v)
}
func PutNode(n *Node) {
nodePool.Put(n)
}

View File

@ -74,27 +74,36 @@ func TestEmptyCompile(t *testing.T) {
}
}
var gql = `query {
products(
# returns only 30 items
limit: 30,
# starts from item 10, commented out for now
# offset: 10,
# orders the response items by highest price
order_by: { price: desc },
# no duplicate prices returned
distinct: [ price ]
# only items with an id >= 30 and < 30 are returned
where: { id: { AND: { greater_or_equals: 20, lt: 28 } } }) {
id
name
price
}
}`
func BenchmarkQCompile(b *testing.B) {
qcompile, _ := NewCompiler(Config{})
val := `query {
products(
where: {
and: {
not: { id: { is_null: true } },
price: { gt: 10 }
}}) {
id
name
price
}
}`
b.ResetTimer()
b.ReportAllocs()
for n := 0; n < b.N; n++ {
_, err := qcompile.CompileQuery(val)
_, err := qcompile.CompileQuery(gql)
if err != nil {
b.Fatal(err)
@ -102,28 +111,19 @@ func BenchmarkQCompile(b *testing.B) {
}
}
func BenchmarkLex(b *testing.B) {
val := `query {
products(
where: {
and: {
not: { id: { is_null: true } },
price: { gt: 10 }
}}) {
id
name
price
}
}`
func BenchmarkQCompileP(b *testing.B) {
qcompile, _ := NewCompiler(Config{})
b.ResetTimer()
b.ReportAllocs()
for n := 0; n < b.N; n++ {
_, err := lex(val)
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_, err := qcompile.CompileQuery(gql)
if err != nil {
b.Fatal(err)
if err != nil {
b.Fatal(err)
}
}
}
})
}

3
qcode/pprof_cpu.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
go test -bench=. -benchmem -cpuprofile cpu.out -run=XXX
go tool pprof -cum cpu.out

3
qcode/pprof_mem.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
go test -bench=. -benchmem -memprofile mem.out -run=XXX
go tool pprof -cum mem.out

View File

@ -97,66 +97,6 @@ const (
OpTsQuery
)
func (t ExpOp) String() string {
var v string
switch t {
case OpNop:
v = "op-nop"
case OpAnd:
v = "op-and"
case OpOr:
v = "op-or"
case OpNot:
v = "op-not"
case OpEquals:
v = "op-equals"
case OpNotEquals:
v = "op-not-equals"
case OpGreaterOrEquals:
v = "op-greater-or-equals"
case OpLesserOrEquals:
v = "op-lesser-or-equals"
case OpGreaterThan:
v = "op-greater-than"
case OpLesserThan:
v = "op-lesser-than"
case OpIn:
v = "op-in"
case OpNotIn:
v = "op-not-in"
case OpLike:
v = "op-like"
case OpNotLike:
v = "op-not-like"
case OpILike:
v = "op-i-like"
case OpNotILike:
v = "op-not-i-like"
case OpSimilar:
v = "op-similar"
case OpNotSimilar:
v = "op-not-similar"
case OpContains:
v = "op-contains"
case OpContainedIn:
v = "op-contained-in"
case OpHasKey:
v = "op-has-key"
case OpHasKeyAny:
v = "op-has-key-any"
case OpHasKeyAll:
v = "op-has-key-all"
case OpIsNull:
v = "op-is-null"
case OpEqID:
v = "op-eq-id"
case OpTsQuery:
v = "op-ts-query"
}
return fmt.Sprintf("<%s>", v)
}
type ValType int
const (
@ -194,29 +134,31 @@ type Config struct {
DefaultFilter []string
FilterMap map[string][]string
Blacklist []string
KeepArgs bool
}
type Compiler struct {
fl *Exp
fm map[string]*Exp
bl map[string]struct{}
ka bool
}
func NewCompiler(conf Config) (*Compiler, error) {
bl := make(map[string]struct{}, len(conf.Blacklist))
func NewCompiler(c Config) (*Compiler, error) {
bl := make(map[string]struct{}, len(c.Blacklist))
for i := range conf.Blacklist {
bl[strings.ToLower(conf.Blacklist[i])] = struct{}{}
for i := range c.Blacklist {
bl[strings.ToLower(c.Blacklist[i])] = struct{}{}
}
fl, err := compileFilter(conf.DefaultFilter)
fl, err := compileFilter(c.DefaultFilter)
if err != nil {
return nil, err
}
fm := make(map[string]*Exp, len(conf.FilterMap))
fm := make(map[string]*Exp, len(c.FilterMap))
for k, v := range conf.FilterMap {
for k, v := range c.FilterMap {
fil, err := compileFilter(v)
if err != nil {
return nil, err
@ -224,7 +166,7 @@ func NewCompiler(conf Config) (*Compiler, error) {
fm[strings.ToLower(k)] = fil
}
return &Compiler{fl, fm, bl}, nil
return &Compiler{fl, fm, bl, c.KeepArgs}, nil
}
func (com *Compiler) CompileQuery(query string) (*QCode, error) {
@ -249,6 +191,8 @@ func (com *Compiler) CompileQuery(query string) (*QCode, error) {
return nil, err
}
opPool.Put(op)
return &qc, nil
}
@ -383,44 +327,46 @@ func (com *Compiler) compileQuery(op *Operation) (*Query, error) {
return &Query{selects[:id]}, nil
}
func (com *Compiler) compileArgs(sel *Select, args []*Arg) error {
func (com *Compiler) compileArgs(sel *Select, args []Arg) error {
var err error
sel.Args = make(map[string]*Node, len(args))
if com.ka {
sel.Args = make(map[string]*Node, len(args))
}
for i := range args {
if args[i] == nil {
return fmt.Errorf("[Args] unexpected nil argument found")
}
an := strings.ToLower(args[i].Name)
if _, ok := sel.Args[an]; ok {
continue
}
arg := &args[i]
an := strings.ToLower(arg.Name)
switch an {
case "id":
if sel.ID == 0 {
err = com.compileArgID(sel, args[i])
err = com.compileArgID(sel, arg)
}
case "search":
err = com.compileArgSearch(sel, args[i])
err = com.compileArgSearch(sel, arg)
case "where":
err = com.compileArgWhere(sel, args[i])
err = com.compileArgWhere(sel, arg)
case "orderby", "order_by", "order":
err = com.compileArgOrderBy(sel, args[i])
err = com.compileArgOrderBy(sel, arg)
case "distinct_on", "distinct":
err = com.compileArgDistinctOn(sel, args[i])
err = com.compileArgDistinctOn(sel, arg)
case "limit":
err = com.compileArgLimit(sel, args[i])
err = com.compileArgLimit(sel, arg)
case "offset":
err = com.compileArgOffset(sel, args[i])
err = com.compileArgOffset(sel, arg)
}
if err != nil {
return err
}
sel.Args[an] = args[i].Val
if sel.Args != nil {
sel.Args[an] = arg.Val
} else {
nodePool.Put(arg.Val)
}
}
return nil
@ -439,15 +385,15 @@ func (com *Compiler) compileArgObj(arg *Arg) (*Exp, error) {
return com.compileArgNode(arg.Val)
}
func (com *Compiler) compileArgNode(val *Node) (*Exp, error) {
func (com *Compiler) compileArgNode(node *Node) (*Exp, error) {
st := util.NewStack()
var root *Exp
if val == nil || len(val.Children) == 0 {
if node == nil || len(node.Children) == 0 {
return nil, errors.New("invalid argument value")
}
st.Push(&expT{nil, val.Children[0]})
st.Push(&expT{nil, node.Children[0]})
for {
if st.Len() == 0 {
@ -483,6 +429,25 @@ func (com *Compiler) compileArgNode(val *Node) (*Exp, error) {
}
}
if com.ka {
return root, nil
}
st.Push(node.Children[0])
for {
if st.Len() == 0 {
break
}
intf := st.Pop()
node, _ := intf.(*Node)
for i := range node.Children {
st.Push(node.Children[i])
}
nodePool.Put(node)
}
return root, nil
}
@ -566,6 +531,9 @@ func (com *Compiler) compileArgOrderBy(sel *Select, arg *Arg) error {
}
if _, ok := com.bl[strings.ToLower(node.Name)]; ok {
if !com.ka {
nodePool.Put(node)
}
continue
}
@ -573,6 +541,9 @@ func (com *Compiler) compileArgOrderBy(sel *Select, arg *Arg) error {
for i := range node.Children {
st.Push(node.Children[i])
}
if !com.ka {
nodePool.Put(node)
}
continue
}
@ -598,6 +569,10 @@ func (com *Compiler) compileArgOrderBy(sel *Select, arg *Arg) error {
setOrderByColName(ob, node)
sel.OrderBy = append(sel.OrderBy, ob)
if !com.ka {
nodePool.Put(node)
}
}
return nil
}
@ -619,6 +594,9 @@ func (com *Compiler) compileArgDistinctOn(sel *Select, arg *Arg) error {
for i := range node.Children {
sel.DistinctOn = append(sel.DistinctOn, node.Children[i].Val)
if !com.ka {
nodePool.Put(node.Children[i])
}
}
return nil
@ -644,7 +622,6 @@ func (com *Compiler) compileArgOffset(sel *Select, arg *Arg) error {
}
sel.Paging.Offset = node.Val
return nil
}
@ -887,3 +864,63 @@ func relID(h *xxhash.Digest, child, parent string) uint64 {
h.Reset()
return v
}
func (t ExpOp) String() string {
var v string
switch t {
case OpNop:
v = "op-nop"
case OpAnd:
v = "op-and"
case OpOr:
v = "op-or"
case OpNot:
v = "op-not"
case OpEquals:
v = "op-equals"
case OpNotEquals:
v = "op-not-equals"
case OpGreaterOrEquals:
v = "op-greater-or-equals"
case OpLesserOrEquals:
v = "op-lesser-or-equals"
case OpGreaterThan:
v = "op-greater-than"
case OpLesserThan:
v = "op-lesser-than"
case OpIn:
v = "op-in"
case OpNotIn:
v = "op-not-in"
case OpLike:
v = "op-like"
case OpNotLike:
v = "op-not-like"
case OpILike:
v = "op-i-like"
case OpNotILike:
v = "op-not-i-like"
case OpSimilar:
v = "op-similar"
case OpNotSimilar:
v = "op-not-similar"
case OpContains:
v = "op-contains"
case OpContainedIn:
v = "op-contained-in"
case OpHasKey:
v = "op-has-key"
case OpHasKeyAny:
v = "op-has-key-any"
case OpHasKeyAll:
v = "op-has-key-all"
case OpIsNull:
v = "op-is-null"
case OpEqID:
v = "op-eq-id"
case OpTsQuery:
v = "op-ts-query"
}
return fmt.Sprintf("<%s>", v)
}