Skip to content

Commit 687118c

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 687118c

File tree

5 files changed

+45
-11
lines changed

5 files changed

+45
-11
lines changed

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: 24 additions & 7 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,11 +200,12 @@ 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."""
200-
method = "leave" if is_leaving else "enter"
201-
visit_fn = getattr(self, f"{method}_{kind}", None)
202-
if not visit_fn:
203-
visit_fn = getattr(self, method, None)
204-
return visit_fn
203+
key = (kind, is_leaving)
204+
if key not in self._visit_fns:
205+
method = "leave" if is_leaving else "enter"
206+
fn = getattr(self, f"{method}_{kind}", None)
207+
self._visit_fns[key] = fn or getattr(self, method, None)
208+
return self._visit_fns[key]
205209

206210

207211
class Stack(NamedTuple):
@@ -367,14 +371,22 @@ class ParallelVisitor(Visitor):
367371

368372
def __init__(self, visitors: Collection[Visitor]):
369373
"""Create a new visitor from the given list of parallel visitors."""
374+
super().__init__()
370375
self.visitors = visitors
371376
self.skipping: List[Any] = [None] * len(visitors)
377+
self._enter_visit_fns = {}
378+
self._leave_visit_fns = {}
372379

373380
def enter(self, node: Node, *args: Any) -> Optional[VisitorAction]:
381+
visit_fns = self._enter_visit_fns.get(node.kind)
382+
if visit_fns is None:
383+
visit_fns = [v.get_visit_fn(node.kind) for v in self.visitors]
384+
self._enter_visit_fns[node.kind] = visit_fns
385+
374386
skipping = self.skipping
375387
for i, visitor in enumerate(self.visitors):
376388
if not skipping[i]:
377-
fn = visitor.get_visit_fn(node.kind)
389+
fn = visit_fns[i]
378390
if fn:
379391
result = fn(node, *args)
380392
if result is SKIP or result is False:
@@ -386,10 +398,15 @@ def enter(self, node: Node, *args: Any) -> Optional[VisitorAction]:
386398
return None
387399

388400
def leave(self, node: Node, *args: Any) -> Optional[VisitorAction]:
401+
visit_fns = self._leave_visit_fns.get(node.kind)
402+
if visit_fns is None:
403+
visit_fns = [v.get_visit_fn(node.kind, is_leaving=True) for v in self.visitors]
404+
self._leave_visit_fns[node.kind] = visit_fns
405+
389406
skipping = self.skipping
390407
for i, visitor in enumerate(self.visitors):
391408
if not skipping[i]:
392-
fn = visitor.get_visit_fn(node.kind, is_leaving=True)
409+
fn = visit_fns[i]
393410
if fn:
394411
result = fn(node, *args)
395412
if result is BREAK or result is True:

src/graphql/utilities/type_info.py

Lines changed: 12 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,23 @@ 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 not in self._visit_fns:
152+
fn = getattr(self, f"{direction}_{kind}", None)
153+
self._visit_fns[key] = fn
154+
return self._visit_fns[key]
155+
156+
148157
# noinspection PyUnusedLocal
149158
def enter_selection_set(self, node: SelectionSetNode) -> None:
150159
named_type = get_named_type(self.get_type())
@@ -301,6 +310,7 @@ class TypeInfoVisitor(Visitor):
301310
"""A visitor which maintains a provided TypeInfo."""
302311

303312
def __init__(self, type_info: "TypeInfo", visitor: Visitor):
313+
super().__init__()
304314
self.type_info = type_info
305315
self.visitor = visitor
306316

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/validation_context.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ class VariableUsageVisitor(Visitor):
4646
usages: List[VariableUsage]
4747

4848
def __init__(self, type_info: TypeInfo):
49+
super().__init__()
4950
self.usages = []
5051
self._append_usage = self.usages.append
5152
self._type_info = type_info

0 commit comments

Comments
 (0)