""" Middleware that profiles the request with cProfile and displays profiling information at the bottom of each page. """ import sys import os import threading import cgi import time from cStringIO import StringIO from paste import response try: # Included in Python 2.5 import cProfile except: try: # Included in lsprof package for Python 2.4 import pkg_resources pkg_resources.require( "lsprof" ) import cProfile except: cProfile = None import pstats template = """
show profile output: inline | new window
""" class ProfileMiddleware(object): """ Middleware that profiles all requests. All HTML pages will have profiling information appended to them. The data is isolated to that single request, and does not include data from previous requests. """ def __init__( self, app, global_conf=None, limit=40 ): self.app = app self.lock = threading.Lock() self.limit = limit def __call__(self, environ, start_response): catch_response = [] body = [] def replace_start_response(status, headers, exc_info=None): catch_response.extend([status, headers]) start_response(status, headers, exc_info) return body.append def run_app(): body.extend(self.app(environ, replace_start_response)) # Run in profiler prof = cProfile.Profile() prof.runctx( "run_app()", globals(), locals() ) # Build up body with stats body = ''.join(body) headers = catch_response[1] content_type = response.header_value(headers, 'content-type') if not content_type.startswith('text/html'): # We can't add info to non-HTML output return [body] stats = pstats.Stats( prof ) stats.strip_dirs() stats.sort_stats( 'time', 'calls' ) output = pstats_as_html( stats, self.limit ) body += template % output return [body] def pstats_as_html( stats, *sel_list ): """ Return an HTML representation of a pstats.Stats object. """ rval = [] # Number of function calls, primitive calls, total time rval.append( "
%d function calls (%d primitive) in %0.3f CPU seconds
" % ( stats.total_calls, stats.prim_calls, stats.total_tt ) ) # Extract functions that match 'sel_list' funcs, order_message, select_message = get_func_list( stats, sel_list ) # Deal with any ordering or selection messages if order_message: rval.append( "
%s
" % cgi.escape( order_message ) ) if select_message: rval.append( "
%s
" % cgi.escape( select_message ) ) # Build a table for the functions if list: rval.append( "" ) # Header rval.append( "" "" "" "" "" "" ) for func in funcs: rval.append( "" ) # Calculate each field cc, nc, tt, ct, callers = stats.stats[ func ] # ncalls ncalls = str(nc) if nc != cc: ncalls = ncalls + '/' + str(cc) rval.append( "" % cgi.escape( ncalls ) ) # tottime rval.append( "" % tt ) # percall if nc == 0: percall = "" else: percall = "%0.8f" % ( tt / nc ) rval.append( "" % cgi.escape( percall ) ) # cumtime rval.append( "" % ct ) # ctpercall if cc == 0: ctpercall = "" else: ctpercall = "%0.8f" % ( ct / cc ) rval.append( "" % cgi.escape( ctpercall ) ) # location rval.append( "" % cgi.escape( func_std_string( func ) ) ) # row complete rval.append( "" ) rval.append( "
ncallstottimepercallcumtimepercallfilename:lineno(function)
%s%0.8f%s%0.8f%s%s
") # Concatenate result return "".join( rval ) def get_func_list( stats, sel_list ): """ Use 'sel_list' to select a list of functions to display. """ # Determine if an ordering was applied if stats.fcn_list: list = stats.fcn_list[:] order_message = "Ordered by: " + stats.sort_type else: list = stats.stats.keys() order_message = "Random listing order was used" # Do the selection and accumulate messages select_message = "" for selection in sel_list: list, select_message = stats.eval_print_amount( selection, list, select_message ) # Return the list of functions selected and the message return list, order_message, select_message def func_std_string( func_name ): """ Match what old profile produced """ if func_name[:2] == ('~', 0): # special case for built-in functions name = func_name[2] if name.startswith('<') and name.endswith('>'): return '{%s}' % name[1:-1] else: return name else: return "%s:%d(%s)" % func_name