Skip to content

Commit ce13609

Browse files
validation.validate slowness
Small scratch script demonstrating validation.validate slowness. You can run by installing graphql-core in a venv, then running scratch.py. Personally, I then run it through gprof2dot for visualisation
1 parent 7d826f0 commit ce13609

12 files changed

+200
-48
lines changed

benchmark/data.json

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
[
2+
{
3+
"cities": [
4+
{
5+
"name": "London"
6+
},
7+
{
8+
"name": "Cambridge"
9+
}
10+
],
11+
"iso_code": "gb",
12+
"name": "United Kingdom"
13+
},
14+
{
15+
"cities": [
16+
{
17+
"name": "Paris"
18+
}
19+
],
20+
"iso_code": "fr",
21+
"name": "France"
22+
},
23+
{
24+
"cities": [
25+
{
26+
"name": "Berlin"
27+
}
28+
],
29+
"iso_code": "de",
30+
"name": "Germany"
31+
},
32+
{
33+
"cities": [
34+
{
35+
"name": "Madrid"
36+
},
37+
{
38+
"name": "Barcelona"
39+
}
40+
],
41+
"iso_code": "es",
42+
"name": "Spain"
43+
}
44+
]

benchmark/schema.graphql

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
type City {
2+
name: String!
3+
}
4+
5+
type Country {
6+
name: String!
7+
iso_code: String!
8+
cities: [City!]!
9+
}
10+
11+
type Query {
12+
country(iso_code: String!): Country
13+
}
14+
15+
schema {
16+
query: Query
17+
}

benchmark/scratch.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#!/opt/bb/bin/python3.7
2+
import asyncio
3+
import json
4+
import os
5+
import cProfile
6+
7+
from graphql import graphql_sync
8+
from graphql import build_schema as graphql_build_schema
9+
from graphql.type.schema import GraphQLSchema
10+
11+
dir_path = os.path.dirname(os.path.realpath(__file__))
12+
13+
14+
with open(os.path.join(dir_path, "schema.graphql"), "r") as fo:
15+
schema = fo.read()
16+
17+
with open(os.path.join(dir_path, "data.json"), "r") as fo:
18+
data = json.load(fo)
19+
20+
iso_to_country = {c["iso_code"]: c for c in data}
21+
22+
23+
def build_schema(schema_definition: str) -> GraphQLSchema:
24+
schema = graphql_build_schema(schema_definition)
25+
26+
schema.query_type.fields["country"].resolve = country_resolver
27+
28+
return schema
29+
30+
31+
def country_resolver(parent, resolve_info, *, iso_code):
32+
return iso_to_country
33+
34+
35+
schema = build_schema(schema)
36+
37+
38+
loop = asyncio.get_event_loop()
39+
40+
41+
with cProfile.Profile() as pr:
42+
graphql_sync(
43+
schema,
44+
"""
45+
{
46+
gb: country(iso_code:"gb")
47+
{
48+
name
49+
iso_code
50+
cities { name }
51+
}
52+
fr: country(iso_code:"fr")
53+
{
54+
name
55+
iso_code
56+
cities { name }
57+
}
58+
de: country(iso_code:"de")
59+
{
60+
name
61+
iso_code
62+
cities { name }
63+
}
64+
es: country(iso_code:"es")
65+
{
66+
name
67+
iso_code
68+
cities { name }
69+
}
70+
}
71+
""",
72+
)
73+
74+
pr.dump_stats("scratch.prof")

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,5 +52,5 @@ tox = "^3.19"
5252
target-version = ['py36', 'py37', 'py38']
5353

5454
[build-system]
55-
requires = ["poetry>=1,<2"]
55+
requires = ["poetry>=1,<2", "setuptools"]
5656
build-backend = "poetry.masonry.api"

src/graphql/language/ast.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,15 +221,17 @@ class Node:
221221
"""AST nodes"""
222222

223223
# allow custom attributes and weak references (not used internally)
224-
__slots__ = "__dict__", "__weakref__", "loc"
224+
__slots__ = "__dict__", "__weakref__", "loc", "_hash"
225225

226226
loc: Optional[Location]
227227

228228
kind: str = "ast" # the kind of the node as a snake_case string
229229
keys = ["loc"] # the names of the attributes of this node
230230

231+
231232
def __init__(self, **kwargs: Any) -> None:
232233
"""Initialize the node with the given keyword arguments."""
234+
self._hash = None
233235
for key in self.keys:
234236
value = kwargs.get(key)
235237
if isinstance(value, list) and not isinstance(value, FrozenList):
@@ -250,7 +252,10 @@ def __eq__(self, other: Any) -> bool:
250252
)
251253

252254
def __hash__(self) -> int:
253-
return hash(tuple(getattr(self, key) for key in self.keys))
255+
if self._hash is None:
256+
self._hash = hash(tuple(getattr(self, key) for key in self.keys))
257+
258+
return self._hash
254259

255260
def __copy__(self) -> "Node":
256261
"""Create a shallow copy of the node."""

src/graphql/language/visitor.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,9 @@ def leave(self, node, key, parent, path, ancestors):
173173
# Provide special return values as attributes
174174
BREAK, SKIP, REMOVE, IDLE = BREAK, SKIP, REMOVE, IDLE
175175

176+
def __init__(self):
177+
self._visit_fns = {}
178+
176179
def __init_subclass__(cls) -> None:
177180
"""Verify that all defined handlers are valid."""
178181
super().__init_subclass__()
@@ -197,10 +200,18 @@ def __init_subclass__(cls) -> None:
197200

198201
def get_visit_fn(self, kind: str, is_leaving: bool = False) -> Callable:
199202
"""Get the visit function for the given node kind and direction."""
203+
204+
key = (kind, is_leaving)
205+
if key in self._visit_fns:
206+
return self._visit_fns[key]
207+
200208
method = "leave" if is_leaving else "enter"
201209
visit_fn = getattr(self, f"{method}_{kind}", None)
202210
if not visit_fn:
203211
visit_fn = getattr(self, method, None)
212+
213+
self._visit_fns[key] = visit_fn
214+
204215
return visit_fn
205216

206217

@@ -367,14 +378,22 @@ class ParallelVisitor(Visitor):
367378

368379
def __init__(self, visitors: Collection[Visitor]):
369380
"""Create a new visitor from the given list of parallel visitors."""
381+
super().__init__()
370382
self.visitors = visitors
371383
self.skipping: List[Any] = [None] * len(visitors)
384+
self._enter_visit_fns = {}
385+
self._leave_visit_fns = {}
372386

373387
def enter(self, node: Node, *args: Any) -> Optional[VisitorAction]:
388+
visit_fns = self._enter_visit_fns.get(node.kind)
389+
if visit_fns is None:
390+
visit_fns = [v.get_visit_fn(node.kind) for v in self.visitors]
391+
self._enter_visit_fns[node.kind] = visit_fns
392+
374393
skipping = self.skipping
375394
for i, visitor in enumerate(self.visitors):
376395
if not skipping[i]:
377-
fn = visitor.get_visit_fn(node.kind)
396+
fn = visit_fns[i]
378397
if fn:
379398
result = fn(node, *args)
380399
if result is SKIP or result is False:
@@ -386,10 +405,15 @@ def enter(self, node: Node, *args: Any) -> Optional[VisitorAction]:
386405
return None
387406

388407
def leave(self, node: Node, *args: Any) -> Optional[VisitorAction]:
408+
visit_fns = self._leave_visit_fns.get(node.kind)
409+
if visit_fns is None:
410+
visit_fns = [v.get_visit_fn(node.kind, is_leaving=True) for v in self.visitors]
411+
self._leave_visit_fns[node.kind] = visit_fns
412+
389413
skipping = self.skipping
390414
for i, visitor in enumerate(self.visitors):
391415
if not skipping[i]:
392-
fn = visitor.get_visit_fn(node.kind, is_leaving=True)
416+
fn = visit_fns[i]
393417
if fn:
394418
result = fn(node, *args)
395419
if result is BREAK or result is True:

src/graphql/utilities/type_info.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ def __init__(
8888
self._argument: Optional[GraphQLArgument] = None
8989
self._enum_value: Optional[GraphQLEnumValue] = None
9090
self._get_field_def = get_field_def_fn or get_field_def
91+
self._visit_fns = {}
9192
if initial_type:
9293
if is_input_type(initial_type):
9394
self._input_type_stack.append(cast(GraphQLInputType, initial_type))
@@ -136,15 +137,25 @@ def get_enum_value(self) -> Optional[GraphQLEnumValue]:
136137
return self._enum_value
137138

138139
def enter(self, node: Node) -> None:
139-
method = getattr(self, "enter_" + node.kind, None)
140+
method = self._get_method("enter", node.kind)
140141
if method:
141142
method(node)
142143

143144
def leave(self, node: Node) -> None:
144-
method = getattr(self, "leave_" + node.kind, None)
145+
method = self._get_method("leave", node.kind)
145146
if method:
146147
method()
147148

149+
def _get_method(self, direction: str, kind: str) -> Optional[Callable[[], None]]:
150+
key = (direction, kind)
151+
if key in self._visit_fns:
152+
return self._visit_fns[key]
153+
154+
fn = getattr(self, f"{direction}_{kind}", None)
155+
self._visit_fns[key] = fn
156+
return fn
157+
158+
148159
# noinspection PyUnusedLocal
149160
def enter_selection_set(self, node: SelectionSetNode) -> None:
150161
named_type = get_named_type(self.get_type())
@@ -301,6 +312,7 @@ class TypeInfoVisitor(Visitor):
301312
"""A visitor which maintains a provided TypeInfo."""
302313

303314
def __init__(self, type_info: "TypeInfo", visitor: Visitor):
315+
super().__init__()
304316
self.type_info = type_info
305317
self.visitor = visitor
306318

src/graphql/validation/rules/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class ASTValidationRule(Visitor):
1717
context: ASTValidationContext
1818

1919
def __init__(self, context: ASTValidationContext):
20+
super().__init__()
2021
self.context = context
2122

2223
def report_error(self, error: GraphQLError) -> None:

src/graphql/validation/rules/lone_schema_definition.py

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,14 @@ def __init__(self, context: SDLValidationContext):
1717
super().__init__(context)
1818
old_schema = context.schema
1919
self.already_defined = old_schema and (
20-
old_schema.ast_node
21-
or old_schema.query_type
22-
or old_schema.mutation_type
23-
or old_schema.subscription_type
20+
old_schema.ast_node or old_schema.query_type or old_schema.mutation_type or old_schema.subscription_type
2421
)
2522
self.schema_definitions_count = 0
2623

2724
def enter_schema_definition(self, node: SchemaDefinitionNode, *_args: Any) -> None:
2825
if self.already_defined:
29-
self.report_error(
30-
GraphQLError(
31-
"Cannot define a new schema within a schema extension.", node
32-
)
33-
)
26+
self.report_error(GraphQLError("Cannot define a new schema within a schema extension.", node))
3427
else:
3528
if self.schema_definitions_count:
36-
self.report_error(
37-
GraphQLError("Must provide only one schema definition.", node)
38-
)
29+
self.report_error(GraphQLError("Must provide only one schema definition.", node))
3930
self.schema_definitions_count += 1

src/graphql/validation/rules/no_undefined_variables.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,7 @@ def __init__(self, context: ValidationContext):
2121
def enter_operation_definition(self, *_args: Any) -> None:
2222
self.defined_variable_names.clear()
2323

24-
def leave_operation_definition(
25-
self, operation: OperationDefinitionNode, *_args: Any
26-
) -> None:
24+
def leave_operation_definition(self, operation: OperationDefinitionNode, *_args: Any) -> None:
2725
usages = self.context.get_recursive_variable_usages(operation)
2826
defined_variables = self.defined_variable_names
2927
for usage in usages:
@@ -32,15 +30,12 @@ def leave_operation_definition(
3230
if var_name not in defined_variables:
3331
self.report_error(
3432
GraphQLError(
35-
f"Variable '${var_name}' is not defined"
36-
f" by operation '{operation.name.value}'."
33+
f"Variable '${var_name}' is not defined" f" by operation '{operation.name.value}'."
3734
if operation.name
3835
else f"Variable '${var_name}' is not defined.",
3936
[node, operation],
4037
)
4138
)
4239

43-
def enter_variable_definition(
44-
self, node: VariableDefinitionNode, *_args: Any
45-
) -> None:
40+
def enter_variable_definition(self, node: VariableDefinitionNode, *_args: Any) -> None:
4641
self.defined_variable_names.add(node.variable.name.value)

src/graphql/validation/specified_rules.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,15 +118,15 @@
118118
PossibleFragmentSpreadsRule,
119119
NoFragmentCyclesRule,
120120
UniqueVariableNamesRule,
121-
NoUndefinedVariablesRule,
122-
NoUnusedVariablesRule,
121+
# NoUndefinedVariablesRule,
122+
# NoUnusedVariablesRule,
123123
KnownDirectivesRule,
124124
UniqueDirectivesPerLocationRule,
125125
KnownArgumentNamesRule,
126126
UniqueArgumentNamesRule,
127127
ValuesOfCorrectTypeRule,
128128
ProvidedRequiredArgumentsRule,
129-
VariablesInAllowedPositionRule,
129+
# VariablesInAllowedPositionRule,
130130
OverlappingFieldsCanBeMergedRule,
131131
UniqueInputFieldNamesRule,
132132
]

0 commit comments

Comments
 (0)