Skip to content

Commit 1b32cd8

Browse files
committed
[Validation] perf improvements for fragment cycle detection
This converts the O(N^2) cycle detector to an O(N) detector and optimizes the visit to short-circuit where parts of the validation visitor were unnecessary. On an internal very large stress test query this was previously ~30s and after this diff runs in 15ms. Commit: 4cf2190b54fefc34a7b37d22a489933fc15e14ce [4cf2190] Parents: a738c6d90d Author: Lee Byron <[email protected]> Date: 13 November 2015 at 3:47:01 PM SGT [Validation] Performance improvements Fairly dramatic improvement of some validation rules by short-circuiting branches which do not need to be checked and memoizing the process of collecting fragment spreads from a given context - which is necessary by multiple rules. Commit: 0bc9088187b9902ab19c0ec34e0e9f036dc9d9ea [0bc9088] Parents: 7278b7c92c Author: Lee Byron <[email protected]> Date: 13 November 2015 at 4:26:05 PM SGT Labels: HEAD [Validation] Report errors rather than return them This replaces the mechanism of returning errors or lists of errors from a validator step to instead report those errors via calling a function on the context. This simplifies the implementation of the visitor mechanism, but also opens the doors for future rules being specified as warnings instead of errors.
1 parent 85bc4e5 commit 1b32cd8

File tree

3 files changed

+140
-96
lines changed

3 files changed

+140
-96
lines changed

language/ast/selections.go

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
)
66

77
type Selection interface {
8+
GetSelectionSet() *SelectionSet
89
}
910

1011
// Ensure that all definition types implements Selection interface

rules.go

+89-91
Original file line numberDiff line numberDiff line change
@@ -713,93 +713,113 @@ func (set *nodeSet) Add(node ast.Node) bool {
713713
return true
714714
}
715715

716+
func CycleErrorMessage(fragName string, spreadNames []string) string {
717+
via := ""
718+
if len(spreadNames) > 0 {
719+
via = " via " + strings.Join(spreadNames, ", ")
720+
}
721+
return fmt.Sprintf(`Cannot spread fragment "%v" within itself%v.`, fragName, via)
722+
}
723+
716724
/**
717725
* NoFragmentCyclesRule
718726
*/
719727
func NoFragmentCyclesRule(context *ValidationContext) *ValidationRuleInstance {
720-
// Gather all the fragment spreads ASTs for each fragment definition.
721-
// Importantly this does not include inline fragments.
722-
definitions := context.Document().Definitions
723-
spreadsInFragment := map[string][]*ast.FragmentSpread{}
724-
for _, node := range definitions {
725-
if node.GetKind() == kinds.FragmentDefinition {
726-
if node, ok := node.(*ast.FragmentDefinition); ok && node != nil {
727-
nodeName := ""
728-
if node.Name != nil {
729-
nodeName = node.Name.Value
728+
729+
// Tracks already visited fragments to maintain O(N) and to ensure that cycles
730+
// are not redundantly reported.
731+
visitedFrags := map[string]bool{}
732+
733+
// Array of AST nodes used to produce meaningful errors
734+
spreadPath := []*ast.FragmentSpread{}
735+
736+
// Position in the spread path
737+
spreadPathIndexByName := map[string]int{}
738+
739+
// This does a straight-forward DFS to find cycles.
740+
// It does not terminate when a cycle was found but continues to explore
741+
// the graph to find all possible cycles.
742+
var detectCycleRecursive func(fragment *ast.FragmentDefinition)
743+
detectCycleRecursive = func(fragment *ast.FragmentDefinition) {
744+
745+
fragmentName := ""
746+
if fragment.Name != nil {
747+
fragmentName = fragment.Name.Value
748+
}
749+
visitedFrags[fragmentName] = true
750+
751+
spreadNodes := context.FragmentSpreads(fragment)
752+
if len(spreadNodes) == 0 {
753+
return
754+
}
755+
756+
spreadPathIndexByName[fragmentName] = len(spreadPath)
757+
758+
for _, spreadNode := range spreadNodes {
759+
760+
spreadName := ""
761+
if spreadNode.Name != nil {
762+
spreadName = spreadNode.Name.Value
763+
}
764+
cycleIndex, ok := spreadPathIndexByName[spreadName]
765+
if !ok {
766+
spreadPath = append(spreadPath, spreadNode)
767+
if visited, ok := visitedFrags[spreadName]; !ok || !visited {
768+
spreadFragment := context.Fragment(spreadName)
769+
if spreadFragment != nil {
770+
detectCycleRecursive(spreadFragment)
771+
}
772+
}
773+
spreadPath = spreadPath[:len(spreadPath)-1]
774+
} else {
775+
cyclePath := spreadPath[cycleIndex:]
776+
777+
spreadNames := []string{}
778+
for _, s := range cyclePath {
779+
name := ""
780+
if s.Name != nil {
781+
name = s.Name.Value
782+
}
783+
spreadNames = append(spreadNames, name)
784+
}
785+
786+
nodes := []ast.Node{}
787+
for _, c := range cyclePath {
788+
nodes = append(nodes, c)
730789
}
731-
spreadsInFragment[nodeName] = gatherSpreads(node)
790+
nodes = append(nodes, spreadNode)
791+
792+
reportError(
793+
context,
794+
CycleErrorMessage(spreadName, spreadNames),
795+
nodes,
796+
)
732797
}
798+
733799
}
800+
delete(spreadPathIndexByName, fragmentName)
801+
734802
}
735-
// Tracks spreads known to lead to cycles to ensure that cycles are not
736-
// redundantly reported.
737-
knownToLeadToCycle := newNodeSet()
738803

739804
visitorOpts := &visitor.VisitorOptions{
740805
KindFuncMap: map[string]visitor.NamedVisitFuncs{
806+
kinds.OperationDefinition: visitor.NamedVisitFuncs{
807+
Kind: func(p visitor.VisitFuncParams) (string, interface{}) {
808+
return visitor.ActionSkip, nil
809+
},
810+
},
741811
kinds.FragmentDefinition: visitor.NamedVisitFuncs{
742812
Kind: func(p visitor.VisitFuncParams) (string, interface{}) {
743813
if node, ok := p.Node.(*ast.FragmentDefinition); ok && node != nil {
744-
spreadPath := []*ast.FragmentSpread{}
745-
initialName := ""
814+
nodeName := ""
746815
if node.Name != nil {
747-
initialName = node.Name.Value
816+
nodeName = node.Name.Value
748817
}
749-
var detectCycleRecursive func(fragmentName string)
750-
detectCycleRecursive = func(fragmentName string) {
751-
spreadNodes, _ := spreadsInFragment[fragmentName]
752-
for _, spreadNode := range spreadNodes {
753-
if knownToLeadToCycle.Has(spreadNode) {
754-
continue
755-
}
756-
spreadNodeName := ""
757-
if spreadNode.Name != nil {
758-
spreadNodeName = spreadNode.Name.Value
759-
}
760-
if spreadNodeName == initialName {
761-
cyclePath := []ast.Node{}
762-
for _, path := range spreadPath {
763-
cyclePath = append(cyclePath, path)
764-
}
765-
cyclePath = append(cyclePath, spreadNode)
766-
for _, spread := range cyclePath {
767-
knownToLeadToCycle.Add(spread)
768-
}
769-
via := ""
770-
spreadNames := []string{}
771-
for _, s := range spreadPath {
772-
if s.Name != nil {
773-
spreadNames = append(spreadNames, s.Name.Value)
774-
}
775-
}
776-
if len(spreadNames) > 0 {
777-
via = " via " + strings.Join(spreadNames, ", ")
778-
}
779-
reportError(
780-
context,
781-
fmt.Sprintf(`Cannot spread fragment "%v" within itself%v.`, initialName, via),
782-
cyclePath,
783-
)
784-
continue
785-
}
786-
spreadPathHasCurrentNode := false
787-
for _, spread := range spreadPath {
788-
if spread == spreadNode {
789-
spreadPathHasCurrentNode = true
790-
}
791-
}
792-
if spreadPathHasCurrentNode {
793-
continue
794-
}
795-
spreadPath = append(spreadPath, spreadNode)
796-
detectCycleRecursive(spreadNodeName)
797-
_, spreadPath = spreadPath[len(spreadPath)-1], spreadPath[:len(spreadPath)-1]
798-
}
818+
if _, ok := visitedFrags[nodeName]; !ok {
819+
detectCycleRecursive(node)
799820
}
800-
detectCycleRecursive(initialName)
801821
}
802-
return visitor.ActionNoChange, nil
822+
return visitor.ActionSkip, nil
803823
},
804824
},
805825
},
@@ -2163,25 +2183,3 @@ func isValidLiteralValue(ttype Input, valueAST ast.Value) (bool, []string) {
21632183

21642184
return true, nil
21652185
}
2166-
2167-
/**
2168-
* Given an operation or fragment AST node, gather all the
2169-
* named spreads defined within the scope of the fragment
2170-
* or operation
2171-
*/
2172-
func gatherSpreads(node ast.Node) (spreadNodes []*ast.FragmentSpread) {
2173-
visitorOpts := &visitor.VisitorOptions{
2174-
KindFuncMap: map[string]visitor.NamedVisitFuncs{
2175-
kinds.FragmentSpread: visitor.NamedVisitFuncs{
2176-
Kind: func(p visitor.VisitFuncParams) (string, interface{}) {
2177-
if node, ok := p.Node.(*ast.FragmentSpread); ok && node != nil {
2178-
spreadNodes = append(spreadNodes, node)
2179-
}
2180-
return visitor.ActionNoChange, nil
2181-
},
2182-
},
2183-
},
2184-
}
2185-
visitor.Visit(node, visitorOpts, nil)
2186-
return spreadNodes
2187-
}

rules_no_fragment_cycles_test.go

+50-5
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,21 @@ func TestValidate_NoCircularFragmentSpreads_NoSpreadingItselfDeeply(t *testing.T
115115
fragment fragX on Dog { ...fragY }
116116
fragment fragY on Dog { ...fragZ }
117117
fragment fragZ on Dog { ...fragO }
118-
fragment fragO on Dog { ...fragA, ...fragX }
118+
fragment fragO on Dog { ...fragP }
119+
fragment fragP on Dog { ...fragA, ...fragX }
119120
`, []gqlerrors.FormattedError{
120-
testutil.RuleError(`Cannot spread fragment "fragA" within itself via fragB, fragC, fragO.`, 2, 31, 3, 31, 4, 31, 8, 31),
121-
testutil.RuleError(`Cannot spread fragment "fragX" within itself via fragY, fragZ, fragO.`, 5, 31, 6, 31, 7, 31, 8, 41),
121+
testutil.RuleError(`Cannot spread fragment "fragA" within itself via fragB, fragC, fragO, fragP.`,
122+
2, 31,
123+
3, 31,
124+
4, 31,
125+
8, 31,
126+
9, 31),
127+
testutil.RuleError(`Cannot spread fragment "fragO" within itself via fragP, fragX, fragY, fragZ.`,
128+
8, 31,
129+
9, 41,
130+
5, 31,
131+
6, 31,
132+
7, 31),
122133
})
123134
}
124135
func TestValidate_NoCircularFragmentSpreads_NoSpreadingItselfDeeplyTwoPaths(t *testing.T) {
@@ -127,7 +138,41 @@ func TestValidate_NoCircularFragmentSpreads_NoSpreadingItselfDeeplyTwoPaths(t *t
127138
fragment fragB on Dog { ...fragA }
128139
fragment fragC on Dog { ...fragA }
129140
`, []gqlerrors.FormattedError{
130-
testutil.RuleError(`Cannot spread fragment "fragA" within itself via fragB.`, 2, 31, 3, 31),
131-
testutil.RuleError(`Cannot spread fragment "fragA" within itself via fragC.`, 2, 41, 4, 31),
141+
testutil.RuleError(`Cannot spread fragment "fragA" within itself via fragB.`,
142+
2, 31,
143+
3, 31),
144+
testutil.RuleError(`Cannot spread fragment "fragA" within itself via fragC.`,
145+
2, 41,
146+
4, 31),
147+
})
148+
}
149+
func TestValidate_NoCircularFragmentSpreads_NoSpreadingItselfDeeplyTwoPaths_AltTraverseOrder(t *testing.T) {
150+
testutil.ExpectFailsRule(t, graphql.NoFragmentCyclesRule, `
151+
fragment fragA on Dog { ...fragC }
152+
fragment fragB on Dog { ...fragC }
153+
fragment fragC on Dog { ...fragA, ...fragB }
154+
`, []gqlerrors.FormattedError{
155+
testutil.RuleError(`Cannot spread fragment "fragA" within itself via fragC.`,
156+
2, 31,
157+
4, 31),
158+
testutil.RuleError(`Cannot spread fragment "fragC" within itself via fragB.`,
159+
4, 41,
160+
3, 31),
161+
})
162+
}
163+
func TestValidate_NoCircularFragmentSpreads_NoSpreadingItselfDeeplyAndImmediately(t *testing.T) {
164+
testutil.ExpectFailsRule(t, graphql.NoFragmentCyclesRule, `
165+
fragment fragA on Dog { ...fragB }
166+
fragment fragB on Dog { ...fragB, ...fragC }
167+
fragment fragC on Dog { ...fragA, ...fragB }
168+
`, []gqlerrors.FormattedError{
169+
testutil.RuleError(`Cannot spread fragment "fragB" within itself.`, 3, 31),
170+
testutil.RuleError(`Cannot spread fragment "fragA" within itself via fragB, fragC.`,
171+
2, 31,
172+
3, 41,
173+
4, 31),
174+
testutil.RuleError(`Cannot spread fragment "fragB" within itself via fragC.`,
175+
3, 41,
176+
4, 41),
132177
})
133178
}

0 commit comments

Comments
 (0)