Merge lp:~gary/launchpad/bug838878 into lp:launchpad
- bug838878
- Merge into devel
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 |
Related bugs: |
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+
- ++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+
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:/
QA should be similar: https:/
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
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 "SQL trace" 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 "Product.blueprints_usage" 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 "SELECT Distribution.answers_usage" 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 "<tal:block replace="trigger/filename"></tal:block>", |
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 "<tal:block |
180 | + replace="trigger/filename"></tal:block>", 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 "<tal:block replace="frame/filename"></tal:block>", |
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) |
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' services/ profile/ profile. pt 2011-08-22 17:08:02 +0000 services/ profile/ profile. pt 2011-09-01 17:17:32 +0000 +profile+ +show</ code>.< /tal:block> <tal:block condition="not: ++profile+ +show</ code>), a KCacheGrind- friendly log on the ++profile+ +show</ code>), a KCacheGrind- friendly log on the ++profile+ +callgrind< /code>) , a PStats-friendly log ++profile+ +pstats< /code>) , or an HTML view of the SQL and the ++profile+ +pstats< /code>) , an HTML view of the SQL and the ++profile+ +sqltrace< /code>) . You can also combine these ++profile+ +show,callgrind </code> or +profile+ +show,pstats< /code> or others, .</tal: block>< /p> ++profile+ +sqltrace< /code>) , or an HTML view of only the SQL ++profile+ +sql</code> ). You can also combine these ++profile+ +show&callgrind </code> or +profile+ +show&pstats< /code> or others, with each action separated .</tal: block>< /p>
> --- lib/lp/
> +++ lib/lp/
> @@ -64,15 +64,21 @@
> get immediate profiling results in the browser, use
> <code>+
> always_log">In order to get profiling results, you need to ask for an HTML
> - view (<code>
> + view of the profile and OOPS data including SQL calls
> + (<code>
> filesystem (<code>
> (Python standard library) on the filesystem
> - (<code>
> + (<code>
> Python stacktrace when each SQL call was made
> - (<code>
> - (<code>
> - <code>+
> - with each action separated with commas)
> + (<code>
> + (<code>
> + (<code>
> + <code>+
> + with commas)
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' ).lower( ) for item in actions.split(',')) ).lower( ).partition( ':')
> @@ -505,24 +521,108 @@
> # include traversal in the profile.
> actions, slash, tail = end.partition('/')
> result.update(
> - action for action in (
> - item.strip(
> - if action)
> + (name.strip(), config.strip())
> + for name, partition, config in (
> + item.strip(
> + 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