Merge lp:~lifeless/launchpad/bug-711071 into lp:launchpad

Proposed by Robert Collins
Status: Merged
Approved by: Robert Collins
Approved revision: no longer in the source branch.
Merged at revision: 12630
Proposed branch: lp:~lifeless/launchpad/bug-711071
Merge into: lp:launchpad
Diff against target: 460 lines (+137/-79)
6 files modified
lib/lp/bugs/browser/bugtask.py (+69/-12)
lib/lp/bugs/browser/tests/special/bugs-fixed-elsewhere.txt (+19/-21)
lib/lp/bugs/browser/tests/test_bugtarget_patches_view.py (+9/-38)
lib/lp/bugs/configure.zcml (+5/-0)
lib/lp/bugs/interfaces/bugtask.py (+23/-1)
lib/lp/bugs/model/bugtask.py (+12/-7)
To merge this branch: bzr merge lp:~lifeless/launchpad/bug-711071
Reviewer Review Type Date Requested Status
Abel Deuring (community) code Approve
Review via email: mp+53956@code.launchpad.net

Commit message

[r=adeuring][bug=711071] Use aggregation rather than repeated expensive queries in bug context stats portlets.

Description of the change

The bugtarget stats portlet was querying each statistic separately; this walks over the same bugs many times and is thus terribly slow. Doing the most expensive examine-all-bugs case and deriving the other information from it is much faster - so this patch does that for the most expensive properties. See the linked bug for analysis of which things are grouped and why.

To post a comment you must log in.
Revision history for this message
Abel Deuring (adeuring) :
review: Approve (code)

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-03-18 15:19:34 +0000
3+++ lib/lp/bugs/browser/bugtask.py 2011-03-20 22:37:28 +0000
4@@ -77,6 +77,7 @@
5 from lazr.uri import URI
6 from pytz import utc
7 from simplejson import dumps
8+from storm.expr import SQL
9 from z3c.ptcompat import ViewPageTemplateFile
10 from zope import (
11 component,
12@@ -247,6 +248,8 @@
13 from lp.bugs.interfaces.bugwatch import BugWatchActivityStatus
14 from lp.bugs.interfaces.cve import ICveSet
15 from lp.bugs.interfaces.malone import IMaloneApplication
16+from lp.bugs.model.bug import Bug
17+from lp.bugs.model.bugtask import BugTask
18 from lp.registry.interfaces.distribution import (
19 IDistribution,
20 IDistributionSet,
21@@ -274,6 +277,21 @@
22 )
23
24
25+DISPLAY_BUG_STATUS_FOR_PATCHES = {
26+ BugTaskStatus.NEW: True,
27+ BugTaskStatus.INCOMPLETE: True,
28+ BugTaskStatus.INVALID: False,
29+ BugTaskStatus.WONTFIX: False,
30+ BugTaskStatus.CONFIRMED: True,
31+ BugTaskStatus.TRIAGED: True,
32+ BugTaskStatus.INPROGRESS: True,
33+ BugTaskStatus.FIXCOMMITTED: True,
34+ BugTaskStatus.FIXRELEASED: False,
35+ BugTaskStatus.UNKNOWN: False,
36+ BugTaskStatus.EXPIRED: False
37+ }
38+
39+
40 @component.adapter(IBugTask, IReference, IWebServiceClientRequest)
41 @implementer(IFieldHTMLRenderer)
42 def bugtarget_renderer(context, field, request):
43@@ -1765,12 +1783,54 @@
44 These can be expensive to obtain.
45 """
46
47+ @cachedproperty
48+ def _bug_stats(self):
49+ bug_task_set = getUtility(IBugTaskSet)
50+ upstream_open_bugs = bug_task_set.open_bugtask_search
51+ upstream_open_bugs.setTarget(self.context)
52+ upstream_open_bugs.resolved_upstream = True
53+ fixed_upstream_clause = SQL(
54+ bug_task_set.buildUpstreamClause(upstream_open_bugs))
55+ open_bugs = bug_task_set.open_bugtask_search
56+ open_bugs.setTarget(self.context)
57+ groups = (BugTask.status, BugTask.importance,
58+ Bug.latest_patch_uploaded != None, fixed_upstream_clause)
59+ counts = bug_task_set.countBugs(open_bugs, groups)
60+ # Sum the split out aggregates.
61+ new = 0
62+ open = 0
63+ inprogress = 0
64+ critical = 0
65+ high = 0
66+ with_patch = 0
67+ resolved_upstream = 0
68+ for metadata, count in counts.items():
69+ status = metadata[0]
70+ importance = metadata[1]
71+ has_patch = metadata[2]
72+ was_resolved_upstream = metadata[3]
73+ if status == BugTaskStatus.NEW:
74+ new += count
75+ elif status == BugTaskStatus.INPROGRESS:
76+ inprogress += count
77+ if importance == BugTaskImportance.CRITICAL:
78+ critical += count
79+ elif importance == BugTaskImportance.HIGH:
80+ high += count
81+ if has_patch and DISPLAY_BUG_STATUS_FOR_PATCHES[status]:
82+ with_patch += count
83+ if was_resolved_upstream:
84+ resolved_upstream += count
85+ open += count
86+ result = dict(new=new, open=open, inprogress=inprogress, high=high,
87+ critical=critical, with_patch=with_patch,
88+ resolved_upstream=resolved_upstream)
89+ return result
90+
91 @property
92 def bugs_fixed_elsewhere_count(self):
93 """A count of bugs fixed elsewhere."""
94- params = get_default_search_params(self.user)
95- params.resolved_upstream = True
96- return self.context.searchTasks(params).count()
97+ return self._bug_stats['resolved_upstream']
98
99 @property
100 def open_cve_bugs_count(self):
101@@ -1813,27 +1873,27 @@
102 @property
103 def new_bugs_count(self):
104 """A count of new bugs."""
105- return self.context.new_bugtasks.count()
106+ return self._bug_stats['new']
107
108 @property
109 def open_bugs_count(self):
110 """A count of open bugs."""
111- return self.context.open_bugtasks.count()
112+ return self._bug_stats['open']
113
114 @property
115 def inprogress_bugs_count(self):
116 """A count of in-progress bugs."""
117- return self.context.inprogress_bugtasks.count()
118+ return self._bug_stats['inprogress']
119
120 @property
121 def critical_bugs_count(self):
122 """A count of critical bugs."""
123- return self.context.critical_bugtasks.count()
124+ return self._bug_stats['critical']
125
126 @property
127 def high_bugs_count(self):
128 """A count of high priority bugs."""
129- return self.context.high_bugtasks.count()
130+ return self._bug_stats['high']
131
132 @property
133 def my_bugs_count(self):
134@@ -1848,10 +1908,7 @@
135 @property
136 def bugs_with_patches_count(self):
137 """A count of unresolved bugs with patches."""
138- return self.context.searchTasks(
139- None, user=self.user,
140- status=UNRESOLVED_BUGTASK_STATUSES,
141- omit_duplicates=True, has_patch=True).count()
142+ return self._bug_stats['with_patch']
143
144
145 class BugListingPortletInfoView(LaunchpadView, BugsInfoMixin):
146
147=== modified file 'lib/lp/bugs/browser/tests/special/bugs-fixed-elsewhere.txt'
148--- lib/lp/bugs/browser/tests/special/bugs-fixed-elsewhere.txt 2010-10-09 16:36:22 +0000
149+++ lib/lp/bugs/browser/tests/special/bugs-fixed-elsewhere.txt 2011-03-20 22:37:28 +0000
150@@ -21,11 +21,14 @@
151 calculate, so it is put on this separate view which can be requested
152 asyncronously.
153
154- >>> view = getMultiAdapter(
155- ... (bugtarget, LaunchpadTestRequest()),
156- ... name='+bugtarget-portlet-bugfilters-stats')
157- >>> view.initialize()
158+ >>> def get_view():
159+ ... view = getMultiAdapter(
160+ ... (bugtarget, LaunchpadTestRequest()),
161+ ... name='+bugtarget-portlet-bugfilters-stats')
162+ ... view.initialize()
163+ ... return view
164
165+ >>> view = get_view()
166 >>> view.bugs_fixed_elsewhere_url
167 u'http://.../+bugs?field.status_upstream=resolved_upstream'
168 >>> view.bugs_fixed_elsewhere_count
169@@ -43,7 +46,7 @@
170 >>> from lp.bugs.interfaces.bugtask import IBugTaskSet
171 >>> elsewhere = getUtility(IBugTaskSet).createTask(
172 ... bug, owner=getUtility(ILaunchBag).user, product=evolution)
173- >>> view.bugs_fixed_elsewhere_count
174+ >>> get_view().bugs_fixed_elsewhere_count
175 0
176
177 But if we mark the bug as fixed in the other, the count will increase
178@@ -55,8 +58,8 @@
179 ... BugTaskStatus.FIXRELEASED, getUtility(ILaunchBag).user)
180 >>> syncUpdate(elsewhere)
181
182- >>> view.bugs_fixed_elsewhere_count
183- 1
184+ >>> get_view().bugs_fixed_elsewhere_count
185+ 1L
186
187 Bugs fixed elsewhere also show up when we perform an advanced bug
188 search, using the appropriate query string parameter to ask for "bugs
189@@ -93,20 +96,15 @@
190 ... BugTaskStatus.FIXRELEASED, getUtility(ILaunchBag).user)
191 >>> syncUpdate(another_elsewhere)
192
193- >>> view.bugs_fixed_elsewhere_count
194- 2
195+ >>> get_view().bugs_fixed_elsewhere_count
196+ 2L
197
198 This means that No Privileges Person will see that there is only one bug
199 fixed elsewhere.
200
201 >>> login('no-priv@canonical.com')
202- >>> view = getMultiAdapter(
203- ... (bugtarget, LaunchpadTestRequest()),
204- ... name='+bugtarget-portlet-bugfilters-stats')
205- >>> view.initialize()
206-
207- >>> view.bugs_fixed_elsewhere_count
208- 1
209+ >>> get_view().bugs_fixed_elsewhere_count
210+ 1L
211
212 If the private bug is made public again, he will of course see that
213 there are two bugs fixed.
214@@ -117,8 +115,8 @@
215 >>> syncUpdate(another_bug)
216
217 >>> login('no-priv@canonical.com')
218- >>> view.bugs_fixed_elsewhere_count
219- 2
220+ >>> get_view().bugs_fixed_elsewhere_count
221+ 2L
222
223
224 == Duplicate Bugs ==
225@@ -128,8 +126,8 @@
226 >>> another_bug.markAsDuplicate(bug)
227 >>> syncUpdate(another_bug)
228
229- >>> view.bugs_fixed_elsewhere_count
230- 1
231+ >>> get_view().bugs_fixed_elsewhere_count
232+ 1L
233
234
235 == Resolved Bugs ==
236@@ -145,5 +143,5 @@
237 ... BugTaskStatus.FIXRELEASED, getUtility(ILaunchBag).user)
238 >>> syncUpdate(bugtask)
239
240- >>> view.bugs_fixed_elsewhere_count
241+ >>> get_view().bugs_fixed_elsewhere_count
242 0
243
244=== modified file 'lib/lp/bugs/browser/tests/test_bugtarget_patches_view.py'
245--- lib/lp/bugs/browser/tests/test_bugtarget_patches_view.py 2010-10-04 19:50:45 +0000
246+++ lib/lp/bugs/browser/tests/test_bugtarget_patches_view.py 2011-03-20 22:37:28 +0000
247@@ -10,26 +10,14 @@
248 from canonical.launchpad.webapp.servers import LaunchpadTestRequest
249 from canonical.testing.layers import LaunchpadFunctionalLayer
250 from lp.bugs.browser.bugtarget import BugsPatchesView
251-from lp.bugs.browser.bugtask import BugListingPortletStatsView
252+from lp.bugs.browser.bugtask import (
253+ BugListingPortletStatsView,
254+ DISPLAY_BUG_STATUS_FOR_PATCHES,
255+ )
256 from lp.bugs.interfaces.bugtask import BugTaskStatus
257 from lp.testing import TestCaseWithFactory
258
259
260-DISPLAY_BUG_STATUS_FOR_PATCHES = {
261- BugTaskStatus.NEW: True,
262- BugTaskStatus.INCOMPLETE: True,
263- BugTaskStatus.INVALID: False,
264- BugTaskStatus.WONTFIX: False,
265- BugTaskStatus.CONFIRMED: True,
266- BugTaskStatus.TRIAGED: True,
267- BugTaskStatus.INPROGRESS: True,
268- BugTaskStatus.FIXCOMMITTED: True,
269- BugTaskStatus.FIXRELEASED: False,
270- BugTaskStatus.UNKNOWN: False,
271- BugTaskStatus.EXPIRED: False
272- }
273-
274-
275 class TestBugTargetPatchCountBase(TestCaseWithFactory):
276
277 layer = LaunchpadFunctionalLayer
278@@ -48,10 +36,6 @@
279
280 class TestBugTargetPatchView(TestBugTargetPatchCountBase):
281
282- def setUp(self):
283- super(TestBugTargetPatchView, self).setUp()
284- self.view = BugsPatchesView(self.product, LaunchpadTestRequest())
285-
286 def test_status_of_bugs_with_patches_shown(self):
287 # Bugs with patches that have the status FIXRELEASED, INVALID,
288 # WONTFIX, UNKNOWN, EXPIRED are not shown in the +patches view; all
289@@ -61,7 +45,8 @@
290 if DISPLAY_BUG_STATUS_FOR_PATCHES[bugtask_status]:
291 number_of_bugs_shown += 1
292 self.makeBugWithPatch(bugtask_status)
293- batched_tasks = self.view.batchedPatchTasks()
294+ view = BugsPatchesView(self.product, LaunchpadTestRequest())
295+ batched_tasks = view.batchedPatchTasks()
296 self.assertEqual(
297 batched_tasks.batch.listlength, number_of_bugs_shown,
298 "Unexpected number of bugs with patches displayed for status "
299@@ -70,11 +55,6 @@
300
301 class TestBugListingPortletStatsView(TestBugTargetPatchCountBase):
302
303- def setUp(self):
304- super(TestBugListingPortletStatsView, self).setUp()
305- self.view = BugListingPortletStatsView(
306- self.product, LaunchpadTestRequest())
307-
308 def test_bugs_with_patches_count(self):
309 # Bugs with patches that have the status FIXRELEASED, INVALID,
310 # WONTFIX, or UNKNOWN are not counted in
311@@ -85,18 +65,9 @@
312 if DISPLAY_BUG_STATUS_FOR_PATCHES[bugtask_status]:
313 number_of_bugs_shown += 1
314 self.makeBugWithPatch(bugtask_status)
315+ view = BugListingPortletStatsView(
316+ self.product, LaunchpadTestRequest())
317 self.assertEqual(
318- self.view.bugs_with_patches_count, number_of_bugs_shown,
319+ view.bugs_with_patches_count, number_of_bugs_shown,
320 "Unexpected number of bugs with patches displayed for status "
321 "%s" % bugtask_status)
322-
323-
324-def test_suite():
325- suite = unittest.TestSuite()
326- suite.addTest(unittest.makeSuite(TestBugTargetPatchView))
327- suite.addTest(unittest.makeSuite(TestBugListingPortletStatsView))
328- return suite
329-
330-
331-if __name__ == '__main__':
332- unittest.TextTestRunner().run(test_suite())
333
334=== modified file 'lib/lp/bugs/configure.zcml'
335--- lib/lp/bugs/configure.zcml 2011-03-16 13:26:30 +0000
336+++ lib/lp/bugs/configure.zcml 2011-03-20 22:37:28 +0000
337@@ -309,6 +309,11 @@
338 attributes="
339 setTarget
340 "/>
341+ <require
342+ permission="zope.Public"
343+ set_attributes="
344+ resolved_upstream
345+ "/>
346 </class>
347
348 <!-- BugTaskSet -->
349
350=== modified file 'lib/lp/bugs/interfaces/bugtask.py'
351--- lib/lp/bugs/interfaces/bugtask.py 2011-03-17 15:25:04 +0000
352+++ lib/lp/bugs/interfaces/bugtask.py 2011-03-20 22:37:28 +0000
353@@ -1274,21 +1274,37 @@
354 supported.
355 """
356 # Yay circular deps.
357+ from lp.registry.interfaces.distribution import IDistribution
358 from lp.registry.interfaces.distroseries import IDistroSeries
359+ from lp.registry.interfaces.product import IProduct
360 from lp.registry.interfaces.productseries import IProductSeries
361 from lp.registry.interfaces.milestone import IMilestone
362+ from lp.registry.interfaces.projectgroup import IProjectGroup
363+ from lp.registry.interfaces.sourcepackage import ISourcePackage
364+ from lp.registry.interfaces.distributionsourcepackage import \
365+ IDistributionSourcePackage
366 if isinstance(target, (any, all)):
367 assert len(target.query_values), \
368 'cannot determine target with no targets'
369 instance = target.query_values[0]
370 else:
371 instance = target
372- if IDistroSeries.providedBy(instance):
373+ if IDistribution.providedBy(instance):
374+ self.setDistribution(target)
375+ elif IDistroSeries.providedBy(instance):
376 self.setDistroSeries(target)
377+ elif IProduct.providedBy(instance):
378+ self.setProduct(target)
379 elif IProductSeries.providedBy(instance):
380 self.setProductSeries(target)
381 elif IMilestone.providedBy(instance):
382 self.milestone = target
383+ elif ISourcePackage.providedBy(instance):
384+ self.setSourcePackage(target)
385+ elif IDistributionSourcePackage.providedBy(instance):
386+ self.setSourcePackage(target)
387+ elif IProjectGroup.providedBy(instance):
388+ self.setProject(target)
389 else:
390 raise AssertionError("unknown target type %r" % target)
391
392@@ -1600,6 +1616,12 @@
393
394 open_bugtask_search = Attribute("A search returning open bugTasks.")
395
396+ def buildUpstreamClause(params):
397+ """Create a SQL clause to do upstream checks in a bug search.
398+
399+ :return: A string SQL expression.
400+ """
401+
402
403
404 def valid_remote_bug_url(value):
405
406=== modified file 'lib/lp/bugs/model/bugtask.py'
407--- lib/lp/bugs/model/bugtask.py 2011-03-18 15:19:34 +0000
408+++ lib/lp/bugs/model/bugtask.py 2011-03-20 22:37:28 +0000
409@@ -1690,6 +1690,14 @@
410 "project group, or distribution")
411 return (join_tables, extra_clauses)
412
413+ def _require_params(self, params):
414+ assert zope_isinstance(params, BugTaskSearchParams)
415+ if not isinstance(params, BugTaskSearchParams):
416+ # Browser code let this get wrapped, unwrap it here as its just a
417+ # dumb data store that has no security implications.
418+ params = removeSecurityProxy(params)
419+ return params
420+
421 def buildQuery(self, params):
422 """Build and return an SQL query with the given parameters.
423
424@@ -1698,11 +1706,7 @@
425 :return: A query, the tables to query, ordering expression and a
426 decorator to call on each returned row.
427 """
428- assert zope_isinstance(params, BugTaskSearchParams)
429- if not isinstance(params, BugTaskSearchParams):
430- # Browser code let this get wrapped, unwrap it here as its just a
431- # dumb data store that has no security implications.
432- params = removeSecurityProxy(params)
433+ params = self._require_params(params)
434 from lp.bugs.model.bug import Bug
435 extra_clauses = ['Bug.id = BugTask.bug']
436 clauseTables = [BugTask, Bug]
437@@ -1934,7 +1938,7 @@
438 """BugTask.sourcepackagename in (
439 select sourcepackagename from spns)""")
440
441- upstream_clause = self._buildUpstreamClause(params)
442+ upstream_clause = self.buildUpstreamClause(params)
443 if upstream_clause:
444 extra_clauses.append(upstream_clause)
445
446@@ -2085,12 +2089,13 @@
447 query, clauseTables, orderby_arg, decorator, join_tables,
448 has_duplicate_results, with_clause)
449
450- def _buildUpstreamClause(self, params):
451+ def buildUpstreamClause(self, params):
452 """Return an clause for returning upstream data if the data exists.
453
454 This method will handles BugTasks that do not have upstream BugTasks
455 as well as thoses that do.
456 """
457+ params = self._require_params(params)
458 upstream_clauses = []
459 if params.pending_bugwatch_elsewhere:
460 if params.product: