Merge lp:~deryck/launchpad/preload-tags-for-buglistings-901122 into lp:launchpad

Proposed by Deryck Hodge
Status: Merged
Approved by: Deryck Hodge
Approved revision: no longer in the source branch.
Merged at revision: 14887
Proposed branch: lp:~deryck/launchpad/preload-tags-for-buglistings-901122
Merge into: lp:launchpad
Diff against target: 201 lines (+58/-7)
7 files modified
lib/lp/bugs/browser/bugtask.py (+10/-2)
lib/lp/bugs/browser/cvereport.py (+2/-1)
lib/lp/bugs/browser/tests/test_bugtask.py (+4/-3)
lib/lp/bugs/doc/bugtask.txt (+15/-0)
lib/lp/bugs/interfaces/bugtask.py (+6/-0)
lib/lp/bugs/model/bugtask.py (+18/-0)
lib/lp/registry/browser/milestone.py (+3/-1)
To merge this branch: bzr merge lp:~deryck/launchpad/preload-tags-for-buglistings-901122
Reviewer Review Type Date Requested Status
Robert Collins (community) Approve
Review via email: mp+95023@code.launchpad.net

Commit message

[r=lifeless][bug=901122][incr] Load tags in a single query for custom buglistings.

Description of the change

This is the first branch of refactorings to load new data that was added in new buglistings without adding more queries. This first bit here is to get tags loaded as a set for a given buglisting batch, rather than accessing them in a loop for each item in the buglist.

I followed the pattern used for bug_badge_properties, which calls out to a method on IBugTaskSet to get bug badge data. In my case, I added tags_for_batch which calls out to IBugTaskSet.getBugTaskTags. getBugTaskTags is where the query happens, and then _getListingItem uses tags_for_batch, which is cached, to get the tags for the listing item.

There were a few test updates required, since the required arguments changed for BugTaskListingItem. I also updated doc/bugtask.txt to test the new BugTaskSet method. Otherwise, it's a pretty straight forward change.

To post a comment you must log in.
Revision history for this message
Robert Collins (lifeless) wrote :

This will certainly do the job. I have some things I would have done differently that you may wish to do, or not.

Firstly, using tags='' to indicate no-tags is horribly confusing when tags is a list the rest of the time. It works (because '' is iterable) but its confusing. I suggest using () instead.

Secondly, load_referencing or load_related would quite likely let you bring back all the tags with less code.
something like:
bug_to_task = dict((task.bugID, task.id) for task in tasks)
tags = dict((bug_to_task[bugtag.bugID], bugtag.tag for bugtag in
    load_referencing(BugTag, tasks, ["bugID"]))

Third, tags_for_batch is only used once and isn't directly tested. It seems a little ugly but I can see why you have it. I'd consider just passing the result down the call stack rather than caching it for the lifetime of the view.

I'd like to see a scaling test for this. Unless this is the entire remaining lazy load it won't be constant, but it will make the scaling clear and documented.

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

Oh, and as you've added a helper with the tags, consider moving the url processing stuff into that, perhaps.

Revision history for this message
Deryck Hodge (deryck) wrote :

Thanks for the review, Robert. I definitely updated to use () rather than '' for empty tags. Thanks for the catch. Not sure what I was thinking there.

In terms of using load_referencing, unless I'm misunderstanding how that works, it won't work because BugTag is not directly related to tasks. I need to join on Bug, hence the direct use of store.find.

And for the other suggestions, I'd like to keep what I have since it follows that pattern of what is already there and makes less change to existing code, while still reducing the query count. I don't think the existing patterns are so off here that it merits diverging partially.

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

On Thu, Mar 1, 2012 at 8:42 AM, Deryck Hodge <email address hidden> wrote:
> Thanks for the review, Robert. I definitely updated to use () rather than '' for empty tags.  Thanks for the catch.  Not sure what I was thinking there.
>
> In terms of using load_referencing, unless I'm misunderstanding how that works, it won't work because BugTag is not directly related to tasks.  I need to join on Bug, hence the direct use of store.find.

You need to query on the bug id, which task has. So yes, you're
misunderstanding how it works :)

-Rob

Revision history for this message
Deryck Hodge (deryck) wrote :

I feel dumb, sorry. Help me understand what I'm not groking. Here's my train of thought...

Your example above didn't work for me:
load_referencing(BugTag, tasks, ["bugID"])

To get it to work, I had to pass in bugs rather than tasks, which made sense to me, since it's bugtag to bug where the reference lies:
load_referencing(BugTag, bugs, ["bugID"])

Which required something like set([bugtask.bug for bugtask in bugtasks]), and, of course, being a loop is bad for query count. At which point I would have had to issue a store.find to get all bugs in a single query, and then load_referencing would issue another query. Which seems like 2 queries, whereas my original code did the join and got back tags in one query.

We can also chat on IRC about this, if you need more bandwidth to undumb me. :)

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 2012-02-23 22:27:32 +0000
3+++ lib/lp/bugs/browser/bugtask.py 2012-02-29 19:30:31 +0000
4@@ -2161,13 +2161,14 @@
5 delegates(IBugTask, 'bugtask')
6
7 def __init__(self, bugtask, has_bug_branch,
8- has_specification, has_patch, request=None,
9+ has_specification, has_patch, tags, request=None,
10 target_context=None):
11 self.bugtask = bugtask
12 self.review_action_widget = None
13 self.has_bug_branch = has_bug_branch
14 self.has_specification = has_specification
15 self.has_patch = has_patch
16+ self.tags = tags
17 self.request = request
18 self.target_context = target_context
19
20@@ -2230,7 +2231,7 @@
21 'status': self.status.title,
22 'status_class': 'status' + self.status.name,
23 'tags': [{'url': base_tag_url + urllib.quote(tag), 'tag': tag}
24- for tag in self.bug.tags],
25+ for tag in self.tags],
26 'title': self.bug.title,
27 }
28
29@@ -2275,6 +2276,11 @@
30 return getUtility(IBugTaskSet).getBugTaskBadgeProperties(
31 self.currentBatch())
32
33+ @cachedproperty
34+ def tags_for_batch(self):
35+ """Return a dict matching bugtask to it's tags."""
36+ return getUtility(IBugTaskSet).getBugTaskTags(self.currentBatch())
37+
38 def getCookieName(self):
39 """Return the cookie name used in bug listings js code."""
40 cookie_name_template = '%s-buglist-fields'
41@@ -2310,6 +2316,7 @@
42 def _getListingItem(self, bugtask):
43 """Return a decorated bugtask for the bug listing."""
44 badge_property = self.bug_badge_properties[bugtask]
45+ tags = self.tags_for_batch.get(bugtask.id, ())
46 if (IMaloneApplication.providedBy(self.target_context) or
47 IPerson.providedBy(self.target_context)):
48 # XXX Tom Berger bug=529846
49@@ -2323,6 +2330,7 @@
50 badge_property['has_branch'],
51 badge_property['has_specification'],
52 badge_property['has_patch'],
53+ tags,
54 request=self.request,
55 target_context=target_context)
56
57
58=== modified file 'lib/lp/bugs/browser/cvereport.py'
59--- lib/lp/bugs/browser/cvereport.py 2012-01-24 15:15:18 +0000
60+++ lib/lp/bugs/browser/cvereport.py 2012-02-29 19:30:31 +0000
61@@ -91,7 +91,8 @@
62 bugtask,
63 has_bug_branch=badges['has_branch'],
64 has_specification=badges['has_specification'],
65- has_patch=badges['has_patch'])
66+ has_patch=badges['has_patch'],
67+ tags=())
68 if bugtask.status in RESOLVED_BUGTASK_STATUSES:
69 current_bugtaskcves = resolved_bugtaskcves
70 else:
71
72=== modified file 'lib/lp/bugs/browser/tests/test_bugtask.py'
73--- lib/lp/bugs/browser/tests/test_bugtask.py 2012-02-24 04:00:24 +0000
74+++ lib/lp/bugs/browser/tests/test_bugtask.py 2012-02-29 19:30:31 +0000
75@@ -1793,7 +1793,7 @@
76 batched_view.activity_and_comments)
77
78
79-def make_bug_task_listing_item(factory):
80+def make_bug_task_listing_item(factory, tags=()):
81 owner = factory.makePerson()
82 bug = factory.makeBug(
83 owner=owner, private=True, security_related=True)
84@@ -1808,6 +1808,7 @@
85 badge_property['has_branch'],
86 badge_property['has_specification'],
87 badge_property['has_patch'],
88+ tags,
89 target_context=bugtask.target)
90
91
92@@ -2339,9 +2340,9 @@
93
94 def test_model_tags(self):
95 """Model contains bug tags."""
96- owner, item = make_bug_task_listing_item(self.factory)
97+ tags = ['tag1', 'tag2']
98+ owner, item = make_bug_task_listing_item(self.factory, tags=tags)
99 with person_logged_in(owner):
100- item.bug.tags = ['tag1', 'tag2']
101 self.assertEqual(2, len(item.model['tags']))
102 self.assertTrue('tag' in item.model['tags'][0].keys())
103 self.assertTrue('url' in item.model['tags'][0].keys())
104
105=== modified file 'lib/lp/bugs/doc/bugtask.txt'
106--- lib/lp/bugs/doc/bugtask.txt 2012-02-08 10:33:31 +0000
107+++ lib/lp/bugs/doc/bugtask.txt 2012-02-29 19:30:31 +0000
108@@ -1153,6 +1153,21 @@
109 has_specification: False
110
111
112+Bugtask Tags
113+------------
114+
115+List of bugtasks often need to display related tags. Since tags are
116+related to bugtasks via bugs, BugTaskSet has a method getBugTaskTags
117+that can calculate the tags in one query.
118+
119+ >>> some_bugtask.bug.tags = [u'foo', u'bar']
120+ >>> another_bugtask.bug.tags = [u'baz', u'bop']
121+ >>> tags_by_task = getUtility(IBugTaskSet).getBugTaskTags([
122+ ... some_bugtask, another_bugtask])
123+ >>> print tags_by_task
124+ {3: [u'foo', u'bar'], 6: [u'bop', u'baz']}
125+
126+
127 Similar bugs
128 ------------
129
130
131=== modified file 'lib/lp/bugs/interfaces/bugtask.py'
132--- lib/lp/bugs/interfaces/bugtask.py 2012-02-28 21:35:06 +0000
133+++ lib/lp/bugs/interfaces/bugtask.py 2012-02-29 19:30:31 +0000
134@@ -1475,6 +1475,12 @@
135 :return: A dictionary mapping the bugs to their bugtasks.
136 """
137
138+ def getBugTaskTags(bugtasks):
139+ """Return a set of bugtasks bug tags
140+
141+ Return a dict mapping from bugtask to tag.
142+ """
143+
144 def getBugTaskBadgeProperties(bugtasks):
145 """Return whether the bugtasks should have badges.
146
147
148=== modified file 'lib/lp/bugs/model/bugtask.py'
149--- lib/lp/bugs/model/bugtask.py 2012-02-15 08:13:51 +0000
150+++ lib/lp/bugs/model/bugtask.py 2012-02-29 19:30:31 +0000
151@@ -21,6 +21,7 @@
152 ]
153
154
155+from collections import defaultdict
156 import datetime
157 from itertools import chain
158 from operator import attrgetter
159@@ -1380,6 +1381,23 @@
160 bugs_and_tasks[bug].append(task)
161 return bugs_and_tasks
162
163+ def getBugTaskTags(self, bugtasks):
164+ """See `IBugTaskSet`"""
165+ # Import locally to avoid circular imports.
166+ from lp.bugs.model.bug import Bug, BugTag
167+ bugtask_ids = set(bugtask.id for bugtask in bugtasks)
168+ bug_ids = set(bugtask.bugID for bugtask in bugtasks)
169+ tags = IStore(BugTag).find(
170+ (BugTag.tag, BugTask.id),
171+ BugTask.bug == Bug.id,
172+ BugTag.bug == Bug.id,
173+ BugTag.bugID.is_in(bug_ids),
174+ BugTask.id.is_in(bugtask_ids))
175+ tags_by_bugtask = defaultdict(list)
176+ for tag_name, bugtask_id in tags:
177+ tags_by_bugtask[bugtask_id].append(tag_name)
178+ return dict(tags_by_bugtask)
179+
180 def getBugTaskBadgeProperties(self, bugtasks):
181 """See `IBugTaskSet`."""
182 # Import locally to avoid circular imports.
183
184=== modified file 'lib/lp/registry/browser/milestone.py'
185--- lib/lp/registry/browser/milestone.py 2012-02-21 22:46:28 +0000
186+++ lib/lp/registry/browser/milestone.py 2012-02-29 19:30:31 +0000
187@@ -254,11 +254,13 @@
188 def _getListingItem(self, bugtask):
189 """Return a decorated bugtask for the bug listing."""
190 badge_property = self._bug_badge_properties[bugtask]
191+ tags = ()
192 return BugTaskListingItem(
193 bugtask,
194 badge_property['has_branch'],
195 badge_property['has_specification'],
196- badge_property['has_patch'])
197+ badge_property['has_patch'],
198+ tags)
199
200 @cachedproperty
201 def bugtasks(self):