Merge lp:~wgrant/launchpad/bug-736002 into lp:launchpad

Proposed by William Grant
Status: Merged
Approved by: William Grant
Approved revision: no longer in the source branch.
Merged at revision: 12808
Proposed branch: lp:~wgrant/launchpad/bug-736002
Merge into: lp:launchpad
Diff against target: 430 lines (+113/-117)
11 files modified
lib/lp/bugs/browser/bugtarget.py (+28/-53)
lib/lp/bugs/doc/bug-tags.txt (+20/-12)
lib/lp/bugs/interfaces/bugtarget.py (+3/-1)
lib/lp/bugs/model/bug.py (+43/-37)
lib/lp/registry/model/distribution.py (+3/-2)
lib/lp/registry/model/distributionsourcepackage.py (+2/-2)
lib/lp/registry/model/distroseries.py (+3/-2)
lib/lp/registry/model/product.py (+3/-2)
lib/lp/registry/model/productseries.py (+3/-2)
lib/lp/registry/model/projectgroup.py (+3/-2)
lib/lp/registry/model/sourcepackage.py (+2/-2)
To merge this branch: bzr merge lp:~wgrant/launchpad/bug-736002
Reviewer Review Type Date Requested Status
Jeroen T. Vermeulen (community) code Approve
Review via email: mp+57278@code.launchpad.net

Commit message

[r=jtv][bug=736002] Speed up +bugtarget-portlet-tags-content.

Description of the change

This branch fixes tags_cloud_data to to issue only two queries, hopefully minimising timeouts on +bugtarget-portlet-tags-content.

It used to display the official tags and the top 10 unofficial tags. This proves to be fairly expensive, so it will now display the official tags and the top 10 tags, some of which may duplicate the official tag set.

I've changed it to query the context's official tags, then perform a single UNION query to get the top 10 and official tag counts. Both sides of the union are done together in a single pass, and I've also avoided a slow join over Bug, letting the query complete in <4s for Ubuntu on staging.

To post a comment you must log in.
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

Looks good. On IRC you said that we don't expect anyone to have a problem with the functional change, or if they do, we can solve it in other ways later.

Another, very small thing, in lib/lp/bugs/browser/bugtask.py:

80 + tags = []
81 + for tag, count in raw_tags.iteritems():
82 + tag_dict = dict(
83 + tag=tag, count=count, url=self._getSearchURL(tag),
84 + factor=1 + (count / max_count))
85 + if tag in official_tags:
86 + tag_dict['factor'] += 0.5
87 + tags.append(tag_dict)

The "factor=1 + (count / maxcount)" is a bit misleading in terms of associativity. If you isolated the factor computation (including the "+= 0.5") in a function, this whole loop could be replaced with a list comprehension.

Jeroen

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-03-31 08:05:07 +0000
3+++ lib/lp/bugs/browser/bugtarget.py 2011-04-12 08:33:41 +0000
4@@ -1373,63 +1373,38 @@
5 # Use path_only here to reduce the size of the rendered page.
6 return "+bugs?field.tag=%s" % urllib.quote(tag)
7
8- def getUsedBugTagsWithURLs(self):
9- """Return the bug tags and their search URLs."""
10- bug_tag_counts = self.context.getUsedBugTagsWithOpenCounts(self.user)
11- return [
12- {'tag': tag, 'count': count, 'url': self._getSearchURL(tag)}
13- for tag, count in bug_tag_counts]
14-
15- @property
16- def official_tags(self):
17- """Get the official tags to diplay."""
18- official_tags = set(self.context.official_bug_tags)
19- tags = [tag for tag in self.getUsedBugTagsWithURLs()
20- if tag['tag'] in official_tags]
21- used_tags = set(tag['tag'] for tag in tags)
22- tags.sort(key=itemgetter('count'), reverse=True)
23- for tag in sorted(official_tags - used_tags):
24- tags.append(
25- {'tag': tag, 'count': 0, 'url': self._getSearchURL(tag)})
26- return tags
27-
28- @property
29- def other_tags(self):
30- """Get the unofficial tags to diplay."""
31- official_tags = set(self.context.official_bug_tags)
32- tags = [tag for tag in self.getUsedBugTagsWithURLs()
33- if tag['tag'] not in official_tags]
34- tags.sort(key=itemgetter('count'), reverse=True)
35- return tags[:10]
36+ def _calculateFactor(self, tag, count, max_count, official_tags):
37+ bonus = 1.5 if tag in official_tags else 1
38+ return (count / max_count) + bonus
39
40 @property
41 def tags_cloud_data(self):
42 """The data for rendering a tags cloud"""
43- official_tags = list(self.context.official_bug_tags)
44- tags = self.getUsedBugTagsWithURLs()
45- other_tags = [tag for tag in tags if tag['tag'] not in official_tags]
46- popular_tags = [tag['tag'] for tag in sorted(
47- other_tags, key=itemgetter('count'), reverse=True)[:10]]
48- tags = [
49- tag for tag in tags
50- if tag['tag'] in official_tags + popular_tags]
51- all_tag_dicts = [tag['tag'] for tag in tags]
52- for official_tag in official_tags:
53- if official_tag not in all_tag_dicts:
54- tags.append({
55- 'tag': official_tag,
56- 'count': 0,
57- 'url': "+bugs?field.tag=%s" % urllib.quote(official_tag)})
58- max_count = float(max([1] + [tag['count'] for tag in tags]))
59- for tag in tags:
60- if tag['tag'] in official_tags:
61- if tag['count'] == 0:
62- tag['factor'] = 1.5
63- else:
64- tag['factor'] = 1.5 + (tag['count'] / max_count)
65- else:
66- tag['factor'] = 1 + (tag['count'] / max_count)
67- return sorted(tags, key=itemgetter('tag'))
68+ official_tags = self.context.official_bug_tags
69+
70+ # Construct a dict of official and top 10 tags.
71+ # getUsedBugTagsWithOpenCounts is expensive, so do the union in
72+ # SQL. Also preseed with 0 for all the official tags, as gUBTWOC
73+ # won't return unused ones.
74+ top_ten = removeSecurityProxy(
75+ self.context.getUsedBugTagsWithOpenCounts(self.user)[:10])
76+ official = removeSecurityProxy(
77+ self.context.getUsedBugTagsWithOpenCounts(
78+ self.user, official_tags))
79+ tags = dict((tag, 0) for tag in official_tags)
80+ tags.update(dict(top_ten.union(official)))
81+
82+ max_count = float(max([1] + tags.values()))
83+
84+ return sorted(
85+ [dict(
86+ tag=tag,
87+ factor=self._calculateFactor(
88+ tag, count, max_count, official_tags),
89+ url=self._getSearchURL(tag),
90+ )
91+ for (tag, count) in tags.iteritems()],
92+ key=itemgetter('tag'))
93
94 @property
95 def show_manage_tags_link(self):
96
97=== modified file 'lib/lp/bugs/doc/bug-tags.txt'
98--- lib/lp/bugs/doc/bug-tags.txt 2011-01-11 09:40:05 +0000
99+++ lib/lp/bugs/doc/bug-tags.txt 2011-04-12 08:33:41 +0000
100@@ -332,27 +332,35 @@
101 We can also get all the used tags, together with the number of open
102 bugs each tag has. Only tags having open bugs are returned.
103
104- >>> firefox.getUsedBugTagsWithOpenCounts(None)
105- [(u'doc', 1L), (u'sco', 1L), (u'svg', 1L)]
106-
107- >>> mozilla.getUsedBugTagsWithOpenCounts(None)
108- [(u'doc', 1L), (u'sco', 1L), (u'svg', 1L)]
109-
110- >>> ubuntu.getUsedBugTagsWithOpenCounts(None)
111+ >>> list(firefox.getUsedBugTagsWithOpenCounts(None))
112+ [(u'doc', 1L), (u'sco', 1L), (u'svg', 1L)]
113+
114+ >>> list(mozilla.getUsedBugTagsWithOpenCounts(None))
115+ [(u'doc', 1L), (u'sco', 1L), (u'svg', 1L)]
116+
117+ >>> list(ubuntu.getUsedBugTagsWithOpenCounts(None))
118 [(u'crash', 2L), (u'dataloss', 1L), (u'pebcak', 1L),
119 (u'sco', 1L), (u'svg', 1L)]
120
121+We can even ask for the counts for a particular set of tags.
122+
123+ >>> list(ubuntu.getUsedBugTagsWithOpenCounts(
124+ ... None, wanted_tags=['pebcak', 'svg', 'fake']))
125+ [(u'pebcak', 1L), (u'svg', 1L)]
126+ >>> list(ubuntu.getUsedBugTagsWithOpenCounts(None, wanted_tags=[]))
127+ []
128+
129 Source packages are a bit special, they return all the tags that are
130 used in the whole distribution, while the bug count includes only bugs
131 in the specific package.
132
133- >>> ubuntu_thunderbird.getUsedBugTagsWithOpenCounts(None)
134+ >>> list(ubuntu_thunderbird.getUsedBugTagsWithOpenCounts(None))
135 [(u'crash', 1L)]
136
137- >>> debian_woody.getUsedBugTagsWithOpenCounts(None)
138+ >>> list(debian_woody.getUsedBugTagsWithOpenCounts(None))
139 [(u'dataloss', 1L), (u'layout-test', 1L), (u'pebcak', 1L)]
140
141- >>> debian_woody_firefox.getUsedBugTagsWithOpenCounts(None)
142+ >>> list(debian_woody_firefox.getUsedBugTagsWithOpenCounts(None))
143 [(u'dataloss', 1L), (u'layout-test', 1L), (u'pebcak', 1L)]
144
145 Only bugs that the supplied user has access to will be counted:
146@@ -362,13 +370,13 @@
147 True
148 >>> flush_database_updates()
149
150- >>> ubuntu_thunderbird.getUsedBugTagsWithOpenCounts(None)
151+ >>> list(ubuntu_thunderbird.getUsedBugTagsWithOpenCounts(None))
152 []
153
154 >>> sample_person = getUtility(ILaunchBag).user
155 >>> bug_nine.isSubscribed(sample_person)
156 True
157- >>> ubuntu_thunderbird.getUsedBugTagsWithOpenCounts(sample_person)
158+ >>> list(ubuntu_thunderbird.getUsedBugTagsWithOpenCounts(sample_person))
159 [(u'crash', 1L)]
160
161 When context doesn't have any tags getUsedBugTags() returns a empty list.
162
163=== modified file 'lib/lp/bugs/interfaces/bugtarget.py'
164--- lib/lp/bugs/interfaces/bugtarget.py 2011-03-28 20:54:50 +0000
165+++ lib/lp/bugs/interfaces/bugtarget.py 2011-04-12 08:33:41 +0000
166@@ -403,13 +403,15 @@
167 def getUsedBugTags():
168 """Return the tags used by the context as a sorted list of strings."""
169
170- def getUsedBugTagsWithOpenCounts(user):
171+ def getUsedBugTagsWithOpenCounts(user, wanted_tags=None):
172 """Return name and bug count of tags having open bugs.
173
174 It returns a list of tuples contining the tag name, and the
175 number of open bugs having that tag. Only the bugs that the user
176 has permission to see are counted, and only tags having open
177 bugs will be returned.
178+
179+ If wanted_tags is specified, only those tags will be returned.
180 """
181
182 def _getOfficialTagClause():
183
184=== modified file 'lib/lp/bugs/model/bug.py'
185--- lib/lp/bugs/model/bug.py 2011-04-07 16:23:24 +0000
186+++ lib/lp/bugs/model/bug.py 2011-04-12 08:33:41 +0000
187@@ -50,6 +50,9 @@
188 from storm.expr import (
189 And,
190 Count,
191+ Desc,
192+ Exists,
193+ Join,
194 LeftJoin,
195 Max,
196 Not,
197@@ -211,6 +214,29 @@
198 %(condition)s GROUP BY BugTag.tag ORDER BY BugTag.tag"""
199
200
201+def snapshot_bug_params(bug_params):
202+ """Return a snapshot of a `CreateBugParams` object."""
203+ return Snapshot(
204+ bug_params, names=[
205+ "owner", "title", "comment", "description", "msg",
206+ "datecreated", "security_related", "private",
207+ "distribution", "sourcepackagename", "binarypackagename",
208+ "product", "status", "subscribers", "tags",
209+ "subscribe_owner", "filed_by"])
210+
211+
212+class BugTag(SQLBase):
213+ """A tag belonging to a bug."""
214+
215+ bug = ForeignKey(dbName='bug', foreignKey='Bug', notNull=True)
216+ tag = StringCol(notNull=True)
217+
218+
219+# We need to always use the same Count instance or the
220+# get_bug_tags_open_count is not UNIONable.
221+tag_count_columns = (BugTag.tag, Count())
222+
223+
224 def get_bug_tags(context_clause):
225 """Return all the bug tags as a list of strings.
226
227@@ -230,59 +256,39 @@
228 return shortlist([row[0] for row in cur.fetchall()])
229
230
231-def get_bug_tags_open_count(context_condition, user):
232+def get_bug_tags_open_count(context_condition, user, wanted_tags=None):
233 """Return all the used bug tags with their open bug count.
234
235 :param context_condition: A Storm SQL expression, limiting the
236 used tags to a specific context. Only the BugTask table may be
237 used to choose the context.
238 :param user: The user performing the search.
239+ :param wanted_tags: A set of tags within which to restrict the search.
240
241 :return: A list of tuples, (tag name, open bug count).
242 """
243- open_statuses_condition = BugTask.status.is_in(
244- UNRESOLVED_BUGTASK_STATUSES)
245- columns = [
246- BugTag.tag,
247- Count(),
248- ]
249- tables = [
250+ tables = (
251 BugTag,
252- LeftJoin(Bug, Bug.id == BugTag.bugID),
253- LeftJoin(
254- BugTask,
255- And(BugTask.bugID == Bug.id, open_statuses_condition)),
256- ]
257+ Join(BugTask, BugTask.bugID == BugTag.bugID),
258+ )
259 where_conditions = [
260- open_statuses_condition,
261+ BugTask.status.is_in(UNRESOLVED_BUGTASK_STATUSES),
262 context_condition,
263 ]
264+ if wanted_tags is not None:
265+ where_conditions.append(BugTag.tag.is_in(wanted_tags))
266 privacy_filter = get_bug_privacy_filter(user)
267 if privacy_filter:
268- where_conditions.append(SQLRaw(privacy_filter))
269+ # The EXISTS sub-select avoids a join against Bug, improving
270+ # performance significantly.
271+ where_conditions.append(
272+ Exists(Select(
273+ columns=[True], tables=[Bug],
274+ where=And(Bug.id == BugTag.bugID, SQLRaw(privacy_filter)))))
275 store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
276- result = store.execute(Select(
277- columns=columns, where=And(*where_conditions), tables=tables,
278- group_by=BugTag.tag, order_by=BugTag.tag))
279- return shortlist([(row[0], row[1]) for row in result.get_all()])
280-
281-
282-def snapshot_bug_params(bug_params):
283- """Return a snapshot of a `CreateBugParams` object."""
284- return Snapshot(
285- bug_params, names=[
286- "owner", "title", "comment", "description", "msg",
287- "datecreated", "security_related", "private",
288- "distribution", "sourcepackagename", "binarypackagename",
289- "product", "status", "subscribers", "tags",
290- "subscribe_owner", "filed_by"])
291-
292-
293-class BugTag(SQLBase):
294- """A tag belonging to a bug."""
295-
296- bug = ForeignKey(dbName='bug', foreignKey='Bug', notNull=True)
297- tag = StringCol(notNull=True)
298+ return store.using(*tables).find(
299+ tag_count_columns, *where_conditions).group_by(BugTag.tag).order_by(
300+ Desc(Count()), BugTag.tag)
301
302
303 class BugBecameQuestionEvent:
304
305=== modified file 'lib/lp/registry/model/distribution.py'
306--- lib/lp/registry/model/distribution.py 2011-04-08 15:44:02 +0000
307+++ lib/lp/registry/model/distribution.py 2011-04-12 08:33:41 +0000
308@@ -649,9 +649,10 @@
309 """See `IBugTarget`."""
310 return get_bug_tags("BugTask.distribution = %s" % sqlvalues(self))
311
312- def getUsedBugTagsWithOpenCounts(self, user):
313+ def getUsedBugTagsWithOpenCounts(self, user, wanted_tags=None):
314 """See `IBugTarget`."""
315- return get_bug_tags_open_count(BugTask.distribution == self, user)
316+ return get_bug_tags_open_count(
317+ BugTask.distribution == self, user, wanted_tags=wanted_tags)
318
319 def getMirrorByName(self, name):
320 """See `IDistribution`."""
321
322=== modified file 'lib/lp/registry/model/distributionsourcepackage.py'
323--- lib/lp/registry/model/distributionsourcepackage.py 2011-03-23 16:28:51 +0000
324+++ lib/lp/registry/model/distributionsourcepackage.py 2011-04-12 08:33:41 +0000
325@@ -488,12 +488,12 @@
326 """See `IBugTarget`."""
327 return self.distribution.getUsedBugTags()
328
329- def getUsedBugTagsWithOpenCounts(self, user):
330+ def getUsedBugTagsWithOpenCounts(self, user, wanted_tags=None):
331 """See `IBugTarget`."""
332 return get_bug_tags_open_count(
333 And(BugTask.distribution == self.distribution,
334 BugTask.sourcepackagename == self.sourcepackagename),
335- user)
336+ user, wanted_tags=wanted_tags)
337
338 def _getOfficialTagClause(self):
339 return self.distribution._getOfficialTagClause()
340
341=== modified file 'lib/lp/registry/model/distroseries.py'
342--- lib/lp/registry/model/distroseries.py 2011-04-08 15:44:02 +0000
343+++ lib/lp/registry/model/distroseries.py 2011-04-12 08:33:41 +0000
344@@ -833,9 +833,10 @@
345 """See `IHasBugs`."""
346 return get_bug_tags("BugTask.distroseries = %s" % sqlvalues(self))
347
348- def getUsedBugTagsWithOpenCounts(self, user):
349+ def getUsedBugTagsWithOpenCounts(self, user, wanted_tags=None):
350 """See `IHasBugs`."""
351- return get_bug_tags_open_count(BugTask.distroseries == self, user)
352+ return get_bug_tags_open_count(
353+ BugTask.distroseries == self, user, wanted_tags=wanted_tags)
354
355 @property
356 def has_any_specifications(self):
357
358=== modified file 'lib/lp/registry/model/product.py'
359--- lib/lp/registry/model/product.py 2011-03-28 20:54:50 +0000
360+++ lib/lp/registry/model/product.py 2011-04-12 08:33:41 +0000
361@@ -800,9 +800,10 @@
362 """See `IBugTarget`."""
363 return get_bug_tags("BugTask.product = %s" % sqlvalues(self))
364
365- def getUsedBugTagsWithOpenCounts(self, user):
366+ def getUsedBugTagsWithOpenCounts(self, user, wanted_tags=None):
367 """See `IBugTarget`."""
368- return get_bug_tags_open_count(BugTask.product == self, user)
369+ return get_bug_tags_open_count(
370+ BugTask.product == self, user, wanted_tags=wanted_tags)
371
372 series = SQLMultipleJoin('ProductSeries', joinColumn='product',
373 orderBy='name')
374
375=== modified file 'lib/lp/registry/model/productseries.py'
376--- lib/lp/registry/model/productseries.py 2011-03-28 20:54:50 +0000
377+++ lib/lp/registry/model/productseries.py 2011-04-12 08:33:41 +0000
378@@ -433,9 +433,10 @@
379 """See IBugTarget."""
380 return get_bug_tags("BugTask.productseries = %s" % sqlvalues(self))
381
382- def getUsedBugTagsWithOpenCounts(self, user):
383+ def getUsedBugTagsWithOpenCounts(self, user, wanted_tags=None):
384 """See IBugTarget."""
385- return get_bug_tags_open_count(BugTask.productseries == self, user)
386+ return get_bug_tags_open_count(
387+ BugTask.productseries == self, user, wanted_tags=wanted_tags)
388
389 def createBug(self, bug_params):
390 """See IBugTarget."""
391
392=== modified file 'lib/lp/registry/model/projectgroup.py'
393--- lib/lp/registry/model/projectgroup.py 2011-01-21 08:30:55 +0000
394+++ lib/lp/registry/model/projectgroup.py 2011-04-12 08:33:41 +0000
395@@ -343,13 +343,14 @@
396 return get_bug_tags(
397 "BugTask.product IN (%s)" % ",".join(product_ids))
398
399- def getUsedBugTagsWithOpenCounts(self, user):
400+ def getUsedBugTagsWithOpenCounts(self, user, wanted_tags=None):
401 """See `IHasBugs`."""
402 if not self.products:
403 return []
404 product_ids = [product.id for product in self.products]
405 return get_bug_tags_open_count(
406- BugTask.productID.is_in(product_ids), user)
407+ BugTask.productID.is_in(product_ids), user,
408+ wanted_tags=wanted_tags)
409
410 def _getBugTaskContextClause(self):
411 """See `HasBugsBase`."""
412
413=== modified file 'lib/lp/registry/model/sourcepackage.py'
414--- lib/lp/registry/model/sourcepackage.py 2011-04-07 19:08:45 +0000
415+++ lib/lp/registry/model/sourcepackage.py 2011-04-12 08:33:41 +0000
416@@ -495,12 +495,12 @@
417 """See `IBugTarget`."""
418 return self.distroseries.getUsedBugTags()
419
420- def getUsedBugTagsWithOpenCounts(self, user):
421+ def getUsedBugTagsWithOpenCounts(self, user, wanted_tags=None):
422 """See `IBugTarget`."""
423 return get_bug_tags_open_count(
424 And(BugTask.distroseries == self.distroseries,
425 BugTask.sourcepackagename == self.sourcepackagename),
426- user)
427+ user, wanted_tags=wanted_tags)
428
429 @property
430 def max_bug_heat(self):