# profile_middleware - a middleware that profiles views # # Inspired by udfalkso's http://www.djangosnippets.org/snippets/186/ # and the Shwagroo Team's http://www.djangosnippets.org/snippets/605/ # # Install this by adding it to your MIDDLEWARE_CLASSES. It is active # if you are logged in as a superuser, or always when settings.DEBUG # is True. # # To use it, pass 'profile=1' as a GET or POST parameter to any HTTP # request. from base64 import b64decode, b64encode import cPickle from cStringIO import StringIO from decimal import Decimal import hotshot, hotshot.stats import pprint import sys import tempfile from django.conf import settings from django.core.exceptions import MiddlewareNotUsed from django.db import connection, reset_queries from django.http import HttpResponse from django.utils import html class StdoutWrapper(object): """Simple wrapper to capture and overload sys.stdout""" def __init__(self): self.stdout = sys.stdout self.stream = StringIO() sys.stdout = self.stream def __del__(self): if self.stdout is not None: sys.stdout = self.stdout def __str__(self): return self.stream.getvalue() def render_stats(stats, sort, format): """ Returns a StringIO containing the formatted statistics from _statsfile_. _sort_ is a list of fields to sort by. _format_ is the name of the method that pstats uses to format the data. """ output = StdoutWrapper() if hasattr(stats, "stream"): stats.stream = output.stream stats.sort_stats(*sort) getattr(stats, format)() return output.stream def render_queries(queries, sort): """ Returns a StringIO containing the formatted SQL queries. _sort_ is a field to sort by. """ output = StringIO() if sort == 'order': print >>output, " time query" for query in queries: print >>output, " %8s %s" % (query["time"], query["sql"]) return output if sort == 'time': def sorter(x, y): return cmp(x[1][1], y[1][1]) elif sort == 'queries': def sorter(x, y): return cmp(x[1][0], y[1][0]) else: raise RuntimeError("Unknown sort: %s" % sort) print >>output, " queries time query" results = {} for query in queries: try: result = results[query["sql"]] result[0] += 1 result[1] += Decimal(query["time"]) except KeyError: results[query["sql"]] = [1, Decimal(query["time"])] results = sorted(results.iteritems(), cmp=sorter, reverse=True) for result in results: print >>output, " %8d %8.3f %s" % (result[1][0], result[1][1], result[0]) return output def pickle_stats(stats): """Pickle a pstats.Stats object""" if hasattr(stats, "stream"): del stats.stream return cPickle.dumps(stats) def unpickle_stats(stats): """Unpickle a pstats.Stats object""" stats = cPickle.loads(stats) stats.stream = True return stats class RadioButton(object): """Generate the HTML for a radio button.""" def __init__(self, name, value, description=None, checked=False): self.name = name self.value = value if description is None: self.description = value else: self.description = description self.checked = checked def __str__(self): checked = "" if self.checked: checked = "checked='checked'" return ("" "%(description)s" "
" % {'name': self.name, 'value': self.value, 'checked': checked, 'description': self.description}) class RadioButtons(object): """Generate the HTML for a list of radio buttons.""" def __init__(self, name, checked, values): self.result = [] for v in values: description = None if isinstance(v, (list, tuple)): value = v[0] description = v[1] else: value = v select = False if value == checked: select = True self.result.append(RadioButton(name, value, description, select)) def __str__(self): return "\n".join([str(button) for button in self.result]) stats_template = """ Profile for %(url)s
Sort by %(sort_first_buttons)s
then by %(sort_second_buttons)s
Format %(format_buttons)s

%(stats)s
""" sort_categories = (('time', 'internal time'), ('cumulative', 'cumulative time'), ('calls', 'call count'), ('pcalls', 'primitive call count'), ('file', 'file name'), ('nfl', 'name/file/line'), ('stdname', 'standard name'), ('name', 'function name')) def display_stats(request, stats, queries): """ Generate a HttpResponse of functions for a profiling run. _stats_ should contain a pstats.Stats of a hotshot session. _queries_ should contain a list of SQL queries. """ sort = [request.REQUEST.get('sort_first', 'time'), request.REQUEST.get('sort_second', 'calls')] format = request.REQUEST.get('format', 'print_stats') sort_first_buttons = RadioButtons('sort_first', sort[0], sort_categories) sort_second_buttons = RadioButtons('sort_second', sort[1], sort_categories) format_buttons = RadioButtons('format', format, (('print_stats', 'by function'), ('print_callers', 'by callers'), ('print_callees', 'by callees'))) output = render_stats(stats, sort, format) output.reset() output = [html.escape(unicode(line)) for line in output.readlines()] response = HttpResponse(mimetype='text/html; charset=utf-8') response.content = (stats_template % {'format_buttons': format_buttons, 'sort_first_buttons': sort_first_buttons, 'sort_second_buttons': sort_second_buttons, 'rawqueries' : b64encode(cPickle.dumps(queries)), 'rawstats': b64encode(pickle_stats(stats)), 'stats': "".join(output), 'url': request.path}) return response queries_template = """ SQL Queries for %(url)s
Sort by %(sort_buttons)s

%(num_queries)d SQL queries:
%(queries)s
""" def display_queries(request, stats, queries): """ Generate a HttpResponse of SQL queries for a profiling run. _stats_ should contain a pstats.Stats of a hotshot session. _queries_ should contain a list of SQL queries. """ sort = request.REQUEST.get('sort_by', 'time') sort_buttons = RadioButtons('sort_by', sort, (('order', 'by order'), ('time', 'time'), ('queries', 'query count'))) output = render_queries(queries, sort) output.reset() output = [html.escape(unicode(line)) for line in output.readlines()] response = HttpResponse(mimetype='text/html; charset=utf-8') response.content = (queries_template % {'sort_buttons': sort_buttons, 'num_queries': len(queries), 'queries': "".join(output), 'rawqueries' : b64encode(cPickle.dumps(queries)), 'rawstats': b64encode(pickle_stats(stats)), 'url': request.path}) return response class ProfileMiddleware(object): """ Displays hotshot profiling for any view. http://yoursite.com/yourview/?profile=1 WARNING: It uses hotshot profiler which is not thread safe. """ def process_request(self, request): """ Setup the profiler for a profiling run and clear the SQL query log. If this is a resort of an existing profiling run, just return the resorted list. """ def unpickle(params): stats = unpickle_stats(b64decode(params.get('stats', ''))) queries = cPickle.loads(b64decode(params.get('queries', ''))) return stats, queries if request.method != 'GET' and \ not (request.META.get('HTTP_CONTENT_TYPE', request.META.get('CONTENT_TYPE', '')) in ['multipart/form-data', 'application/x-www-form-urlencoded']): return if (request.REQUEST.get('profile', False) and (settings.DEBUG == True or request.user.is_staff)): request.statsfile = tempfile.NamedTemporaryFile() params = request.REQUEST if (params.get('show_stats', False) and params.get('show_queries', '1') == '1'): # Instantly re-sort the existing stats data stats, queries = unpickle(params) return display_stats(request, stats, queries) elif (params.get('show_queries', False) and params.get('show_stats', '1') == '1'): stats, queries = unpickle(params) return display_queries(request, stats, queries) else: # We don't have previous data, so initialize the profiler request.profiler = hotshot.Profile(request.statsfile.name) reset_queries() def process_view(self, request, view_func, view_args, view_kwargs): """Run the profiler on _view_func_.""" profiler = getattr(request, 'profiler', None) if profiler: # Make sure profiler variables don't get passed into view_func original_get = request.GET request.GET = original_get.copy() request.GET.pop('profile', None) request.GET.pop('show_queries', None) request.GET.pop('show_stats', None) try: return profiler.runcall(view_func, request, *view_args, **view_kwargs) finally: request.GET = original_get def process_response(self, request, response): """Finish profiling and render the results.""" profiler = getattr(request, 'profiler', None) if profiler: profiler.close() params = request.REQUEST stats = hotshot.stats.load(request.statsfile.name) queries = connection.queries if (params.get('show_queries', False) and params.get('show_stats', '1') == '1'): response = display_queries(request, stats, queries) else: response = display_stats(request, stats, queries) return response