Merge lp:~stevenk/launchpad/no-more-o-n-queries-bug-dupes into lp:launchpad

Proposed by Steve Kowalik
Status: Merged
Merged at revision: 16140
Proposed branch: lp:~stevenk/launchpad/no-more-o-n-queries-bug-dupes
Merge into: lp:launchpad
Diff against target: 345 lines (+88/-90)
5 files modified
lib/lp/bugs/browser/tests/test_bug_views.py (+25/-10)
lib/lp/bugs/browser/tests/test_bugtask.py (+1/-1)
lib/lp/bugs/model/bug.py (+25/-22)
lib/lp/bugs/model/personsubscriptioninfo.py (+36/-56)
lib/lp/bugs/model/tests/test_personsubscriptioninfo.py (+1/-1)
To merge this branch: bzr merge lp:~stevenk/launchpad/no-more-o-n-queries-bug-dupes
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+129355@code.launchpad.net

Commit message

Rewrite the heavy-lifting queries in IBug.getSubscriptionForPerson() and IPersonSubscriptionInfo._getDirectAndDuplicateSubscriptions() to be performant using two CTEs, and destroy the cause of excessive queries in the second function by bulk loading.

Description of the change

Rewrite the heavy-lifting queries in IBug.getSubscriptionForPerson() and IPersonSubscriptionInfo._getDirectAndDuplicateSubscriptions() to be performant using two CTEs. Also destroy the cause of excessive queries in the second function by bulk loading all related people, bugtasks and pillars.

Do a small number of whitespace function, and also rewrite IPersonSubscriptionInfo._isMuted() to be a shadow of it's former self. Drop the preloading browser.bugtask does with IBug.getSubscriptionForPerson(), but it may have to return.

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

8 - # XXX 2010-10-05 gmb bug=655597:
9 - # This line of code keeps the view's query count down,
10 - # possibly using witchcraft. It should be rewritten to be
11 - # useful or removed in favour of making other queries more
12 - # efficient. The witchcraft is because the subscribers are accessed
13 - # in the initial page load, so the data is actually used.
14 - if self.user is not None:
15 - list(bug.getSubscribersForPerson(self.user))

I'd suggest restoring this for now. We know the rest of the changes are probably safe, but about this we're just guessing. It's not detrimental to leave, since we deferred the plan to redesign the method.

153 tzinfo=pytz.UTC)
154 +
155 +def generate_subscription_with(bug, person):

Looks like a missing newline.

155 +def generate_subscription_with(bug, person):
156 + return [
157 + With('all_bugsubscriptions', Select(
158 + (BugSubscription.id, BugSubscription.person_id),
159 + tables=[BugSubscription, Join(
160 + Bug, Bug.id == BugSubscription.bug_id)],
161 + where=Or(Bug.id == bug.id, Bug.duplicateofID == bug.id))),
162 + With('bugsubscriptions', Select(
163 + SQL('all_bugsubscriptions.id'),
164 + tables=[
165 + SQL('all_bugsubscriptions'), Join(
166 + TeamParticipation, TeamParticipation.teamID == SQL(
167 + 'all_bugsubscriptions.person'))],
168 + where=[TeamParticipation.personID == person.id]))]

Some of the wrapping here is a bit off.

274 + pillar = self._getTaskPillar(task)

Can you just use task.pillar instead?

292 + is_muted = Store.of(person).find(
293 + BugMute, BugMute.bug == bug, BugMute.person == person).one()
294 + return is_muted is not None

This could even be 'return not Store.of(person).find(BugMute, bug=bug, person=person).is_empty()'

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/tests/test_bug_views.py'
2--- lib/lp/bugs/browser/tests/test_bug_views.py 2012-09-28 16:47:42 +0000
3+++ lib/lp/bugs/browser/tests/test_bug_views.py 2012-10-12 11:59:29 +0000
4@@ -269,8 +269,7 @@
5 self.assertFalse(self.bug.isSubscribed(person))
6 self.assertFalse(self.bug.isMuted(person))
7 self.assertFalse(
8- self.bug.personIsAlsoNotifiedSubscriber(
9- person))
10+ self.bug.personIsAlsoNotifiedSubscriber(person))
11 view = create_initialized_view(
12 self.bug, name="+portlet-subscription")
13 self.assertFalse(view.user_should_see_mute_link)
14@@ -305,14 +304,31 @@
15 self.assertTrue(self.bug.isMuted(person))
16 view = create_initialized_view(
17 self.bug, name="+portlet-subscription")
18- self.assertTrue(view.user_should_see_mute_link,
19- "User should see mute link.")
20+ self.assertTrue(
21+ view.user_should_see_mute_link, "User should see mute link.")
22 contents = view.render()
23- self.assertTrue('mute_subscription' in contents,
24- "'mute_subscription' not in contents.")
25+ self.assertTrue(
26+ 'mute_subscription' in contents,
27+ "'mute_subscription' not in contents.")
28 self.assertFalse(
29- self._hasCSSClass(
30- contents, 'mute-link-container', 'hidden'))
31+ self._hasCSSClass(contents, 'mute-link-container', 'hidden'))
32+
33+ def test_bug_portlet_subscription_query_count(self):
34+ # Bug:+portlet-subscription doesn't make O(n) queries based on the
35+ # number of duplicate bugs.
36+ user = self.factory.makePerson()
37+ bug = self.factory.makeBug()
38+ for n in range(20):
39+ dupe = self.factory.makeBug()
40+ removeSecurityProxy(dupe)._markAsDuplicate(bug)
41+ removeSecurityProxy(dupe).subscribe(user, dupe.owner)
42+ Store.of(bug).invalidate()
43+ with person_logged_in(user):
44+ with StormStatementRecorder() as recorder:
45+ view = create_initialized_view(
46+ bug, name='+portlet-subscription', principal=user)
47+ view.render()
48+ self.assertThat(recorder, HasQueryCount(Equals(21)))
49
50
51 class TestBugSecrecyViews(TestCaseWithFactory):
52@@ -328,12 +344,11 @@
53 if bug is None:
54 bug = self.factory.makeBug()
55 with person_logged_in(person):
56- view = create_initialized_view(
57+ return create_initialized_view(
58 bug.default_bugtask, name='+secrecy', form={
59 'field.information_type': 'USERDATA',
60 'field.actions.change': 'Change',
61 }, request=request)
62- return view
63
64 def test_notification_shown_if_marking_private_and_not_subscribed(self):
65 # If a user who is not subscribed to a bug marks that bug as
66
67=== modified file 'lib/lp/bugs/browser/tests/test_bugtask.py'
68--- lib/lp/bugs/browser/tests/test_bugtask.py 2012-10-10 02:45:36 +0000
69+++ lib/lp/bugs/browser/tests/test_bugtask.py 2012-10-12 11:59:29 +0000
70@@ -156,7 +156,7 @@
71 def test_rendered_query_counts_constant_with_attachments(self):
72 with celebrity_logged_in('admin'):
73 browses_under_limit = BrowsesWithQueryLimit(
74- 95, self.factory.makePerson())
75+ 96, self.factory.makePerson())
76
77 # First test with a single attachment.
78 task = self.factory.makeBugTask()
79
80=== modified file 'lib/lp/bugs/model/bug.py'
81--- lib/lp/bugs/model/bug.py 2012-10-11 04:21:07 +0000
82+++ lib/lp/bugs/model/bug.py 2012-10-12 11:59:29 +0000
83@@ -13,6 +13,7 @@
84 'BugSet',
85 'BugTag',
86 'FileBugData',
87+ 'generate_subscription_with',
88 'get_also_notified_subscribers',
89 'get_bug_tags_open_count',
90 ]
91@@ -54,6 +55,7 @@
92 SQL,
93 Sum,
94 Union,
95+ With,
96 )
97 from storm.info import ClassAlias
98 from storm.locals import (
99@@ -1056,36 +1058,21 @@
100 else:
101 self._subscriber_dups_cache.add(subscriber)
102 return subscriber
103- return DecoratedResultSet(Store.of(self).find(
104+ with_statement = generate_subscription_with(self, person)
105+ store = Store.of(self).with_(with_statement)
106+ return DecoratedResultSet(store.find(
107 # Return people and subscriptions
108 (Person, BugSubscription),
109- # For this bug or its duplicates
110- Or(
111- Bug.id == self.id,
112- Bug.duplicateof == self.id),
113- # Get subscriptions for these bugs
114- BugSubscription.bug_id == Bug.id,
115- # Filter by subscriptions to any team person is in.
116- # Note that teamparticipation includes self-participation entries
117- # (person X is in the team X)
118- TeamParticipation.person == person.id,
119- # XXX: Storm fails to compile this, so manually done.
120- # bug=https://bugs.launchpad.net/storm/+bug/627137
121- # RBC 20100831
122- SQL("""TeamParticipation.team = BugSubscription.person"""),
123- # Join in the Person rows we want
124- # XXX: Storm fails to compile this, so manually done.
125- # bug=https://bugs.launchpad.net/storm/+bug/627137
126- # RBC 20100831
127- SQL("""Person.id = TeamParticipation.team"""),
128+ BugSubscription.id.is_in(
129+ SQL('SELECT bugsubscriptions.id FROM bugsubscriptions')),
130+ Person.id == BugSubscription.person_id,
131 ).order_by(Person.name).config(
132 distinct=(Person.name, BugSubscription.person_id)),
133 cache_subscriber, pre_iter_hook=cache_unsubscribed)
134
135 def getSubscriptionForPerson(self, person):
136 """See `IBug`."""
137- store = Store.of(self)
138- return store.find(
139+ return Store.of(self).find(
140 BugSubscription,
141 BugSubscription.person == person,
142 BugSubscription.bug == self).one()
143@@ -2830,3 +2817,19 @@
144 date_created = DateTime(
145 "date_created", allow_none=False, default=UTC_NOW,
146 tzinfo=pytz.UTC)
147+
148+
149+def generate_subscription_with(bug, person):
150+ return [
151+ With('all_bugsubscriptions', Select(
152+ (BugSubscription.id, BugSubscription.person_id),
153+ tables=[
154+ BugSubscription, Join(Bug, Bug.id == BugSubscription.bug_id)],
155+ where=Or(Bug.id == bug.id, Bug.duplicateofID == bug.id))),
156+ With('bugsubscriptions', Select(
157+ SQL('all_bugsubscriptions.id'),
158+ tables=[
159+ SQL('all_bugsubscriptions'),
160+ Join(TeamParticipation, TeamParticipation.teamID == SQL(
161+ 'all_bugsubscriptions.person'))],
162+ where=[TeamParticipation.personID == person.id]))]
163
164=== modified file 'lib/lp/bugs/model/personsubscriptioninfo.py'
165--- lib/lp/bugs/model/personsubscriptioninfo.py 2012-08-22 01:41:50 +0000
166+++ lib/lp/bugs/model/personsubscriptioninfo.py 2012-10-12 11:59:29 +0000
167@@ -1,4 +1,4 @@
168-# Copyright 2011 Canonical Ltd. This software is licensed under the
169+# Copyright 2011-2012 Canonical Ltd. This software is licensed under the
170 # GNU Affero General Public License version 3 (see the file LICENSE).
171
172 __metaclass__ = type
173@@ -6,8 +6,9 @@
174 'PersonSubscriptions',
175 ]
176
177-from storm.expr import Or
178+from storm.expr import SQL
179 from storm.store import Store
180+from zope.component import getUtility
181 from zope.interface import implements
182 from zope.proxy import sameProxiedObjects
183
184@@ -19,17 +20,17 @@
185 IVirtualSubscriptionInfo,
186 IVirtualSubscriptionInfoCollection,
187 )
188-from lp.bugs.interfaces.structuralsubscription import (
189- IStructuralSubscriptionTargetHelper,
190- )
191 from lp.bugs.model.bug import (
192 Bug,
193 BugMute,
194+ generate_subscription_with,
195 )
196 from lp.bugs.model.bugsubscription import BugSubscription
197-from lp.registry.interfaces.sourcepackage import ISourcePackage
198+from lp.registry.interfaces.person import IPersonSet
199+from lp.registry.model.distribution import Distribution
200 from lp.registry.model.person import Person
201-from lp.registry.model.teammembership import TeamParticipation
202+from lp.registry.model.product import Product
203+from lp.services.database.bulk import load_related
204
205
206 class RealSubscriptionInfo:
207@@ -96,8 +97,8 @@
208 person, administrated_team_ids)
209 self._principal_pillar_to_info = {}
210
211- def _add_item_to_collection(self,
212- collection, principal, bug, pillar, task):
213+ def _add_item_to_collection(self, collection, principal, bug, pillar,
214+ task):
215 key = (principal, pillar)
216 info = self._principal_pillar_to_info.get(key)
217 if info is None:
218@@ -118,8 +119,8 @@
219 person, administrated_team_ids)
220 self._principal_bug_to_infos = {}
221
222- def _add_item_to_collection(self, collection, principal,
223- bug, subscription):
224+ def _add_item_to_collection(self, collection, principal, bug,
225+ subscription):
226 info = RealSubscriptionInfo(principal, bug, subscription)
227 key = (principal, bug)
228 infos = self._principal_bug_to_infos.get(key)
229@@ -158,34 +159,17 @@
230 """See `IPersonSubscriptions`."""
231 self.loadSubscriptionsFor(self.person, self.bug)
232
233- def _getTaskPillar(self, bugtask):
234- """Return a pillar for a given BugTask."""
235- # There is no adaptor for ISourcePackage. Perhaps there
236- # should be since the data model doesn't seem to prohibit it.
237- # For now, we simply work around the problem. It Would Be Nice If
238- # there were a reliable generic way of getting the pillar for any
239- # bugtarget, but we are not going to tackle that right now.
240- if ISourcePackage.providedBy(bugtask.target):
241- pillar = IStructuralSubscriptionTargetHelper(
242- bugtask.target.distribution_sourcepackage).pillar
243- else:
244- pillar = IStructuralSubscriptionTargetHelper(
245- bugtask.target).pillar
246- return pillar
247-
248 def _getDirectAndDuplicateSubscriptions(self, person, bug):
249 # Fetch all information for direct and duplicate
250 # subscriptions (including indirect through team
251 # membership) in a single query.
252- store = Store.of(person)
253- bug_id_options = [Bug.id == bug.id, Bug.duplicateofID == bug.id]
254- info = store.find(
255+ with_statement = generate_subscription_with(bug, person)
256+ info = Store.of(person).with_(with_statement).find(
257 (BugSubscription, Bug, Person),
258- BugSubscription.bug == Bug.id,
259- BugSubscription.person == Person.id,
260- Or(*bug_id_options),
261- TeamParticipation.personID == person.id,
262- TeamParticipation.teamID == Person.id)
263+ BugSubscription.id.is_in(
264+ SQL('SELECT bugsubscriptions.id FROM bugsubscriptions')),
265+ Person.id == BugSubscription.person_id,
266+ Bug.id == BugSubscription.bug_id)
267
268 direct = RealSubscriptionInfoCollection(
269 self.person, self.administrated_team_ids)
270@@ -202,30 +186,27 @@
271 collection = direct
272 collection.add(
273 subscriber, subscribed_bug, subscription)
274+ # Preload bug owners, then all pillars.
275+ list(getUtility(IPersonSet).getPrecachedPersonsFromIDs(
276+ [bug.ownerID for bug in bugs]))
277+ all_tasks = [task for task in bug.bugtasks for bug in bugs]
278+ load_related(Product, all_tasks, ['productID'])
279+ load_related(Distribution, all_tasks, ['distributionID'])
280 for bug in bugs:
281 # indicate the reporter and bug_supervisor
282 duplicates.annotateReporter(bug, bug.owner)
283 direct.annotateReporter(bug, bug.owner)
284- for task in bug.bugtasks:
285- # Get bug_supervisor.
286- pillar = self._getTaskPillar(task)
287- duplicates.annotateBugTaskResponsibilities(
288- task, pillar, pillar.bug_supervisor)
289- direct.annotateBugTaskResponsibilities(
290- task, pillar, pillar.bug_supervisor)
291+ for task in all_tasks:
292+ # Get bug_supervisor.
293+ duplicates.annotateBugTaskResponsibilities(
294+ task, task.pillar, task.pillar.bug_supervisor)
295+ direct.annotateBugTaskResponsibilities(
296+ task, task.pillar, task.pillar.bug_supervisor)
297 return (direct, duplicates)
298
299 def _isMuted(self, person, bug):
300- store = Store.of(person)
301- mutes = store.find(
302- BugMute,
303- BugMute.bug == bug,
304- BugMute.person == person)
305- is_muted = mutes.one()
306- if is_muted is None:
307- return False
308- else:
309- return True
310+ return not Store.of(person).find(
311+ BugMute, bug=bug, person=person).is_empty()
312
313 def loadSubscriptionsFor(self, person, bug):
314 self.person = person
315@@ -246,13 +227,12 @@
316 as_assignee = VirtualSubscriptionInfoCollection(
317 self.person, self.administrated_team_ids)
318 for bugtask in bug.bugtasks:
319- pillar = self._getTaskPillar(bugtask)
320- owner = pillar.owner
321- if person.inTeam(owner) and pillar.bug_supervisor is None:
322- as_owner.add(owner, bug, pillar, bugtask)
323+ owner = bugtask.pillar.owner
324+ if person.inTeam(owner) and bugtask.pillar.bug_supervisor is None:
325+ as_owner.add(owner, bug, bugtask.pillar, bugtask)
326 assignee = bugtask.assignee
327 if person.inTeam(assignee):
328- as_assignee.add(assignee, bug, pillar, bugtask)
329+ as_assignee.add(assignee, bug, bugtask.pillar, bugtask)
330 self.count = 0
331 for name, collection in (
332 ('direct', direct), ('from_duplicate', from_duplicate),
333
334=== modified file 'lib/lp/bugs/model/tests/test_personsubscriptioninfo.py'
335--- lib/lp/bugs/model/tests/test_personsubscriptioninfo.py 2012-08-21 04:04:47 +0000
336+++ lib/lp/bugs/model/tests/test_personsubscriptioninfo.py 2012-10-12 11:59:29 +0000
337@@ -502,7 +502,7 @@
338 with StormStatementRecorder() as recorder:
339 self.subscriptions.reload()
340 # This should produce a very small number of queries.
341- self.assertThat(recorder, HasQueryCount(LessThan(5)))
342+ self.assertThat(recorder, HasQueryCount(LessThan(6)))
343 count_with_one_subscribed_duplicate = recorder.count
344 # It should have the correct result.
345 self.assertCollectionsAreEmpty(except_='from_duplicate')