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

Proposed by Gary Poster
Status: Merged
Approved by: Graham Binns
Approved revision: no longer in the source branch.
Merged at revision: 13772
Proposed branch: lp:~gary/launchpad/bug724025
Merge into: lp:launchpad
Diff against target: 522 lines (+144/-162)
5 files modified
lib/lp/bugs/browser/bugtask.py (+72/-81)
lib/lp/bugs/browser/configure.zcml (+1/-2)
lib/lp/bugs/templates/bugtask-tasks-and-nominations-table-row.pt (+66/-74)
setup.py (+1/-2)
versions.cfg (+4/-3)
To merge this branch: bzr merge lp:~gary/launchpad/bug724025
Reviewer Review Type Date Requested Status
Graham Binns (community) code Approve
Review via email: mp+72484@code.launchpad.net

Commit message

[incr] [r=gmb][bug=724025] Speed up an inner loop for BugTask:+index with many tasks.

Description of the change

This branch takes the previous work to reduce the Python cost of many tasks on the BugTask:+index page and extends it a bit more. This time, we optimize the inner loop of rendering many tasks by calculating the shared data for the view carefully at initialization, and by using a Chameleon template for the rendering.

Note that this code has *no* SQL queries while the inner loop is run, thanks to previous work by Robert. This is entirely rendering time.

The initialization saves a bit less than half a second on my system with 160/320 bugtasks (the doubled number represents the parent bugtask for a sourcepackage). Hopefully it is fairly straightforward to read: I collect the data, and then the template is simplified a bit.

The switch to Chameleon is even less to read. It also saves around half a second in the conditions I described above. You might know that I disabled Chameleon earlier for ++profile++. The new version does not write compiled files to disk by default. While I would prefer it to do this in the future, it is simpler to do what I have done here. The first time BugTask:+index is rendered, a bit of time will be spent to calculate the bytecode for the template; subsequently, the cost will be gone until the process restarts.

There are no tests, because it is existing functionality, and we already were not generating SQL in the inner loop.

Lint is happy.

To QA, go to any BugTask:+index page. The first time a +index page is rendered for that process, it will be a bit slower, as described above. Subsequently, you should have a modest speed gain.

This bug will get one or two more changes before I move on. Minimally, I plan to make only the first 50 bugtasks render initially, with the current context bugtask at the top, and then JS to render the remaining tasks on demand. Ideally, I'll also address an unrelated aspect of why this page is slow: we need to pre-load associated branches, using code that Danilo worked on in another branch.

Thank you

To post a comment you must log in.
Revision history for this message
Graham Binns (gmb) :
review: Approve (code)
Revision history for this message
Gary Poster (gary) wrote :

This should have been [incr]. :-/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/bugs/browser/bugtask.py'
2--- lib/lp/bugs/browser/bugtask.py 2011-08-22 13:13:28 +0000
3+++ lib/lp/bugs/browser/bugtask.py 2011-08-23 01:53:28 +0000
4@@ -75,7 +75,7 @@
5 from lazr.uri import URI
6 from pytz import utc
7 from simplejson import dumps
8-from z3c.ptcompat import ViewPageTemplateFile
9+from z3c.pt.pagetemplate import ViewPageTemplateFile
10 from zope import (
11 component,
12 formlib,
13@@ -1067,7 +1067,7 @@
14 class BugTaskBugWatchMixin:
15 """A mixin to be used where a BugTask view displays BugWatch data."""
16
17- @property
18+ @cachedproperty
19 def bug_watch_error_message(self):
20 """Return a browser-useable error message for a bug watch."""
21 if not self.context.bugwatch:
22@@ -3383,10 +3383,50 @@
23 target_link_title = None
24 many_bugtasks = False
25
26+ template = ViewPageTemplateFile(
27+ '../templates/bugtask-tasks-and-nominations-table-row.pt')
28+
29 def __init__(self, context, request):
30 super(BugTaskTableRowView, self).__init__(context, request)
31 self.milestone_source = MilestoneVocabulary
32
33+ def initialize(self):
34+ super(BugTaskTableRowView, self).initialize()
35+ link = canonical_url(self.context)
36+ edit_link = link + '/+editstatus'
37+ view_link = link + '/+viewstatus'
38+ can_edit = check_permission('launchpad.Edit', self.context)
39+ task_link = edit_link if can_edit else view_link
40+ bugtask_id = self.context.id
41+ launchbag = getUtility(ILaunchBag)
42+ is_primary = self.context.id == launchbag.bugtask.id
43+ self.data = dict(
44+ # Looking at many_bugtasks is an important optimization. With
45+ # 150+ bugtasks, it can save three or four seconds of rendering
46+ # time.
47+ expandable=(not self.many_bugtasks and self.canSeeTaskDetails()),
48+ indent_task=ISeriesBugTarget.providedBy(self.context.target),
49+ is_conjoined_slave=self.is_conjoined_slave,
50+ task_link=task_link,
51+ edit_link=edit_link,
52+ view_link=view_link,
53+ can_edit=can_edit,
54+ link=link,
55+ id=bugtask_id,
56+ row_id='tasksummary%d' % bugtask_id,
57+ form_row_id='task%d' % bugtask_id,
58+ row_css_class='highlight' if is_primary else None,
59+ target_link=canonical_url(self.context.target),
60+ target_link_title=self.target_link_title,
61+ user_can_edit_importance=self.context.userCanEditImportance(
62+ self.user),
63+ importance_css_class='importance' + self.context.importance.name,
64+ importance_title=self.context.importance.title,
65+ # We always look up all milestones, so there's no harm
66+ # using len on the list here and avoid the COUNT query.
67+ target_has_milestones=len(self._visible_milestones) > 0,
68+ )
69+
70 def canSeeTaskDetails(self):
71 """Whether someone can see a task's status details.
72
73@@ -3405,42 +3445,6 @@
74 self.context.bug.duplicateof is None and
75 not self.is_converted_to_question)
76
77- def expandable(self):
78- """Can the task's details be expanded?
79-
80- They can if there are not too many bugtasks, and if the user can see
81- the task details."""
82- # Looking at many_bugtasks is an important optimization. With 150+
83- # bugtasks, it can save three or four seconds of rendering time.
84- return not self.many_bugtasks and self.canSeeTaskDetails()
85-
86- def getTaskRowCSSClass(self):
87- """The appropriate CSS class for the row in the Affects table.
88-
89- Currently this consists solely of highlighting the current context.
90- """
91- bugtask = self.context
92- if bugtask == getUtility(ILaunchBag).bugtask:
93- return 'highlight'
94- else:
95- return None
96-
97- def shouldIndentTask(self):
98- """Should this task be indented in the task listing on the bug page?
99-
100- Returns True or False.
101- """
102- return ISeriesBugTarget.providedBy(self.context.target)
103-
104- def taskLink(self):
105- """Return the proper link to the bugtask whether it's editable."""
106- user = getUtility(ILaunchBag).user
107- bugtask = self.context
108- if check_permission('launchpad.Edit', user):
109- return canonical_url(bugtask) + "/+editstatus"
110- else:
111- return canonical_url(bugtask) + "/+viewstatus"
112-
113 def _getSeriesTargetNameHelper(self, bugtask):
114 """Return the short name of bugtask's targeted series."""
115 series = bugtask.distroseries or bugtask.productseries
116@@ -3535,15 +3539,6 @@
117
118 return items
119
120- @cachedproperty
121- def target_has_milestones(self):
122- """Are there any milestones we can target?
123-
124- We always look up all milestones, so there's no harm
125- using len on the list here and avoid the COUNT query.
126- """
127- return len(self._visible_milestones) > 0
128-
129 def bugtask_canonical_url(self):
130 """Return the canonical url for the bugtask."""
131 return canonical_url(self.context)
132@@ -3564,7 +3559,7 @@
133 """
134 return self.user is not None
135
136- @property
137+ @cachedproperty
138 def user_can_edit_milestone(self):
139 """Can the user edit the Milestone field?
140
141@@ -3602,44 +3597,40 @@
142 'title': filter.title,
143 'description': filter.description,
144 })
145-
146 # Display the search field only if the user can set any person
147 # or team
148- user = getUtility(ILaunchBag).user
149+ user = self.user
150 hide_assignee_team_selection = (
151 not self.context.userCanSetAnyAssignee(user) and
152 (user is None or user.teams_participated_in.count() == 0))
153- return dumps({
154- 'row_id': 'tasksummary%s' % self.context.id,
155- 'bugtask_path': '/'.join(
156- [''] + canonical_url(self.context).split('/')[3:]),
157- 'prefix': get_prefix(self.context),
158- 'assignee_value': self.context.assignee
159- and self.context.assignee.name,
160- 'assignee_is_team': self.context.assignee
161- and self.context.assignee.is_team,
162- 'assignee_vocabulary': assignee_vocabulary,
163- 'assignee_vocabulary_filters': filter_details,
164- 'hide_assignee_team_selection': hide_assignee_team_selection,
165- 'user_can_unassign': self.context.userCanUnassign(user),
166- 'target_is_product': IProduct.providedBy(self.context.target),
167- 'status_widget_items': self.status_widget_items,
168- 'status_value': self.context.status.title,
169- 'importance_widget_items': self.importance_widget_items,
170- 'importance_value': self.context.importance.title,
171- 'milestone_widget_items': self.milestone_widget_items,
172- 'milestone_value': (self.context.milestone and
173- canonical_url(
174- self.context.milestone,
175- request=IWebServiceClientRequest(
176- self.request)) or
177- None),
178- 'user_can_edit_assignee': self.user_can_edit_assignee,
179- 'user_can_edit_milestone': self.user_can_edit_milestone,
180- 'user_can_edit_status': not self.context.bugwatch,
181- 'user_can_edit_importance': (
182- self.user_can_edit_importance and
183- not self.context.bugwatch)})
184+ cx = self.context
185+ return dumps(dict(
186+ row_id=self.data['row_id'],
187+ bugtask_path='/'.join([''] + self.data['link'].split('/')[3:]),
188+ prefix=get_prefix(cx),
189+ assignee_value=cx.assignee and cx.assignee.name,
190+ assignee_is_team=cx.assignee and cx.assignee.is_team,
191+ assignee_vocabulary=assignee_vocabulary,
192+ assignee_vocabulary_filters=filter_details,
193+ hide_assignee_team_selection=hide_assignee_team_selection,
194+ user_can_unassign=cx.userCanUnassign(user),
195+ target_is_product=IProduct.providedBy(cx.target),
196+ status_widget_items=self.status_widget_items,
197+ status_value=cx.status.title,
198+ importance_widget_items=self.importance_widget_items,
199+ importance_value=cx.importance.title,
200+ milestone_widget_items=self.milestone_widget_items,
201+ milestone_value=(
202+ canonical_url(
203+ cx.milestone,
204+ request=IWebServiceClientRequest(self.request))
205+ if cx.milestone else None),
206+ user_can_edit_assignee=self.user_can_edit_assignee,
207+ user_can_edit_milestone=self.user_can_edit_milestone,
208+ user_can_edit_status=not cx.bugwatch,
209+ user_can_edit_importance=(
210+ self.user_can_edit_importance and not cx.bugwatch)
211+ ))
212
213
214 class BugsBugTaskSearchListingView(BugTaskSearchListingView):
215
216=== modified file 'lib/lp/bugs/browser/configure.zcml'
217--- lib/lp/bugs/browser/configure.zcml 2011-08-16 14:21:39 +0000
218+++ lib/lp/bugs/browser/configure.zcml 2011-08-23 01:53:28 +0000
219@@ -564,8 +564,7 @@
220 for="lp.bugs.interfaces.bugtask.IBugTask"
221 class="lp.bugs.browser.bugtask.BugTaskTableRowView"
222 permission="launchpad.View"
223- name="+bugtasks-and-nominations-table-row"
224- template="../templates/bugtask-tasks-and-nominations-table-row.pt"/>
225+ name="+bugtasks-and-nominations-table-row"/>
226 <browser:page
227 for="lp.bugs.interfaces.bugtarget.IBugTarget"
228 permission="zope.Public"
229
230=== modified file 'lib/lp/bugs/templates/bugtask-tasks-and-nominations-table-row.pt'
231--- lib/lp/bugs/templates/bugtask-tasks-and-nominations-table-row.pt 2011-08-10 16:14:13 +0000
232+++ lib/lp/bugs/templates/bugtask-tasks-and-nominations-table-row.pt 2011-08-23 01:53:28 +0000
233@@ -1,44 +1,30 @@
234 <tal:bugtask
235 xmlns:tal="http://xml.zope.org/namespaces/tal"
236 xmlns:metal="http://xml.zope.org/namespaces/metal"
237- define="expandable view/expandable;
238- indent_task view/shouldIndentTask;
239- is_conjoined_slave view/is_conjoined_slave;
240- tasklink view/taskLink;
241- row_id string:tasksummary${context/id};
242- form_row_id string:task${context/id}">
243- <tr tal:define="editstatus_url string:${context/fmt:url}/+editstatus"
244- tal:attributes="class view/getTaskRowCSSClass; id row_id">
245+ define="data view/data">
246+ <tr tal:attributes="class data/row_css_class; id data/row_id">
247 <td>
248- <metal:expander
249- metal:define-macro="expander"
250- tal:define="expander_link_text expander_link_text|nothing">
251- <a tal:condition="expandable"
252- tal:attributes="href tasklink" class="bugtask-expander">
253- <tal:link_text tal:replace="expander_link_text" />
254- &#8203;
255- </a>
256- <tal:no_expander tal:condition="not: expandable"
257- tal:replace="expander_link_text" />
258- </metal:expander>
259+ <a tal:condition="data/expandable"
260+ tal:attributes="href data/task_link" class="bugtask-expander">
261+ &#8203;
262+ </a>
263 </td>
264 <td style="padding: 0.3em 0em 0.3em 1.5em"
265- tal:condition="indent_task"
266- tal:define="series_targetname view/getSeriesTargetName">
267+ tal:condition="data/indent_task">
268 <span class="sprite milestone"></span>
269- <tal:not-conjoined-task condition="not: is_conjoined_slave">
270+ <tal:not-conjoined-task condition="not: data/is_conjoined_slave">
271 <a
272- tal:attributes="href context/target/fmt:url"
273- tal:content="series_targetname"
274+ tal:attributes="href data/target_link"
275+ tal:content="view/getSeriesTargetName"
276 />
277 </tal:not-conjoined-task>
278 </td>
279- <td tal:condition="not:indent_task">
280- <span tal:attributes="id string:bugtarget-picker-${row_id}">
281+ <td tal:condition="not:data/indent_task">
282+ <span tal:attributes="id string:bugtarget-picker-${data/row_id}">
283 <span class="yui3-activator-data-box">
284 <span title="This project&rsquo;s license has not been specified.">
285- <a tal:attributes="href context/target/fmt:url;
286- title view/target_link_title;
287+ <a tal:attributes="href data/target_link;
288+ title data/target_link_title;
289 class context/target/image:sprite_css"
290 tal:content="context/bugtargetdisplayname" />
291 </span>
292@@ -50,7 +36,7 @@
293 </span>
294 </td>
295
296- <tal:conjoined-task condition="is_conjoined_slave">
297+ <tal:conjoined-task condition="data/is_conjoined_slave">
298 <td colspan="5" style="vertical-align: middle">
299 <span class="discreet lesser" style="font-size: 100%">
300 Status tracked in
301@@ -61,79 +47,85 @@
302 </td>
303 </tal:conjoined-task>
304
305- <tal:not-conjoined-task condition="not:is_conjoined_slave">
306+ <tal:not-conjoined-task condition="not:data/is_conjoined_slave">
307 <td style="width: 20%; vertical-align: middle">
308 <div class="status-content"
309- style="width: 100%; float: left">
310+ style="width: 100%; float: left"
311+ tal:define="status context/status">
312 <a href="+editstatus"
313- tal:attributes="class string:value status${context/status/name};
314- href editstatus_url"
315+ tal:attributes="class string:value status${status/name};
316+ href data/edit_link"
317 style="float: left"
318- tal:content="context/status/title" />
319+ tal:content="status/title" />
320 <a href="+editstatus" style="margin-left: 3px"
321- tal:attributes="href editstatus_url">
322+ tal:attributes="href data/edit_link">
323 <img class="editicon" src="/@@/edit" />
324 </a>
325 </div>
326 </td>
327
328- <td tal:condition="view/user_can_edit_importance"
329+ <td tal:condition="data/user_can_edit_importance"
330 style="width: 20%; vertical-align: middle">
331 <div class="importance-content"
332 style="width: 100%; float: left">
333 <a href="+editstatus"
334- tal:attributes="class string:value importance${context/importance/name};
335- href editstatus_url"
336+ tal:attributes="class string:value ${data/importance_css_class};
337+ href data/edit_link"
338 style="float: left"
339- tal:content="context/importance/title" />
340+ tal:content="data/importance_title" />
341 <a href="+editstatus" style="margin-left: 3px"
342- tal:attributes="href editstatus_url">
343+ tal:attributes="href data/edit_link">
344 <img class="editicon" src="/@@/edit" />
345 </a>
346 </div>
347 </td>
348- <td tal:condition="not: view/user_can_edit_importance"
349+ <td tal:condition="not: data/user_can_edit_importance"
350 style="width: 15em; vertical-align: middle">
351 <div class="importance-content"
352 style="width: 100%; float: left">
353 <span
354- tal:attributes="class string:value importance${context/importance/name}"
355+ tal:attributes="class string:value ${data/importance_css_class}"
356 style="float: left"
357- tal:content="context/importance/title" />
358+ tal:content="data/importance_title" />
359 </div>
360 </td>
361
362 <td style="width:20%; margin: 0; padding: 0;
363- vertical-align: middle; padding-left: 0.5em">
364- <tal:has_watch condition="context/bugwatch">
365- <div style="text-decoration: none; padding: 0.25em">
366- <tal:bugtracker-active
367- condition="context/bugwatch/bugtracker/active">
368- <span tal:condition="not:context/bugwatch/last_error_type"
369- class="sprite bug-remote"></span>
370- <a tal:condition="context/bugwatch/last_error_type"
371- tal:attributes="href view/bug_watch_error_message/help_url"
372- target="help"
373- class="icon help">
374- <span class="sprite warning-icon"
375- tal:attributes="title view/bug_watch_error_message/message"
376- id="bugwatch-error-sprite"></span>
377- </a>
378+ vertical-align: middle; padding-left: 0.5em"
379+ tal:define="bugwatch context/bugwatch;">
380+ <tal:has_watch condition="bugwatch">
381+ <div style="text-decoration: none; padding: 0.25em"
382+ tal:define="active_bugtracker bugwatch/bugtracker/active;">
383+ <tal:bugtracker-active condition="active_bugtracker">
384+ <tal:block define="last_error_type bugwatch/last_error_type;">
385+ <span tal:condition="not:last_error_type"
386+ class="sprite bug-remote"></span>
387+ <a tal:condition="last_error_type"
388+ tal:attributes="href view/bug_watch_error_message/help_url"
389+ target="help"
390+ class="icon help">
391+ <span class="sprite warning-icon"
392+ tal:attributes=
393+ "title view/bug_watch_error_message/message"
394+ id="bugwatch-error-sprite"></span>
395+ </a>
396+ </tal:block>
397 </tal:bugtracker-active>
398- <span tal:condition="not:context/bugwatch/bugtracker/active"
399+ <span tal:condition="not:active_bugtracker"
400 class="sprite warning-icon"></span>
401- <a tal:replace="structure context/bugwatch/fmt:external-link" />
402+ <a tal:replace="structure bugwatch/fmt:external-link" />
403 </div>
404 </tal:has_watch>
405
406- <tal:has_no_watch condition="not: context/bugwatch">
407- <span tal:attributes="id string:assignee-picker-${row_id}">
408+ <tal:has_no_watch condition="not: bugwatch">
409+ <span tal:attributes="id string:assignee-picker-${data/row_id}"
410+ tal:define="assignee context/assignee">
411 <span class="yui3-activator-data-box">
412- <a tal:condition="context/assignee"
413- tal:attributes="href context/assignee/fmt:url;
414- class context/assignee/image:sprite_css"
415- tal:content="context/assignee/fmt:displayname" />
416- <tal:unassigned condition="not: context/assignee">
417+ <a tal:condition="assignee"
418+ tal:attributes="href assignee/fmt:url;
419+ class assignee/image:sprite_css"
420+ tal:content="assignee/fmt:displayname" />
421+ <tal:unassigned condition="not: assignee">
422 Unassigned
423 </tal:unassigned>
424 </span>
425@@ -147,11 +139,11 @@
426
427 <td style="width: 20%; vertical-align: middle">
428 <div class="milestone-content"
429- tal:condition="view/target_has_milestones"
430+ tal:condition="data/target_has_milestones"
431 style="width: 100%; float: left">
432 <a tal:condition="view/user_can_edit_milestone"
433 class="nulltext addicon js-action sprite add"
434- tal:attributes="href editstatus_url;
435+ tal:attributes="href data/edit_link;
436 style view/style_for_add_milestone">
437 Target to milestone
438 </a>
439@@ -159,7 +151,7 @@
440 tal:attributes="href context/milestone/fmt:url | nothing"
441 tal:content="context/milestone/title | nothing" />
442 <a tal:condition="view/user_can_edit_milestone"
443- tal:attributes="href editstatus_url"
444+ tal:attributes="href data/edit_link"
445 ><img class="editicon" src="/@@/edit"
446 tal:attributes="style view/style_for_edit_milestone"
447 /></a>
448@@ -178,8 +170,8 @@
449 Y.lp.bugs.bugtask_index.setup_bugtask_row(${view/js_config});
450
451 // Set-up the expander on the bug task summary row.
452- var icon_node = Y.one('tr#${row_id} a.bugtask-expander');
453- var row_node = Y.one('tr#${form_row_id}');
454+ var icon_node = Y.one('tr#${data/row_id} a.bugtask-expander');
455+ var row_node = Y.one('tr#${data/form_row_id}');
456 if (Y.Lang.isValue(row_node)) {
457 // When no row is present, this is bug task on a project with
458 // multiple per-series tasks, so we do not need to set
459@@ -195,8 +187,8 @@
460
461 <tal:form condition="view/displayEditForm">
462 <tr
463- tal:attributes="id form_row_id"
464- tal:condition="expandable"
465+ tal:attributes="id data/form_row_id"
466+ tal:condition="data/expandable"
467 class="bugtask-collapsible-content unseen"
468 >
469 <td colspan="7" tal:content="structure view/edit_view" />
470
471=== modified file 'setup.py'
472--- setup.py 2011-08-21 22:06:09 +0000
473+++ setup.py 2011-08-23 01:53:28 +0000
474@@ -29,8 +29,7 @@
475 'ampoule',
476 'BeautifulSoup',
477 'bzr',
478- 'chameleon.core',
479- 'chameleon.zpt',
480+ 'Chameleon',
481 'cssutils',
482 # Required for pydkim
483 'dnspython',
484
485=== modified file 'versions.cfg'
486--- versions.cfg 2011-08-22 07:09:21 +0000
487+++ versions.cfg 2011-08-23 01:53:28 +0000
488@@ -8,8 +8,7 @@
489 amqplib = 0.6.1
490 BeautifulSoup = 3.1.0.1
491 bzr = 2.3.3
492-chameleon.core = 1.0b35
493-chameleon.zpt = 1.0b17
494+Chameleon = 2.4.0
495 ClientForm = 0.2.10
496 cssutils = 0.9.6
497 docutils = 0.5
498@@ -52,6 +51,7 @@
499 oops = 0.0.6
500 oops-datedir-repo = 0.0.3
501 oops-wsgi = 0.0.2
502+ordereddict = 1.1
503 paramiko = 1.7.4
504 Paste = 1.7.2
505 PasteDeploy = 1.3.3
506@@ -91,6 +91,7 @@
507 transaction = 1.0.0
508 txamqp = 0.4
509 Twisted = 11.0.0
510+unittest2 = 0.5.1
511 uuid = 1.30
512 van.testing = 2.0.1
513 wadllib = 1.2.0
514@@ -116,7 +117,7 @@
515 z3c.menu = 0.2.0
516 z3c.optionstorage = 1.0.4
517 z3c.pagelet = 1.0.2
518-z3c.pt = 1.0b16
519+z3c.pt = 2.1.3
520 z3c.ptcompat = 0.5.3
521 z3c.recipe.filetemplate = 2.1.0
522 z3c.recipe.i18n = 0.5.3