Skip to content

Commit 631513f

Browse files
authored
Add benchmark for connection fields (#259)
Add `pytest-benchmark` so we can easily track performance changes over time Others: * disable tests for Python 3.4 * upgrade coveralls
1 parent d90de4a commit 631513f

File tree

8 files changed

+245
-17
lines changed

8 files changed

+245
-17
lines changed

Diff for: .travis.yml

-3
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@ matrix:
55
- env: TOXENV=py27
66
python: 2.7
77
# Python 3.5
8-
- env: TOXENV=py34
9-
python: 3.4
10-
# Python 3.5
118
- env: TOXENV=py35
129
python: 3.5
1310
# Python 3.6

Diff for: graphene_sqlalchemy/batching.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55

66

77
def get_batch_resolver(relationship_prop):
8+
9+
# Cache this across `batch_load_fn` calls
10+
# This is so SQL string generation is cached under-the-hood via `bakery`
11+
selectin_loader = strategies.SelectInLoader(relationship_prop, (('lazy', 'selectin'),))
12+
813
class RelationshipLoader(dataloader.DataLoader):
914
cache = False
1015

@@ -43,15 +48,13 @@ def batch_load_fn(self, parents): # pylint: disable=method-hidden
4348
# The behavior of `selectin` is undefined if the parent is dirty
4449
assert parent not in session.dirty
4550

46-
loader = strategies.SelectInLoader(relationship_prop, (('lazy', 'selectin'),))
47-
4851
# Should the boolean be set to False? Does it matter for our purposes?
4952
states = [(sqlalchemy.inspect(parent), True) for parent in parents]
5053

5154
# For our purposes, the query_context will only used to get the session
5255
query_context = QueryContext(session.query(parent_mapper.entity))
5356

54-
loader._load_for_path(
57+
selectin_loader._load_for_path(
5558
query_context,
5659
parent_mapper._path_registry,
5760
states,

Diff for: graphene_sqlalchemy/tests/test_batching.py

+1-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import contextlib
22
import logging
33

4-
import pkg_resources
54
import pytest
65

76
import graphene
@@ -10,7 +9,7 @@
109
from ..fields import BatchSQLAlchemyConnectionField
1110
from ..types import SQLAlchemyObjectType
1211
from .models import Article, HairKind, Pet, Reporter
13-
from .utils import to_std_dicts
12+
from .utils import is_sqlalchemy_version_less_than, to_std_dicts
1413

1514

1615
class MockLoggingHandler(logging.Handler):
@@ -71,10 +70,6 @@ def resolve_reporters(self, info):
7170
return graphene.Schema(query=Query)
7271

7372

74-
def is_sqlalchemy_version_less_than(version_string):
75-
return pkg_resources.get_distribution('SQLAlchemy').parsed_version < pkg_resources.parse_version(version_string)
76-
77-
7873
if is_sqlalchemy_version_less_than('1.2'):
7974
pytest.skip('SQL batching only works for SQLAlchemy 1.2+', allow_module_level=True)
8075

Diff for: graphene_sqlalchemy/tests/test_benchmark.py

+226
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
import pytest
2+
from graphql.backend import GraphQLCachedBackend, GraphQLCoreBackend
3+
4+
import graphene
5+
from graphene import relay
6+
7+
from ..fields import BatchSQLAlchemyConnectionField
8+
from ..types import SQLAlchemyObjectType
9+
from .models import Article, HairKind, Pet, Reporter
10+
from .utils import is_sqlalchemy_version_less_than
11+
12+
if is_sqlalchemy_version_less_than('1.2'):
13+
pytest.skip('SQL batching only works for SQLAlchemy 1.2+', allow_module_level=True)
14+
15+
16+
def get_schema():
17+
class ReporterType(SQLAlchemyObjectType):
18+
class Meta:
19+
model = Reporter
20+
interfaces = (relay.Node,)
21+
connection_field_factory = BatchSQLAlchemyConnectionField.from_relationship
22+
23+
class ArticleType(SQLAlchemyObjectType):
24+
class Meta:
25+
model = Article
26+
interfaces = (relay.Node,)
27+
connection_field_factory = BatchSQLAlchemyConnectionField.from_relationship
28+
29+
class PetType(SQLAlchemyObjectType):
30+
class Meta:
31+
model = Pet
32+
interfaces = (relay.Node,)
33+
connection_field_factory = BatchSQLAlchemyConnectionField.from_relationship
34+
35+
class Query(graphene.ObjectType):
36+
articles = graphene.Field(graphene.List(ArticleType))
37+
reporters = graphene.Field(graphene.List(ReporterType))
38+
39+
def resolve_articles(self, info):
40+
return info.context.get('session').query(Article).all()
41+
42+
def resolve_reporters(self, info):
43+
return info.context.get('session').query(Reporter).all()
44+
45+
return graphene.Schema(query=Query)
46+
47+
48+
def benchmark_query(session_factory, benchmark, query):
49+
schema = get_schema()
50+
cached_backend = GraphQLCachedBackend(GraphQLCoreBackend())
51+
cached_backend.document_from_string(schema, query) # Prime cache
52+
53+
@benchmark
54+
def execute_query():
55+
result = schema.execute(
56+
query,
57+
context_value={"session": session_factory()},
58+
backend=cached_backend,
59+
)
60+
assert not result.errors
61+
62+
63+
def test_one_to_one(session_factory, benchmark):
64+
session = session_factory()
65+
66+
reporter_1 = Reporter(
67+
first_name='Reporter_1',
68+
)
69+
session.add(reporter_1)
70+
reporter_2 = Reporter(
71+
first_name='Reporter_2',
72+
)
73+
session.add(reporter_2)
74+
75+
article_1 = Article(headline='Article_1')
76+
article_1.reporter = reporter_1
77+
session.add(article_1)
78+
79+
article_2 = Article(headline='Article_2')
80+
article_2.reporter = reporter_2
81+
session.add(article_2)
82+
83+
session.commit()
84+
session.close()
85+
86+
benchmark_query(session_factory, benchmark, """
87+
query {
88+
reporters {
89+
firstName
90+
favoriteArticle {
91+
headline
92+
}
93+
}
94+
}
95+
""")
96+
97+
98+
def test_many_to_one(session_factory, benchmark):
99+
session = session_factory()
100+
101+
reporter_1 = Reporter(
102+
first_name='Reporter_1',
103+
)
104+
session.add(reporter_1)
105+
reporter_2 = Reporter(
106+
first_name='Reporter_2',
107+
)
108+
session.add(reporter_2)
109+
110+
article_1 = Article(headline='Article_1')
111+
article_1.reporter = reporter_1
112+
session.add(article_1)
113+
114+
article_2 = Article(headline='Article_2')
115+
article_2.reporter = reporter_2
116+
session.add(article_2)
117+
118+
session.commit()
119+
session.close()
120+
121+
benchmark_query(session_factory, benchmark, """
122+
query {
123+
articles {
124+
headline
125+
reporter {
126+
firstName
127+
}
128+
}
129+
}
130+
""")
131+
132+
133+
def test_one_to_many(session_factory, benchmark):
134+
session = session_factory()
135+
136+
reporter_1 = Reporter(
137+
first_name='Reporter_1',
138+
)
139+
session.add(reporter_1)
140+
reporter_2 = Reporter(
141+
first_name='Reporter_2',
142+
)
143+
session.add(reporter_2)
144+
145+
article_1 = Article(headline='Article_1')
146+
article_1.reporter = reporter_1
147+
session.add(article_1)
148+
149+
article_2 = Article(headline='Article_2')
150+
article_2.reporter = reporter_1
151+
session.add(article_2)
152+
153+
article_3 = Article(headline='Article_3')
154+
article_3.reporter = reporter_2
155+
session.add(article_3)
156+
157+
article_4 = Article(headline='Article_4')
158+
article_4.reporter = reporter_2
159+
session.add(article_4)
160+
161+
session.commit()
162+
session.close()
163+
164+
benchmark_query(session_factory, benchmark, """
165+
query {
166+
reporters {
167+
firstName
168+
articles(first: 2) {
169+
edges {
170+
node {
171+
headline
172+
}
173+
}
174+
}
175+
}
176+
}
177+
""")
178+
179+
180+
def test_many_to_many(session_factory, benchmark):
181+
session = session_factory()
182+
183+
reporter_1 = Reporter(
184+
first_name='Reporter_1',
185+
)
186+
session.add(reporter_1)
187+
reporter_2 = Reporter(
188+
first_name='Reporter_2',
189+
)
190+
session.add(reporter_2)
191+
192+
pet_1 = Pet(name='Pet_1', pet_kind='cat', hair_kind=HairKind.LONG)
193+
session.add(pet_1)
194+
195+
pet_2 = Pet(name='Pet_2', pet_kind='cat', hair_kind=HairKind.LONG)
196+
session.add(pet_2)
197+
198+
reporter_1.pets.append(pet_1)
199+
reporter_1.pets.append(pet_2)
200+
201+
pet_3 = Pet(name='Pet_3', pet_kind='cat', hair_kind=HairKind.LONG)
202+
session.add(pet_3)
203+
204+
pet_4 = Pet(name='Pet_4', pet_kind='cat', hair_kind=HairKind.LONG)
205+
session.add(pet_4)
206+
207+
reporter_2.pets.append(pet_3)
208+
reporter_2.pets.append(pet_4)
209+
210+
session.commit()
211+
session.close()
212+
213+
benchmark_query(session_factory, benchmark, """
214+
query {
215+
reporters {
216+
firstName
217+
pets(first: 2) {
218+
edges {
219+
node {
220+
name
221+
}
222+
}
223+
}
224+
}
225+
}
226+
""")

Diff for: graphene_sqlalchemy/tests/utils.py

+8
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import pkg_resources
2+
3+
14
def to_std_dicts(value):
25
"""Convert nested ordered dicts to normal dicts for better comparison."""
36
if isinstance(value, dict):
@@ -6,3 +9,8 @@ def to_std_dicts(value):
69
return [to_std_dicts(v) for v in value]
710
else:
811
return value
12+
13+
14+
def is_sqlalchemy_version_less_than(version_string):
15+
"""Check the installed SQLAlchemy version"""
16+
return pkg_resources.get_distribution('SQLAlchemy').parsed_version < pkg_resources.parse_version(version_string)

Diff for: setup.cfg

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ max-line-length = 120
99
no_lines_before=FIRSTPARTY
1010
known_graphene=graphene,graphql_relay,flask_graphql,graphql_server,sphinx_graphene_theme
1111
known_first_party=graphene_sqlalchemy
12-
known_third_party=app,database,flask,mock,models,nameko,pkg_resources,promise,pytest,schema,setuptools,singledispatch,six,sqlalchemy,sqlalchemy_utils
12+
known_third_party=app,database,flask,graphql,mock,models,nameko,pkg_resources,promise,pytest,schema,setuptools,singledispatch,six,sqlalchemy,sqlalchemy_utils
1313
sections=FUTURE,STDLIB,THIRDPARTY,GRAPHENE,FIRSTPARTY,LOCALFOLDER
1414
skip_glob=examples/nameko_sqlalchemy
1515

Diff for: setup.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"mock==2.0.0",
3131
"pytest-cov==2.6.1",
3232
"sqlalchemy_utils==0.33.9",
33+
"pytest-benchmark==3.2.1",
3334
]
3435

3536
setup(
@@ -48,8 +49,6 @@
4849
"Programming Language :: Python :: 2",
4950
"Programming Language :: Python :: 2.7",
5051
"Programming Language :: Python :: 3",
51-
"Programming Language :: Python :: 3.3",
52-
"Programming Language :: Python :: 3.4",
5352
"Programming Language :: Python :: 3.5",
5453
"Programming Language :: Python :: 3.6",
5554
"Programming Language :: Python :: 3.7",
@@ -61,7 +60,7 @@
6160
extras_require={
6261
"dev": [
6362
"tox==3.7.0", # Should be kept in sync with tox.ini
64-
"coveralls==1.7.0",
63+
"coveralls==1.10.0",
6564
"pre-commit==1.14.4",
6665
],
6766
"test": tests_require,

Diff for: tox.ini

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[tox]
2-
envlist = pre-commit,py{27,34,35,36,37}-sql{11,12,13}
2+
envlist = pre-commit,py{27,35,36,37}-sql{11,12,13}
33
skipsdist = true
44
minversion = 3.7.0
55

0 commit comments

Comments
 (0)