Skip to content

Commit ca2c290

Browse files
author
Ryan P Kilby
authored
Performance test improvements (#157)
* Isolate perf tests into separate build job in CI * Add backend perf test, factor out common code
1 parent 29ba932 commit ca2c290

File tree

5 files changed

+200
-50
lines changed

5 files changed

+200
-50
lines changed

.travis.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,15 @@ install:
1717
- travis_retry pip install -q $DJANGO $REST_FRAMEWORK coverage
1818
- pip install .
1919
script:
20-
- coverage run manage.py test -v 2
20+
- coverage run manage.py test -v 2 $OPTS
2121
- coverage report
2222
after_success:
2323
- bash <(curl -s https://codecov.io/bash)
2424

2525
matrix:
26+
include:
27+
- python: "3.6"
28+
env: DJANGO="django>=1.10.0,<1.11.0" REST_FRAMEWORK="djangorestframework>=3.5,<3.6" OPTS="tests.perf"
2629
exclude:
2730
- python: "3.3"
2831
env: DJANGO="django>=1.10.0,<1.11.0" REST_FRAMEWORK="djangorestframework>=3.5,<3.6"

tests/perf/README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
2+
## Performance Testing
3+
4+
The included tests are an initial pass at guaging the performance of filtering across
5+
relationships via `RelatedFilter`. The intent is to provide some assurance that:
6+
7+
- the package does not perform glaringly worse than pure django-filter.
8+
- new changes do no inadvertantly decrease performance.
9+
10+
11+
### Running the tests
12+
13+
The performance tests have been isolated from the main test suite so that they can be
14+
ran independently. Simply run:
15+
16+
$ python manage.py test tests.perf
17+
18+
19+
### Notes:
20+
21+
Although the performance tests are relatively quick, they will occasionally fail in CI
22+
due to fluctuations in VM performance. The easiest way to reduce the number of random
23+
failures is to simply run the performance tests in a single, separate build.

tests/perf/__init__.py

Whitespace-only changes.

tests/perf/tests.py

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
from __future__ import print_function
2+
from __future__ import absolute_import
3+
4+
import argparse
5+
from timeit import repeat
6+
7+
from django.test import TestCase, Client, override_settings
8+
from rest_framework.test import APIRequestFactory
9+
from tests.perf import views
10+
from tests.testapp import models
11+
12+
client = Client()
13+
factory = APIRequestFactory()
14+
15+
16+
# parse command verbosity level & use for results output
17+
parser = argparse.ArgumentParser()
18+
parser.add_argument(
19+
'-v', '--verbosity', action='store', dest='verbosity', default=1,
20+
type=int, choices=[0, 1, 2, 3],
21+
)
22+
args, _ = parser.parse_known_args()
23+
verbosity = args.verbosity
24+
25+
26+
class PerfTestMixin(object):
27+
"""
28+
This mixin provides common setup for testing the performance differences
29+
between django-filter and django-rest-framework-filters. A callable for
30+
each implementation should be generated that will return semantically
31+
equivalent results.
32+
"""
33+
iterations = 1000
34+
repeat = 5
35+
threshold = 1.0
36+
label = None
37+
38+
@classmethod
39+
def setUpTestData(cls):
40+
bob = models.User.objects.create(username='bob')
41+
joe = models.User.objects.create(username='joe')
42+
43+
models.Note.objects.create(author=bob, title='Note 1')
44+
models.Note.objects.create(author=bob, title='Note 2')
45+
models.Note.objects.create(author=joe, title='Note 3')
46+
models.Note.objects.create(author=joe, title='Note 4')
47+
48+
def get_callable(self, *args):
49+
"""
50+
Returns the callable and callable's *args to be used for each test
51+
iteration. The performance of the callable is what is under test.
52+
"""
53+
raise NotImplementedError
54+
55+
def django_filter_args(self):
56+
"""
57+
Arguments passed to `get_callable()` in order to create
58+
django-filter test iterations.
59+
"""
60+
raise NotImplementedError
61+
62+
def rest_framework_filters_args(self):
63+
"""
64+
Arguments passed to `get_callable()` in order to create
65+
django-rest-framework-filters test iterations.
66+
"""
67+
raise NotImplementedError
68+
69+
def validate_result(self, result):
70+
"""
71+
Provides the validation logic that the sanity test uses to check
72+
its test call results against.
73+
74+
Since the calls for both implementations must be comparable or
75+
at least semantically equivalent, this method should validate
76+
both results.
77+
"""
78+
raise NotImplementedError
79+
80+
def test_sanity(self):
81+
# sanity check to ensure the call results are valid
82+
call, args = self.get_callable(*self.django_filter_args())
83+
self.validate_result(call(*args))
84+
85+
call, args = self.get_callable(*self.rest_framework_filters_args())
86+
self.validate_result(call(*args))
87+
88+
def test_performance(self):
89+
call, args = self.get_callable(*self.django_filter_args())
90+
df_time = min(repeat(
91+
lambda: call(*args),
92+
number=self.iterations,
93+
repeat=self.repeat,
94+
))
95+
96+
call, args = self.get_callable(*self.rest_framework_filters_args())
97+
drf_time = min(repeat(
98+
lambda: call(*args),
99+
number=self.iterations,
100+
repeat=self.repeat,
101+
))
102+
103+
diff = (drf_time - df_time) / df_time * 100.0
104+
105+
if verbosity >= 2:
106+
print('\n' + '-' * 32)
107+
print('%s performance' % self.label)
108+
print('django-filter time:\t%.4fs' % df_time)
109+
print('drf-filters time:\t%.4fs' % drf_time)
110+
print('performance diff:\t%+.2f%% ' % diff)
111+
print('-' * 32)
112+
113+
self.assertTrue(drf_time < (df_time * self.threshold))
114+
115+
116+
class FilterBackendTests(PerfTestMixin, TestCase):
117+
"""
118+
How much faster or slower is drf-filters than django-filter?
119+
"""
120+
threshold = 1.5
121+
label = 'Filter Backend'
122+
123+
def get_callable(self, view_class):
124+
view = view_class(action_map={'get': 'list'})
125+
request = factory.get('/', data={'author__username': 'bob'})
126+
request = view.initialize_request(request)
127+
backend = view.filter_backends[0]
128+
129+
call = backend().filter_queryset
130+
args = [
131+
request, view.get_queryset(), view,
132+
]
133+
134+
return call, args
135+
136+
def django_filter_args(self):
137+
return [views.DFNoteViewSet]
138+
139+
def rest_framework_filters_args(self):
140+
return [views.DRFFNoteViewSet]
141+
142+
def validate_result(self, qs):
143+
self.assertEqual(qs.count(), 2)
144+
145+
146+
@override_settings(ROOT_URLCONF='tests.perf.urls')
147+
class WSGIResponseTests(PerfTestMixin, TestCase):
148+
"""
149+
How much does drf-filters affect the request/response cycle?
150+
151+
This includes response rendering, which provides a more practical
152+
picture of the performance costs of using drf-filters.
153+
"""
154+
threshold = 1.3
155+
label = 'WSGI Response'
156+
157+
def get_callable(self, url):
158+
call = self.client.get
159+
args = [
160+
url, {'author__username': 'bob'},
161+
]
162+
163+
return call, args
164+
165+
def django_filter_args(self):
166+
return ['/df-notes/']
167+
168+
def rest_framework_filters_args(self):
169+
return ['/drf-notes/']
170+
171+
def validate_result(self, response):
172+
self.assertEqual(response.status_code, 200, response.content)
173+
self.assertEqual(len(response.data), 2)

tests/test_performance.py

Lines changed: 0 additions & 49 deletions
This file was deleted.

0 commit comments

Comments
 (0)