1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350 | # 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 ("<input "
"type='radio' "
"name='%(name)s' "
"value='%(value)s' "
"%(checked)s>"
"%(description)s"
"</input><br />" %
{'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 = """
<html>
<head><title>Profile for %(url)s</title></head>
<body>
<form method='post' action='?profile=1'>
<fieldset style='float: left'>
<legend style='font-weight: bold'>Sort by</legend>
%(sort_first_buttons)s
</fieldset>
<fieldset style='float: left'>
<legend style='font-weight: bold'>then by</legend>
%(sort_second_buttons)s
</fieldset>
<fieldset style='float: left'>
<legend style='font-weight: bold'>Format</legend>
%(format_buttons)s
</fieldset>
<div style='clear: both'></div>
<input type='hidden' name='queries' value='%(rawqueries)s' />
<input type='hidden' name='stats' value='%(rawstats)s' />
<input type='hidden' name='show_stats' value='1' />
<input type='submit' name='show_queries' value='Show Queries' />
<input type='submit' name='sort' value='Sort' />
</form>
<hr />
<pre>%(stats)s</pre>
</body>
</html>
"""
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 = """
<html>
<head><title>SQL Queries for %(url)s</title></head>
<body>
<form method='post' action='?profile=1'>
<fieldset style='float: left'>
<legend style='font-weight: bold'>Sort by</legend>
%(sort_buttons)s
</fieldset>
<div style='clear: both'></div>
<input type='hidden' name='queries' value='%(rawqueries)s' />
<input type='hidden' name='stats' value='%(rawstats)s' />
<input type='hidden' name='show_queries' value='1' />
<input type='submit' name='show_stats' value='Show Profile' />
<input type='submit' name='sort' value='Sort' />
</form>
<hr />
%(num_queries)d SQL queries:
<pre>%(queries)s</pre>
</body>
</html>
"""
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
|
Comments
Pretty sweet! One issue is that I found that if I was profiling a page that already had a query string in the url, then submitting the form would wipe out my existing query string.
If you instead change the form code to be action='' it retains everything nicely.
#