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

Proposed by Gary Poster
Status: Merged
Approved by: Gary Poster
Approved revision: no longer in the source branch.
Merged at revision: 13858
Proposed branch: lp:~gary/launchpad/bug838878
Merge into: lp:launchpad
Prerequisite: lp:~gary/launchpad/bug838869
Diff against target: 913 lines (+430/-117)
3 files modified
lib/lp/services/profile/profile.pt (+121/-36)
lib/lp/services/profile/profile.py (+153/-53)
lib/lp/services/profile/tests.py (+156/-28)
To merge this branch: bzr merge lp:~gary/launchpad/bug838878
Reviewer Review Type Date Requested Status
j.c.sackett (community) Approve
Review via email: mp+73702@code.launchpad.net

Commit message

[r=jcsackett][bug=838878] ++profile++ has a new conditional sqltrace feature and a new sql option.

Description of the change

This branch lets ++profile++ expose the new conditional sqltrace functionality enabled by changes in lp:~gary/launchpad/bug838869 .

This branch is just over the limit, for which I apologize.

The new features for ++profile++ are as follows:

 - ++profile++sqltrace adds a mini-language for specifying what SQL requests you want to trace. ++profile++sqltrace continues to get tracebacks for all SQL. "++profile++sqltrace:includes STRING" only gets tracebacks for SQL statements that include STRING (whitespace- and case-normalized). "++profile++sqltrace:startswith STRING" gets tracebacks for SQL statements that start with STRING. "++profile++sqltrace:endswith STRING" does the same if the SQL ends with STRING. They can be combined with a pipe, "|", meaning logical or. Therefore, "++profile++sqltrace:includes FOOBAR BAZ| includes shazam" will match SQL statements that include "FOOBAR BAZ" or "SHAZAM" (again, case- and whitespace- normalized).

 - ++profile++sql is now available, only giving SQL without any profiling or stacktrace information.

The changes for ++profile++ are as follows:

You combine requests in ++profile++ with "&" now, not ",", so "++profile++show,pstats" becomes "++profile++show&pstats". This is so that the strings used by the conditions described above can include commas, which SQL often does.

Lint is happy except for its usual confusion with the beginning of the template.

Trying this out on a dev machine should be as easy as going to https://launchpad.dev/++profile++ and messing around with some of the new options, like "https://launchpad.dev/++profile++sql" and "https://launchpad.dev/++profile++sqltrace:includes AccountPassword.id".

QA should be similar: https://qastaging.launchpad.net/++profile++ should still work, and https://qastaging.launchpad.net/++profile++show and https://qastaging.launchpad.net/++profile++sql and https://qastaging.launchpad.net/++profile++sqltrace and "https://qastaging.launchpad.net/++profile++sqltrace:includes AccountPassword.id".

To post a comment you must log in.
Revision history for this message
j.c.sackett (jcsackett) wrote :

Gary--

This looks good. I see one possible problem in the updated profile template, pointed out below.

> === modified file 'lib/lp/services/profile/profile.pt'
> --- lib/lp/services/profile/profile.pt 2011-08-22 17:08:02 +0000
> +++ lib/lp/services/profile/profile.pt 2011-09-01 17:17:32 +0000
> @@ -64,15 +64,21 @@
> get immediate profiling results in the browser, use
> <code>++profile++show</code>.</tal:block> <tal:block condition="not:
> always_log">In order to get profiling results, you need to ask for an HTML
> - view (<code>++profile++show</code>), a KCacheGrind-friendly log on the
> + view of the profile and OOPS data including SQL calls
> + (<code>++profile++show</code>), a KCacheGrind-friendly log on the
> filesystem (<code>++profile++callgrind</code>), a PStats-friendly log
> (Python standard library) on the filesystem
> - (<code>++profile++pstats</code>), or an HTML view of the SQL and the
> + (<code>++profile++pstats</code>), an HTML view of the SQL and the
> Python stacktrace when each SQL call was made
> - (<code>++profile++sqltrace</code>). You can also combine these
> - (<code>++profile++show,callgrind</code> or
> - <code>++profile++show,pstats</code> or others,
> - with each action separated with commas).</tal:block></p>
> + (<code>++profile++sqltrace</code>), or an HTML view of only the SQL
> + (<code>++profile++sql</code>). You can also combine these
> + (<code>++profile++show&callgrind</code> or
> + <code>++profile++show&pstats</code> or others, with each action separated
> + with commas).</tal:block></p>

I may be misunderstanding this, but shouldn't this be "separated with ambersands" as shown in the example?

> === modified file 'lib/lp/services/profile/profile.py'
> @@ -505,24 +521,108 @@
> # include traversal in the profile.
> actions, slash, tail = end.partition('/')
> result.update(
> - action for action in (
> - item.strip().lower() for item in actions.split(','))
> - if action)
> + (name.strip(), config.strip())
> + for name, partition, config in (
> + item.strip().lower().partition(':')
> + for item in actions.split('&'))
> + if name.strip())

There's no problem in the above bit, just thought that was one hell of a generator expression. :-P

review: Approve
Revision history for this message
Gary Poster (gary) wrote :

On Sep 1, 2011, at 3:45 PM, j.c.sackett wrote:

> Review: Approve
> Gary--
>
> This looks good. I see one possible problem in the updated profile template, pointed out below.

...

>
> I may be misunderstanding this, but shouldn't this be "separated with ambersands" as shown in the example?

Ah, yes, thanks, good catch!

...

> There's no problem in the above bit, just thought that was one hell of a generator expression. :-P

Yeah...not the prettiest thing in the world. Maybe I should have been more explicit/less concise...

Thanks again,

Gary

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/services/profile/profile.pt'
2--- lib/lp/services/profile/profile.pt 2011-08-22 17:08:02 +0000
3+++ lib/lp/services/profile/profile.pt 2011-09-02 16:34:47 +0000
4@@ -47,7 +47,7 @@
5 multiple_profiles options/multiple_profiles|nothing;
6 inline_ignored actions/inline_ignored|nothing;
7 inline actions/inline|nothing;
8- sqltrace actions/sqltrace|nothing;
9+ sqltrace actions/sql|nothing;
10 sqltrace_steps options/sqltrace|nothing;
11 ">
12 <div class="hide_reveal_profiling" id="hide_profiling">
13@@ -64,15 +64,21 @@
14 get immediate profiling results in the browser, use
15 <code>++profile++show</code>.</tal:block> <tal:block condition="not:
16 always_log">In order to get profiling results, you need to ask for an HTML
17- view (<code>++profile++show</code>), a KCacheGrind-friendly log on the
18+ view of the profile and OOPS data including SQL calls
19+ (<code>++profile++show</code>), a KCacheGrind-friendly log on the
20 filesystem (<code>++profile++callgrind</code>), a PStats-friendly log
21 (Python standard library) on the filesystem
22- (<code>++profile++pstats</code>), or an HTML view of the SQL and the
23+ (<code>++profile++pstats</code>), an HTML view of the SQL and the
24 Python stacktrace when each SQL call was made
25- (<code>++profile++sqltrace</code>). You can also combine these
26- (<code>++profile++show,callgrind</code> or
27- <code>++profile++show,pstats</code> or others,
28- with each action separated with commas).</tal:block></p>
29+ (<code>++profile++sqltrace</code>), or an HTML view of only the SQL
30+ (<code>++profile++sql</code>). You can also combine these
31+ (<code>++profile++show&callgrind</code> or
32+ <code>++profile++show&pstats</code> or others, with each action separated
33+ with ampersands).</tal:block></p>
34+ <p>The <code>++profile++sqltrace</code> can take too much time to let a
35+ page practically render, especially on qastaging and staging. You can
36+ filter to only get the stacktraces you want in this case; see information
37+ in the &quot;SQL trace&quot; section below on how to do this.</p>
38 <p>The "++profile++" path segment can usually go anywhere in the URL
39 (e.g., <code>http://launchpad.dev/++profile++show</code> or
40 <code>http://launchpad.dev/++profile++show/Firefox</code> or
41@@ -119,8 +125,8 @@
42 as in <code>++profile++pstats</code>. Profiles are logged to <tal:block
43 replace="dump_path" />.</p>
44 <p> You can see both the log information and immediate results with
45- <code>++profile++callgrind,show</code> or
46- <code>++profile++pstats,show</code>.</p>
47+ <code>++profile++callgrind&show</code> or
48+ <code>++profile++pstats&show</code>.</p>
49 </tal:block>
50 <tal:block condition="log">
51 <p tal:condition="always_log"><strong>You have configured every
52@@ -144,11 +150,72 @@
53 <tal:block condition="not:sqltrace">
54 <p>Use <code>++profile++sqltrace</code> in your URL to see the SQL that
55 the page runs, along with start and stop times, the database used, and the
56- stacktrace for when each sql call is made.</p> <p>Note that if you do not
57- want the tracebacks, <code>++profile++show</code> will include OOPS data
58- at the end of the profiling information, which has what what you want.</p>
59+ stacktrace for when each sql call is made.</p>
60+ <p>If you only want SQL and not the tracebacks,
61+ <code>++profile++sql</code> will give you what you want, or
62+ <code>++profile++show</code> will include OOPS data at the end of the
63+ profiling information.</p>
64+ <p>If you want stacktraces only for certain SQL statements, you can do
65+ that too. You can use <code>startswith</code>, <code>endswith</code>
66+ or <code>includes</code> to specify parts of the SQL that you want to use
67+ as triggers. Note that this is whitespace and case normalized, so you
68+ just need to get the spelling right. Here are some examples.</p>
69+ <dl>
70+ <dt><code>/++profile++sqltrace: includes Product.blueprints_usage/</code>
71+ </dt>
72+ <dd>This will get a stacktrace for any SQL statement that has the
73+ string &quot;Product.blueprints_usage&quot; in it.</dd>
74+ <dt><code>/++profile++sqltrace:startswith SELECT
75+ Distribution.answers_usage/</code> </dt>
76+ <dd>This will get a stacktrace for any SQL statement that starts with the
77+ string &quot;SELECT Distribution.answers_usage&quot; in it.</dd>
78+ <dt><code>/++profile++sqltrace:endswith WHERE Project.id = 5 LIMIT
79+ 1/</code> </dt>
80+ <dd>This will get a stacktrace for any SQL statement that ends with the
81+ string given. Note that it does not end in a semicolon, and note that
82+ parameters are filled in.</dd>
83+ </dl>
84+ <p>You can also combine requests with the <code>|</code> mark (the
85+ pipe). Here's an example:
86+ <code>/++profile++sqltrace:startswith SELECT Distribution.answers_usage |
87+ includes Product.blueprints_usage/</code>.</p>
88 </tal:block>
89 <tal:block condition="sqltrace">
90+ <p tal:condition="options/sql_traceback_all|nothing">You have requested
91+ tracebacks for all SQL statements. You can filter these with conditions
92+ like <code>startswith</code>, <code>endswith</code> or
93+ <code>includes</code>. Here's a simple example:
94+ <code>/++profile++sqltrace: includes Product.blueprints_usage/</code>.</p>
95+ <p tal:condition="options/sql_traceback_none|nothing">You have requested no
96+ tracebacks. Use <code>++profile++sqltrace</code> to get tracebacks.</p>
97+ <tal:block condition="options/sql_traceback_conditions|nothing">
98+ <p>You have requested tracebacks for statements that match only certain
99+ conditions.</p>
100+ <tal:block
101+ condition="not:options/sql_traceback_conditions/included">
102+ <p><strong> You had no valid conditions, so no tracebacks were
103+ included.</strong> The available conditions are <code>startswith</code>,
104+ <code>endswith</code> or <code>includes</code>. Here's a simple
105+ example.</p>
106+ <pre>++profile++sqltrace: includes Product.blueprints_usage/</pre>
107+ </tal:block>
108+ <ul>
109+ <li tal:repeat="condition options/sql_traceback_conditions/included">
110+ <em tal:content="condition/constraint">CONSTRAINT</em>
111+ <span tal:replace="condition/value">VALUE</span>
112+ </li>
113+ </ul>
114+ <tal:block condition="options/sql_traceback_conditions/ignored">
115+ <p><strong>Warning</strong>: these conditions were not understood, and
116+ so ignored.</p>
117+ <ul>
118+ <li tal:repeat="condition options/sql_traceback_conditions/ignored">
119+ <em tal:content="condition/constraint">CONSTRAINT</em>
120+ <span tal:replace="condition/value">VALUE</span>
121+ </li>
122+ </ul>
123+ </tal:block>
124+ </tal:block>
125 <h3>Top 10 SQL times</h3>
126 <ol>
127 <li tal:repeat="info options/top_sql" tal:attributes="class info/cls">
128@@ -158,6 +225,14 @@
129 </li>
130 </ol>
131 <h3>Top 10 Python times</h3>
132+ <p tal:condition="options/sql_traceback_all|nothing">Note that generating
133+ stacktraces can add significantly to these numbers, and skew results. Use
134+ <code>++profile++sql</code> for a more accurate view.</p>
135+ <p tal:condition="options/sql_traceback_conditions|nothing">Note that
136+ generating stacktraces can add significantly to these numbers, and
137+ selectively choosing when to generate stacktraces will produce so much
138+ skew as to potentially make these values meaningless. Use
139+ <code>++profile++sql</code> for the most accurate view.</p>
140 <ol>
141 <li tal:repeat="info options/top_python" tal:attributes="class info/cls">
142 <a tal:attributes="href string:#pythonstep-${info/ix}">Before query
143@@ -165,32 +240,37 @@
144 replace="info/value"></tal:block> milliseconds)
145 </li>
146 </ol>
147- <h3>Repeated Python SQL Triggers</h3>
148- <p tal:condition="not: options/top_triggers">None.</p>
149- <div tal:condition="options/top_triggers">
150- <p>Typically, these show loops. The traceback may show extra
151- information, such as from page templates, that indicate the variable
152- within the loop. If you want to add more information to identify the
153- variables in the loop, in your Python add either an informational string
154- with the variable name <code>__traceback_info__</code>, or add a
155- <code>__traceback_supplement__</code> as used in
156- lp.services.stacktrace.</p>
157- <div tal:repeat="trigger options/top_triggers">
158- <div>File &quot;<tal:block replace="trigger/filename"></tal:block>&quot;,
159- line <tal:block replace="trigger/lineno"></tal:block></div>
160- <div class="querylinks">
161- <tal:block replace="trigger/count"></tal:block> related queries (<a
162- href="#" class="expandall">expand all tracebacks</a>): <span
163- tal:repeat="ix trigger/indexes"> <a tal:attributes="href
164- string:#sqlstep-${ix}" tal:content="ix"></a> </span>
165+ <tal:block condition="not: options/sql_traceback_none|nothing">
166+ <h3>Repeated Python SQL Triggers</h3>
167+ <p tal:condition="options/sql_traceback_conditions|nothing">These are
168+ only calculated for the stacktraces that were collected.</p>
169+ <p tal:condition="not: options/top_triggers">None.</p>
170+ <div tal:condition="options/top_triggers">
171+ <p>Typically, these show loops. The traceback may show extra
172+ information, such as from page templates, that indicate the variable
173+ within the loop. If you want to add more information to identify the
174+ variables in the loop, in your Python add either an informational
175+ string with the variable name <code>__traceback_info__</code>, or add
176+ a <code>__traceback_supplement__</code> as used in
177+ lp.services.stacktrace.</p>
178+ <div tal:repeat="trigger options/top_triggers">
179+ <div>File &quot;<tal:block
180+ replace="trigger/filename"></tal:block>&quot;, line <tal:block
181+ replace="trigger/lineno"></tal:block></div>
182+ <div class="querylinks"> <tal:block
183+ replace="trigger/count"></tal:block> related queries (<a href="#"
184+ class="expandall">expand all tracebacks</a>): <span tal:repeat="ix
185+ trigger/indexes"> <a tal:attributes="href string:#sqlstep-${ix}"
186+ tal:content="ix"></a> </span> </div>
187 </div>
188 </div>
189- </div>
190+ </tal:block>
191 <h3>SQL (<tal:block replace="options/sql_count"></tal:block> queries)</h3>
192 <p>Each sql line begins with the start time and the stop time, along with
193 the name of the database on which the query ran. Times are in
194 milliseconds since request start.</p>
195- <div><a href="#" id="show-all-tracebacks">Show all tracebacks</a> / <a href="#"
196+ <div tal:condition="not:options/sql_traceback_none|nothing"><a href="#"
197+ id="show-all-tracebacks">Show all tracebacks</a> / <a href="#"
198 id="hide-all-tracebacks">Hide all tracebacks</a></div>
199 <div tal:repeat="step sqltrace_steps">
200 <div tal:condition="step/python_rank|nothing" tal:attributes="class
201@@ -210,9 +290,10 @@
202 tal:condition="step/sql_rank|nothing"><strong>Top ten (# <tal:block
203 replace="step/sql_rank"></tal:block>):</strong> SQL took <span
204 tal:replace="step/sql_time" /> milliseconds</span></div> <a href="#"
205- class="clicktext">Click to toggle Python traceback.</a>
206+ class="clicktext" tal:condition="step/stack">Click to toggle Python
207+ traceback.</a><span tal:condition="not:step/stack">--</span>
208 </div>
209- <div class="trace"><div class="inner">
210+ <div class="trace" tal:condition="step/stack"><div class="inner">
211 <div class="appstack" tal:define="stack step/app_stack">
212 <div tal:repeat="frame stack" metal:define-macro="draw-stack">
213 <div>File &quot;<tal:block replace="frame/filename"></tal:block>&quot;,
214@@ -247,8 +328,8 @@
215 <h2>Profile quick view</h2>
216 <tal:block condition="not:show">
217 <p>Use <code>++profile++show</code> in your URL in order to see immediate
218- profile results (or <code>++profile++callgrind,show</code> or
219- <code>++profile++pstats,show</code> to see both the log information and
220+ profile results (or <code>++profile++callgrind&show</code> or
221+ <code>++profile++pstats&show</code> to see both the log information and
222 immediate results).</p>
223 </tal:block>
224 <tal:block condition="show">
225@@ -275,6 +356,10 @@
226 Y.one('div#profiling_info').setStyle('display', 'none');
227 e.preventDefault();
228 });
229+});
230+</script>
231+<script tal:condition="not:options/sql_traceback_none|nothing">
232+LPS.use('node', 'lp', 'transition', function (Y) {
233 function slideIn(target) {
234 target.transition({
235 easing: 'ease-in',
236
237=== modified file 'lib/lp/services/profile/profile.py'
238--- lib/lp/services/profile/profile.py 2011-09-01 14:55:10 +0000
239+++ lib/lp/services/profile/profile.py 2011-09-02 16:34:47 +0000
240@@ -13,9 +13,11 @@
241 import contextlib
242 from cProfile import Profile
243 from datetime import datetime
244+from functools import partial
245 import heapq
246 import os
247 import pstats
248+import re
249 import StringIO
250 import sys
251 import threading
252@@ -210,11 +212,14 @@
253 _profilers.actions = actions
254 _profilers.profiling = True
255 if config.profiling.profile_all_requests:
256- actions.add('callgrind')
257+ actions['callgrind'] = ''
258 if actions:
259- if 'sqltrace' in actions:
260- da.start_sql_logging(tracebacks_if=True)
261- if 'show' in actions or actions.intersection(available_profilers):
262+ if 'sql' in actions:
263+ condition = actions['sql']
264+ if condition not in (True, False):
265+ condition = condition['condition']
266+ da.start_sql_logging(condition)
267+ if 'show' in actions or available_profilers.intersection(actions):
268 _profilers.profiler = Profiler()
269 _profilers.profiler.start()
270 if config.profiling.memory_profile_log:
271@@ -233,12 +238,14 @@
272 actions = _profilers.actions
273 profiler = _profilers.profiler
274 if actions is None:
275- actions = _profilers.actions = set()
276+ actions = _profilers.actions = {}
277 _profilers.profiling = True
278- elif actions.difference(('help',)) and 'inline' not in actions:
279- actions.add('inline_ignored')
280+ elif 'inline' not in actions and (
281+ 'show' in actions or available_profilers.intersection(actions)):
282+ # We have a request-based profile in progress. That wins.
283+ actions['inline_ignored'] = ''
284 return
285- actions.update(('pstats', 'show', 'inline'))
286+ actions.update((k, '') for k in ('pstats', 'show', 'inline'))
287 if profiler is None:
288 profiler = _profilers.profiler = Profiler()
289 profiler.start()
290@@ -261,8 +268,10 @@
291 @contextlib.contextmanager
292 def profiling():
293 start()
294- yield
295- stop()
296+ try:
297+ yield
298+ finally:
299+ stop()
300
301
302 @adapter(IEndRequestEvent)
303@@ -293,8 +302,7 @@
304 _major, _minor, content_type_params = parse(content_type)
305 is_html = _major == 'text' and _minor == 'html'
306 template_context = {
307- # Dicts are easier for tal expressions.
308- 'actions': dict((action, True) for action in actions),
309+ 'actions': dict((key, True) for key in actions),
310 'always_log': config.profiling.profile_all_requests}
311 dump_path = config.profiling.profile_dir
312 if _profilers.profiler is not None:
313@@ -345,16 +353,15 @@
314 # Try to free some more memory.
315 del prof_stats
316 trace = None
317- if 'sqltrace' in actions:
318+ if 'sql' in actions:
319 trace = da.stop_sql_logging() or ()
320- # The trace is a list of dicts, each with the keys "sql" and
321- # "stack". "sql" is a tuple of start time, stop time, database
322- # name (with a "SQL-" prefix), and sql statement. "stack" is a
323- # tuple of filename, line number, function name, text, module
324- # name, optional supplement dict, and optional info string. The
325- # supplement dict has keys 'source_url', 'line', 'column',
326- # 'expression', 'warnings' (an iterable), and 'extra', any of
327- # which may be None.
328+ # The trace is a list of dicts, each with the keys "sql" and "stack".
329+ # "sql" is a tuple of start time, stop time, database name (with a
330+ # "SQL-" prefix), and sql statement. "stack" is None or a tuple of
331+ # filename, line number, function name, text, module name, optional
332+ # supplement dict, and optional info string. The supplement dict has
333+ # keys 'source_url', 'line', 'column', 'expression', 'warnings' (an
334+ # iterable), and 'extra', any of which may be None.
335 top_sql = []
336 top_python = []
337 triggers = {}
338@@ -366,29 +373,30 @@
339 step['id'] = ix
340 step['sql'] = dict(zip(
341 ('start', 'stop', 'name', 'statement'), step['sql']))
342- # Divide up the stack into the more unique (app) and less
343- # unique (db) bits.
344- app_stack = step['app_stack'] = []
345- db_stack = step['db_stack'] = []
346- storm_found = False
347- for f in step['stack']:
348- f_data = dict(zip(
349- ('filename', 'lineno', 'name', 'line', 'module',
350- 'supplement', 'info'), f))
351- storm_found = storm_found or (
352- f_data['module'] and
353- f_data['module'].startswith('storm.'))
354- if storm_found:
355- db_stack.append(f_data)
356- else:
357- app_stack.append(f_data)
358- # Begin to gather what app code is triggering the most SQL
359- # calls.
360- trigger_key = tuple(
361- (f['filename'], f['lineno']) for f in app_stack)
362- if trigger_key not in triggers:
363- triggers[trigger_key] = []
364- triggers[trigger_key].append(ix)
365+ if step['stack'] is not None:
366+ # Divide up the stack into the more unique (app) and less
367+ # unique (db) bits.
368+ app_stack = step['app_stack'] = []
369+ db_stack = step['db_stack'] = []
370+ storm_found = False
371+ for f in step['stack']:
372+ f_data = dict(zip(
373+ ('filename', 'lineno', 'name', 'line', 'module',
374+ 'supplement', 'info'), f))
375+ storm_found = storm_found or (
376+ f_data['module'] and
377+ f_data['module'].startswith('storm.'))
378+ if storm_found:
379+ db_stack.append(f_data)
380+ else:
381+ app_stack.append(f_data)
382+ # Begin to gather what app code is triggering the most SQL
383+ # calls.
384+ trigger_key = tuple(
385+ (f['filename'], f['lineno']) for f in app_stack)
386+ if trigger_key not in triggers:
387+ triggers[trigger_key] = []
388+ triggers[trigger_key].append(ix)
389 # Get the nbest (n=10) sql and python times
390 step['python_time'] = step['sql']['start'] - last_stop_time
391 step['sql_time'] = step['sql']['stop'] - step['sql']['start']
392@@ -449,6 +457,14 @@
393 top_python=top_python_ids,
394 top_triggers=top_triggers,
395 sql_count=len(trace)))
396+ if actions['sql'] is True:
397+ template_context['sql_traceback_all'] = True
398+ elif actions['sql'] is False:
399+ template_context['sql_traceback_none'] = True
400+ else:
401+ conditions = actions['sql'].copy()
402+ del conditions['condition']
403+ template_context['sql_traceback_conditions'] = conditions
404 template_context['dump_path'] = os.path.abspath(dump_path)
405 if actions and is_html:
406 # Hack the new HTML in at the end of the page.
407@@ -488,7 +504,7 @@
408 URL. Note that ++profile++ alone without actions is interpreted as
409 the "help" action.
410 """
411- result = set()
412+ result = dict()
413 path_info = request.get('PATH_INFO')
414 if path_info:
415 # if not, this is almost certainly a test not bothering to set up
416@@ -505,24 +521,108 @@
417 # include traversal in the profile.
418 actions, slash, tail = end.partition('/')
419 result.update(
420- action for action in (
421- item.strip().lower() for item in actions.split(','))
422- if action)
423+ (name.strip(), config.strip())
424+ for name, partition, config in (
425+ item.strip().lower().partition(':')
426+ for item in actions.split('&'))
427+ if name.strip())
428 # 'log' is backwards compatible for 'callgrind'.
429 if 'log' in result:
430- result.remove('log')
431- result.add('callgrind')
432+ result['callgrind'] = result.pop('log')
433+ if 'sqltrace' in result:
434+ condition = result['sqltrace']
435+ if condition:
436+ condition = _make_condition_function(condition)
437+ else:
438+ condition = True
439+ del result['sqltrace']
440+ result['sql'] = condition
441+ elif 'sql' in result:
442+ result['sql'] = False
443 # Only honor the available options.
444- available_options = set(('show', 'sqltrace', 'help'))
445+ available_options = set(('show', 'sql', 'help'))
446 available_options.update(available_profilers)
447- result.intersection_update(available_options)
448+ # .keys() gives a list, not mutated during iteration.
449+ for key in result.keys():
450+ if key not in available_options:
451+ del result[key]
452 # If we didn't end up with any known actions, we need to help the
453 # user.
454 if not result:
455- result.add('help')
456+ result['help'] = ''
457 return result
458
459
460+def _make_startswith(value):
461+ """Return a function that returns true if its argument startswith value"""
462+ def startswith(sql):
463+ return sql.startswith(value)
464+ return startswith
465+
466+
467+def _make_endswith(value):
468+ """Return a function that returns true if its argument endswith value"""
469+ def endswith(sql):
470+ return sql.endswith(value)
471+ return endswith
472+
473+
474+def _make_includes(value):
475+ """Return a function that returns true if its argument includes value"""
476+ def includes(sql):
477+ return value in sql
478+ return includes
479+
480+_condition_functions = {
481+ 'STARTSWITH': _make_startswith,
482+ 'ENDSWITH': _make_endswith,
483+ 'INCLUDES': _make_includes
484+ }
485+
486+_normalize_whitespace = partial(re.compile('\s+').sub, ' ')
487+
488+
489+def _make_condition_function(condition_string):
490+ """Given DSL string, return dict including function implementing string.
491+
492+ DSL is very simple. Conditions are separated by semicolons, which
493+ represent logical or. Each condition should begin with (case insensitive)
494+ "startswith ", "endswith ", or "includes ". The part of the condition
495+ after this word and before the next semicolon or end-of-string is the
496+ match string, which is case-insensitive and whitespace-normalized.
497+
498+ Returned dict has three elements: the function that implements the DSL,
499+ the list of conditions that were included, and the list of conditions
500+ that were excluded (because they were not understood).
501+ """
502+ conditions = []
503+ included = []
504+ ignored = []
505+ for constraint, partition, value in (
506+ c.strip().partition(' ') for c
507+ in condition_string.upper().split('|')):
508+ # Process each condition.
509+ constraint = constraint.strip()
510+ if not constraint:
511+ # This was something ignorable, like a trailing | or something.
512+ continue
513+ value = _normalize_whitespace(value.strip())
514+ description = dict(constraint=constraint, value=value)
515+ if constraint in _condition_functions:
516+ conditions.append(_condition_functions[constraint](value))
517+ included.append(description)
518+ else:
519+ ignored.append(description)
520+
521+ def condition_aggregate(sql):
522+ for condition in conditions:
523+ if condition(sql):
524+ return True
525+ return False
526+ return dict(
527+ condition=condition_aggregate, included=included, ignored=ignored)
528+
529+
530 class ProfileNamespace(view):
531 """A see-through namespace that handles traversals with ++profile++."""
532
533
534=== modified file 'lib/lp/services/profile/tests.py'
535--- lib/lp/services/profile/tests.py 2011-09-01 14:55:10 +0000
536+++ lib/lp/services/profile/tests.py 2011-09-02 16:34:47 +0000
537@@ -43,6 +43,85 @@
538 EXAMPLE_HTML = EXAMPLE_HTML_START + EXAMPLE_HTML_END
539
540
541+class SQLDSLTest(TestCase):
542+
543+ def assertCondition(self,
544+ condition_string, succeeds, fails, included, ignored):
545+ results = profile._make_condition_function(condition_string)
546+ self.assertEqual(included, results['included'])
547+ self.assertEqual(ignored, results['ignored'])
548+ for example in succeeds:
549+ self.assertTrue(results['condition'](example))
550+ for example in fails:
551+ self.assertFalse(results['condition'](example))
552+
553+ def test_startswith(self):
554+ self.assertCondition(
555+ 'startswith foo bar',
556+ succeeds=['FOO BARBAZ'],
557+ fails=['BARBAZ FOO'],
558+ included=[dict(constraint='STARTSWITH', value='FOO BAR')],
559+ ignored=[])
560+
561+ def test_endswith(self):
562+ self.assertCondition(
563+ 'endswith foo bar',
564+ succeeds=['BAZ FOO BAR'],
565+ fails=['BARBAZ FOO'],
566+ included=[dict(constraint='ENDSWITH', value='FOO BAR')],
567+ ignored=[])
568+
569+ def test_includes(self):
570+ self.assertCondition(
571+ 'includes foo bar',
572+ succeeds=['BAZ FOO BAR BING'],
573+ fails=['BARBAZ FOO'],
574+ included=[dict(constraint='INCLUDES', value='FOO BAR')],
575+ ignored=[])
576+
577+ def test_whitespace_normalized(self):
578+ self.assertCondition(
579+ ' startswith foo bar ',
580+ succeeds=['FOO BARBAZ'],
581+ fails=['BARBAZ FOO'],
582+ included=[dict(constraint='STARTSWITH', value='FOO BAR')],
583+ ignored=[])
584+
585+ def test_many_conditions(self):
586+ self.assertCondition(
587+ 'startswith foo bar | endswith shazam|includes balooba',
588+ succeeds=['FOO BARBAZ', 'SALAMI SHAZAM', 'FORTUNA BALOOBA CAT'],
589+ fails=['BARBAZ FOO'],
590+ included=[dict(constraint='STARTSWITH', value='FOO BAR'),
591+ dict(constraint='ENDSWITH', value='SHAZAM'),
592+ dict(constraint='INCLUDES', value='BALOOBA')],
593+ ignored=[])
594+
595+ def test_trailing_or(self):
596+ self.assertCondition(
597+ 'startswith foo bar|',
598+ succeeds=['FOO BARBAZ'],
599+ fails=['BARBAZ FOO'],
600+ included=[dict(constraint='STARTSWITH', value='FOO BAR')],
601+ ignored=[])
602+
603+ def test_one_ignored(self):
604+ self.assertCondition(
605+ 'matches foo bar',
606+ succeeds=[],
607+ fails=['BARBAZ FOO'],
608+ included=[],
609+ ignored=[dict(constraint='MATCHES', value='FOO BAR')])
610+
611+ def test_one_included_one_ignored(self):
612+ self.assertCondition(
613+ 'matches kumquat | startswith foo bar',
614+ succeeds=['FOO BAR HAMSTER'],
615+ fails=['BARBAZ FOO'],
616+ included=[dict(constraint='STARTSWITH', value='FOO BAR')],
617+ ignored=[dict(constraint='MATCHES', value='KUMQUAT')])
618+
619+
620 class BaseTest(TestCase):
621
622 def _get_request(self, path='/'):
623@@ -99,7 +178,7 @@
624 profiling_allowed='False', profile_all_requests='True',
625 memory_profile_log='.')
626 profile.start_request(self._get_start_event(
627- '/++profile++show,callgrind'))
628+ '/++profile++show&callgrind'))
629 self.assertCleanProfilerState('config was ignored')
630
631 def test_optional_profiling_without_marked_request_has_no_profile(self):
632@@ -107,7 +186,7 @@
633 # request.
634 self.pushProfilingConfig(profiling_allowed='True')
635 profile.start_request(self._get_start_event('/'))
636- self.assertEqual(profile._profilers.actions, set())
637+ self.assertEqual(profile._profilers.actions, {})
638 self.assertIs(getattr(profile._profilers, 'profiler', None), None)
639 self.assertIs(
640 getattr(profile._profilers, 'memory_profile_start', None), None)
641@@ -121,7 +200,7 @@
642 self.assertIs(
643 getattr(profile._profilers, 'memory_profile_start', None),
644 None)
645- self.assertEquals(profile._profilers.actions, set(('show', )))
646+ self.assertEquals(set(profile._profilers.actions), set(('show', )))
647
648 def test_optional_profiling_with_callgrind_request_starts_profiling(self):
649 # If profiling is allowed and a request with the "callgrind" marker
650@@ -132,7 +211,8 @@
651 self.assertIs(
652 getattr(profile._profilers, 'memory_profile_start', None),
653 None)
654- self.assertEquals(profile._profilers.actions, set(('callgrind', )))
655+ self.assertEquals(
656+ set(profile._profilers.actions), set(('callgrind', )))
657
658 def test_optional_profiling_with_log_request_starts_profiling(self):
659 # If profiling is allowed and a request with the "log" marker URL
660@@ -143,20 +223,21 @@
661 self.assertIs(
662 getattr(profile._profilers, 'memory_profile_start', None),
663 None)
664- self.assertEquals(profile._profilers.actions, set(('callgrind', )))
665+ self.assertEquals(
666+ set(profile._profilers.actions), set(('callgrind', )))
667
668 def test_optional_profiling_with_combined_request_starts_profiling(self):
669 # If profiling is allowed and a request with the "callgrind" and
670 # "show" marker URL segment is made, profiling starts.
671 self.pushProfilingConfig(profiling_allowed='True')
672 profile.start_request(
673- self._get_start_event('/++profile++callgrind,show/'))
674+ self._get_start_event('/++profile++callgrind&show/'))
675 self.assertIsInstance(profile._profilers.profiler, profile.Profiler)
676 self.assertIs(
677 getattr(profile._profilers, 'memory_profile_start', None),
678 None)
679 self.assertEquals(
680- profile._profilers.actions, set(('callgrind', 'show')))
681+ set(profile._profilers.actions), set(('callgrind', 'show')))
682
683 def test_optional_profiling_with_reversed_request_starts_profiling(self):
684 # If profiling is allowed and a request with the "show" and the
685@@ -166,13 +247,13 @@
686 # difference from the previous test. Also, it doesn't have a
687 # trailing slash. :-P
688 profile.start_request(
689- self._get_start_event('/++profile++show,callgrind'))
690+ self._get_start_event('/++profile++show&callgrind'))
691 self.assertIsInstance(profile._profilers.profiler, profile.Profiler)
692 self.assertIs(
693 getattr(profile._profilers, 'memory_profile_start', None),
694 None)
695 self.assertEquals(
696- profile._profilers.actions, set(('callgrind', 'show')))
697+ set(profile._profilers.actions), set(('callgrind', 'show')))
698
699 def test_optional_profiling_with_pstats_request_starts_profiling(self):
700 # If profiling is allowed and a request with the "pstats" marker,
701@@ -185,7 +266,7 @@
702 self.assertIs(
703 getattr(profile._profilers, 'memory_profile_start', None),
704 None)
705- self.assertEquals(profile._profilers.actions, set(('pstats',)))
706+ self.assertEquals(set(profile._profilers.actions), set(('pstats',)))
707
708 def test_optional_profiling_with_log_pstats(self):
709 # If profiling is allowed and a request with the "log" and "pstats"
710@@ -193,13 +274,13 @@
711 # and pstats request.
712 self.pushProfilingConfig(profiling_allowed='True')
713 profile.start_request(
714- self._get_start_event('/++profile++log,pstats/'))
715+ self._get_start_event('/++profile++log&pstats/'))
716 self.assertIsInstance(profile._profilers.profiler, profile.Profiler)
717 self.assertIs(
718 getattr(profile._profilers, 'memory_profile_start', None),
719 None)
720 self.assertEquals(
721- profile._profilers.actions, set(('callgrind', 'pstats',)))
722+ set(profile._profilers.actions), set(('callgrind', 'pstats',)))
723
724 def test_optional_profiling_with_callgrind_pstats(self):
725 # If profiling is allowed and a request with both the "pstats" and
726@@ -207,14 +288,14 @@
727 # profiler.
728 self.pushProfilingConfig(profiling_allowed='True')
729 profile.start_request(
730- self._get_start_event('/++profile++pstats,callgrind/'))
731+ self._get_start_event('/++profile++pstats&callgrind/'))
732 self.assertIsInstance(profile._profilers.profiler,
733 profile.Profiler)
734 self.assertIs(
735 getattr(profile._profilers, 'memory_profile_start', None),
736 None)
737 self.assertEquals(
738- profile._profilers.actions, set(('pstats', 'callgrind')))
739+ set(profile._profilers.actions), set(('pstats', 'callgrind')))
740
741 def test_forced_profiling_registers_action(self):
742 # profile_all_requests should register as a callgrind action.
743@@ -225,7 +306,8 @@
744 self.assertIs(
745 getattr(profile._profilers, 'memory_profile_start', None),
746 None)
747- self.assertEquals(profile._profilers.actions, set(('callgrind', )))
748+ self.assertEquals(
749+ set(profile._profilers.actions), set(('callgrind', )))
750
751 def test_optional_profiling_with_wrong_request_helps(self):
752 # If profiling is allowed and a request with the marker URL segment
753@@ -236,7 +318,7 @@
754 self.assertIs(
755 getattr(profile._profilers, 'memory_profile_start', None),
756 None)
757- self.assertEquals(profile._profilers.actions, set(('help', )))
758+ self.assertEquals(set(profile._profilers.actions), set(('help', )))
759
760 def test_forced_profiling_with_wrong_request_helps(self):
761 # If profiling is forced and a request with the marker URL segment
762@@ -249,7 +331,7 @@
763 getattr(profile._profilers, 'memory_profile_start', None),
764 None)
765 self.assertEquals(
766- profile._profilers.actions, set(('help', 'callgrind')))
767+ set(profile._profilers.actions), set(('help', 'callgrind')))
768
769 def test_memory_profile_start(self):
770 self.pushProfilingConfig(
771@@ -258,7 +340,7 @@
772 self.assertIs(getattr(profile._profilers, 'profiler', None), None)
773 self.assertIsInstance(profile._profilers.memory_profile_start, tuple)
774 self.assertEqual(len(profile._profilers.memory_profile_start), 2)
775- self.assertEqual(profile._profilers.actions, set())
776+ self.assertEqual(profile._profilers.actions, {})
777
778 def test_combo_memory_and_profile_start(self):
779 self.pushProfilingConfig(
780@@ -267,13 +349,30 @@
781 self.assertIsInstance(profile._profilers.profiler, profile.Profiler)
782 self.assertIsInstance(profile._profilers.memory_profile_start, tuple)
783 self.assertEqual(len(profile._profilers.memory_profile_start), 2)
784- self.assertEquals(profile._profilers.actions, set(('show', )))
785+ self.assertEquals(set(profile._profilers.actions), set(('show', )))
786
787 def test_sqltrace_start(self):
788 self.pushProfilingConfig(profiling_allowed='True')
789 profile.start_request(self._get_start_event('/++profile++sqltrace/'))
790 self.assertIs(getattr(profile._profilers, 'profiler', None), None)
791- self.assertEquals(profile._profilers.actions, set(('sqltrace', )))
792+ self.assertEquals(profile._profilers.actions, dict(sql=True))
793+ self.assertEqual([], da.stop_sql_logging())
794+
795+ def test_sql_start(self):
796+ self.pushProfilingConfig(profiling_allowed='True')
797+ profile.start_request(self._get_start_event('/++profile++sql/'))
798+ self.assertIs(getattr(profile._profilers, 'profiler', None), None)
799+ self.assertEquals(profile._profilers.actions, dict(sql=False))
800+ self.assertEqual([], da.stop_sql_logging())
801+
802+ def test_sqltrace_filtered_start(self):
803+ self.pushProfilingConfig(profiling_allowed='True')
804+ profile.start_request(self._get_start_event(
805+ '/++profile++sqltrace:includes bugsubscription/'))
806+ self.assertIs(getattr(profile._profilers, 'profiler', None), None)
807+ self.assertEquals(set(profile._profilers.actions), set(('sql', )))
808+ data = profile._profilers.actions['sql']
809+ self.assertTrue(data['condition']('SELECT BUGSUBSCRIPTION FROM FOO'))
810 self.assertEqual([], da.stop_sql_logging())
811
812
813@@ -380,7 +479,7 @@
814 self.pushProfilingConfig(
815 profiling_allowed='False', profile_all_requests='True',
816 memory_profile_log=self.memory_profile_log)
817- request = self.endRequest('/++profile++show,callgrind')
818+ request = self.endRequest('/++profile++show&callgrind')
819 self.assertIs(getattr(request, 'oops', None), None)
820 self.assertEqual(self.getAddedResponse(request), '')
821 self.assertEqual(self.getMemoryLog(), [])
822@@ -469,7 +568,7 @@
823 # If profiling is allowed and a request with the "callgrind" and
824 # "show" marker URL segment is made, profiling starts.
825 self.pushProfilingConfig(profiling_allowed='True')
826- request = self.endRequest('/++profile++callgrind,show')
827+ request = self.endRequest('/++profile++callgrind&show')
828 self.assertProfilePaths(
829 self.assertBasicProfileExists(request, show=True))
830
831@@ -499,7 +598,7 @@
832 # "pstats" markers is made, profiling starts with the callgrind
833 # approach only.
834 self.pushProfilingConfig(profiling_allowed='True')
835- request = self.endRequest('/++profile++callgrind,pstats/')
836+ request = self.endRequest('/++profile++callgrind&pstats/')
837 self.assertBothProfiles(self.assertBasicProfileExists(request))
838 # We had a bug in which the callgrind file was actually a pstats
839 # file. What we can do minimally to prevent this in the future is
840@@ -606,14 +705,14 @@
841 profiling_allowed='False', profile_all_requests='True',
842 memory_profile_log='.')
843 event = BeforeTraverseEvent(None,
844- self._get_request('/++profile++show,callgrind'))
845+ self._get_request('/++profile++show&callgrind'))
846 with FeatureFixture({'profiling.enabled': 'on'}):
847 profile.before_traverse(event)
848 self.assertTrue(profile._profilers.profiling)
849 self.assertIsInstance(
850 profile._profilers.profiler, profile.Profiler)
851 self.assertEquals(
852- set(('show', 'callgrind')), profile._profilers.actions)
853+ set(('show', 'callgrind')), set(profile._profilers.actions))
854
855
856 class TestInlineProfiling(BaseRequestEndHandlerTest):
857@@ -643,7 +742,7 @@
858 # ++profile++ wins over inline.
859 self.pushProfilingConfig(profiling_allowed='True')
860 request = self.endRequest(
861- '/++profile++show,callgrind', work=self.make_work())
862+ '/++profile++show&callgrind', work=self.make_work())
863 response = self.assertBasicProfileExists(request, show=True)
864 self.assertCallgrindProfile(response)
865 self.assertIn('Inline request ignored', response)
866@@ -658,12 +757,23 @@
867 self.assertBasicProfileExists(request, show=True))
868
869
870-class TestSqlTrace(TestCaseWithFactory, BaseRequestEndHandlerTest):
871+class TestSqlLogging(TestCaseWithFactory, BaseRequestEndHandlerTest):
872
873 layer = layers.DatabaseFunctionalLayer
874
875 def testLogging(self):
876- # This is basically a smoketest. The underlying machinery has tests.
877+ self.pushProfilingConfig(profiling_allowed='True')
878+ request = self.endRequest(
879+ '/++profile++sql/', work=self.factory.makeBug)
880+ response = self.getAddedResponse(request)
881+ self.assertIn('Top 10 SQL times', response)
882+ self.assertIn('Query number', response)
883+ self.assertIn('Top 10 Python times', response)
884+ self.assertIn('Before query', response)
885+ self.assertTrue('Repeated Python SQL Triggers' not in response)
886+ self.assertTrue('Show all tracebacks' not in response)
887+
888+ def testTraceLogging(self):
889 self.pushProfilingConfig(profiling_allowed='True')
890 request = self.endRequest(
891 '/++profile++sqltrace/', work=self.factory.makeBug)
892@@ -676,3 +786,21 @@
893 self.assertIn('Show all tracebacks', response)
894 # This file should be part of several of the tracebacks.
895 self.assertIn(__file__.replace('.pyc', '.py'), response)
896+
897+ def testTraceLoggingConditionally(self):
898+ self.pushProfilingConfig(profiling_allowed='True')
899+ request = self.endRequest(
900+ '/++profile++sqltrace:includes SELECT/',
901+ work=self.factory.makeBug)
902+ response = self.getAddedResponse(request)
903+ self.assertIn('Top 10 SQL times', response)
904+ self.assertIn('Query number', response)
905+ self.assertIn('Top 10 Python times', response)
906+ self.assertIn('Before query', response)
907+ self.assertIn('Repeated Python SQL Triggers', response)
908+ self.assertIn('Show all tracebacks', response)
909+ self.assertIn(
910+ 'You have requested tracebacks for statements that match only',
911+ response)
912+ # This file should be part of several of the tracebacks.
913+ self.assertIn(__file__.replace('.pyc', '.py'), response)