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 |
---|