Skip to content

Commit

Permalink
[Validation] Parallelize validation rules.
Browse files Browse the repository at this point in the history
This provides a performance improvement and simplification to the validator by providing two new generic visitor utilities. One for tracking a TypeInfo instance alongside a visitor instance, and another for stepping through multiple visitors in parallel. The two can be composed together.

Rather than 23 passes of AST visitation with one rule each, this now performs one pass of AST visitation with 23 rules. Since visitation is costly but rules are inexpensive, this nets out to a much faster overall validation, especially noticable for very large queries.

Commit:
957704188b0a103c5f2fe0ab99479267d5d1ae43 [9577041]
Parents:
439a3e2f4f
Author:
Lee Byron <[email protected]>
Date:
17 November 2015 at 12:33:54 PM SGT

----

[Validation] Memoize collecting variable usage.

During multiple validation passes we need to know about variable usage within a de-fragmented operation. Memoizing this ensures each pass is O(N) - each fragment is no longer visited per operation, but once total.

In doing so, `visitSpreadFragments` is no longer used, which will be cleaned up in a later PR

Commit:
2afbff79bfd2b89f03ca7913577556b73980f974 [2afbff7]
Parents:
88acc01b99
Author:
Lee Byron <[email protected]>
Date:
17 November 2015 at 9:54:30 AM SGT
  • Loading branch information
sogko committed Mar 10, 2016
1 parent fec8a0d commit a921397
Show file tree
Hide file tree
Showing 7 changed files with 536 additions and 201 deletions.
12 changes: 12 additions & 0 deletions language/ast/selections.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ func (f *Field) GetLoc() *Location {
return f.Loc
}

func (f *Field) GetSelectionSet() *SelectionSet {
return f.SelectionSet
}

// FragmentSpread implements Node, Selection
type FragmentSpread struct {
Kind string
Expand Down Expand Up @@ -74,6 +78,10 @@ func (fs *FragmentSpread) GetLoc() *Location {
return fs.Loc
}

func (fs *FragmentSpread) GetSelectionSet() *SelectionSet {
return nil
}

// InlineFragment implements Node, Selection
type InlineFragment struct {
Kind string
Expand Down Expand Up @@ -104,6 +112,10 @@ func (f *InlineFragment) GetLoc() *Location {
return f.Loc
}

func (f *InlineFragment) GetSelectionSet() *SelectionSet {
return f.SelectionSet
}

// SelectionSet implements Node
type SelectionSet struct {
Kind string
Expand Down
14 changes: 14 additions & 0 deletions language/type_info/type_info.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package type_info

import (
"github.com/graphql-go/graphql/language/ast"
)

/**
* TypeInfoI defines the interface for TypeInfo
* Implementation
*/
type TypeInfoI interface {
Enter(node ast.Node)
Leave(node ast.Node)
}
177 changes: 175 additions & 2 deletions language/visitor/visitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"github.com/graphql-go/graphql/language/ast"
"github.com/graphql-go/graphql/language/type_info"
"reflect"
)

Expand Down Expand Up @@ -380,7 +381,7 @@ Loop:
kind = node.GetKind()
}

visitFn := GetVisitFn(visitorOpts, isLeaving, kind)
visitFn := GetVisitFn(visitorOpts, kind, isLeaving)
if visitFn != nil {
p := VisitFuncParams{
Node: nodeIn,
Expand Down Expand Up @@ -709,7 +710,144 @@ func isNilNode(node interface{}) bool {
return val.Interface() == nil
}

func GetVisitFn(visitorOpts *VisitorOptions, isLeaving bool, kind string) VisitFunc {
/**
* Creates a new visitor instance which delegates to many visitors to run in
* parallel. Each visitor will be visited for each node before moving on.
*
* Visitors must not directly modify the AST nodes and only returning false to
* skip sub-branches is supported.
*/
func VisitInParallel(visitorOptsSlice []*VisitorOptions) *VisitorOptions {
skipping := map[int]interface{}{}

return &VisitorOptions{
Enter: func(p VisitFuncParams) (string, interface{}) {
for i, visitorOpts := range visitorOptsSlice {
if _, ok := skipping[i]; !ok {
switch node := p.Node.(type) {
case ast.Node:
kind := node.GetKind()
fn := GetVisitFn(visitorOpts, kind, false)
if fn != nil {
action, _ := fn(p)
if action == ActionSkip {
skipping[i] = node
}
}
}
}
}
return ActionNoChange, nil
},
Leave: func(p VisitFuncParams) (string, interface{}) {
for i, visitorOpts := range visitorOptsSlice {
if _, ok := skipping[i]; !ok {
switch node := p.Node.(type) {
case ast.Node:
kind := node.GetKind()
fn := GetVisitFn(visitorOpts, kind, true)
if fn != nil {
fn(p)
}
}
} else {
delete(skipping, i)
}
}
return ActionNoChange, nil
},
}
}

/**
* Creates a new visitor instance which maintains a provided TypeInfo instance
* along with visiting visitor.
*
* Visitors must not directly modify the AST nodes and only returning false to
* skip sub-branches is supported.
*/
func VisitWithTypeInfo(typeInfo type_info.TypeInfoI, visitorOpts *VisitorOptions) *VisitorOptions {
return &VisitorOptions{
Enter: func(p VisitFuncParams) (string, interface{}) {
if node, ok := p.Node.(ast.Node); ok {
typeInfo.Enter(node)
fn := GetVisitFn(visitorOpts, node.GetKind(), false)
if fn != nil {
action, _ := fn(p)
if action == ActionSkip {
typeInfo.Leave(node)
return ActionSkip, nil
}
}
}
return ActionNoChange, nil
},
Leave: func(p VisitFuncParams) (string, interface{}) {
if node, ok := p.Node.(ast.Node); ok {
fn := GetVisitFn(visitorOpts, node.GetKind(), true)
if fn != nil {
fn(p)
}
typeInfo.Leave(node)
}
return ActionNoChange, nil
},
}
}

/**
* Given a visitor instance, if it is leaving or not, and a node kind, return
* the function the visitor runtime should call.
*/
func GetVisitFn(visitorOpts *VisitorOptions, kind string, isLeaving bool) VisitFunc {
if visitorOpts == nil {
return nil
}
kindVisitor, ok := visitorOpts.KindFuncMap[kind]
if ok {
if !isLeaving && kindVisitor.Kind != nil {
// { Kind() {} }
return kindVisitor.Kind
}
if isLeaving {
// { Kind: { leave() {} } }
return kindVisitor.Leave
} else {
// { Kind: { enter() {} } }
return kindVisitor.Enter
}
} else {

if isLeaving {
// { enter() {} }
specificVisitor := visitorOpts.Leave
if specificVisitor != nil {
return specificVisitor
}
if specificKindVisitor, ok := visitorOpts.LeaveKindMap[kind]; ok {
// { leave: { Kind() {} } }
return specificKindVisitor
}

} else {
// { leave() {} }
specificVisitor := visitorOpts.Enter
if specificVisitor != nil {
return specificVisitor
}
if specificKindVisitor, ok := visitorOpts.EnterKindMap[kind]; ok {
// { enter: { Kind() {} } }
return specificKindVisitor
}
}
}

return nil
}

///// DELETE ////

func GetVisitFnOld(visitorOpts *VisitorOptions, isLeaving bool, kind string) VisitFunc {
if visitorOpts == nil {
return nil
}
Expand Down Expand Up @@ -753,3 +891,38 @@ func GetVisitFn(visitorOpts *VisitorOptions, isLeaving bool, kind string) VisitF

return nil
}

/*
export function getVisitFn(visitor, isLeaving, kind) {
var kindVisitor = visitor[kind];
if (kindVisitor) {
if (!isLeaving && typeof kindVisitor === 'function') {
// { Kind() {} }
return kindVisitor;
}
var kindSpecificVisitor = isLeaving ? kindVisitor.leave : kindVisitor.enter;
if (typeof kindSpecificVisitor === 'function') {
// { Kind: { enter() {}, leave() {} } }
return kindSpecificVisitor;
}
return;
}
var specificVisitor = isLeaving ? visitor.leave : visitor.enter;
if (specificVisitor) {
if (typeof specificVisitor === 'function') {
// { enter() {}, leave() {} }
return specificVisitor;
}
var specificKindVisitor = specificVisitor[kind];
if (typeof specificKindVisitor === 'function') {
// { enter: { Kind() {} }, leave: { Kind() {} } }
return specificKindVisitor;
}
}
}
*/
Loading

0 comments on commit a921397

Please sign in to comment.