Merge lp:~gary/launchpad/bug821531 into lp:launchpad
- bug821531
- Merge into devel
Status: | Merged |
---|---|
Approved by: | Gary Poster |
Approved revision: | no longer in the source branch. |
Merged at revision: | 13632 |
Proposed branch: | lp:~gary/launchpad/bug821531 |
Merge into: | lp:launchpad |
Diff against target: |
808 lines (+387/-131) 4 files modified
.bzrignore (+1/-0) lib/lp/services/profile/profile.pt (+45/-21) lib/lp/services/profile/profile.py (+108/-8) lib/lp/services/profile/tests.py (+233/-102) |
To merge this branch: | bzr merge lp:~gary/launchpad/bug821531 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Benji York (community) | code | Approve | |
Review via email: mp+70738@code.launchpad.net |
Commit message
[r=benji][bug=821531] Make it possible to request a pstats ++profile++ with "++profile++pstats" (in addition to a callgrind one, still available as "++pstats+
Description of the change
This change adds the ability to get a stdlib-friendly profile output to ++profile++. This lets you dig in farther than KCacheGrind does, which has a maximum of the top 499 calls displayed for a given sorting.
Lint is happy except for being confused with the start of profile.pt.
Thanks
Gary Poster (gary) wrote : | # |
I made all changes that you suggested/requested except the '-'.join(...) approach: I prefer the template approach for the filename.
I changed all test docstrings to comments: many of them were wrapping, and as you hinted, comments are reasonable and maybe even preferred for these descriptions anyway.
I'm asking you for a re-review given the size of the changes.
Lint is still as happy as it was ("happy except for being confused with the start of profile.pt"). Thanks!
Benji York (benji) wrote : | # |
This looks good. Thanks for considering my input.
One thing I didn't notice the first time around (assuming I'm reading
the code correctly): starting on line 279 of the diff, if the user
provides just "log" they will get callgrind output, if they provide
"callgrind" and "pstats" they get both, but if they provide "log" and
"pstats" they will get just "pstats". Given the internal nature of the
code, this corner case is not pressing.
Gary Poster (gary) wrote : | # |
Thanks again. I added a test for the "log,pstats" behavior: it is intentional ("log" is ambiguous and "pstats" is explicit). Note that if you request "callgrind,pstats" you actually get only callgrind and a warning that we can't do both at the same time.
Preview Diff
1 | === modified file '.bzrignore' |
2 | --- .bzrignore 2011-08-05 16:24:32 +0000 |
3 | +++ .bzrignore 2011-08-08 19:27:37 +0000 |
4 | @@ -83,3 +83,4 @@ |
5 | lib/canonical/launchpad/icing/icon-sprites.positioning |
6 | lib/canonical/launchpad/icing/yui |
7 | .emacs.desktop |
8 | +callgrind.out.* |
9 | |
10 | === modified file 'lib/lp/services/profile/profile.pt' |
11 | --- lib/lp/services/profile/profile.pt 2011-08-05 16:24:32 +0000 |
12 | +++ lib/lp/services/profile/profile.pt 2011-08-08 19:27:37 +0000 |
13 | @@ -6,7 +6,10 @@ |
14 | <div class="profiling_info" id="profiling_info" |
15 | tal:define="actions options/actions; |
16 | help actions/help|nothing; |
17 | - log actions/log|nothing; |
18 | + callgrind actions/callgrind|nothing; |
19 | + pstats actions/pstats|nothing; |
20 | + log python: callgrind or pstats; |
21 | + log_conflict python: callgrind and pstats; |
22 | show actions/show|nothing; |
23 | always_log options/always_log; |
24 | dump_path options/dump_path; |
25 | @@ -28,25 +31,35 @@ |
26 | you'll always see some logging information below. In order to also |
27 | get immediate profiling results in the browser, use |
28 | <code>++profile++show</code>.</tal:block> <tal:block condition="not: |
29 | - always_log">In order to get profiling results, you need to ask for |
30 | - an HTML view (<code>++profile++show</code>), a KCacheGrind-friendly |
31 | - log on the filesystem (<code>++profile++log</code>), or both |
32 | - (<code>++profile++show,log</code>).</tal:block> This can usually go |
33 | - anywhere in the URL (e.g., |
34 | - <code>http://launchpad.dev/++profile++show</code> or |
35 | + always_log">In order to get profiling results, you need to ask for an HTML |
36 | + view (<code>++profile++show</code>), a KCacheGrind-friendly log on the |
37 | + filesystem (<code>++profile++callgrind</code>), or a PStats-friendly log |
38 | + (Python standard library) on the filesystem |
39 | + (<code>++profile++pstats</code>). You can also ask for local HTML |
40 | + profiling plus one of the two log files |
41 | + (<code>++profile++show,callgrind</code> or |
42 | + <code>++profile++show,pstats</code>).</tal:block></p> |
43 | + <p>The "++profile++" path segment can usually go anywhere in the URL |
44 | + (e.g., <code>http://launchpad.dev/++profile++show</code> or |
45 | <code>http://launchpad.dev/++profile++show/Firefox</code> or |
46 | - <code>http://launchpad.dev/Firefox/++profile++show</code>) but some |
47 | - pages do seem to redirect in surprising ways, so experimentation may |
48 | - be necessary.</p> |
49 | + <code>http://launchpad.dev/Firefox/++profile++show</code>) but some pages |
50 | + do seem to redirect in surprising ways, so experimentation may be |
51 | + necessary.</p> |
52 | </tal:block> |
53 | <h2>Log information</h2> |
54 | <tal:block condition="not:log"> |
55 | <p>Profile was not logged to file.</p> |
56 | - <p>Use <code>++profile++log</code> in your URL in order to log the |
57 | - information to file for later KCacheGrind analysis (or |
58 | - <code>++profile++log,show</code> to see both the log information and |
59 | - immediate results). Profiles are logged to |
60 | - <tal:block replace="dump_path" />.</p> |
61 | + <p>Use <code>++profile++callgrind</code> in your URL in order to log the |
62 | + information to file for later KCacheGrind analysis. KCacheGrind is great |
63 | + for visualizing a profile, but it only allows analyzing a limited depth of |
64 | + calls. If you want a greater profile depth, or if for any other reason |
65 | + you want to use the standard Python library's pstats analysis instead, you |
66 | + can. To do this, use "pstats" instead as part of your ++profile++ request, |
67 | + as in <code>++profile++pstats</code>. Profiles are logged to <tal:block |
68 | + replace="dump_path" />.</p> |
69 | + <p> You can see both the log information and immediate results with |
70 | + <code>++profile++callgrind,show</code> or |
71 | + <code>++profile++pstats,show</code>.</p> |
72 | </tal:block> |
73 | <tal:block condition="log"> |
74 | <p tal:condition="always_log"><strong>You have configured every |
75 | @@ -54,17 +67,28 @@ |
76 | <code>profile_all_requests: True</code> in the |
77 | <code>[profiling]</code> section of your launchpad-lazr.conf.</p> |
78 | <p>Profile was logged to <tal:block replace="dump_path" />.</p> |
79 | - <p>You should be able to use this simply by entering |
80 | - <code>kcachegrind <tal:block replace="dump_path" /></code> in the |
81 | - console for a great visualization of the profile. The <a |
82 | + <p tal:condition="callgrind">You should be able to use this |
83 | + simply by entering <code>kcachegrind <tal:block replace="dump_path" |
84 | + /></code> in the console for a great visualization of the profile. The <a |
85 | href="https://dev.launchpad.net/Debugging#Profiling%%20page%%20requests" |
86 | >dev wiki</a> may have more information on how to use this.</p> |
87 | + <p tal:condition="not:callgrind">You should be able to use this |
88 | + simply by entering <code>python -m pstats <tal:block replace="dump_path" |
89 | + /></code> in the console to use the normal Python profile statistics |
90 | + interface. Type "help" at the prompt, or see the <a |
91 | + href="http://docs.python.org/library/profile.html#instant-user-s-manual" |
92 | + >Python documentation</a>.</p> |
93 | + <p tal:condition="log_conflict"><strong>You asked for both callgrind and |
94 | + pstats logging. We can only choose one at a time, and the default is |
95 | + callgrind. If you want a pstats profile as well, request it |
96 | + separately.</strong></p> |
97 | </tal:block> |
98 | <h2>Profile quick view</h2> |
99 | <tal:block condition="not:show"> |
100 | - <p>Use <code>++profile++show</code> in your URL in order to see |
101 | - immediate profile results (or <code>++profile++log,show</code> to |
102 | - see both the log information and immediate results).</p> |
103 | + <p>Use <code>++profile++show</code> in your URL in order to see immediate |
104 | + profile results (or <code>++profile++callgrind,show</code> or |
105 | + <code>++profile++pstats,show</code> to see both the log information and |
106 | + immediate results).</p> |
107 | </tal:block> |
108 | <tal:block condition="show"> |
109 | <h3>Top Inline Time</h3> |
110 | |
111 | === modified file 'lib/lp/services/profile/profile.py' |
112 | --- lib/lp/services/profile/profile.py 2011-08-05 16:24:32 +0000 |
113 | +++ lib/lp/services/profile/profile.py 2011-08-08 19:27:37 +0000 |
114 | @@ -7,11 +7,14 @@ |
115 | |
116 | __metaclass__ = type |
117 | |
118 | +from cProfile import Profile |
119 | from datetime import datetime |
120 | import os |
121 | +import pstats |
122 | import threading |
123 | import StringIO |
124 | |
125 | +from bzrlib import errors |
126 | from bzrlib.lsprof import BzrProfiler |
127 | from zope.pagetemplate.pagetemplatefile import PageTemplateFile |
128 | from zope.app.publication.interfaces import IEndRequestEvent |
129 | @@ -40,6 +43,84 @@ |
130 | """Fake exception used to log OOPS information when profiling pages.""" |
131 | |
132 | |
133 | +class PStatsProfiler(BzrProfiler): |
134 | + """This provides a wrapper around the standard library's profiler. |
135 | + |
136 | + It makes the BzrProfiler and the PStatsProfiler follow a similar API. |
137 | + It also makes them both honor the BzrProfiler's thread lock. |
138 | + """ |
139 | + |
140 | + def start(self): |
141 | + """Start profiling. |
142 | + |
143 | + This will record all calls made until stop() is called. |
144 | + |
145 | + Unlike the BzrProfiler, we do not try to get profiles of sub-threads. |
146 | + """ |
147 | + self.p = Profile() |
148 | + permitted = self.__class__.profiler_lock.acquire( |
149 | + self.__class__.profiler_block) |
150 | + if not permitted: |
151 | + raise errors.InternalBzrError(msg="Already profiling something") |
152 | + try: |
153 | + self.p.enable(subcalls=True) |
154 | + except: |
155 | + self.__class__.profiler_lock.release() |
156 | + raise |
157 | + |
158 | + def stop(self): |
159 | + """Stop profiling. |
160 | + |
161 | + This unhooks from threading and cleans up the profiler, returning |
162 | + the gathered Stats object. |
163 | + |
164 | + :return: A bzrlib.lsprof.Stats object. |
165 | + """ |
166 | + try: |
167 | + self.p.disable() |
168 | + p = self.p |
169 | + self.p = None |
170 | + return PStats(p) |
171 | + finally: |
172 | + self.__class__.profiler_lock.release() |
173 | + |
174 | + |
175 | +class PStats: |
176 | + """Emulate enough of the Bzr stats class for our needs.""" |
177 | + |
178 | + _stats = None |
179 | + |
180 | + def __init__(self, profiler): |
181 | + self.p = profiler |
182 | + |
183 | + @property |
184 | + def stats(self): |
185 | + if self._stats is None: |
186 | + self._stats = pstats.Stats(self.p).strip_dirs() |
187 | + return self._stats |
188 | + |
189 | + def save(self, filename): |
190 | + self.p.dump_stats(filename) |
191 | + |
192 | + def sort(self, name): |
193 | + mapping = { |
194 | + 'inlinetime': 'time', |
195 | + 'totaltime': 'cumulative', |
196 | + 'callcount': 'calls', |
197 | + } |
198 | + self.stats.sort_stats(mapping[name]) |
199 | + |
200 | + def pprint(self, file): |
201 | + stats = self.stats |
202 | + stream = stats.stream |
203 | + stats.stream = file |
204 | + try: |
205 | + stats.print_stats() |
206 | + finally: |
207 | + stats.stream = stream |
208 | + |
209 | + |
210 | +# Profilers may only run one at a time, but block and serialize. |
211 | _profilers = threading.local() |
212 | |
213 | |
214 | @@ -85,17 +166,20 @@ |
215 | pass |
216 | actions = get_desired_profile_actions(event.request) |
217 | if config.profiling.profile_all_requests: |
218 | - actions.add('log') |
219 | + actions.add('callgrind') |
220 | _profilers.actions = actions |
221 | _profilers.profiler = None |
222 | _profilers.profiling = True |
223 | if actions: |
224 | - if actions.difference(('help', )): |
225 | + if actions.difference(('help',)): |
226 | # If this assertion has reason to fail, we'll need to add code |
227 | # to try and stop the profiler before we delete it, in case it is |
228 | # still running. |
229 | assert getattr(_profilers, 'profiler', None) is None |
230 | - _profilers.profiler = BzrProfiler() |
231 | + if 'pstats' in actions and 'callgrind' not in actions: |
232 | + _profilers.profiler = PStatsProfiler() |
233 | + else: # 'callgrind' is the default, and wins in a conflict. |
234 | + _profilers.profiler = BzrProfiler() |
235 | _profilers.profiler.start() |
236 | if config.profiling.memory_profile_log: |
237 | _profilers.memory_profile_start = (memory(), resident()) |
238 | @@ -138,7 +222,7 @@ |
239 | dump_path = config.profiling.profile_dir |
240 | if _profilers.profiler is not None: |
241 | prof_stats = _profilers.profiler.stop() |
242 | - # Free some memory. |
243 | + # Free some memory (at least for the BzrProfiler). |
244 | del _profilers.profiler |
245 | if oopsid is None: |
246 | # Log an OOPS to get a log of the SQL queries, and other |
247 | @@ -150,12 +234,21 @@ |
248 | oopsid = oops.id |
249 | else: |
250 | oops = request.oops |
251 | - if 'log' in actions: |
252 | - filename = '%s-%s-%s-%s.prof' % ( |
253 | + if actions.intersection(('callgrind', 'pstats')): |
254 | + filename = '%s-%s-%s-%s' % ( |
255 | timestamp, pageid, oopsid, |
256 | threading.currentThread().getName()) |
257 | + if 'callgrind' in actions: |
258 | + # callgrind wins in a conflict between it and pstats, as |
259 | + # documented in the help. |
260 | + # The Bzr stats class looks at the filename to know to use |
261 | + # callgrind syntax. |
262 | + filename = 'callgrind.out.' + filename |
263 | + else: |
264 | + filename += '.prof' |
265 | dump_path = os.path.join(dump_path, filename) |
266 | - prof_stats.save(dump_path, format="callgrind") |
267 | + prof_stats.save(dump_path) |
268 | + template_context['dump_path'] = os.path.abspath(dump_path) |
269 | if is_html and 'show' in actions: |
270 | # Generate raw OOPS results. |
271 | f = StringIO.StringIO() |
272 | @@ -167,6 +260,8 @@ |
273 | f = StringIO.StringIO() |
274 | prof_stats.pprint(file=f) |
275 | template_context[name] = f.getvalue() |
276 | + # Try to free some more memory. |
277 | + del prof_stats |
278 | template_context['dump_path'] = os.path.abspath(dump_path) |
279 | if actions and is_html: |
280 | # Hack the new HTML in at the end of the page. |
281 | @@ -219,7 +314,12 @@ |
282 | action for action in ( |
283 | item.strip().lower() for item in actions.split(',')) |
284 | if action) |
285 | - result.intersection_update(('log', 'show')) |
286 | + # 'log' is backwards compatible for 'callgrind' |
287 | + result.intersection_update(('log', 'callgrind', 'show', 'pstats')) |
288 | + if 'log' in result: |
289 | + result.remove('log') |
290 | + if 'pstats' not in result: |
291 | + result.add('callgrind') |
292 | if not result: |
293 | result.add('help') |
294 | return result |
295 | |
296 | === modified file 'lib/lp/services/profile/tests.py' |
297 | --- lib/lp/services/profile/tests.py 2011-02-24 06:33:25 +0000 |
298 | +++ lib/lp/services/profile/tests.py 2011-08-08 19:27:37 +0000 |
299 | @@ -12,7 +12,6 @@ |
300 | import glob |
301 | import os |
302 | import time |
303 | -import unittest |
304 | |
305 | from lp.testing import TestCase |
306 | from bzrlib.lsprof import BzrProfiler |
307 | @@ -101,12 +100,13 @@ |
308 | self.pushProfilingConfig( |
309 | profiling_allowed='False', profile_all_requests='True', |
310 | memory_profile_log='.') |
311 | - profile.start_request(self._get_start_event('/++profile++show,log')) |
312 | + profile.start_request(self._get_start_event( |
313 | + '/++profile++show,callgrind')) |
314 | self.assertCleanProfilerState('config was ignored') |
315 | |
316 | def test_optional_profiling_without_marked_request_has_no_profile(self): |
317 | - """Even if profiling is allowed, it does not happen with a normal |
318 | - request.""" |
319 | + # Even if profiling is allowed, it does not happen with a normal |
320 | + # request. |
321 | self.pushProfilingConfig(profiling_allowed='True') |
322 | profile.start_request(self._get_start_event('/')) |
323 | self.assertEqual(profile._profilers.actions, set()) |
324 | @@ -115,8 +115,8 @@ |
325 | getattr(profile._profilers, 'memory_profile_start', None), None) |
326 | |
327 | def test_optional_profiling_with_show_request_starts_profiling(self): |
328 | - """If profiling is allowed and a request with the "show" marker |
329 | - URL segment is made, profiling starts.""" |
330 | + # If profiling is allowed and a request with the "show" marker |
331 | + # URL segment is made, profiling starts. |
332 | self.pushProfilingConfig(profiling_allowed='True') |
333 | profile.start_request(self._get_start_event('/++profile++show/')) |
334 | self.assertIsInstance(profile._profilers.profiler, BzrProfiler) |
335 | @@ -125,44 +125,100 @@ |
336 | None) |
337 | self.assertEquals(profile._profilers.actions, set(('show', ))) |
338 | |
339 | + def test_optional_profiling_with_callgrind_request_starts_profiling(self): |
340 | + # If profiling is allowed and a request with the "callgrind" marker |
341 | + # URL segment is made, profiling starts. |
342 | + self.pushProfilingConfig(profiling_allowed='True') |
343 | + profile.start_request(self._get_start_event('/++profile++callgrind/')) |
344 | + self.assertIsInstance(profile._profilers.profiler, BzrProfiler) |
345 | + self.assertIs( |
346 | + getattr(profile._profilers, 'memory_profile_start', None), |
347 | + None) |
348 | + self.assertEquals(profile._profilers.actions, set(('callgrind', ))) |
349 | + |
350 | def test_optional_profiling_with_log_request_starts_profiling(self): |
351 | - """If profiling is allowed and a request with the "log" marker |
352 | - URL segment is made, profiling starts.""" |
353 | + # If profiling is allowed and a request with the "log" marker URL |
354 | + # segment is made, profiling starts as a callgrind profile request. |
355 | self.pushProfilingConfig(profiling_allowed='True') |
356 | profile.start_request(self._get_start_event('/++profile++log/')) |
357 | self.assertIsInstance(profile._profilers.profiler, BzrProfiler) |
358 | self.assertIs( |
359 | getattr(profile._profilers, 'memory_profile_start', None), |
360 | None) |
361 | - self.assertEquals(profile._profilers.actions, set(('log', ))) |
362 | + self.assertEquals(profile._profilers.actions, set(('callgrind', ))) |
363 | |
364 | def test_optional_profiling_with_combined_request_starts_profiling(self): |
365 | - """If profiling is allowed and a request with the "log" and |
366 | - "show" marker URL segment is made, profiling starts.""" |
367 | + # If profiling is allowed and a request with the "callgrind" and |
368 | + # "show" marker URL segment is made, profiling starts. |
369 | self.pushProfilingConfig(profiling_allowed='True') |
370 | - profile.start_request(self._get_start_event('/++profile++log,show/')) |
371 | + profile.start_request( |
372 | + self._get_start_event('/++profile++callgrind,show/')) |
373 | self.assertIsInstance(profile._profilers.profiler, BzrProfiler) |
374 | self.assertIs( |
375 | getattr(profile._profilers, 'memory_profile_start', None), |
376 | None) |
377 | - self.assertEquals(profile._profilers.actions, set(('log', 'show'))) |
378 | + self.assertEquals( |
379 | + profile._profilers.actions, set(('callgrind', 'show'))) |
380 | |
381 | def test_optional_profiling_with_reversed_request_starts_profiling(self): |
382 | - """If profiling is allowed and a request with the "show" and |
383 | - "log" marker URL segment is made, profiling starts.""" |
384 | + # If profiling is allowed and a request with the "show" and the |
385 | + # "callgrind" marker URL segment is made, profiling starts. |
386 | self.pushProfilingConfig(profiling_allowed='True') |
387 | # The fact that this is reversed from the previous request is the only |
388 | # difference from the previous test. Also, it doesn't have a |
389 | # trailing slash. :-P |
390 | - profile.start_request(self._get_start_event('/++profile++show,log')) |
391 | - self.assertIsInstance(profile._profilers.profiler, BzrProfiler) |
392 | - self.assertIs( |
393 | - getattr(profile._profilers, 'memory_profile_start', None), |
394 | - None) |
395 | - self.assertEquals(profile._profilers.actions, set(('log', 'show'))) |
396 | + profile.start_request( |
397 | + self._get_start_event('/++profile++show,callgrind')) |
398 | + self.assertIsInstance(profile._profilers.profiler, BzrProfiler) |
399 | + self.assertIs( |
400 | + getattr(profile._profilers, 'memory_profile_start', None), |
401 | + None) |
402 | + self.assertEquals( |
403 | + profile._profilers.actions, set(('callgrind', 'show'))) |
404 | + |
405 | + def test_optional_profiling_with_pstats_request_starts_profiling(self): |
406 | + # If profiling is allowed and a request with the "pstats" marker, |
407 | + # profiling starts with the pstats profiler. |
408 | + self.pushProfilingConfig(profiling_allowed='True') |
409 | + profile.start_request( |
410 | + self._get_start_event('/++profile++pstats/')) |
411 | + self.assertIsInstance(profile._profilers.profiler, |
412 | + profile.PStatsProfiler) |
413 | + self.assertIs( |
414 | + getattr(profile._profilers, 'memory_profile_start', None), |
415 | + None) |
416 | + self.assertEquals(profile._profilers.actions, set(('pstats',))) |
417 | + |
418 | + def test_optional_profiling_with_log_request_starts_profiling(self): |
419 | + # If profiling is allowed and a request with the "log" and "pstats" |
420 | + # marker URL segments is made, profiling starts as a callgrind profile |
421 | + # request. |
422 | + self.pushProfilingConfig(profiling_allowed='True') |
423 | + profile.start_request( |
424 | + self._get_start_event('/++profile++log,pstats/')) |
425 | + self.assertIsInstance(profile._profilers.profiler, BzrProfiler) |
426 | + self.assertIs( |
427 | + getattr(profile._profilers, 'memory_profile_start', None), |
428 | + None) |
429 | + self.assertEquals(profile._profilers.actions, set(('pstats',))) |
430 | + |
431 | + def test_optional_profiling_with_conflicting_request(self): |
432 | + # If profiling is allowed and a request with both the "pstats" and |
433 | + # "callgrind" markers, profiling starts with the bzr/callgrind |
434 | + # profiler. |
435 | + self.pushProfilingConfig(profiling_allowed='True') |
436 | + profile.start_request( |
437 | + self._get_start_event('/++profile++pstats,callgrind/')) |
438 | + self.assertIsInstance(profile._profilers.profiler, |
439 | + profile.BzrProfiler) |
440 | + self.assertIs( |
441 | + getattr(profile._profilers, 'memory_profile_start', None), |
442 | + None) |
443 | + self.assertEquals( |
444 | + profile._profilers.actions, set(('pstats', 'callgrind'))) |
445 | |
446 | def test_forced_profiling_registers_action(self): |
447 | - """profile_all_requests should register as a log action""" |
448 | + # profile_all_requests should register as a callgrind action. |
449 | self.pushProfilingConfig( |
450 | profiling_allowed='True', profile_all_requests='True') |
451 | profile.start_request(self._get_start_event('/')) |
452 | @@ -170,12 +226,11 @@ |
453 | self.assertIs( |
454 | getattr(profile._profilers, 'memory_profile_start', None), |
455 | None) |
456 | - self.assertEquals(profile._profilers.actions, set(('log', ))) |
457 | + self.assertEquals(profile._profilers.actions, set(('callgrind', ))) |
458 | |
459 | def test_optional_profiling_with_wrong_request_helps(self): |
460 | - """If profiling is allowed and a request with the marker URL segment |
461 | - is made incorrectly, profiling does not start and help is an action. |
462 | - """ |
463 | + # If profiling is allowed and a request with the marker URL segment |
464 | + # is made incorrectly, profiling does not start and help is an action. |
465 | self.pushProfilingConfig(profiling_allowed='True') |
466 | profile.start_request(self._get_start_event('/++profile++/')) |
467 | self.assertIs(getattr(profile._profilers, 'profiler', None), None) |
468 | @@ -185,9 +240,8 @@ |
469 | self.assertEquals(profile._profilers.actions, set(('help', ))) |
470 | |
471 | def test_forced_profiling_with_wrong_request_helps(self): |
472 | - """If profiling is forced and a request with the marker URL segment |
473 | - is made incorrectly, profiling starts and help is an action. |
474 | - """ |
475 | + # If profiling is forced and a request with the marker URL segment |
476 | + # is made incorrectly, profiling starts and help is an action. |
477 | self.pushProfilingConfig( |
478 | profiling_allowed='True', profile_all_requests='True') |
479 | profile.start_request(self._get_start_event('/++profile++/')) |
480 | @@ -195,7 +249,8 @@ |
481 | self.assertIs( |
482 | getattr(profile._profilers, 'memory_profile_start', None), |
483 | None) |
484 | - self.assertEquals(profile._profilers.actions, set(('help', 'log'))) |
485 | + self.assertEquals( |
486 | + profile._profilers.actions, set(('help', 'callgrind'))) |
487 | |
488 | def test_memory_profile_start(self): |
489 | self.pushProfilingConfig( |
490 | @@ -216,15 +271,9 @@ |
491 | self.assertEquals(profile._profilers.actions, set(('show', ))) |
492 | |
493 | |
494 | -class TestRequestEndHandler(BaseTest): |
495 | - """Tests for the end-request handler. |
496 | - |
497 | - If the start-request handler is broken, these tests will fail too, so fix |
498 | - the tests in the above test case first. |
499 | - |
500 | - See lib/canonical/doc/profiling.txt for an end-user description |
501 | - of the functionality. |
502 | - """ |
503 | +class BaseRequestEndHandlerTest(BaseTest): |
504 | + # These are shared by tests of the bzr profiler, the stdlib profiler, |
505 | + # and the memory analysis. |
506 | |
507 | def setUp(self): |
508 | TestCase.setUp(self) |
509 | @@ -271,17 +320,26 @@ |
510 | return result |
511 | |
512 | def getProfilePaths(self): |
513 | - return glob.glob(os.path.join(self.profile_dir, '*.prof')) |
514 | - |
515 | - ######################################################################### |
516 | - # Tests |
517 | + return glob.glob(os.path.join(self.profile_dir, 'callgrind.out.*')) |
518 | + |
519 | + |
520 | +class TestBasicRequestEndHandler(BaseRequestEndHandlerTest): |
521 | + """Tests for the end-request handler. |
522 | + |
523 | + If the start-request handler is broken, these tests will fail too, so fix |
524 | + the tests in the above test case first. |
525 | + |
526 | + See lib/canonical/doc/profiling.txt for an end-user description |
527 | + of the functionality. |
528 | + """ |
529 | + |
530 | def test_config_stops_profiling(self): |
531 | - """The ``profiling_allowed`` configuration should disable all |
532 | - profiling, even if it is requested""" |
533 | + # The ``profiling_allowed`` configuration should disable all |
534 | + # profiling, even if it is requested. |
535 | self.pushProfilingConfig( |
536 | profiling_allowed='False', profile_all_requests='True', |
537 | memory_profile_log=self.memory_profile_log) |
538 | - request = self.endRequest('/++profile++show,log') |
539 | + request = self.endRequest('/++profile++show,callgrind') |
540 | self.assertIs(getattr(request, 'oops', None), None) |
541 | self.assertEqual(self.getAddedResponse(request), '') |
542 | self.assertEqual(self.getMemoryLog(), []) |
543 | @@ -289,8 +347,8 @@ |
544 | self.assertCleanProfilerState() |
545 | |
546 | def test_optional_profiling_without_marked_request_has_no_profile(self): |
547 | - """Even if profiling is allowed, it does not happen with a normal |
548 | - request.""" |
549 | + # Even if profiling is allowed, it does not happen with a normal |
550 | + # request. |
551 | self.pushProfilingConfig(profiling_allowed='True') |
552 | request = self.endRequest('/') |
553 | self.assertIs(getattr(request, 'oops', None), None) |
554 | @@ -299,9 +357,70 @@ |
555 | self.assertEqual(self.getProfilePaths(), []) |
556 | self.assertCleanProfilerState() |
557 | |
558 | + def test_forced_profiling_logs(self): |
559 | + # profile_all_requests should register as a log action. |
560 | + self.pushProfilingConfig( |
561 | + profiling_allowed='True', profile_all_requests='True') |
562 | + request = self.endRequest('/') |
563 | + self.assertIsInstance(request.oops, ErrorReport) |
564 | + response = self.getAddedResponse(request) |
565 | + self.assertIn('Profile was logged to', response) |
566 | + self.assertIn('profile_all_requests: True', response) |
567 | + self.assertNotIn('Top Inline Time', response) |
568 | + self.assertEqual(self.getMemoryLog(), []) |
569 | + paths = self.getProfilePaths() |
570 | + self.assertEqual(len(paths), 1) |
571 | + self.assertIn(paths[0], response) |
572 | + self.assertCleanProfilerState() |
573 | + |
574 | + def test_optional_profiling_with_wrong_request_helps(self): |
575 | + # If profiling is allowed and a request with the marker URL segment |
576 | + # is made incorrectly, profiling does not start and help is an action. |
577 | + self.pushProfilingConfig(profiling_allowed='True') |
578 | + request = self.endRequest('/++profile++') |
579 | + self.assertIs(getattr(request, 'oops', None), None) |
580 | + response = self.getAddedResponse(request) |
581 | + self.assertIn('<h2>Help</h2>', response) |
582 | + self.assertNotIn('Top Inline Time', response) |
583 | + self.assertEqual(self.getMemoryLog(), []) |
584 | + self.assertEqual(self.getProfilePaths(), []) |
585 | + self.assertCleanProfilerState() |
586 | + |
587 | + def test_forced_profiling_with_wrong_request_helps(self): |
588 | + # If profiling is forced and a request with the marker URL segment |
589 | + # is made incorrectly, profiling starts and help is an action. |
590 | + self.pushProfilingConfig( |
591 | + profiling_allowed='True', profile_all_requests='True') |
592 | + request = self.endRequest('/++profile++') |
593 | + self.assertIsInstance(request.oops, ErrorReport) |
594 | + response = self.getAddedResponse(request) |
595 | + self.assertIn('<h2>Help</h2>', response) |
596 | + self.assertIn('Profile was logged to', response) |
597 | + self.assertIn('profile_all_requests: True', response) |
598 | + self.assertNotIn('Top Inline Time', response) |
599 | + self.assertEqual(self.getMemoryLog(), []) |
600 | + paths = self.getProfilePaths() |
601 | + self.assertEqual(len(paths), 1) |
602 | + self.assertIn(paths[0], response) |
603 | + self.assertCleanProfilerState() |
604 | + |
605 | + |
606 | +class TestBzrProfilerRequestEndHandler(BaseRequestEndHandlerTest): |
607 | + """Tests for the end-request handler of the BzrProfiler. |
608 | + |
609 | + If the start-request handler is broken, these tests will fail too, so fix |
610 | + the tests in the above test case first. |
611 | + |
612 | + See lib/canonical/doc/profiling.txt for an end-user description |
613 | + of the functionality. |
614 | + """ |
615 | + |
616 | + # Note that these tests are re-used by TestStdLibProfilerRequestEndHandler |
617 | + # below. |
618 | + |
619 | def test_optional_profiling_with_show_request_profiles(self): |
620 | - """If profiling is allowed and a request with the "show" marker |
621 | - URL segment is made, profiling starts.""" |
622 | + # If profiling is allowed and a request with the "show" marker |
623 | + # URL segment is made, profiling starts. |
624 | self.pushProfilingConfig(profiling_allowed='True') |
625 | request = self.endRequest('/++profile++show/') |
626 | self.assertIsInstance(request.oops, ErrorReport) |
627 | @@ -310,11 +429,11 @@ |
628 | self.assertEqual(self.getProfilePaths(), []) |
629 | self.assertCleanProfilerState() |
630 | |
631 | - def test_optional_profiling_with_log_request_profiles(self): |
632 | - """If profiling is allowed and a request with the "log" marker |
633 | - URL segment is made, profiling starts.""" |
634 | + def test_optional_profiling_with_callgrind_request_profiles(self): |
635 | + # If profiling is allowed and a request with the "callgrind" marker |
636 | + # URL segment is made, profiling starts. |
637 | self.pushProfilingConfig(profiling_allowed='True') |
638 | - request = self.endRequest('/++profile++log/') |
639 | + request = self.endRequest('/++profile++callgrind/') |
640 | self.assertIsInstance(request.oops, ErrorReport) |
641 | response = self.getAddedResponse(request) |
642 | self.assertIn('Profile was logged to', response) |
643 | @@ -326,10 +445,10 @@ |
644 | self.assertCleanProfilerState() |
645 | |
646 | def test_optional_profiling_with_combined_request_profiles(self): |
647 | - """If profiling is allowed and a request with the "log" and |
648 | - "show" marker URL segment is made, profiling starts.""" |
649 | + # If profiling is allowed and a request with the "callgrind" and |
650 | + # "show" marker URL segment is made, profiling starts. |
651 | self.pushProfilingConfig(profiling_allowed='True') |
652 | - request = self.endRequest('/++profile++log,show') |
653 | + request = self.endRequest('/++profile++callgrind,show') |
654 | self.assertIsInstance(request.oops, ErrorReport) |
655 | response = self.getAddedResponse(request) |
656 | self.assertIn('Profile was logged to', response) |
657 | @@ -340,57 +459,58 @@ |
658 | self.assertIn(paths[0], response) |
659 | self.assertCleanProfilerState() |
660 | |
661 | - def test_forced_profiling_logs(self): |
662 | - """profile_all_requests should register as a log action""" |
663 | - self.pushProfilingConfig( |
664 | - profiling_allowed='True', profile_all_requests='True') |
665 | - request = self.endRequest('/') |
666 | - self.assertIsInstance(request.oops, ErrorReport) |
667 | - response = self.getAddedResponse(request) |
668 | - self.assertIn('Profile was logged to', response) |
669 | - self.assertIn('profile_all_requests: True', response) |
670 | - self.assertNotIn('Top Inline Time', response) |
671 | - self.assertEqual(self.getMemoryLog(), []) |
672 | - paths = self.getProfilePaths() |
673 | - self.assertEqual(len(paths), 1) |
674 | - self.assertIn(paths[0], response) |
675 | - self.assertCleanProfilerState() |
676 | - |
677 | - def test_optional_profiling_with_wrong_request_helps(self): |
678 | - """If profiling is allowed and a request with the marker URL segment |
679 | - is made incorrectly, profiling does not start and help is an action. |
680 | - """ |
681 | + |
682 | +class TestStdLibProfilerRequestEndHandler(TestBzrProfilerRequestEndHandler): |
683 | + """Tests for the end-request handler of the stdlib profiler. |
684 | + |
685 | + If the start-request handler is broken, these tests will fail too, so fix |
686 | + the tests in the above test case first. |
687 | + |
688 | + See lib/canonical/doc/profiling.txt for an end-user description |
689 | + of the functionality. |
690 | + """ |
691 | + # Take over the BzrProfiler questions to test the stdlib variant. |
692 | + |
693 | + def getProfilePaths(self): |
694 | + return glob.glob(os.path.join(self.profile_dir, '*.prof')) |
695 | + |
696 | + def endRequest(self, path): |
697 | + return TestBzrProfilerRequestEndHandler.endRequest(self, |
698 | + path.replace('callgrind', 'pstats')) |
699 | + |
700 | + |
701 | +class TestConflictingProfilerRequestEndHandler(BaseRequestEndHandlerTest): |
702 | + |
703 | + def test_optional_profiling_with_conflicting_request_profiles(self): |
704 | + # If profiling is allowed and a request with the "callgrind" and |
705 | + # "pstats" markers is made, profiling starts with the callgrind |
706 | + # approach only. |
707 | self.pushProfilingConfig(profiling_allowed='True') |
708 | - request = self.endRequest('/++profile++') |
709 | - self.assertIs(getattr(request, 'oops', None), None) |
710 | - response = self.getAddedResponse(request) |
711 | - self.assertIn('<h2>Help</h2>', response) |
712 | - self.assertNotIn('Top Inline Time', response) |
713 | - self.assertEqual(self.getMemoryLog(), []) |
714 | - self.assertEqual(self.getProfilePaths(), []) |
715 | - self.assertCleanProfilerState() |
716 | - |
717 | - def test_forced_profiling_with_wrong_request_helps(self): |
718 | - """If profiling is forced and a request with the marker URL segment |
719 | - is made incorrectly, profiling starts and help is an action. |
720 | - """ |
721 | - self.pushProfilingConfig( |
722 | - profiling_allowed='True', profile_all_requests='True') |
723 | - request = self.endRequest('/++profile++') |
724 | + request = self.endRequest('/++profile++callgrind,pstats/') |
725 | self.assertIsInstance(request.oops, ErrorReport) |
726 | response = self.getAddedResponse(request) |
727 | - self.assertIn('<h2>Help</h2>', response) |
728 | self.assertIn('Profile was logged to', response) |
729 | - self.assertIn('profile_all_requests: True', response) |
730 | self.assertNotIn('Top Inline Time', response) |
731 | + self.assertIn('You asked for both callgrind and', response) |
732 | self.assertEqual(self.getMemoryLog(), []) |
733 | paths = self.getProfilePaths() |
734 | self.assertEqual(len(paths), 1) |
735 | self.assertIn(paths[0], response) |
736 | self.assertCleanProfilerState() |
737 | |
738 | + |
739 | +class TestMemoryProfilerRequestEndHandler(BaseRequestEndHandlerTest): |
740 | + """Tests for the end-request handler of the memory profile. |
741 | + |
742 | + If the start-request handler is broken, these tests will fail too, so fix |
743 | + the tests in the above test case first. |
744 | + |
745 | + See lib/canonical/doc/profiling.txt for an end-user description |
746 | + of the functionality. |
747 | + """ |
748 | + |
749 | def test_memory_profile(self): |
750 | - "Does the memory profile work?" |
751 | + # Does the memory profile work? |
752 | self.pushProfilingConfig( |
753 | profiling_allowed='True', |
754 | memory_profile_log=self.memory_profile_log) |
755 | @@ -408,7 +528,7 @@ |
756 | self.assertCleanProfilerState() |
757 | |
758 | def test_memory_profile_with_non_defaults(self): |
759 | - "Does the memory profile work with an oops and pageid?" |
760 | + # Does the memory profile work with an oops and pageid? |
761 | self.pushProfilingConfig( |
762 | profiling_allowed='True', |
763 | memory_profile_log=self.memory_profile_log) |
764 | @@ -432,10 +552,20 @@ |
765 | self.assertEqual(self.getProfilePaths(), []) |
766 | self.assertCleanProfilerState() |
767 | |
768 | + |
769 | +class TestOOPSRequestEndHandler(BaseRequestEndHandlerTest): |
770 | + """Tests for the end-request handler of the OOPS output. |
771 | + |
772 | + If the start-request handler is broken, these tests will fail too, so fix |
773 | + the tests in the above test case first. |
774 | + |
775 | + See lib/canonical/doc/profiling.txt for an end-user description |
776 | + of the functionality. |
777 | + """ |
778 | + |
779 | def test_profiling_oops_is_informational(self): |
780 | self.pushProfilingConfig(profiling_allowed='True') |
781 | request = self.endRequest('/++profile++show/') |
782 | - response = self.getAddedResponse(request) |
783 | self.assertIsInstance(request.oops, ErrorReport) |
784 | self.assertTrue(request.oops.informational) |
785 | self.assertEquals(request.oops.type, 'ProfilingOops') |
786 | @@ -454,7 +584,7 @@ |
787 | |
788 | def test_oopsid_is_in_profile_filename(self): |
789 | self.pushProfilingConfig(profiling_allowed='True') |
790 | - request = self.endRequest('/++profile++log/') |
791 | + request = self.endRequest('/++profile++callgrind/') |
792 | self.assertIn("-" + request.oopsid + "-", self.getProfilePaths()[0]) |
793 | self.assertCleanProfilerState() |
794 | |
795 | @@ -471,9 +601,10 @@ |
796 | profiling_allowed='False', profile_all_requests='True', |
797 | memory_profile_log='.') |
798 | event = BeforeTraverseEvent(None, |
799 | - self._get_request('/++profile++show,log')) |
800 | - with FeatureFixture({'profiling.enabled':'on'}): |
801 | + self._get_request('/++profile++show,callgrind')) |
802 | + with FeatureFixture({'profiling.enabled': 'on'}): |
803 | profile.before_traverse(event) |
804 | self.assertTrue(profile._profilers.profiling) |
805 | self.assertIsInstance(profile._profilers.profiler, BzrProfiler) |
806 | - self.assertEquals(profile._profilers.actions, set(('show', 'log'))) |
807 | + self.assertEquals(profile._profilers.actions, set( |
808 | + ('show', 'callgrind'))) |
This branch looks good. I can't wait to try it out. Here are some
relatively minor comments.
The name "pstats" might be a little better than "stdlib" because it's a
pstats-formatted file that's generated. For that matter, now that there
are more than one output formats, "log" might be better replaced with a
word that describes the generated format ("callgrind") while perhaps
leaving "log" as a backward-compatible alias.
Line 111 of the diff: the docstring summary for StdLibStats wraps.
Line 130 of the diff: JavaScript has warped your brain, you're missing a
comma on the last element of "mapping".
Line 183 of the diff, teeny-tiny suggestion: you could use '-'.join(...)
instead of '%s-%s-%s-%s' % (...).
Lines 235 and 256 of the diff: more docstring wrapping. Maybe those
should just be comments instead.