| 5 | | [http://www.mysoftparade.com/blog/django-profile-sql-performance/ django-profile.py] is a script that can be used to profile any Django-powered web-site and find how many SQL-queries are used per page, how heavy html-pages are, etc. See also [http://www.djangosnippets.org/snippets/461/ django snippet 461]. |
| | 5 | Python has several profiling modules, the least worst of which is the apparently unmaintained [http://docs.python.org/library/hotshot.html hotshot]. It's better than the Profiler module for our purposes as it has simple 'start' and 'stop' methods, as well as a method that takes a callable and it's arguments. |
| | 6 | |
| | 7 | I wrap views and other callables I want to examine in a decorator like this one: |
| | 8 | |
| | 9 | {{{ |
| | 10 | import hotshot |
| | 11 | import os |
| | 12 | import time |
| | 13 | |
| | 14 | PROFILE_LOG_BASE = "/tmp" |
| | 15 | import settings |
| | 16 | |
| | 17 | |
| | 18 | def profile(log_file): |
| | 19 | """Profile some callable. |
| | 20 | |
| | 21 | This decorator uses the hotshot profiler to profile some callable (like |
| | 22 | a view function or method) and dumps the profile data somewhere sensible |
| | 23 | for later processing and examination. |
| | 24 | |
| | 25 | It takes one argument, the profile log name. If it's a relative path, it |
| | 26 | places it under the PROFILE_LOG_BASE. It also inserts a time stamp into the |
| | 27 | file name, such that 'my_view.prof' become 'my_view-20100211T170321.prof', |
| | 28 | where the time stamp is in UTC. This makes it easy to run and compare |
| | 29 | multiple trials. |
| | 30 | """ |
| | 31 | |
| | 32 | if not os.path.isabs(log_file): |
| | 33 | log_file = os.path.join(PROFILE_LOG_BASE, log_file) |
| | 34 | |
| | 35 | def _outer(f): |
| | 36 | def _inner(*args, **kwargs): |
| | 37 | # Add a timestamp to the profile output when the callable |
| | 38 | # is actually called. |
| | 39 | (base, ext) = os.path.splitext(log_file) |
| | 40 | base = base + "-" + time.strftime("%Y%m%dT%H%M%S", time.gmtime()) |
| | 41 | final_log_file = base + ext |
| | 42 | |
| | 43 | prof = hotshot.Profile(final_log_file) |
| | 44 | try: |
| | 45 | ret = prof.runcall(f, *args, **kwargs) |
| | 46 | finally: |
| | 47 | prof.close() |
| | 48 | return ret |
| | 49 | |
| | 50 | return _inner |
| | 51 | return _outer |
| | 52 | }}} |
| | 53 | |
| | 54 | Set a base path for storing profiling logs in your `settings.py` using `PROFILE_LOG_BASE`, then use it like this: |
| | 55 | |
| | 56 | {{{ |
| | 57 | @profile("my_view.prof") |
| | 58 | def myview(request): |
| | 59 | ... |
| | 60 | }}} |
| | 61 | |
| | 62 | This will produces files of the form `/path/to/logs/my_view-20100211T170321.prof`, so you can test your view under various input conditions and compare the results. |
| | 63 | |
| | 64 | To actually examine the results, you'll need to use the `hotshot.stats` module. A simple script like this: |
| | 65 | |
| | 66 | {{{ |
| | 67 | #!/usr/bin/python |
| | 68 | |
| | 69 | import hotshot.stats |
| | 70 | import sys |
| | 71 | |
| | 72 | stats = hotshot.stats.load(sys.argv[1]) |
| | 73 | #stats.strip_dirs() |
| | 74 | stats.sort_stats('time', 'calls') |
| | 75 | stats.print_stats(20) |
| | 76 | }}} |
| | 77 | |
| | 78 | (`stats.strip_dirs()` results in a more compact output, but I prefer to know which {{{__init__.py}}} I'm dealing with.) |
| | 79 | |
| | 80 | Here's some output from a profile of an authentication module I was having trouble with: |
| | 81 | {{{ |
| | 82 | 7986 function calls (7850 primitive calls) in 1.725 CPU seconds |
| | 83 | |
| | 84 | Ordered by: internal time, call count |
| | 85 | List reduced from 392 to 20 due to restriction <20> |
| | 86 | |
| | 87 | ncalls tottime percall cumtime percall filename:lineno(function) |
| | 88 | 2 1.570 0.785 1.570 0.785 /usr/local/lib/python2.5/django/db/backends/__init__.py:36(_commit) |
| | 89 | 15 0.043 0.003 0.043 0.003 /usr/local/lib/python2.5/linecache.py:68(updatecache) |
| | 90 | 1 0.020 0.020 0.027 0.027 /usr/local/lib/python2.5/django/contrib/auth/models.py:1(<module>) |
| | 91 | 12 0.014 0.001 0.030 0.002 /usr/local/lib/python2.5/django/utils/importlib.py:18(import_module) |
| | 92 | 1013 0.010 0.000 0.010 0.000 /usr/local/lib/python2.5/posixpath.py:56(join) |
| | 93 | }}} |
| | 94 | |
| | 95 | This shows that the culprit is the database commit code; while my queries where fast, actually committing the results to a SQLite database is causing me grief. |
| | 96 | |
| | 97 | You can use [http://code.djangoproject.com/browser/django/trunk/django/bin/profiling/gather_profile_stats.py gather_profile_stats.py] in the Django distribution to aggregate the generated profile logs and open them with `pstats.Stats`. |
| | 98 | |
| | 99 | You can also examine the results with [http://kcachegrind.sourceforge.net/ KCacheGrind] by using it's `hotshot2calltree` conversion scripts. |
| | 100 | |
| | 101 | == Profiling Everything == |
| | 102 | |
| | 103 | === Middleware === |