Skip to content

Instantly share code, notes, and snippets.

@versae
Created November 4, 2012 15:20
Show Gist options
  • Save versae/4012267 to your computer and use it in GitHub Desktop.
Save versae/4012267 to your computer and use it in GitHub Desktop.
Global Profiling for Django using `hotshot`, including the result as HTML comments, even in XML and JSON responses.

Adapted from this: http://www.no-ack.org/2010/12/yet-another-profiling-middleware-for.html Settings:

  • PROFILE_MIDDLEWARE_SORT: A list of criteria according to which the profiler stats will be sorted. Equivalent to Stats.sort_stats.
  • PROFILE_MIDDLEWARE_RESTRICTIONS: A list of restrictions used to limit the profiler stats. Each restriction is either an integer (to select a count of lines) or a decimal fraction between 0.0 and 1.0 inclusive (to select a percentage of lines), or a regular expression (to pattern match the standard name that is printed). Equivalent to the arguments passed to Stats.print_stats.
  • PROFILE_MIDDLEWARE_STRIP_DIRS: If set to True, removes all leading path information from filenames in the profiler stats. Equivalent to Stats.strip_dirs.
  • PROFILE_MIDDLEWARE_JSON: Enable the output for JSON responses.
  • ENABLE_PROFILE: Enable the middleware.
# Adapted from http://www.no-ack.org/2010/12/yet-another-profiling-middleware-for.html
import os
import re
import tempfile
from cStringIO import StringIO
import hotshot
import hotshot.stats
from django.conf import settings
from django.core.exceptions import MiddlewareNotUsed
COMMENT_SYNTAX = (
(re.compile(r'^application/(.*\+)?xml|text/html$', re.I),
'<!--', '-->'),
)
if getattr(settings, "PROFILE_MIDDLEWARE_JSON", True):
COMMENT_SYNTAX += (
(re.compile(r'^application/j(avascript|son)$', re.I),
'/*', '*/'),
)
class ProfileMiddleware(object):
def __init__(self):
if not getattr(settings, "ENABLE_PROFILE", False):
raise MiddlewareNotUsed("Profiling middleware not used")
def process_view(self, request, callback, args, kwargs):
# Create a profile, writing into a temporary file.
filename = tempfile.mktemp()
profile = hotshot.Profile(filename)
try:
try:
# Profile the call of the view function.
response = profile.runcall(callback, request, *args, **kwargs)
# If we have got a 3xx status code, further
# action needs to be taken by the user agent
# in order to fulfill the request. So don't
# attach any stats to the content, because of
# the content is supposed to be empty and is
# ignored by the user agent.
if response.status_code // 100 == 3:
return response
# Detect the appropriate syntax based on the
# Content-Type header.
for regex, begin_comment, end_comment in COMMENT_SYNTAX:
split_response = response['Content-Type'].split(';')
if regex.match(split_response[0].strip()):
break
else:
# If the given Content-Type is not
# supported, don't attach any stats to
# the content and return the unchanged
# response.
return response
# The response can hold an iterator, that
# is executed when the content property
# is accessed. So we also have to profile
# the call of the content property.
content = profile.runcall(response.__class__.content.fget,
response)
finally:
profile.close()
# Load the stats from the temporary file and
# write them in a human readable format,
# respecting some optional settings into a
# StringIO object.
stats = hotshot.stats.load(filename)
if getattr(settings, 'PROFILE_MIDDLEWARE_STRIP_DIRS', False):
stats.strip_dirs()
if getattr(settings, 'PROFILE_MIDDLEWARE_SORT', None):
stats.sort_stats(*settings.PROFILE_MIDDLEWARE_SORT)
stats.stream = StringIO()
stats.print_stats(*getattr(settings,
'PROFILE_MIDDLEWARE_RESTRICTIONS', []))
finally:
os.unlink(filename)
# Construct an HTML/XML or Javascript comment, with
# the formatted stats, written to the StringIO object
# and attach it to the content of the response.
comment = '\n%s\n\n%s\n\n%s\n' % (begin_comment,
stats.stream.getvalue().strip(),
end_comment)
response.content = content + comment
# If the Content-Length header is given, add the
# number of bytes we have added to it. If the
# Content-Length header is ommited or incorrect,
# it remains so in order to don't change the
# behaviour of the web server or user agent.
if response.has_header('Content-Length'):
content_lenght = int(response['Content-Length']) + len(comment)
response['Content-Length'] = content_lenght
return response
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment