launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #13354
[Merge] lp:~stevenk/launchpad/no-more-o-n-queries-bug-dupes into lp:launchpad
Steve Kowalik has proposed merging lp:~stevenk/launchpad/no-more-o-n-queries-bug-dupes into lp:launchpad.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #839141 in Launchpad itself: "Bug page times out getting subscriptions for logged in users on bugs with many duplicates"
https://bugs.launchpad.net/launchpad/+bug/839141
Bug #1062291 in Launchpad itself: "timeout on Bugtask:+Index in subscription lookups"
https://bugs.launchpad.net/launchpad/+bug/1062291
For more details, see:
https://code.launchpad.net/~stevenk/launchpad/no-more-o-n-queries-bug-dupes/+merge/129355
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.
--
https://code.launchpad.net/~stevenk/launchpad/no-more-o-n-queries-bug-dupes/+merge/129355
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~stevenk/launchpad/no-more-o-n-queries-bug-dupes into lp:launchpad.
=== modified file 'lib/lp/bugs/browser/bugtask.py'
--- lib/lp/bugs/browser/bugtask.py 2012-10-10 03:12:08 +0000
+++ lib/lp/bugs/browser/bugtask.py 2012-10-12 06:28:20 +0000
@@ -739,15 +739,6 @@
edit_url=canonical_url(self.context, view_name='+edit'),
max_width='95%', truncate_lines=6)
- # XXX 2010-10-05 gmb bug=655597:
- # This line of code keeps the view's query count down,
- # possibly using witchcraft. It should be rewritten to be
- # useful or removed in favour of making other queries more
- # efficient. The witchcraft is because the subscribers are accessed
- # in the initial page load, so the data is actually used.
- if self.user is not None:
- list(bug.getSubscribersForPerson(self.user))
-
def userIsSubscribed(self):
"""Is the user subscribed to this bug?"""
return (
=== 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 06:28:20 +0000
@@ -269,8 +269,7 @@
self.assertFalse(self.bug.isSubscribed(person))
self.assertFalse(self.bug.isMuted(person))
self.assertFalse(
- self.bug.personIsAlsoNotifiedSubscriber(
- person))
+ self.bug.personIsAlsoNotifiedSubscriber(person))
view = create_initialized_view(
self.bug, name="+portlet-subscription")
self.assertFalse(view.user_should_see_mute_link)
@@ -305,14 +304,31 @@
self.assertTrue(self.bug.isMuted(person))
view = create_initialized_view(
self.bug, name="+portlet-subscription")
- self.assertTrue(view.user_should_see_mute_link,
- "User should see mute link.")
+ self.assertTrue(
+ view.user_should_see_mute_link, "User should see mute link.")
contents = view.render()
- self.assertTrue('mute_subscription' in contents,
- "'mute_subscription' not in contents.")
+ self.assertTrue(
+ 'mute_subscription' in contents,
+ "'mute_subscription' not in contents.")
self.assertFalse(
- self._hasCSSClass(
- contents, 'mute-link-container', 'hidden'))
+ self._hasCSSClass(contents, 'mute-link-container', 'hidden'))
+
+ def test_bug_portlet_subscription_query_count(self):
+ # Bug:+portlet-subscription doesn't make O(n) queries based on the
+ # number of duplicate bugs.
+ user = self.factory.makePerson()
+ bug = self.factory.makeBug()
+ for n in range(20):
+ dupe = self.factory.makeBug()
+ removeSecurityProxy(dupe)._markAsDuplicate(bug)
+ removeSecurityProxy(dupe).subscribe(user, dupe.owner)
+ Store.of(bug).invalidate()
+ with person_logged_in(user):
+ with StormStatementRecorder() as recorder:
+ view = create_initialized_view(
+ bug, name='+portlet-subscription', principal=user)
+ view.render()
+ self.assertThat(recorder, HasQueryCount(Equals(21)))
class TestBugSecrecyViews(TestCaseWithFactory):
@@ -328,12 +344,11 @@
if bug is None:
bug = self.factory.makeBug()
with person_logged_in(person):
- view = create_initialized_view(
+ return create_initialized_view(
bug.default_bugtask, name='+secrecy', form={
'field.information_type': 'USERDATA',
'field.actions.change': 'Change',
}, request=request)
- return view
def test_notification_shown_if_marking_private_and_not_subscribed(self):
# If a user who is not subscribed to a bug marks that bug as
=== 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 06:28:20 +0000
@@ -13,6 +13,7 @@
'BugSet',
'BugTag',
'FileBugData',
+ 'generate_subscription_with',
'get_also_notified_subscribers',
'get_bug_tags_open_count',
]
@@ -54,6 +55,7 @@
SQL,
Sum,
Union,
+ With,
)
from storm.info import ClassAlias
from storm.locals import (
@@ -1056,36 +1058,21 @@
else:
self._subscriber_dups_cache.add(subscriber)
return subscriber
- return DecoratedResultSet(Store.of(self).find(
+ with_statement = generate_subscription_with(self, person)
+ store = Store.of(self).with_(with_statement)
+ return DecoratedResultSet(store.find(
# Return people and subscriptions
(Person, BugSubscription),
- # For this bug or its duplicates
- Or(
- Bug.id == self.id,
- Bug.duplicateof == self.id),
- # Get subscriptions for these bugs
- BugSubscription.bug_id == Bug.id,
- # Filter by subscriptions to any team person is in.
- # Note that teamparticipation includes self-participation entries
- # (person X is in the team X)
- TeamParticipation.person == person.id,
- # XXX: Storm fails to compile this, so manually done.
- # bug=https://bugs.launchpad.net/storm/+bug/627137
- # RBC 20100831
- SQL("""TeamParticipation.team = BugSubscription.person"""),
- # Join in the Person rows we want
- # XXX: Storm fails to compile this, so manually done.
- # bug=https://bugs.launchpad.net/storm/+bug/627137
- # RBC 20100831
- SQL("""Person.id = TeamParticipation.team"""),
+ BugSubscription.id.is_in(
+ SQL('SELECT bugsubscriptions.id FROM bugsubscriptions')),
+ Person.id == BugSubscription.person_id,
).order_by(Person.name).config(
distinct=(Person.name, BugSubscription.person_id)),
cache_subscriber, pre_iter_hook=cache_unsubscribed)
def getSubscriptionForPerson(self, person):
"""See `IBug`."""
- store = Store.of(self)
- return store.find(
+ return Store.of(self).find(
BugSubscription,
BugSubscription.person == person,
BugSubscription.bug == self).one()
@@ -2830,3 +2817,18 @@
date_created = DateTime(
"date_created", allow_none=False, default=UTC_NOW,
tzinfo=pytz.UTC)
+
+def generate_subscription_with(bug, person):
+ return [
+ With('all_bugsubscriptions', Select(
+ (BugSubscription.id, BugSubscription.person_id),
+ tables=[BugSubscription, Join(
+ Bug, Bug.id == BugSubscription.bug_id)],
+ where=Or(Bug.id == bug.id, Bug.duplicateofID == bug.id))),
+ With('bugsubscriptions', Select(
+ SQL('all_bugsubscriptions.id'),
+ tables=[
+ SQL('all_bugsubscriptions'), Join(
+ TeamParticipation, TeamParticipation.teamID == SQL(
+ 'all_bugsubscriptions.person'))],
+ where=[TeamParticipation.personID == person.id]))]
=== 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 06:28:20 +0000
@@ -1,4 +1,4 @@
-# Copyright 2011 Canonical Ltd. This software is licensed under the
+# Copyright 2011-2012 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
__metaclass__ = type
@@ -6,8 +6,9 @@
'PersonSubscriptions',
]
-from storm.expr import Or
+from storm.expr import SQL
from storm.store import Store
+from zope.component import getUtility
from zope.interface import implements
from zope.proxy import sameProxiedObjects
@@ -25,11 +26,15 @@
from lp.bugs.model.bug import (
Bug,
BugMute,
+ generate_subscription_with,
)
from lp.bugs.model.bugsubscription import BugSubscription
+from lp.registry.interfaces.person import IPersonSet
from lp.registry.interfaces.sourcepackage import ISourcePackage
+from lp.registry.model.distribution import Distribution
from lp.registry.model.person import Person
-from lp.registry.model.teammembership import TeamParticipation
+from lp.registry.model.product import Product
+from lp.services.database.bulk import load_related
class RealSubscriptionInfo:
@@ -96,8 +101,8 @@
person, administrated_team_ids)
self._principal_pillar_to_info = {}
- def _add_item_to_collection(self,
- collection, principal, bug, pillar, task):
+ def _add_item_to_collection(self, collection, principal, bug, pillar,
+ task):
key = (principal, pillar)
info = self._principal_pillar_to_info.get(key)
if info is None:
@@ -118,8 +123,8 @@
person, administrated_team_ids)
self._principal_bug_to_infos = {}
- def _add_item_to_collection(self, collection, principal,
- bug, subscription):
+ def _add_item_to_collection(self, collection, principal, bug,
+ subscription):
info = RealSubscriptionInfo(principal, bug, subscription)
key = (principal, bug)
infos = self._principal_bug_to_infos.get(key)
@@ -177,15 +182,13 @@
# Fetch all information for direct and duplicate
# subscriptions (including indirect through team
# membership) in a single query.
- store = Store.of(person)
- bug_id_options = [Bug.id == bug.id, Bug.duplicateofID == bug.id]
- info = store.find(
+ with_statement = generate_subscription_with(bug, person)
+ info = Store.of(person).with_(with_statement).find(
(BugSubscription, Bug, Person),
- BugSubscription.bug == Bug.id,
- BugSubscription.person == Person.id,
- Or(*bug_id_options),
- TeamParticipation.personID == person.id,
- TeamParticipation.teamID == Person.id)
+ BugSubscription.id.is_in(
+ SQL('SELECT bugsubscriptions.id FROM bugsubscriptions')),
+ Person.id == BugSubscription.person_id,
+ Bug.id == BugSubscription.bug_id)
direct = RealSubscriptionInfoCollection(
self.person, self.administrated_team_ids)
@@ -202,30 +205,30 @@
collection = direct
collection.add(
subscriber, subscribed_bug, subscription)
+ # Preload bug owners, then all pillars.
+ list(getUtility(IPersonSet).getPrecachedPersonsFromIDs(
+ [bug.ownerID for bug in bugs]))
+ all_tasks = [task for task in bug.bugtasks for bug in bugs]
+ load_related(Product, all_tasks, ['productID'])
+ load_related(Distribution, all_tasks, ['distributionID'])
for bug in bugs:
# indicate the reporter and bug_supervisor
duplicates.annotateReporter(bug, bug.owner)
direct.annotateReporter(bug, bug.owner)
- for task in bug.bugtasks:
- # Get bug_supervisor.
- pillar = self._getTaskPillar(task)
- duplicates.annotateBugTaskResponsibilities(
- task, pillar, pillar.bug_supervisor)
- direct.annotateBugTaskResponsibilities(
- task, pillar, pillar.bug_supervisor)
+ for task in all_tasks:
+ # Get bug_supervisor.
+ pillar = self._getTaskPillar(task)
+ duplicates.annotateBugTaskResponsibilities(
+ task, pillar, pillar.bug_supervisor)
+ direct.annotateBugTaskResponsibilities(
+ task, pillar, pillar.bug_supervisor)
return (direct, duplicates)
def _isMuted(self, person, bug):
store = Store.of(person)
- mutes = store.find(
- BugMute,
- BugMute.bug == bug,
- BugMute.person == person)
- is_muted = mutes.one()
- if is_muted is None:
- return False
- else:
- return True
+ is_muted = store.find(
+ BugMute, BugMute.bug == bug, BugMute.person == person).one()
+ return is_muted is not None
def loadSubscriptionsFor(self, person, bug):
self.person = person
Follow ups