[2] | 1 | """ |
---|
| 2 | Middleware that profiles the request with cProfile and displays profiling |
---|
| 3 | information at the bottom of each page. |
---|
| 4 | """ |
---|
| 5 | |
---|
| 6 | import sys |
---|
| 7 | import os |
---|
| 8 | import threading |
---|
| 9 | import cgi |
---|
| 10 | import time |
---|
| 11 | from cStringIO import StringIO |
---|
| 12 | from paste import response |
---|
| 13 | |
---|
| 14 | try: |
---|
| 15 | # Included in Python 2.5 |
---|
| 16 | import cProfile |
---|
| 17 | except: |
---|
| 18 | try: |
---|
| 19 | # Included in lsprof package for Python 2.4 |
---|
| 20 | import pkg_resources |
---|
| 21 | pkg_resources.require( "lsprof" ) |
---|
| 22 | import cProfile |
---|
| 23 | except: |
---|
| 24 | cProfile = None |
---|
| 25 | |
---|
| 26 | import pstats |
---|
| 27 | |
---|
| 28 | template = """ |
---|
| 29 | <script> |
---|
| 30 | function show_profile_output() |
---|
| 31 | { |
---|
| 32 | var win = window.open("", "win"); // a window object |
---|
| 33 | var doc = win.document; |
---|
| 34 | doc.open("text/html", "replace"); |
---|
| 35 | doc.write("<HTML><HEAD><TITLE>Profiler output</TITLE></HEAD><BODY>") |
---|
| 36 | doc.write(document.getElementById( 'profile_output' ).innerHTML) |
---|
| 37 | doc.write("</BODY></HTML>"); |
---|
| 38 | doc.close(); |
---|
| 39 | } |
---|
| 40 | function show_inline() |
---|
| 41 | { |
---|
| 42 | document.getElementById( 'profile_output' ).style.display="block"; |
---|
| 43 | } |
---|
| 44 | </script> |
---|
| 45 | <div style="background-color: #ff9; color: #000; border: 2px solid #000; padding: 5px;"> |
---|
| 46 | show profile output: <a href="javascript:show_inline();">inline</a> | <a href="javascript:show_profile_output();">new window</a> |
---|
| 47 | <div id="profile_output" style="display: none"> |
---|
| 48 | <hr /> |
---|
| 49 | %s |
---|
| 50 | </div> |
---|
| 51 | </div> |
---|
| 52 | """ |
---|
| 53 | |
---|
| 54 | class ProfileMiddleware(object): |
---|
| 55 | |
---|
| 56 | """ |
---|
| 57 | Middleware that profiles all requests. |
---|
| 58 | |
---|
| 59 | All HTML pages will have profiling information appended to them. |
---|
| 60 | The data is isolated to that single request, and does not include |
---|
| 61 | data from previous requests. |
---|
| 62 | """ |
---|
| 63 | |
---|
| 64 | def __init__( self, app, global_conf=None, limit=40 ): |
---|
| 65 | self.app = app |
---|
| 66 | self.lock = threading.Lock() |
---|
| 67 | self.limit = limit |
---|
| 68 | |
---|
| 69 | def __call__(self, environ, start_response): |
---|
| 70 | catch_response = [] |
---|
| 71 | body = [] |
---|
| 72 | def replace_start_response(status, headers, exc_info=None): |
---|
| 73 | catch_response.extend([status, headers]) |
---|
| 74 | start_response(status, headers, exc_info) |
---|
| 75 | return body.append |
---|
| 76 | def run_app(): |
---|
| 77 | body.extend(self.app(environ, replace_start_response)) |
---|
| 78 | # Run in profiler |
---|
| 79 | prof = cProfile.Profile() |
---|
| 80 | prof.runctx( "run_app()", globals(), locals() ) |
---|
| 81 | # Build up body with stats |
---|
| 82 | body = ''.join(body) |
---|
| 83 | headers = catch_response[1] |
---|
| 84 | content_type = response.header_value(headers, 'content-type') |
---|
| 85 | if not content_type.startswith('text/html'): |
---|
| 86 | # We can't add info to non-HTML output |
---|
| 87 | return [body] |
---|
| 88 | stats = pstats.Stats( prof ) |
---|
| 89 | stats.strip_dirs() |
---|
| 90 | stats.sort_stats( 'time', 'calls' ) |
---|
| 91 | output = pstats_as_html( stats, self.limit ) |
---|
| 92 | body += template % output |
---|
| 93 | return [body] |
---|
| 94 | |
---|
| 95 | def pstats_as_html( stats, *sel_list ): |
---|
| 96 | """ |
---|
| 97 | Return an HTML representation of a pstats.Stats object. |
---|
| 98 | """ |
---|
| 99 | rval = [] |
---|
| 100 | # Number of function calls, primitive calls, total time |
---|
| 101 | rval.append( "<div>%d function calls (%d primitive) in %0.3f CPU seconds</div>" |
---|
| 102 | % ( stats.total_calls, stats.prim_calls, stats.total_tt ) ) |
---|
| 103 | # Extract functions that match 'sel_list' |
---|
| 104 | funcs, order_message, select_message = get_func_list( stats, sel_list ) |
---|
| 105 | # Deal with any ordering or selection messages |
---|
| 106 | if order_message: |
---|
| 107 | rval.append( "<div>%s</div>" % cgi.escape( order_message ) ) |
---|
| 108 | if select_message: |
---|
| 109 | rval.append( "<div>%s</div>" % cgi.escape( select_message ) ) |
---|
| 110 | # Build a table for the functions |
---|
| 111 | if list: |
---|
| 112 | rval.append( "<table>" ) |
---|
| 113 | # Header |
---|
| 114 | rval.append( "<tr><th>ncalls</th>" |
---|
| 115 | "<th>tottime</th>" |
---|
| 116 | "<th>percall</th>" |
---|
| 117 | "<th>cumtime</th>" |
---|
| 118 | "<th>percall</th>" |
---|
| 119 | "<th>filename:lineno(function)</th></tr>" ) |
---|
| 120 | for func in funcs: |
---|
| 121 | rval.append( "<tr>" ) |
---|
| 122 | # Calculate each field |
---|
| 123 | cc, nc, tt, ct, callers = stats.stats[ func ] |
---|
| 124 | # ncalls |
---|
| 125 | ncalls = str(nc) |
---|
| 126 | if nc != cc: |
---|
| 127 | ncalls = ncalls + '/' + str(cc) |
---|
| 128 | rval.append( "<td>%s</td>" % cgi.escape( ncalls ) ) |
---|
| 129 | # tottime |
---|
| 130 | rval.append( "<td>%0.8f</td>" % tt ) |
---|
| 131 | # percall |
---|
| 132 | if nc == 0: |
---|
| 133 | percall = "" |
---|
| 134 | else: |
---|
| 135 | percall = "%0.8f" % ( tt / nc ) |
---|
| 136 | rval.append( "<td>%s</td>" % cgi.escape( percall ) ) |
---|
| 137 | # cumtime |
---|
| 138 | rval.append( "<td>%0.8f</td>" % ct ) |
---|
| 139 | # ctpercall |
---|
| 140 | if cc == 0: |
---|
| 141 | ctpercall = "" |
---|
| 142 | else: |
---|
| 143 | ctpercall = "%0.8f" % ( ct / cc ) |
---|
| 144 | rval.append( "<td>%s</td>" % cgi.escape( ctpercall ) ) |
---|
| 145 | # location |
---|
| 146 | rval.append( "<td>%s</td>" % cgi.escape( func_std_string( func ) ) ) |
---|
| 147 | # row complete |
---|
| 148 | rval.append( "</tr>" ) |
---|
| 149 | rval.append( "</table>") |
---|
| 150 | # Concatenate result |
---|
| 151 | return "".join( rval ) |
---|
| 152 | |
---|
| 153 | def get_func_list( stats, sel_list ): |
---|
| 154 | """ |
---|
| 155 | Use 'sel_list' to select a list of functions to display. |
---|
| 156 | """ |
---|
| 157 | # Determine if an ordering was applied |
---|
| 158 | if stats.fcn_list: |
---|
| 159 | list = stats.fcn_list[:] |
---|
| 160 | order_message = "Ordered by: " + stats.sort_type |
---|
| 161 | else: |
---|
| 162 | list = stats.stats.keys() |
---|
| 163 | order_message = "Random listing order was used" |
---|
| 164 | # Do the selection and accumulate messages |
---|
| 165 | select_message = "" |
---|
| 166 | for selection in sel_list: |
---|
| 167 | list, select_message = stats.eval_print_amount( selection, list, select_message ) |
---|
| 168 | # Return the list of functions selected and the message |
---|
| 169 | return list, order_message, select_message |
---|
| 170 | |
---|
| 171 | def func_std_string( func_name ): |
---|
| 172 | """ |
---|
| 173 | Match what old profile produced |
---|
| 174 | """ |
---|
| 175 | if func_name[:2] == ('~', 0): |
---|
| 176 | # special case for built-in functions |
---|
| 177 | name = func_name[2] |
---|
| 178 | if name.startswith('<') and name.endswith('>'): |
---|
| 179 | return '{%s}' % name[1:-1] |
---|
| 180 | else: |
---|
| 181 | return name |
---|
| 182 | else: |
---|
| 183 | return "%s:%d(%s)" % func_name |
---|