Skip to content

Commit e503d50

Browse files
authored
Merge pull request graphql-python#72 from graphql-python/features/next-performance
PR focus on performance for GraphQL-core
2 parents 750d177 + 7964332 commit e503d50

33 files changed

+342
-213
lines changed

.travis.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ python:
66
- 3.4
77
- 3.5
88
- pypy
9-
cache: pip
109
before_install:
1110
- |
1211
if [ "$TRAVIS_PYTHON_VERSION" = "pypy" ]; then
@@ -22,8 +21,8 @@ before_install:
2221
source "$HOME/virtualenvs/pypy-$PYPY_VERSION/bin/activate"
2322
fi
2423
install:
25-
- pip install --cache-dir $HOME/.cache/pip pytest-cov pytest-mock coveralls flake8 isort==3.9.6 gevent==1.1b5 six>=1.10.0 promise>=0.4.2
26-
- pip install --cache-dir $HOME/.cache/pip pytest>=2.7.3 --upgrade
24+
- pip install pytest-cov pytest-mock coveralls flake8 isort==3.9.6 gevent==1.1b5 six>=1.10.0 promise>=0.4.2 pytest-benchmark
25+
- pip install pytest==2.9.2
2726
- pip install -e .
2827
script:
2928
- flake8

graphql/execution/base.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# -*- coding: utf-8 -*-
22
from ..error import GraphQLError
33
from ..language import ast
4+
from ..pyutils.default_ordered_dict import DefaultOrderedDict
45
from ..type.definition import GraphQLInterfaceType, GraphQLUnionType
56
from ..type.directives import GraphQLIncludeDirective, GraphQLSkipDirective
67
from ..type.introspection import (SchemaMetaFieldDef, TypeMetaFieldDef,
@@ -18,7 +19,7 @@ class ExecutionContext(object):
1819
and the fragments defined in the query document"""
1920

2021
__slots__ = 'schema', 'fragments', 'root_value', 'operation', 'variable_values', 'errors', 'context_value', \
21-
'argument_values_cache', 'executor', 'middleware_manager'
22+
'argument_values_cache', 'executor', 'middleware_manager', '_subfields_cache'
2223

2324
def __init__(self, schema, document_ast, root_value, context_value, variable_values, operation_name, executor, middleware_manager):
2425
"""Constructs a ExecutionContext object from the arguments passed
@@ -64,6 +65,7 @@ def __init__(self, schema, document_ast, root_value, context_value, variable_val
6465
self.argument_values_cache = {}
6566
self.executor = executor
6667
self.middleware_manager = middleware_manager
68+
self._subfields_cache = {}
6769

6870
def get_field_resolver(self, field_resolver):
6971
if not self.middleware_manager:
@@ -80,6 +82,21 @@ def get_argument_values(self, field_def, field_ast):
8082

8183
return result
8284

85+
def get_sub_fields(self, return_type, field_asts):
86+
k = return_type, tuple(field_asts)
87+
if k not in self._subfields_cache:
88+
subfield_asts = DefaultOrderedDict(list)
89+
visited_fragment_names = set()
90+
for field_ast in field_asts:
91+
selection_set = field_ast.selection_set
92+
if selection_set:
93+
subfield_asts = collect_fields(
94+
self, return_type, selection_set,
95+
subfield_asts, visited_fragment_names
96+
)
97+
self._subfields_cache[k] = subfield_asts
98+
return self._subfields_cache[k]
99+
83100

84101
class ExecutionResult(object):
85102
"""The result of execution. `data` is the result of executing the
@@ -251,6 +268,8 @@ def get_field_entry_key(node):
251268

252269

253270
class ResolveInfo(object):
271+
__slots__ = ('field_name', 'field_asts', 'return_type', 'parent_type',
272+
'schema', 'fragments', 'root_value', 'operation', 'variable_values')
254273

255274
def __init__(self, field_name, field_asts, return_type, parent_type,
256275
schema, fragments, root_value, operation, variable_values):
@@ -289,4 +308,4 @@ def get_field_def(schema, parent_type, field_name):
289308
return TypeMetaFieldDef
290309
elif field_name == '__typename':
291310
return TypeNameMetaFieldDef
292-
return parent_type.get_fields().get(field_name)
311+
return parent_type.fields.get(field_name)

graphql/execution/executor.py

Lines changed: 33 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
import logging
44
import sys
55

6-
from promise import Promise, is_thenable, promise_for_dict, promisify
6+
from promise import Promise, promise_for_dict, promisify
77

88
from ..error import GraphQLError, GraphQLLocatedError
99
from ..pyutils.default_ordered_dict import DefaultOrderedDict
10+
from ..pyutils.ordereddict import OrderedDict
1011
from ..type import (GraphQLEnumType, GraphQLInterfaceType, GraphQLList,
1112
GraphQLNonNull, GraphQLObjectType, GraphQLScalarType,
1213
GraphQLSchema, GraphQLUnionType)
@@ -19,6 +20,10 @@
1920
logger = logging.getLogger(__name__)
2021

2122

23+
def is_promise(obj):
24+
return type(obj) == Promise
25+
26+
2227
def execute(schema, document_ast, root_value=None, context_value=None,
2328
variable_values=None, operation_name=None, executor=None,
2429
return_promise=False, middlewares=None):
@@ -92,7 +97,7 @@ def execute_field_callback(results, response_name):
9297
if result is Undefined:
9398
return results
9499

95-
if is_thenable(result):
100+
if is_promise(result):
96101
def collect_result(resolved_result):
97102
results[response_name] = resolved_result
98103
return results
@@ -111,15 +116,15 @@ def execute_field(prev_promise, response_name):
111116
def execute_fields(exe_context, parent_type, source_value, fields):
112117
contains_promise = False
113118

114-
final_results = collections.OrderedDict()
119+
final_results = OrderedDict()
115120

116121
for response_name, field_asts in fields.items():
117122
result = resolve_field(exe_context, parent_type, source_value, field_asts)
118123
if result is Undefined:
119124
continue
120125

121126
final_results[response_name] = result
122-
if is_thenable(result):
127+
if is_promise(result):
123128
contains_promise = True
124129

125130
if not contains_promise:
@@ -198,7 +203,7 @@ def complete_value_catching_error(exe_context, return_type, field_asts, info, re
198203
# resolving a null value for this field if one is encountered.
199204
try:
200205
completed = complete_value(exe_context, return_type, field_asts, info, result)
201-
if is_thenable(completed):
206+
if is_promise(completed):
202207
def handle_error(error):
203208
exe_context.errors.append(error)
204209
return Promise.fulfilled(None)
@@ -232,7 +237,7 @@ def complete_value(exe_context, return_type, field_asts, info, result):
232237
"""
233238
# If field type is NonNull, complete for inner type, and throw field error if result is null.
234239

235-
if is_thenable(result):
240+
if is_promise(result):
236241
return promisify(result).then(
237242
lambda resolved: complete_value(
238243
exe_context,
@@ -248,16 +253,7 @@ def complete_value(exe_context, return_type, field_asts, info, result):
248253
raise GraphQLLocatedError(field_asts, original_error=result)
249254

250255
if isinstance(return_type, GraphQLNonNull):
251-
completed = complete_value(
252-
exe_context, return_type.of_type, field_asts, info, result
253-
)
254-
if completed is None:
255-
raise GraphQLError(
256-
'Cannot return null for non-nullable field {}.{}.'.format(info.parent_type, info.field_name),
257-
field_asts
258-
)
259-
260-
return completed
256+
return complete_nonnull_value(exe_context, return_type, field_asts, info, result)
261257

262258
# If result is null-like, return null.
263259
if result is None:
@@ -293,7 +289,7 @@ def complete_list_value(exe_context, return_type, field_asts, info, result):
293289
contains_promise = False
294290
for item in result:
295291
completed_item = complete_value_catching_error(exe_context, item_type, field_asts, info, item)
296-
if not contains_promise and is_thenable(completed_item):
292+
if not contains_promise and is_promise(completed_item):
297293
contains_promise = True
298294

299295
completed_results.append(completed_item)
@@ -305,15 +301,10 @@ def complete_leaf_value(return_type, result):
305301
"""
306302
Complete a Scalar or Enum by serializing to a valid value, returning null if serialization is not possible.
307303
"""
308-
serialize = getattr(return_type, 'serialize', None)
309-
assert serialize, 'Missing serialize method on type'
304+
# serialize = getattr(return_type, 'serialize', None)
305+
# assert serialize, 'Missing serialize method on type'
310306

311-
serialized_result = serialize(result)
312-
313-
if serialized_result is None:
314-
return None
315-
316-
return serialized_result
307+
return return_type.serialize(result)
317308

318309

319310
def complete_abstract_value(exe_context, return_type, field_asts, info, result):
@@ -368,14 +359,21 @@ def complete_object_value(exe_context, return_type, field_asts, info, result):
368359
)
369360

370361
# Collect sub-fields to execute to complete this value.
371-
subfield_asts = DefaultOrderedDict(list)
372-
visited_fragment_names = set()
373-
for field_ast in field_asts:
374-
selection_set = field_ast.selection_set
375-
if selection_set:
376-
subfield_asts = collect_fields(
377-
exe_context, return_type, selection_set,
378-
subfield_asts, visited_fragment_names
379-
)
380-
362+
subfield_asts = exe_context.get_sub_fields(return_type, field_asts)
381363
return execute_fields(exe_context, return_type, result, subfield_asts)
364+
365+
366+
def complete_nonnull_value(exe_context, return_type, field_asts, info, result):
367+
"""
368+
Complete a NonNull value by completing the inner type
369+
"""
370+
completed = complete_value(
371+
exe_context, return_type.of_type, field_asts, info, result
372+
)
373+
if completed is None:
374+
raise GraphQLError(
375+
'Cannot return null for non-nullable field {}.{}.'.format(info.parent_type, info.field_name),
376+
field_asts
377+
)
378+
379+
return completed

graphql/execution/executors/asyncio.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from asyncio import Future, get_event_loop, iscoroutine, wait
44

5+
from promise import promisify
6+
57
try:
68
from asyncio import ensure_future
79
except ImportError:
@@ -45,5 +47,5 @@ def execute(self, fn, *args, **kwargs):
4547
if isinstance(result, Future) or iscoroutine(result):
4648
future = ensure_future(result, loop=self.loop)
4749
self.futures.append(future)
48-
return future
50+
return promisify(future)
4951
return result
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
from collections import namedtuple
2+
from functools import partial
3+
4+
from graphql import (GraphQLField, GraphQLInt, GraphQLList, GraphQLObjectType,
5+
GraphQLSchema, Source, execute, parse)
6+
7+
8+
def test_big_list_of_ints(benchmark):
9+
big_int_list = [x for x in range(5000)]
10+
11+
def resolve_all_ints(root, args, context, info):
12+
return big_int_list
13+
14+
Query = GraphQLObjectType('Query', fields={
15+
'allInts': GraphQLField(
16+
GraphQLList(GraphQLInt),
17+
resolver=resolve_all_ints
18+
)
19+
})
20+
hello_schema = GraphQLSchema(Query)
21+
source = Source('{ allInts }')
22+
ast = parse(source)
23+
big_list_query = partial(execute, hello_schema, ast)
24+
result = benchmark(big_list_query)
25+
# result = big_list_query()
26+
assert not result.errors
27+
assert result.data == {'allInts': big_int_list}
28+
29+
30+
31+
def test_big_list_of_ints(benchmark):
32+
big_int_list = [x for x in range(5000)]
33+
from ..executor import complete_leaf_value
34+
# def convert_item(i):
35+
# return i
36+
37+
def convert_list():
38+
r = []
39+
for i in big_int_list:
40+
r.append(GraphQLInt.serialize(i))
41+
return r
42+
benchmark(convert_list)
43+
def test_big_list_of_containers_with_one_field(benchmark):
44+
Container = namedtuple('Container', 'x y z o')
45+
46+
ContainerType = GraphQLObjectType('Container', fields={
47+
'x': GraphQLField(GraphQLInt),
48+
'y': GraphQLField(GraphQLInt),
49+
'z': GraphQLField(GraphQLInt),
50+
'o': GraphQLField(GraphQLInt),
51+
})
52+
53+
big_container_list = [Container(x=x, y=x, z=x, o=x) for x in range(5000)]
54+
55+
def resolve_all_containers(root, args, context, info):
56+
return big_container_list
57+
58+
Query = GraphQLObjectType('Query', fields={
59+
'allContainers': GraphQLField(
60+
GraphQLList(ContainerType),
61+
resolver=resolve_all_containers
62+
)
63+
})
64+
hello_schema = GraphQLSchema(Query)
65+
source = Source('{ allContainers { x } }')
66+
ast = parse(source)
67+
big_list_query = partial(execute, hello_schema, ast)
68+
result = benchmark(big_list_query)
69+
# result = big_list_query()
70+
assert not result.errors
71+
assert result.data == {'allContainers': [{'x': c.x} for c in big_container_list]}
72+
73+
74+
def test_big_list_of_containers_with_multiple_fields(benchmark):
75+
Container = namedtuple('Container', 'x y z o')
76+
77+
ContainerType = GraphQLObjectType('Container', fields={
78+
'x': GraphQLField(GraphQLInt),
79+
'y': GraphQLField(GraphQLInt),
80+
'z': GraphQLField(GraphQLInt),
81+
'o': GraphQLField(GraphQLInt),
82+
})
83+
84+
big_container_list = [Container(x=x, y=x, z=x, o=x) for x in range(5000)]
85+
86+
def resolve_all_containers(root, args, context, info):
87+
return big_container_list
88+
89+
Query = GraphQLObjectType('Query', fields={
90+
'allContainers': GraphQLField(
91+
GraphQLList(ContainerType),
92+
resolver=resolve_all_containers
93+
)
94+
})
95+
hello_schema = GraphQLSchema(Query)
96+
source = Source('{ allContainers { x, y, z } }')
97+
ast = parse(source)
98+
big_list_query = partial(execute, hello_schema, ast)
99+
result = benchmark(big_list_query)
100+
# result = big_list_query()
101+
assert not result.errors
102+
assert result.data == {'allContainers': [{'x': c.x, 'y': c.y, 'z': c.z} for c in big_container_list]}

graphql/execution/tests/test_executor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from pytest import raises
44

55
from graphql.error import GraphQLError
6-
from graphql.execution import execute, MiddlewareManager
6+
from graphql.execution import MiddlewareManager, execute
77
from graphql.language.parser import parse
88
from graphql.type import (GraphQLArgument, GraphQLBoolean, GraphQLField,
99
GraphQLInt, GraphQLList, GraphQLObjectType,

graphql/execution/tests/test_variables.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
from collections import OrderedDict
23

34
from pytest import raises
45

@@ -17,12 +18,12 @@
1718
parse_literal=lambda v: 'DeserializedValue' if v.value == 'SerializedValue' else None
1819
)
1920

20-
TestInputObject = GraphQLInputObjectType('TestInputObject', {
21-
'a': GraphQLInputObjectField(GraphQLString),
22-
'b': GraphQLInputObjectField(GraphQLList(GraphQLString)),
23-
'c': GraphQLInputObjectField(GraphQLNonNull(GraphQLString)),
24-
'd': GraphQLInputObjectField(TestComplexScalar)
25-
})
21+
TestInputObject = GraphQLInputObjectType('TestInputObject', OrderedDict([
22+
('a', GraphQLInputObjectField(GraphQLString)),
23+
('b', GraphQLInputObjectField(GraphQLList(GraphQLString))),
24+
('c', GraphQLInputObjectField(GraphQLNonNull(GraphQLString))),
25+
('d', GraphQLInputObjectField(TestComplexScalar))
26+
]))
2627

2728
stringify = lambda obj: json.dumps(obj, sort_keys=True)
2829

graphql/execution/values.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ def coerce_value(type, value):
125125
return [coerce_value(item_type, value)]
126126

127127
if isinstance(type, GraphQLInputObjectType):
128-
fields = type.get_fields()
128+
fields = type.fields
129129
obj = {}
130130
for field_name, field in fields.items():
131131
field_value = coerce_value(field.type, value.get(field_name))

0 commit comments

Comments
 (0)