How to Improve Django Performance. Optimization Tips
I frequently face a recurring situation when developers receive a task to make a performance optimization on Django. Pretty often they are trying to make it in a wrong way. In this short article I want to shed some light on the common mistakes, and show you the way I’m searching for bottlenecks.
Database optimization
I saw how people start the optimization of database queries that take about 5% of all the request for so many times. Unfortunately, most of the developers simply add select_related
/prefetch_related
to their Django QuerySets to decrease the count of queries to the database. That decreases the number of queries indeed, but what about time? Such changes will increase the amount of time needed to complete the request, and what is more important it can increase the time needed on production server significantly.
Never try to optimize queries on your development machine
“The Postgres planner collects statistics about your data that help identify the best possible execution plan for your query. In fact, it will just use heuristics to determine the query plan if the table has little to no data in it. Not only do you need realistic production data in order to analyze reasonable query plans, but also the Postgres server’s configuration has a big effect. For this reason, it’s required that you run your analysis on either the production box, or on a staging box that is configured just like production, and where you’ve restored production data”.
(an excerpt from the Harold’s Geminez article)
In terms of database optimization, I prefer to have the log of long queries and work with it. It doesn’t matter if that’s going to be a NewRelic or just a PostgreSQL log.
Code optimization
Probably, everyone knows about django-extension
with RunProfileServer
but I think that this solution is not very comfortable to work with. It provides you with a lot of data in the format that is quite hard to read.
I use line_profiler instead. This package allows you to check the performance of specific parts of the code. Basically, you have to write the script to evaluate code that you need and put @profile
decorator to methods you are interested in.
As a result, you will receive:
- The amount of time taken by each method
- Total time spent
- Time per hit
- Amount of hits
- Time for each line of the method shown in percents
I use two options to run view in Django project to check performance. The first one is easier but it doesn’t reveal middlewares and Django code. The second one is a bit more complicated but it gives the possibility to measure middlewares.
#!/usr/bin/env python
import os
os.environ.setdefault(
'DJANGO_SETTINGS_MODULE',
'django_classifier_profile.settings'
)
import django
django.setup()
from django.test.client import RequestFactory
from django_classifier_profile.apps.account.models import User
from django_classifier_profile.apps.account.views import ProfileEditView
request_factory = RequestFactory()
user = User.objects.get()
request = request_factory.get('/')
request.session = {}
request.user = user
view = ProfileEditView.as_view()
view(request).render()
Here I create a fake request and call the view directly. We need to call render method of the view to run template rendering and evaluate lazy objects.
#!/usr/bin/env python
import os
os.environ.setdefault(
'DJANGO_SETTINGS_MODULE',
'django_classifier_profile.settings'
)
import django
django.setup()
from django.core.servers.basehttp import get_internal_wsgi_application
from django.test.client import RequestFactory
from django_classifier_profile.apps.account.models import User
request_factory = RequestFactory()
user = User.objects.get()
request = request_factory.get('/')
request.session = {}
request._cached_user = user
#request.user = user
app = get_internal_wsgi_application()
app.get_response(request)
In this script, I use WSGI application to call view and it gives the possibility to evaluate all the Django flow with middlewares and template rendering. To get results you should run two commands only. First one to evaluate profiled code, write and dump statistics to file:
$ kernprof –l <script_name.py>
and the second one to show the results of profiling
$ python -m line_profiler <script_name.py>.lprof
The result will look like this:
Timer unit: 1e-06 s
Total time: 1.4e-05 s
File: /Users/quard/.pyenv/versions/3.5.3/envs/django-classifier-shop/lib/python3.5/site-packages/django/contrib/auth/middleware.py
Function: process_request at line 17
Line # Hits Time Per Hit % Time Line Contents
==============================================================
17 @profile
18 def process_request(self, request):
19 1 2 2.0 14.3 assert hasattr(request, 'session'), (
20 "The Django authentication middleware requires session middleware "
21 "to be installed. Edit your MIDDLEWARE%s setting to insert "
22 "'django.contrib.sessions.middleware.SessionMiddleware' before "
23 "'django.contrib.auth.middleware.AuthenticationMiddleware'."
24 ) % ("_CLASSES" if settings.MIDDLEWARE is None else "")
25 1 12 12.0 85.7 request.user = SimpleLazyObject(lambda: get_user(request))
Total time: 0.005354 s
File: /Users/quard/Projects/learn/django-classifier-profile/django_classifier_profile/apps/account/views.py
Function: get_object at line 18
Line # Hits Time Per Hit % Time Line Contents
==============================================================
18 @profile
19 def get_object(self, queryset=None):
20 if (
21 3 9 3.0 0.2 not self.kwargs.get(self.pk_url_kwarg)
22 1 2 2.0 0.0 and not self.kwargs.get(self.slug_url_kwarg)
23 ):
24 1 9 9.0 0.2 self.kwargs[self.pk_url_kwarg] = self.request.user.pk
25
26 3 5272 1757.3 98.5 user = super(ProfileEditView, self).get_object(queryset=queryset)
27
28 3 60 20.0 1.1 if user != self.request.user and not self.request.user.is_superuser:
29 raise HttpResponseForbidden
30
31 3 2 0.7 0.0 return user
Total time: 0.010449 s
File: /Users/quard/Projects/learn/django-classifier-profile/django_classifier_profile/apps/account/views.py
Function: get_formset at line 59
Line # Hits Time Per Hit % Time Line Contents
==============================================================
59 @profile
60 def get_formset(self):
61 """
62 create formset of attributes with help of custome formset class
63 """
64 1 1 1.0 0.0 FormSetClass = modelformset_factory(
65 1 1 1.0 0.0 UserAttribute,
66 1 1 1.0 0.0 formset=UserClassifierFormSet,
67 1 1 1.0 0.0 form=UserAttributeForm,
68 1 0 0.0 0.0 can_delete=True,
69 1 892 892.0 8.5 extra=0
70 )
This article about django is originally posted on Django Stars blog.