From 631513fe42cb0b0613349f17118faabf879107ac Mon Sep 17 00:00:00 2001 From: Julien Nakache Date: Fri, 24 Jan 2020 13:34:53 -0500 Subject: [PATCH] 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 --- .travis.yml | 3 - graphene_sqlalchemy/batching.py | 9 +- graphene_sqlalchemy/tests/test_batching.py | 7 +- graphene_sqlalchemy/tests/test_benchmark.py | 226 ++++++++++++++++++++ graphene_sqlalchemy/tests/utils.py | 8 + setup.cfg | 2 +- setup.py | 5 +- tox.ini | 2 +- 8 files changed, 245 insertions(+), 17 deletions(-) create mode 100644 graphene_sqlalchemy/tests/test_benchmark.py diff --git a/.travis.yml b/.travis.yml index 39151a5d..5a988428 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/graphene_sqlalchemy/batching.py b/graphene_sqlalchemy/batching.py index 0665248f..baf01deb 100644 --- a/graphene_sqlalchemy/batching.py +++ b/graphene_sqlalchemy/batching.py @@ -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 @@ -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, diff --git a/graphene_sqlalchemy/tests/test_batching.py b/graphene_sqlalchemy/tests/test_batching.py index 77681069..d8393fb0 100644 --- a/graphene_sqlalchemy/tests/test_batching.py +++ b/graphene_sqlalchemy/tests/test_batching.py @@ -1,7 +1,6 @@ import contextlib import logging -import pkg_resources import pytest import graphene @@ -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): @@ -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) diff --git a/graphene_sqlalchemy/tests/test_benchmark.py b/graphene_sqlalchemy/tests/test_benchmark.py new file mode 100644 index 00000000..1e5ee4f1 --- /dev/null +++ b/graphene_sqlalchemy/tests/test_benchmark.py @@ -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 + } + } + } + } + } + """) diff --git a/graphene_sqlalchemy/tests/utils.py b/graphene_sqlalchemy/tests/utils.py index b59ab0e8..428757c3 100644 --- a/graphene_sqlalchemy/tests/utils.py +++ b/graphene_sqlalchemy/tests/utils.py @@ -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): @@ -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) diff --git a/setup.cfg b/setup.cfg index 880c87d6..4e8e5029 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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 diff --git a/setup.py b/setup.py index 4e7c4f9c..f16c8ff5 100644 --- a/setup.py +++ b/setup.py @@ -30,6 +30,7 @@ "mock==2.0.0", "pytest-cov==2.6.1", "sqlalchemy_utils==0.33.9", + "pytest-benchmark==3.2.1", ] setup( @@ -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", @@ -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, diff --git a/tox.ini b/tox.ini index e55f7d9b..562da2dc 100644 --- a/tox.ini +++ b/tox.ini @@ -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