Merge lp:~gary/launchpad/bug821531 into lp:launchpad

Proposed by Gary Poster
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
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++callgrind").

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

To post a comment you must log in.
Revision history for this message
Benji York (benji) wrote :

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.

review: Approve (code)
Revision history for this message
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!

Revision history for this message
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.

review: Approve (code)
Revision history for this message
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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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 &quot;help&quot; 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')))