Skip to content

Commit

Permalink
Add benchmark for connection fields (#259)
Browse files Browse the repository at this point in the history
Add `pytest-benchmark` so we can easily track performance changes over time


Others:
* disable tests for Python 3.4 
* upgrade coveralls
  • Loading branch information
jnak authored Jan 24, 2020
1 parent d90de4a commit 631513f
Show file tree
Hide file tree
Showing 8 changed files with 245 additions and 17 deletions.
3 changes: 0 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ matrix:
- env: TOXENV=py27
python: 2.7
# Python 3.5
- env: TOXENV=py34
python: 3.4
# Python 3.5
- env: TOXENV=py35
python: 3.5
# Python 3.6
Expand Down
9 changes: 6 additions & 3 deletions graphene_sqlalchemy/batching.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@


def get_batch_resolver(relationship_prop):

# Cache this across `batch_load_fn` calls
# This is so SQL string generation is cached under-the-hood via `bakery`
selectin_loader = strategies.SelectInLoader(relationship_prop, (('lazy', 'selectin'),))

class RelationshipLoader(dataloader.DataLoader):
cache = False

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

loader = strategies.SelectInLoader(relationship_prop, (('lazy', 'selectin'),))

# Should the boolean be set to False? Does it matter for our purposes?
states = [(sqlalchemy.inspect(parent), True) for parent in parents]

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

loader._load_for_path(
selectin_loader._load_for_path(
query_context,
parent_mapper._path_registry,
states,
Expand Down
7 changes: 1 addition & 6 deletions graphene_sqlalchemy/tests/test_batching.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import contextlib
import logging

import pkg_resources
import pytest

import graphene
Expand All @@ -10,7 +9,7 @@
from ..fields import BatchSQLAlchemyConnectionField
from ..types import SQLAlchemyObjectType
from .models import Article, HairKind, Pet, Reporter
from .utils import to_std_dicts
from .utils import is_sqlalchemy_version_less_than, to_std_dicts


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


def is_sqlalchemy_version_less_than(version_string):
return pkg_resources.get_distribution('SQLAlchemy').parsed_version < pkg_resources.parse_version(version_string)


if is_sqlalchemy_version_less_than('1.2'):
pytest.skip('SQL batching only works for SQLAlchemy 1.2+', allow_module_level=True)

Expand Down
226 changes: 226 additions & 0 deletions graphene_sqlalchemy/tests/test_benchmark.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
import pytest
from graphql.backend import GraphQLCachedBackend, GraphQLCoreBackend

import graphene
from graphene import relay

from ..fields import BatchSQLAlchemyConnectionField
from ..types import SQLAlchemyObjectType
from .models import Article, HairKind, Pet, Reporter
from .utils import is_sqlalchemy_version_less_than

if is_sqlalchemy_version_less_than('1.2'):
pytest.skip('SQL batching only works for SQLAlchemy 1.2+', allow_module_level=True)


def get_schema():
class ReporterType(SQLAlchemyObjectType):
class Meta:
model = Reporter
interfaces = (relay.Node,)
connection_field_factory = BatchSQLAlchemyConnectionField.from_relationship

class ArticleType(SQLAlchemyObjectType):
class Meta:
model = Article
interfaces = (relay.Node,)
connection_field_factory = BatchSQLAlchemyConnectionField.from_relationship

class PetType(SQLAlchemyObjectType):
class Meta:
model = Pet
interfaces = (relay.Node,)
connection_field_factory = BatchSQLAlchemyConnectionField.from_relationship

class Query(graphene.ObjectType):
articles = graphene.Field(graphene.List(ArticleType))
reporters = graphene.Field(graphene.List(ReporterType))

def resolve_articles(self, info):
return info.context.get('session').query(Article).all()

def resolve_reporters(self, info):
return info.context.get('session').query(Reporter).all()

return graphene.Schema(query=Query)


def benchmark_query(session_factory, benchmark, query):
schema = get_schema()
cached_backend = GraphQLCachedBackend(GraphQLCoreBackend())
cached_backend.document_from_string(schema, query) # Prime cache

@benchmark
def execute_query():
result = schema.execute(
query,
context_value={"session": session_factory()},
backend=cached_backend,
)
assert not result.errors


def test_one_to_one(session_factory, benchmark):
session = session_factory()

reporter_1 = Reporter(
first_name='Reporter_1',
)
session.add(reporter_1)
reporter_2 = Reporter(
first_name='Reporter_2',
)
session.add(reporter_2)

article_1 = Article(headline='Article_1')
article_1.reporter = reporter_1
session.add(article_1)

article_2 = Article(headline='Article_2')
article_2.reporter = reporter_2
session.add(article_2)

session.commit()
session.close()

benchmark_query(session_factory, benchmark, """
query {
reporters {
firstName
favoriteArticle {
headline
}
}
}
""")


def test_many_to_one(session_factory, benchmark):
session = session_factory()

reporter_1 = Reporter(
first_name='Reporter_1',
)
session.add(reporter_1)
reporter_2 = Reporter(
first_name='Reporter_2',
)
session.add(reporter_2)

article_1 = Article(headline='Article_1')
article_1.reporter = reporter_1
session.add(article_1)

article_2 = Article(headline='Article_2')
article_2.reporter = reporter_2
session.add(article_2)

session.commit()
session.close()

benchmark_query(session_factory, benchmark, """
query {
articles {
headline
reporter {
firstName
}
}
}
""")


def test_one_to_many(session_factory, benchmark):
session = session_factory()

reporter_1 = Reporter(
first_name='Reporter_1',
)
session.add(reporter_1)
reporter_2 = Reporter(
first_name='Reporter_2',
)
session.add(reporter_2)

article_1 = Article(headline='Article_1')
article_1.reporter = reporter_1
session.add(article_1)

article_2 = Article(headline='Article_2')
article_2.reporter = reporter_1
session.add(article_2)

article_3 = Article(headline='Article_3')
article_3.reporter = reporter_2
session.add(article_3)

article_4 = Article(headline='Article_4')
article_4.reporter = reporter_2
session.add(article_4)

session.commit()
session.close()

benchmark_query(session_factory, benchmark, """
query {
reporters {
firstName
articles(first: 2) {
edges {
node {
headline
}
}
}
}
}
""")


def test_many_to_many(session_factory, benchmark):
session = session_factory()

reporter_1 = Reporter(
first_name='Reporter_1',
)
session.add(reporter_1)
reporter_2 = Reporter(
first_name='Reporter_2',
)
session.add(reporter_2)

pet_1 = Pet(name='Pet_1', pet_kind='cat', hair_kind=HairKind.LONG)
session.add(pet_1)

pet_2 = Pet(name='Pet_2', pet_kind='cat', hair_kind=HairKind.LONG)
session.add(pet_2)

reporter_1.pets.append(pet_1)
reporter_1.pets.append(pet_2)

pet_3 = Pet(name='Pet_3', pet_kind='cat', hair_kind=HairKind.LONG)
session.add(pet_3)

pet_4 = Pet(name='Pet_4', pet_kind='cat', hair_kind=HairKind.LONG)
session.add(pet_4)

reporter_2.pets.append(pet_3)
reporter_2.pets.append(pet_4)

session.commit()
session.close()

benchmark_query(session_factory, benchmark, """
query {
reporters {
firstName
pets(first: 2) {
edges {
node {
name
}
}
}
}
}
""")
8 changes: 8 additions & 0 deletions graphene_sqlalchemy/tests/utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import pkg_resources


def to_std_dicts(value):
"""Convert nested ordered dicts to normal dicts for better comparison."""
if isinstance(value, dict):
Expand All @@ -6,3 +9,8 @@ def to_std_dicts(value):
return [to_std_dicts(v) for v in value]
else:
return value


def is_sqlalchemy_version_less_than(version_string):
"""Check the installed SQLAlchemy version"""
return pkg_resources.get_distribution('SQLAlchemy').parsed_version < pkg_resources.parse_version(version_string)
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ max-line-length = 120
no_lines_before=FIRSTPARTY
known_graphene=graphene,graphql_relay,flask_graphql,graphql_server,sphinx_graphene_theme
known_first_party=graphene_sqlalchemy
known_third_party=app,database,flask,mock,models,nameko,pkg_resources,promise,pytest,schema,setuptools,singledispatch,six,sqlalchemy,sqlalchemy_utils
known_third_party=app,database,flask,graphql,mock,models,nameko,pkg_resources,promise,pytest,schema,setuptools,singledispatch,six,sqlalchemy,sqlalchemy_utils
sections=FUTURE,STDLIB,THIRDPARTY,GRAPHENE,FIRSTPARTY,LOCALFOLDER
skip_glob=examples/nameko_sqlalchemy

Expand Down
5 changes: 2 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"mock==2.0.0",
"pytest-cov==2.6.1",
"sqlalchemy_utils==0.33.9",
"pytest-benchmark==3.2.1",
]

setup(
Expand All @@ -48,8 +49,6 @@
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.3",
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
Expand All @@ -61,7 +60,7 @@
extras_require={
"dev": [
"tox==3.7.0", # Should be kept in sync with tox.ini
"coveralls==1.7.0",
"coveralls==1.10.0",
"pre-commit==1.14.4",
],
"test": tests_require,
Expand Down
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[tox]
envlist = pre-commit,py{27,34,35,36,37}-sql{11,12,13}
envlist = pre-commit,py{27,35,36,37}-sql{11,12,13}
skipsdist = true
minversion = 3.7.0

Expand Down

0 comments on commit 631513f

Please sign in to comment.