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
=== modified file 'lib/lp/bugs/browser/tests/test_bug_views.py'
--- lib/lp/bugs/browser/tests/test_bug_views.py 2012-09-28 16:47:42 +0000
+++ lib/lp/bugs/browser/tests/test_bug_views.py 2012-10-12 11:59:29 +0000
@@ -269,8 +269,7 @@
269 self.assertFalse(self.bug.isSubscribed(person))269 self.assertFalse(self.bug.isSubscribed(person))
270 self.assertFalse(self.bug.isMuted(person))270 self.assertFalse(self.bug.isMuted(person))
271 self.assertFalse(271 self.assertFalse(
272 self.bug.personIsAlsoNotifiedSubscriber(272 self.bug.personIsAlsoNotifiedSubscriber(person))
273 person))
274 view = create_initialized_view(273 view = create_initialized_view(
275 self.bug, name="+portlet-subscription")274 self.bug, name="+portlet-subscription")
276 self.assertFalse(view.user_should_see_mute_link)275 self.assertFalse(view.user_should_see_mute_link)
@@ -305,14 +304,31 @@
305 self.assertTrue(self.bug.isMuted(person))304 self.assertTrue(self.bug.isMuted(person))
306 view = create_initialized_view(305 view = create_initialized_view(
307 self.bug, name="+portlet-subscription")306 self.bug, name="+portlet-subscription")
308 self.assertTrue(view.user_should_see_mute_link,307 self.assertTrue(
309 "User should see mute link.")308 view.user_should_see_mute_link, "User should see mute link.")
310 contents = view.render()309 contents = view.render()
311 self.assertTrue('mute_subscription' in contents,310 self.assertTrue(
312 "'mute_subscription' not in contents.")311 'mute_subscription' in contents,
312 "'mute_subscription' not in contents.")
313 self.assertFalse(313 self.assertFalse(
314 self._hasCSSClass(314 self._hasCSSClass(contents, 'mute-link-container', 'hidden'))
315 contents, 'mute-link-container', 'hidden'))315
316 def test_bug_portlet_subscription_query_count(self):
317 # Bug:+portlet-subscription doesn't make O(n) queries based on the
318 # number of duplicate bugs.
319 user = self.factory.makePerson()
320 bug = self.factory.makeBug()
321 for n in range(20):
322 dupe = self.factory.makeBug()
323 removeSecurityProxy(dupe)._markAsDuplicate(bug)
324 removeSecurityProxy(dupe).subscribe(user, dupe.owner)
325 Store.of(bug).invalidate()
326 with person_logged_in(user):
327 with StormStatementRecorder() as recorder:
328 view = create_initialized_view(
329 bug, name='+portlet-subscription', principal=user)
330 view.render()
331 self.assertThat(recorder, HasQueryCount(Equals(21)))
316332
317333
318class TestBugSecrecyViews(TestCaseWithFactory):334class TestBugSecrecyViews(TestCaseWithFactory):
@@ -328,12 +344,11 @@
328 if bug is None:344 if bug is None:
329 bug = self.factory.makeBug()345 bug = self.factory.makeBug()
330 with person_logged_in(person):346 with person_logged_in(person):
331 view = create_initialized_view(347 return create_initialized_view(
332 bug.default_bugtask, name='+secrecy', form={348 bug.default_bugtask, name='+secrecy', form={
333 'field.information_type': 'USERDATA',349 'field.information_type': 'USERDATA',
334 'field.actions.change': 'Change',350 'field.actions.change': 'Change',
335 }, request=request)351 }, request=request)
336 return view
337352
338 def test_notification_shown_if_marking_private_and_not_subscribed(self):353 def test_notification_shown_if_marking_private_and_not_subscribed(self):
339 # If a user who is not subscribed to a bug marks that bug as354 # If a user who is not subscribed to a bug marks that bug as
340355
=== modified file 'lib/lp/bugs/browser/tests/test_bugtask.py'
--- lib/lp/bugs/browser/tests/test_bugtask.py 2012-10-10 02:45:36 +0000
+++ lib/lp/bugs/browser/tests/test_bugtask.py 2012-10-12 11:59:29 +0000
@@ -156,7 +156,7 @@
156 def test_rendered_query_counts_constant_with_attachments(self):156 def test_rendered_query_counts_constant_with_attachments(self):
157 with celebrity_logged_in('admin'):157 with celebrity_logged_in('admin'):
158 browses_under_limit = BrowsesWithQueryLimit(158 browses_under_limit = BrowsesWithQueryLimit(
159 95, self.factory.makePerson())159 96, self.factory.makePerson())
160160
161 # First test with a single attachment.161 # First test with a single attachment.
162 task = self.factory.makeBugTask()162 task = self.factory.makeBugTask()
163163
=== modified file 'lib/lp/bugs/model/bug.py'
--- lib/lp/bugs/model/bug.py 2012-10-11 04:21:07 +0000
+++ lib/lp/bugs/model/bug.py 2012-10-12 11:59:29 +0000
@@ -13,6 +13,7 @@
13 'BugSet',13 'BugSet',
14 'BugTag',14 'BugTag',
15 'FileBugData',15 'FileBugData',
16 'generate_subscription_with',
16 'get_also_notified_subscribers',17 'get_also_notified_subscribers',
17 'get_bug_tags_open_count',18 'get_bug_tags_open_count',
18 ]19 ]
@@ -54,6 +55,7 @@
54 SQL,55 SQL,
55 Sum,56 Sum,
56 Union,57 Union,
58 With,
57 )59 )
58from storm.info import ClassAlias60from storm.info import ClassAlias
59from storm.locals import (61from storm.locals import (
@@ -1056,36 +1058,21 @@
1056 else:1058 else:
1057 self._subscriber_dups_cache.add(subscriber)1059 self._subscriber_dups_cache.add(subscriber)
1058 return subscriber1060 return subscriber
1059 return DecoratedResultSet(Store.of(self).find(1061 with_statement = generate_subscription_with(self, person)
1062 store = Store.of(self).with_(with_statement)
1063 return DecoratedResultSet(store.find(
1060 # Return people and subscriptions1064 # Return people and subscriptions
1061 (Person, BugSubscription),1065 (Person, BugSubscription),
1062 # For this bug or its duplicates1066 BugSubscription.id.is_in(
1063 Or(1067 SQL('SELECT bugsubscriptions.id FROM bugsubscriptions')),
1064 Bug.id == self.id,1068 Person.id == BugSubscription.person_id,
1065 Bug.duplicateof == self.id),
1066 # Get subscriptions for these bugs
1067 BugSubscription.bug_id == Bug.id,
1068 # Filter by subscriptions to any team person is in.
1069 # Note that teamparticipation includes self-participation entries
1070 # (person X is in the team X)
1071 TeamParticipation.person == person.id,
1072 # XXX: Storm fails to compile this, so manually done.
1073 # bug=https://bugs.launchpad.net/storm/+bug/627137
1074 # RBC 20100831
1075 SQL("""TeamParticipation.team = BugSubscription.person"""),
1076 # Join in the Person rows we want
1077 # XXX: Storm fails to compile this, so manually done.
1078 # bug=https://bugs.launchpad.net/storm/+bug/627137
1079 # RBC 20100831
1080 SQL("""Person.id = TeamParticipation.team"""),
1081 ).order_by(Person.name).config(1069 ).order_by(Person.name).config(
1082 distinct=(Person.name, BugSubscription.person_id)),1070 distinct=(Person.name, BugSubscription.person_id)),
1083 cache_subscriber, pre_iter_hook=cache_unsubscribed)1071 cache_subscriber, pre_iter_hook=cache_unsubscribed)
10841072
1085 def getSubscriptionForPerson(self, person):1073 def getSubscriptionForPerson(self, person):
1086 """See `IBug`."""1074 """See `IBug`."""
1087 store = Store.of(self)1075 return Store.of(self).find(
1088 return store.find(
1089 BugSubscription,1076 BugSubscription,
1090 BugSubscription.person == person,1077 BugSubscription.person == person,
1091 BugSubscription.bug == self).one()1078 BugSubscription.bug == self).one()
@@ -2830,3 +2817,19 @@
2830 date_created = DateTime(2817 date_created = DateTime(
2831 "date_created", allow_none=False, default=UTC_NOW,2818 "date_created", allow_none=False, default=UTC_NOW,
2832 tzinfo=pytz.UTC)2819 tzinfo=pytz.UTC)
2820
2821
2822def generate_subscription_with(bug, person):
2823 return [
2824 With('all_bugsubscriptions', Select(
2825 (BugSubscription.id, BugSubscription.person_id),
2826 tables=[
2827 BugSubscription, Join(Bug, Bug.id == BugSubscription.bug_id)],
2828 where=Or(Bug.id == bug.id, Bug.duplicateofID == bug.id))),
2829 With('bugsubscriptions', Select(
2830 SQL('all_bugsubscriptions.id'),
2831 tables=[
2832 SQL('all_bugsubscriptions'),
2833 Join(TeamParticipation, TeamParticipation.teamID == SQL(
2834 'all_bugsubscriptions.person'))],
2835 where=[TeamParticipation.personID == person.id]))]
28332836
=== modified file 'lib/lp/bugs/model/personsubscriptioninfo.py'
--- lib/lp/bugs/model/personsubscriptioninfo.py 2012-08-22 01:41:50 +0000
+++ lib/lp/bugs/model/personsubscriptioninfo.py 2012-10-12 11:59:29 +0000
@@ -1,4 +1,4 @@
1# Copyright 2011 Canonical Ltd. This software is licensed under the1# Copyright 2011-2012 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4__metaclass__ = type4__metaclass__ = type
@@ -6,8 +6,9 @@
6 'PersonSubscriptions',6 'PersonSubscriptions',
7 ]7 ]
88
9from storm.expr import Or9from storm.expr import SQL
10from storm.store import Store10from storm.store import Store
11from zope.component import getUtility
11from zope.interface import implements12from zope.interface import implements
12from zope.proxy import sameProxiedObjects13from zope.proxy import sameProxiedObjects
1314
@@ -19,17 +20,17 @@
19 IVirtualSubscriptionInfo,20 IVirtualSubscriptionInfo,
20 IVirtualSubscriptionInfoCollection,21 IVirtualSubscriptionInfoCollection,
21 )22 )
22from lp.bugs.interfaces.structuralsubscription import (
23 IStructuralSubscriptionTargetHelper,
24 )
25from lp.bugs.model.bug import (23from lp.bugs.model.bug import (
26 Bug,24 Bug,
27 BugMute,25 BugMute,
26 generate_subscription_with,
28 )27 )
29from lp.bugs.model.bugsubscription import BugSubscription28from lp.bugs.model.bugsubscription import BugSubscription
30from lp.registry.interfaces.sourcepackage import ISourcePackage29from lp.registry.interfaces.person import IPersonSet
30from lp.registry.model.distribution import Distribution
31from lp.registry.model.person import Person31from lp.registry.model.person import Person
32from lp.registry.model.teammembership import TeamParticipation32from lp.registry.model.product import Product
33from lp.services.database.bulk import load_related
3334
3435
35class RealSubscriptionInfo:36class RealSubscriptionInfo:
@@ -96,8 +97,8 @@
96 person, administrated_team_ids)97 person, administrated_team_ids)
97 self._principal_pillar_to_info = {}98 self._principal_pillar_to_info = {}
9899
99 def _add_item_to_collection(self,100 def _add_item_to_collection(self, collection, principal, bug, pillar,
100 collection, principal, bug, pillar, task):101 task):
101 key = (principal, pillar)102 key = (principal, pillar)
102 info = self._principal_pillar_to_info.get(key)103 info = self._principal_pillar_to_info.get(key)
103 if info is None:104 if info is None:
@@ -118,8 +119,8 @@
118 person, administrated_team_ids)119 person, administrated_team_ids)
119 self._principal_bug_to_infos = {}120 self._principal_bug_to_infos = {}
120121
121 def _add_item_to_collection(self, collection, principal,122 def _add_item_to_collection(self, collection, principal, bug,
122 bug, subscription):123 subscription):
123 info = RealSubscriptionInfo(principal, bug, subscription)124 info = RealSubscriptionInfo(principal, bug, subscription)
124 key = (principal, bug)125 key = (principal, bug)
125 infos = self._principal_bug_to_infos.get(key)126 infos = self._principal_bug_to_infos.get(key)
@@ -158,34 +159,17 @@
158 """See `IPersonSubscriptions`."""159 """See `IPersonSubscriptions`."""
159 self.loadSubscriptionsFor(self.person, self.bug)160 self.loadSubscriptionsFor(self.person, self.bug)
160161
161 def _getTaskPillar(self, bugtask):
162 """Return a pillar for a given BugTask."""
163 # There is no adaptor for ISourcePackage. Perhaps there
164 # should be since the data model doesn't seem to prohibit it.
165 # For now, we simply work around the problem. It Would Be Nice If
166 # there were a reliable generic way of getting the pillar for any
167 # bugtarget, but we are not going to tackle that right now.
168 if ISourcePackage.providedBy(bugtask.target):
169 pillar = IStructuralSubscriptionTargetHelper(
170 bugtask.target.distribution_sourcepackage).pillar
171 else:
172 pillar = IStructuralSubscriptionTargetHelper(
173 bugtask.target).pillar
174 return pillar
175
176 def _getDirectAndDuplicateSubscriptions(self, person, bug):162 def _getDirectAndDuplicateSubscriptions(self, person, bug):
177 # Fetch all information for direct and duplicate163 # Fetch all information for direct and duplicate
178 # subscriptions (including indirect through team164 # subscriptions (including indirect through team
179 # membership) in a single query.165 # membership) in a single query.
180 store = Store.of(person)166 with_statement = generate_subscription_with(bug, person)
181 bug_id_options = [Bug.id == bug.id, Bug.duplicateofID == bug.id]167 info = Store.of(person).with_(with_statement).find(
182 info = store.find(
183 (BugSubscription, Bug, Person),168 (BugSubscription, Bug, Person),
184 BugSubscription.bug == Bug.id,169 BugSubscription.id.is_in(
185 BugSubscription.person == Person.id,170 SQL('SELECT bugsubscriptions.id FROM bugsubscriptions')),
186 Or(*bug_id_options),171 Person.id == BugSubscription.person_id,
187 TeamParticipation.personID == person.id,172 Bug.id == BugSubscription.bug_id)
188 TeamParticipation.teamID == Person.id)
189173
190 direct = RealSubscriptionInfoCollection(174 direct = RealSubscriptionInfoCollection(
191 self.person, self.administrated_team_ids)175 self.person, self.administrated_team_ids)
@@ -202,30 +186,27 @@
202 collection = direct186 collection = direct
203 collection.add(187 collection.add(
204 subscriber, subscribed_bug, subscription)188 subscriber, subscribed_bug, subscription)
189 # Preload bug owners, then all pillars.
190 list(getUtility(IPersonSet).getPrecachedPersonsFromIDs(
191 [bug.ownerID for bug in bugs]))
192 all_tasks = [task for task in bug.bugtasks for bug in bugs]
193 load_related(Product, all_tasks, ['productID'])
194 load_related(Distribution, all_tasks, ['distributionID'])
205 for bug in bugs:195 for bug in bugs:
206 # indicate the reporter and bug_supervisor196 # indicate the reporter and bug_supervisor
207 duplicates.annotateReporter(bug, bug.owner)197 duplicates.annotateReporter(bug, bug.owner)
208 direct.annotateReporter(bug, bug.owner)198 direct.annotateReporter(bug, bug.owner)
209 for task in bug.bugtasks:199 for task in all_tasks:
210 # Get bug_supervisor.200 # Get bug_supervisor.
211 pillar = self._getTaskPillar(task)201 duplicates.annotateBugTaskResponsibilities(
212 duplicates.annotateBugTaskResponsibilities(202 task, task.pillar, task.pillar.bug_supervisor)
213 task, pillar, pillar.bug_supervisor)203 direct.annotateBugTaskResponsibilities(
214 direct.annotateBugTaskResponsibilities(204 task, task.pillar, task.pillar.bug_supervisor)
215 task, pillar, pillar.bug_supervisor)
216 return (direct, duplicates)205 return (direct, duplicates)
217206
218 def _isMuted(self, person, bug):207 def _isMuted(self, person, bug):
219 store = Store.of(person)208 return not Store.of(person).find(
220 mutes = store.find(209 BugMute, bug=bug, person=person).is_empty()
221 BugMute,
222 BugMute.bug == bug,
223 BugMute.person == person)
224 is_muted = mutes.one()
225 if is_muted is None:
226 return False
227 else:
228 return True
229210
230 def loadSubscriptionsFor(self, person, bug):211 def loadSubscriptionsFor(self, person, bug):
231 self.person = person212 self.person = person
@@ -246,13 +227,12 @@
246 as_assignee = VirtualSubscriptionInfoCollection(227 as_assignee = VirtualSubscriptionInfoCollection(
247 self.person, self.administrated_team_ids)228 self.person, self.administrated_team_ids)
248 for bugtask in bug.bugtasks:229 for bugtask in bug.bugtasks:
249 pillar = self._getTaskPillar(bugtask)230 owner = bugtask.pillar.owner
250 owner = pillar.owner231 if person.inTeam(owner) and bugtask.pillar.bug_supervisor is None:
251 if person.inTeam(owner) and pillar.bug_supervisor is None:232 as_owner.add(owner, bug, bugtask.pillar, bugtask)
252 as_owner.add(owner, bug, pillar, bugtask)
253 assignee = bugtask.assignee233 assignee = bugtask.assignee
254 if person.inTeam(assignee):234 if person.inTeam(assignee):
255 as_assignee.add(assignee, bug, pillar, bugtask)235 as_assignee.add(assignee, bug, bugtask.pillar, bugtask)
256 self.count = 0236 self.count = 0
257 for name, collection in (237 for name, collection in (
258 ('direct', direct), ('from_duplicate', from_duplicate),238 ('direct', direct), ('from_duplicate', from_duplicate),
259239
=== modified file 'lib/lp/bugs/model/tests/test_personsubscriptioninfo.py'
--- lib/lp/bugs/model/tests/test_personsubscriptioninfo.py 2012-08-21 04:04:47 +0000
+++ lib/lp/bugs/model/tests/test_personsubscriptioninfo.py 2012-10-12 11:59:29 +0000
@@ -502,7 +502,7 @@
502 with StormStatementRecorder() as recorder:502 with StormStatementRecorder() as recorder:
503 self.subscriptions.reload()503 self.subscriptions.reload()
504 # This should produce a very small number of queries.504 # This should produce a very small number of queries.
505 self.assertThat(recorder, HasQueryCount(LessThan(5)))505 self.assertThat(recorder, HasQueryCount(LessThan(6)))
506 count_with_one_subscribed_duplicate = recorder.count506 count_with_one_subscribed_duplicate = recorder.count
507 # It should have the correct result.507 # It should have the correct result.
508 self.assertCollectionsAreEmpty(except_='from_duplicate')508 self.assertCollectionsAreEmpty(except_='from_duplicate')