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

Proposed by Robert Collins
Status: Merged
Approved by: William Grant
Approved revision: no longer in the source branch.
Merged at revision: 13171
Proposed branch: lp:~lifeless/launchpad/bug-793809
Merge into: lp:launchpad
Diff against target: 416 lines (+126/-100)
11 files modified
lib/lp/bugs/browser/bugtarget.py (+4/-15)
lib/lp/bugs/doc/bug-tags.txt (+20/-21)
lib/lp/bugs/interfaces/bugtarget.py (+9/-7)
lib/lp/bugs/model/bug.py (+43/-30)
lib/lp/registry/model/distribution.py (+7/-3)
lib/lp/registry/model/distributionsourcepackage.py (+7/-5)
lib/lp/registry/model/distroseries.py (+10/-3)
lib/lp/registry/model/product.py (+6/-3)
lib/lp/registry/model/productseries.py (+5/-2)
lib/lp/registry/model/projectgroup.py (+8/-6)
lib/lp/registry/model/sourcepackage.py (+7/-5)
To merge this branch: bzr merge lp:~lifeless/launchpad/bug-793809
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+63635@code.launchpad.net

Commit message

[r=wgrant][bug=793809] Use BugSummary to do tag portlet calculations

Description of the change

Per the linked bug generating bug tag portlets is extraordinarily slow. This is meant to be addressed by querying bugsummary, which this branch implements.

I have changed the interface to match our needs allowing a faster query (the with clause on teams for visibility).

Other than that I can see a path to reduce the duplicate code that we use here, but its unrelated to fixing this bug, so I've focused on getting the bug fixed ;)

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) wrote :

21 + tags = self.context.getUsedBugTagsWithOpenCounts(self.user, 10,
22 + official_tags)

Nice cleanup. But bad indentation.

33 + official_tags = self.context.official_bug_tags

Sure this doesn't need to be materialised?

131 + :param tag_limit: The number of tags to return (excludes those found by
132 + matching include_tags). If 0 then all tags are returned. If
133 + non-zero then the most frequently used tags are returned.

0? Why not None?

172 +def get_bug_tags_open_count(context_condition, user, tag_limit=0,
173 + include_tags=None):

Bad indentation.

200 + store = store.with_(SQL(
201 + "teams AS ("
202 + "SELECT team from TeamParticipation WHERE person=%s)" % user.id))

String formatting in SQL: Australia says no.

Also, calling that "store" is a bit too much of a lie for my tastes.

228 + BugSummary.viewed_by_id.is_in(SQL("SELECT team FROM teams"))

What's the benefit of the WITH clause here? I doubt there is any performance gain, and including the full thing here is shorter and clearer.

review: Approve (code)
Revision history for this message
Robert Collins (lifeless) wrote :

> 21 + tags = self.context.getUsedBugTagsWithOpenCounts(self.user,
> 10,
> 22 + official_tags)
>
> Nice cleanup. But bad indentation.

How so ?

> 33 + official_tags = self.context.official_bug_tags
>
> Sure this doesn't need to be materialised?

The core function returns a list.

> 131 + :param tag_limit: The number of tags to return (excludes
> those found by
> 132 + matching include_tags). If 0 then all tags are returned.
> If
> 133 + non-zero then the most frequently used tags are returned.
>
> 0? Why not None?

Shrug; can change it to that, but 0 would be noddy. Using 0 as the flag avoids bad behaviour if a limit of 0 is passed in.

> 172 +def get_bug_tags_open_count(context_condition, user, tag_limit=0,
> 173 + include_tags=None):
>
> Bad indentation.

How so ?

> 200 + store = store.with_(SQL(
> 201 + "teams AS ("
> 202 + "SELECT team from TeamParticipation WHERE person=%s)" %
> user.id))
>
> String formatting in SQL: Australia says no.
>
> Also, calling that "store" is a bit too much of a lie for my tastes.

It meets the core contract and is better understood than 'resultset_factory'.

> 228 + BugSummary.viewed_by_id.is_in(SQL("SELECT team FROM
> teams"))
>
> What's the benefit of the WITH clause here? I doubt there is any performance
> gain, and including the full thing here is shorter and clearer.

Its faster. 150ms difference when I was testing.

Revision history for this message
William Grant (wgrant) wrote :

Thanks!

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/bugtarget.py'
2--- lib/lp/bugs/browser/bugtarget.py 2011-06-06 06:44:23 +0000
3+++ lib/lp/bugs/browser/bugtarget.py 2011-06-07 05:39:33 +0000
4@@ -1405,19 +1405,8 @@
5 def tags_cloud_data(self):
6 """The data for rendering a tags cloud"""
7 official_tags = self.context.official_bug_tags
8-
9- # Construct a dict of official and top 10 tags.
10- # getUsedBugTagsWithOpenCounts is expensive, so do the union in
11- # SQL. Also preseed with 0 for all the official tags, as gUBTWOC
12- # won't return unused ones.
13- top_ten = removeSecurityProxy(
14- self.context.getUsedBugTagsWithOpenCounts(self.user)[:10])
15- official = removeSecurityProxy(
16- self.context.getUsedBugTagsWithOpenCounts(
17- self.user, official_tags))
18- tags = dict((tag, 0) for tag in official_tags)
19- tags.update(dict(top_ten.union(official)))
20-
21+ tags = self.context.getUsedBugTagsWithOpenCounts(
22+ self.user, 10, official_tags)
23 max_count = float(max([1] + tags.values()))
24
25 return sorted(
26@@ -1462,8 +1451,8 @@
27 @property
28 def tags_js_data(self):
29 """Return the JSON representation of the bug tags."""
30- used_tags = dict(self.context.getUsedBugTagsWithOpenCounts(self.user))
31- official_tags = list(self.context.official_bug_tags)
32+ used_tags = self.context.getUsedBugTagsWithOpenCounts(self.user)
33+ official_tags = self.context.official_bug_tags
34 return """<script type="text/javascript">
35 var used_bug_tags = %s;
36 var official_bug_tags = %s;
37
38=== modified file 'lib/lp/bugs/doc/bug-tags.txt'
39--- lib/lp/bugs/doc/bug-tags.txt 2011-04-12 06:40:51 +0000
40+++ lib/lp/bugs/doc/bug-tags.txt 2011-06-07 05:39:33 +0000
41@@ -332,35 +332,34 @@
42 We can also get all the used tags, together with the number of open
43 bugs each tag has. Only tags having open bugs are returned.
44
45- >>> list(firefox.getUsedBugTagsWithOpenCounts(None))
46- [(u'doc', 1L), (u'sco', 1L), (u'svg', 1L)]
47-
48- >>> list(mozilla.getUsedBugTagsWithOpenCounts(None))
49- [(u'doc', 1L), (u'sco', 1L), (u'svg', 1L)]
50-
51- >>> list(ubuntu.getUsedBugTagsWithOpenCounts(None))
52+ >>> sorted(firefox.getUsedBugTagsWithOpenCounts(None).items())
53+ [(u'doc', 1L), (u'sco', 1L), (u'svg', 1L)]
54+
55+ >>> sorted(mozilla.getUsedBugTagsWithOpenCounts(None).items())
56+ [(u'doc', 1L), (u'sco', 1L), (u'svg', 1L)]
57+
58+ >>> sorted(ubuntu.getUsedBugTagsWithOpenCounts(None).items())
59 [(u'crash', 2L), (u'dataloss', 1L), (u'pebcak', 1L),
60 (u'sco', 1L), (u'svg', 1L)]
61
62-We can even ask for the counts for a particular set of tags.
63+We can require that some tags be included in the output even when limiting the
64+results.
65
66- >>> list(ubuntu.getUsedBugTagsWithOpenCounts(
67- ... None, wanted_tags=['pebcak', 'svg', 'fake']))
68- [(u'pebcak', 1L), (u'svg', 1L)]
69- >>> list(ubuntu.getUsedBugTagsWithOpenCounts(None, wanted_tags=[]))
70- []
71+ >>> sorted(ubuntu.getUsedBugTagsWithOpenCounts(None,
72+ ... tag_limit=1, include_tags=[u'pebcak', u'svg', u'fake']).items())
73+ [(u'crash', 2L), (u'fake', 0), (u'pebcak', 1L), (u'svg', 1L)]
74
75 Source packages are a bit special, they return all the tags that are
76 used in the whole distribution, while the bug count includes only bugs
77 in the specific package.
78
79- >>> list(ubuntu_thunderbird.getUsedBugTagsWithOpenCounts(None))
80- [(u'crash', 1L)]
81+ >>> ubuntu_thunderbird.getUsedBugTagsWithOpenCounts(None)
82+ {u'crash': 1L}
83
84- >>> list(debian_woody.getUsedBugTagsWithOpenCounts(None))
85+ >>> sorted(debian_woody.getUsedBugTagsWithOpenCounts(None).items())
86 [(u'dataloss', 1L), (u'layout-test', 1L), (u'pebcak', 1L)]
87
88- >>> list(debian_woody_firefox.getUsedBugTagsWithOpenCounts(None))
89+ >>> sorted(debian_woody_firefox.getUsedBugTagsWithOpenCounts(None).items())
90 [(u'dataloss', 1L), (u'layout-test', 1L), (u'pebcak', 1L)]
91
92 Only bugs that the supplied user has access to will be counted:
93@@ -370,14 +369,14 @@
94 True
95 >>> flush_database_updates()
96
97- >>> list(ubuntu_thunderbird.getUsedBugTagsWithOpenCounts(None))
98- []
99+ >>> ubuntu_thunderbird.getUsedBugTagsWithOpenCounts(None)
100+ {}
101
102 >>> sample_person = getUtility(ILaunchBag).user
103 >>> bug_nine.isSubscribed(sample_person)
104 True
105- >>> list(ubuntu_thunderbird.getUsedBugTagsWithOpenCounts(sample_person))
106- [(u'crash', 1L)]
107+ >>> ubuntu_thunderbird.getUsedBugTagsWithOpenCounts(sample_person)
108+ {u'crash': 1L}
109
110 When context doesn't have any tags getUsedBugTags() returns a empty list.
111
112
113=== modified file 'lib/lp/bugs/interfaces/bugtarget.py'
114--- lib/lp/bugs/interfaces/bugtarget.py 2011-04-12 06:21:39 +0000
115+++ lib/lp/bugs/interfaces/bugtarget.py 2011-06-07 05:39:33 +0000
116@@ -403,15 +403,17 @@
117 def getUsedBugTags():
118 """Return the tags used by the context as a sorted list of strings."""
119
120- def getUsedBugTagsWithOpenCounts(user, wanted_tags=None):
121+ def getUsedBugTagsWithOpenCounts(user, tag_limit=0, include_tags=None):
122 """Return name and bug count of tags having open bugs.
123
124- It returns a list of tuples contining the tag name, and the
125- number of open bugs having that tag. Only the bugs that the user
126- has permission to see are counted, and only tags having open
127- bugs will be returned.
128-
129- If wanted_tags is specified, only those tags will be returned.
130+ :param user: The user who wants the report.
131+ :param tag_limit: The number of tags to return (excludes those found by
132+ matching include_tags). If 0 then all tags are returned. If
133+ non-zero then the most frequently used tags are returned.
134+ :param include_tags: A list of string tags to return irrespective of
135+ usage. Tags in this list that have no open bugs are returned with a
136+ count of 0. May be None if there are tags to require inclusion of.
137+ :return: A dict from tag -> count.
138 """
139
140 def _getOfficialTagClause():
141
142=== modified file 'lib/lp/bugs/model/bug.py'
143--- lib/lp/bugs/model/bug.py 2011-06-03 10:38:25 +0000
144+++ lib/lp/bugs/model/bug.py 2011-06-07 05:39:33 +0000
145@@ -63,6 +63,7 @@
146 Select,
147 SQL,
148 SQLRaw,
149+ Sum,
150 Union,
151 )
152 from storm.info import ClassAlias
153@@ -242,11 +243,6 @@
154 tag = StringCol(notNull=True)
155
156
157-# We need to always use the same Count instance or the
158-# get_bug_tags_open_count is not UNIONable.
159-tag_count_columns = (BugTag.tag, Count())
160-
161-
162 def get_bug_tags(context_clause):
163 """Return all the bug tags as a list of strings.
164
165@@ -266,39 +262,56 @@
166 return shortlist([row[0] for row in cur.fetchall()])
167
168
169-def get_bug_tags_open_count(context_condition, user, wanted_tags=None):
170- """Return all the used bug tags with their open bug count.
171-
172+def get_bug_tags_open_count(context_condition, user, tag_limit=0,
173+ include_tags=None):
174+ """Worker for IBugTarget.getUsedBugTagsWithOpenCounts.
175+
176+ See `IBugTarget` for details.
177+
178+ The only change is that this function takes a SQL expression for limiting
179+ the found tags.
180 :param context_condition: A Storm SQL expression, limiting the
181 used tags to a specific context. Only the BugTask table may be
182 used to choose the context.
183- :param user: The user performing the search.
184- :param wanted_tags: A set of tags within which to restrict the search.
185-
186- :return: A list of tuples, (tag name, open bug count).
187 """
188- tables = (
189- BugTag,
190- Join(BugTask, BugTask.bugID == BugTag.bugID),
191- )
192+ # Circular fail.
193+ from lp.bugs.model.bugsummary import BugSummary
194+ tags = {}
195+ if include_tags:
196+ tags = dict((tag, 0) for tag in include_tags)
197+ store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
198+ admin_team = getUtility(ILaunchpadCelebrities).admin
199+ if user is not None and not user.inTeam(admin_team):
200+ store = store.with_(SQL(
201+ "teams AS ("
202+ "SELECT team from TeamParticipation WHERE person=?)", (user.id,)))
203 where_conditions = [
204- BugTask.status.is_in(UNRESOLVED_BUGTASK_STATUSES),
205+ BugSummary.status.is_in(UNRESOLVED_BUGTASK_STATUSES),
206+ BugSummary.tag != None,
207 context_condition,
208 ]
209- if wanted_tags is not None:
210- where_conditions.append(BugTag.tag.is_in(wanted_tags))
211- privacy_filter = get_bug_privacy_filter(user)
212- if privacy_filter:
213- # The EXISTS sub-select avoids a join against Bug, improving
214- # performance significantly.
215+ if user is None:
216+ where_conditions.append(BugSummary.viewed_by_id == None)
217+ elif not user.inTeam(admin_team):
218 where_conditions.append(
219- Exists(Select(
220- columns=[True], tables=[Bug],
221- where=And(Bug.id == BugTag.bugID, SQLRaw(privacy_filter)))))
222- store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
223- return store.using(*tables).find(
224- tag_count_columns, *where_conditions).group_by(BugTag.tag).order_by(
225- Desc(Count()), BugTag.tag)
226+ Or(
227+ BugSummary.viewed_by_id == None,
228+ BugSummary.viewed_by_id.is_in(SQL("SELECT team FROM teams"))
229+ ))
230+ tag_count_columns = (BugSummary.tag, Sum(BugSummary.count))
231+ # Always query for used
232+ def _query(*args):
233+ return store.find(tag_count_columns, *(where_conditions + list(args))
234+ ).group_by(BugSummary.tag).order_by(
235+ Desc(Sum(BugSummary.count)), BugSummary.tag)
236+ used = _query()
237+ if tag_limit:
238+ used = used[:tag_limit]
239+ if include_tags:
240+ # Union in a query for just include_tags.
241+ used = used.union(_query(BugSummary.tag.is_in(include_tags)))
242+ tags.update(dict(used))
243+ return tags
244
245
246 class BugBecameQuestionEvent:
247
248=== modified file 'lib/lp/registry/model/distribution.py'
249--- lib/lp/registry/model/distribution.py 2011-05-27 21:12:25 +0000
250+++ lib/lp/registry/model/distribution.py 2011-06-07 05:39:33 +0000
251@@ -646,10 +646,14 @@
252 """See `IBugTarget`."""
253 return get_bug_tags("BugTask.distribution = %s" % sqlvalues(self))
254
255- def getUsedBugTagsWithOpenCounts(self, user, wanted_tags=None):
256- """See `IBugTarget`."""
257+ def getUsedBugTagsWithOpenCounts(self, user, tag_limit=0, include_tags=None):
258+ """See IBugTarget."""
259+ # Circular fail.
260+ from lp.bugs.model.bugsummary import BugSummary
261 return get_bug_tags_open_count(
262- BugTask.distribution == self, user, wanted_tags=wanted_tags)
263+ And(BugSummary.distribution_id == self.id,
264+ BugSummary.sourcepackagename_id == None),
265+ user, tag_limit=tag_limit, include_tags=include_tags)
266
267 def getMirrorByName(self, name):
268 """See `IDistribution`."""
269
270=== modified file 'lib/lp/registry/model/distributionsourcepackage.py'
271--- lib/lp/registry/model/distributionsourcepackage.py 2011-05-12 04:18:32 +0000
272+++ lib/lp/registry/model/distributionsourcepackage.py 2011-06-07 05:39:33 +0000
273@@ -486,12 +486,14 @@
274 """See `IBugTarget`."""
275 return self.distribution.getUsedBugTags()
276
277- def getUsedBugTagsWithOpenCounts(self, user, wanted_tags=None):
278- """See `IBugTarget`."""
279+ def getUsedBugTagsWithOpenCounts(self, user, tag_limit=0, include_tags=None):
280+ """See IBugTarget."""
281+ # Circular fail.
282+ from lp.bugs.model.bugsummary import BugSummary
283 return get_bug_tags_open_count(
284- And(BugTask.distribution == self.distribution,
285- BugTask.sourcepackagename == self.sourcepackagename),
286- user, wanted_tags=wanted_tags)
287+ And(BugSummary.distribution == self.distribution,
288+ BugSummary.sourcepackagename == self.sourcepackagename),
289+ user, tag_limit=tag_limit, include_tags=include_tags)
290
291 def _getOfficialTagClause(self):
292 return self.distribution._getOfficialTagClause()
293
294=== modified file 'lib/lp/registry/model/distroseries.py'
295--- lib/lp/registry/model/distroseries.py 2011-05-31 15:45:19 +0000
296+++ lib/lp/registry/model/distroseries.py 2011-06-07 05:39:33 +0000
297@@ -28,6 +28,7 @@
298 StringCol,
299 )
300 from storm.locals import (
301+ And,
302 Desc,
303 Join,
304 SQL,
305@@ -853,10 +854,16 @@
306 """See `IHasBugs`."""
307 return get_bug_tags("BugTask.distroseries = %s" % sqlvalues(self))
308
309- def getUsedBugTagsWithOpenCounts(self, user, wanted_tags=None):
310- """See `IHasBugs`."""
311+ def getUsedBugTagsWithOpenCounts(self, user, tag_limit=0, include_tags=None):
312+ """See IBugTarget."""
313+ # Circular fail.
314+ from lp.bugs.model.bugsummary import BugSummary
315 return get_bug_tags_open_count(
316- BugTask.distroseries == self, user, wanted_tags=wanted_tags)
317+ And(
318+ BugSummary.distroseries_id == self.id,
319+ BugSummary.sourcepackagename_id == None
320+ ),
321+ user, tag_limit=tag_limit, include_tags=include_tags)
322
323 @property
324 def has_any_specifications(self):
325
326=== modified file 'lib/lp/registry/model/product.py'
327--- lib/lp/registry/model/product.py 2011-05-27 21:12:25 +0000
328+++ lib/lp/registry/model/product.py 2011-06-07 05:39:33 +0000
329@@ -799,10 +799,13 @@
330 """See `IBugTarget`."""
331 return get_bug_tags("BugTask.product = %s" % sqlvalues(self))
332
333- def getUsedBugTagsWithOpenCounts(self, user, wanted_tags=None):
334- """See `IBugTarget`."""
335+ def getUsedBugTagsWithOpenCounts(self, user, tag_limit=0, include_tags=None):
336+ """See IBugTarget."""
337+ # Circular fail.
338+ from lp.bugs.model.bugsummary import BugSummary
339 return get_bug_tags_open_count(
340- BugTask.product == self, user, wanted_tags=wanted_tags)
341+ BugSummary.product_id == self.id,
342+ user, tag_limit=tag_limit, include_tags=include_tags)
343
344 series = SQLMultipleJoin('ProductSeries', joinColumn='product',
345 orderBy='name')
346
347=== modified file 'lib/lp/registry/model/productseries.py'
348--- lib/lp/registry/model/productseries.py 2011-05-27 21:12:25 +0000
349+++ lib/lp/registry/model/productseries.py 2011-06-07 05:39:33 +0000
350@@ -446,10 +446,13 @@
351 """See IBugTarget."""
352 return get_bug_tags("BugTask.productseries = %s" % sqlvalues(self))
353
354- def getUsedBugTagsWithOpenCounts(self, user, wanted_tags=None):
355+ def getUsedBugTagsWithOpenCounts(self, user, tag_limit=0, include_tags=None):
356 """See IBugTarget."""
357+ # Circular fail.
358+ from lp.bugs.model.bugsummary import BugSummary
359 return get_bug_tags_open_count(
360- BugTask.productseries == self, user, wanted_tags=wanted_tags)
361+ BugSummary.productseries_id == self.id, user, tag_limit=tag_limit,
362+ include_tags=include_tags)
363
364 def createBug(self, bug_params):
365 """See IBugTarget."""
366
367=== modified file 'lib/lp/registry/model/projectgroup.py'
368--- lib/lp/registry/model/projectgroup.py 2011-04-26 16:22:11 +0000
369+++ lib/lp/registry/model/projectgroup.py 2011-06-07 05:39:33 +0000
370@@ -343,14 +343,16 @@
371 return get_bug_tags(
372 "BugTask.product IN (%s)" % ",".join(product_ids))
373
374- def getUsedBugTagsWithOpenCounts(self, user, wanted_tags=None):
375- """See `IHasBugs`."""
376- if not self.products:
377- return []
378+ def getUsedBugTagsWithOpenCounts(self, user, tag_limit=0, include_tags=None):
379+ """See IBugTarget."""
380+ # Circular fail.
381+ from lp.bugs.model.bugsummary import BugSummary
382 product_ids = [product.id for product in self.products]
383+ if not product_ids:
384+ return {}
385 return get_bug_tags_open_count(
386- BugTask.productID.is_in(product_ids), user,
387- wanted_tags=wanted_tags)
388+ BugSummary.product_id.is_in(product_ids),
389+ user, tag_limit=tag_limit, include_tags=include_tags)
390
391 def _getBugTaskContextClause(self):
392 """See `HasBugsBase`."""
393
394=== modified file 'lib/lp/registry/model/sourcepackage.py'
395--- lib/lp/registry/model/sourcepackage.py 2011-05-14 15:03:04 +0000
396+++ lib/lp/registry/model/sourcepackage.py 2011-06-07 05:39:33 +0000
397@@ -494,12 +494,14 @@
398 """See `IBugTarget`."""
399 return self.distroseries.getUsedBugTags()
400
401- def getUsedBugTagsWithOpenCounts(self, user, wanted_tags=None):
402- """See `IBugTarget`."""
403+ def getUsedBugTagsWithOpenCounts(self, user, tag_limit=0, include_tags=None):
404+ """See IBugTarget."""
405+ # Circular fail.
406+ from lp.bugs.model.bugsummary import BugSummary
407 return get_bug_tags_open_count(
408- And(BugTask.distroseries == self.distroseries,
409- BugTask.sourcepackagename == self.sourcepackagename),
410- user, wanted_tags=wanted_tags)
411+ And(BugSummary.distroseries == self.distroseries,
412+ BugSummary.sourcepackagename == self.sourcepackagename),
413+ user, tag_limit=tag_limit, include_tags=include_tags)
414
415 @property
416 def max_bug_heat(self):