← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~rvb/launchpad/activereviews-bug-893702 into lp:launchpad

 

Raphaël Badin has proposed merging lp:~rvb/launchpad/activereviews-bug-893702 into lp:launchpad with lp:~rvb/launchpad/activereviews-bug-867941-eagerload2 as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #893702 in Launchpad itself: "product:+activereviews times out"
  https://bugs.launchpad.net/launchpad/+bug/893702

For more details, see:
https://code.launchpad.net/~rvb/launchpad/activereviews-bug-893702/+merge/83172

This branch reuses the method created while fixing 867941 to eager load the objects related to a list of branch merge proposals for Product:+activereviews.

The real change is the addition of eager_load=True to the call to getMergeProposals in ActiveReviewsView.getProposals.

All the rest is refactoring / cleaning up.  I unified the various pre-loading methods to be all static methods and merged the two existing methods to preload objects related to BMPs.  I also explicitly added an eager_load argument to the only method in branchcollection.py that was doing that by default previously.

I used store.find instead of load_related/load_referencing when it could save a query.

= Tests =
./bin/test -vvc test_branchmergeproposallisting test_product_activereviews_query_count

= Q/A =
The number of repeated queries on product:+activereviews should be zero.

Here are a few numbers:
qastaging https://code.qastaging.launchpad.net/launchpad/+activereviews => 206 queries
qastaging https://code.qastaging.launchpad.net/openstack/+activereviews => 306 queries
prod https://code.launchpad.net/openobject-addons/+activereviews => 1858+ (timeout), 1243+ (timeout)

The proper QA here is to trigger an oops: https://code.qastaging.launchpad.net/launchpad/+activereviews/++oops++ and then have a look at the repeated queries to make sure all the required objects have been pre loaded.
-- 
https://code.launchpad.net/~rvb/launchpad/activereviews-bug-893702/+merge/83172
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~rvb/launchpad/activereviews-bug-893702 into lp:launchpad.
=== modified file 'lib/lp/code/browser/branchmergeproposallisting.py'
--- lib/lp/code/browser/branchmergeproposallisting.py	2011-11-22 22:15:11 +0000
+++ lib/lp/code/browser/branchmergeproposallisting.py	2011-11-24 09:15:59 +0000
@@ -286,7 +286,8 @@
         collection = collection.visibleByUser(self.user)
         proposals = collection.getMergeProposals(
             [BranchMergeProposalStatus.CODE_APPROVED,
-             BranchMergeProposalStatus.NEEDS_REVIEW, ])
+             BranchMergeProposalStatus.NEEDS_REVIEW, ],
+            eager_load=True)
         return proposals
 
     def _getReviewGroup(self, proposal, votes, reviewer):
@@ -447,7 +448,8 @@
         proposals = collection.getMergeProposalsForPerson(
             self._getReviewer(),
             [BranchMergeProposalStatus.CODE_APPROVED,
-             BranchMergeProposalStatus.NEEDS_REVIEW])
+             BranchMergeProposalStatus.NEEDS_REVIEW],
+            eager_load=True)
 
         return proposals
 

=== modified file 'lib/lp/code/browser/tests/test_branchmergeproposallisting.py'
--- lib/lp/code/browser/tests/test_branchmergeproposallisting.py	2011-11-22 10:18:22 +0000
+++ lib/lp/code/browser/tests/test_branchmergeproposallisting.py	2011-11-24 09:15:59 +0000
@@ -393,7 +393,12 @@
 
     layer = LaunchpadFunctionalLayer
 
-    def createBMP(self, reviewer=None, target_branch_owner=None):
+    def setupBMP(self, bmp):
+        self.factory.makePreviewDiff(merge_proposal=bmp)
+        login_person(bmp.source_branch.owner)
+        bmp.requestReview()
+
+    def createUserBMP(self, reviewer=None, target_branch_owner=None):
         target_branch = None
         if target_branch_owner is not None:
             target_branch = self.factory.makePackageBranch(
@@ -401,14 +406,12 @@
         bmp = self.factory.makeBranchMergeProposal(
             reviewer=reviewer,
             target_branch=target_branch)
-        self.factory.makePreviewDiff(merge_proposal=bmp)
-        login_person(bmp.source_branch.owner)
-        bmp.requestReview()
+        self.setupBMP(bmp)
         return bmp
 
-    def createBMPsAndRecordQueries(self, number_of_bmps):
+    def createUserBMPsAndRecordQueries(self, number_of_bmps):
         # Create {number_of_bmps} branch merge proposals related to a
-        # user, render the person's +activereviews page and return the
+        # user, render the person's +activereviews page, and return the
         # view and a recorder of the queries generated by this page
         # rendering.
         user = self.factory.makePerson()
@@ -419,9 +422,9 @@
             # - A BMP for which the person is the owner of the target
             # branch.
             if i % 2 == 0:
-                self.createBMP(target_branch_owner=user)
+                self.createUserBMP(target_branch_owner=user)
             else:
-                self.createBMP(reviewer=user)
+                self.createUserBMP(reviewer=user)
         login_person(user)
         flush_database_caches()
         with StormStatementRecorder() as recorder:
@@ -431,16 +434,50 @@
             view.render()
         return recorder, view
 
-    def test_activereviews_query_count(self):
+    def test_person_activereviews_query_count(self):
         # Note that we keep the number of bmps created small (3 and 7)
         # so that the all the per-cached objects will fit into the cache
         # used in tests (storm_cache_size: 100).
         base_bmps = 3
         added_bmps = 4
-        recorder1, view1 = self.createBMPsAndRecordQueries(base_bmps)
-        self.assertEqual(base_bmps, view1.proposal_count)
-        self.addDetail("r1tb", Content(UTF8_TEXT, lambda: [str(recorder1)]))
-        recorder2, view2 = self.createBMPsAndRecordQueries(
+        recorder1, view1 = self.createUserBMPsAndRecordQueries(base_bmps)
+        self.assertEqual(base_bmps, view1.proposal_count)
+        self.addDetail("r1tb", Content(UTF8_TEXT, lambda: [str(recorder1)]))
+        recorder2, view2 = self.createUserBMPsAndRecordQueries(
+            base_bmps + added_bmps)
+        self.assertEqual(base_bmps + added_bmps, view2.proposal_count)
+        self.assertThat(recorder2, HasQueryCount(Equals(recorder1.count)))
+
+    def createProductBMP(self, product):
+        bmp = self.factory.makeBranchMergeProposal(product=product)
+        self.setupBMP(bmp)
+        return bmp
+
+    def createProductBMPsAndRecordQueries(self, number_of_bmps):
+        # Create {number_of_bmps} branch merge proposals related to a
+        # product, render the product's +activereviews page, and return the
+        # view and a recorder of the queries generated by this page
+        # rendering.
+        product = self.factory.makeProduct()
+        for i in xrange(number_of_bmps):
+            self.createProductBMP(product=product)
+        login_person(product.owner)
+        flush_database_caches()
+        with StormStatementRecorder() as recorder:
+            view = create_initialized_view(
+                product, name='+activereviews', rootsite='code',
+                principal=product.owner)
+            view.render()
+        return recorder, view
+
+    def test_product_activereviews_query_count(self):
+        # We keep the number of bmps created small (3 and 7), see above.
+        base_bmps = 3
+        added_bmps = 4
+        recorder1, view1 = self.createProductBMPsAndRecordQueries(base_bmps)
+        self.assertEqual(base_bmps, view1.proposal_count)
+        self.addDetail("r1tb", Content(UTF8_TEXT, lambda: [str(recorder1)]))
+        recorder2, view2 = self.createProductBMPsAndRecordQueries(
             base_bmps + added_bmps)
         self.assertEqual(base_bmps + added_bmps, view2.proposal_count)
         self.assertThat(recorder2, HasQueryCount(Equals(recorder1.count)))

=== modified file 'lib/lp/code/model/branchcollection.py'
--- lib/lp/code/model/branchcollection.py	2011-11-23 15:58:34 +0000
+++ lib/lp/code/model/branchcollection.py	2011-11-24 09:15:59 +0000
@@ -64,18 +64,12 @@
 from lp.code.model.codeimport import CodeImport
 from lp.code.model.codereviewcomment import CodeReviewComment
 from lp.code.model.codereviewvote import CodeReviewVoteReference
-from lp.code.model.diff import (
-    Diff,
-    PreviewDiff,
-    )
 from lp.code.model.seriessourcepackagebranch import SeriesSourcePackageBranch
-from lp.registry.interfaces.person import IPersonSet
 from lp.registry.model.distribution import Distribution
 from lp.registry.model.distroseries import DistroSeries
 from lp.registry.model.person import (
     Owner,
     Person,
-    ValidPersonCache,
     )
 from lp.registry.model.product import Product
 from lp.registry.model.sourcepackagename import SourcePackageName
@@ -223,7 +217,8 @@
         return [
             With("candidate_branches", SQL("SELECT id from scope_branches"))]
 
-    def _preloadDataForBranches(self, branches):
+    @staticmethod
+    def _preloadDataForBranches(branches):
         """Preload branches cached associated product series and
         suite source packages."""
         caches = dict((branch.id, get_property_cache(branch))
@@ -239,7 +234,7 @@
         # associatedProductSeries
         # Imported here to avoid circular import.
         from lp.registry.model.productseries import ProductSeries
-        for productseries in self.store.find(
+        for productseries in IStore(ProductSeries).find(
             ProductSeries,
             ProductSeries.branchID.is_in(branch_ids)):
             cache = caches[productseries.branchID]
@@ -273,7 +268,7 @@
             branch_ids = set(branch.id for branch in rows)
             if not branch_ids:
                 return
-            self._preloadDataForBranches(rows)
+            GenericBranchCollection._preloadDataForBranches(rows)
             load_related(Product, rows, ['productID'])
             # So far have only needed the persons for their canonical_url - no
             # need for validity etc in the /branches API call.
@@ -297,46 +292,17 @@
             target_branch is not None or
             merged_revnos is not None):
             return self._naiveGetMergeProposals(statuses, for_branches,
-                target_branch, merged_revnos, eager_load)
+                target_branch, merged_revnos, eager_load=eager_load)
         else:
             # When examining merge proposals in a scope, this is a moderately
             # effective set of constrained queries. It is not effective when
             # unscoped or when tight constraints on branches are present.
-            return self._scopedGetMergeProposals(statuses)
+            return self._scopedGetMergeProposals(
+                statuses, eager_load=eager_load)
 
     def _naiveGetMergeProposals(self, statuses=None, for_branches=None,
-        target_branch=None, merged_revnos=None, eager_load=False):
-
-        def do_eager_load(rows):
-            branch_ids = set()
-            person_ids = set()
-            diff_ids = set()
-            for mp in rows:
-                branch_ids.add(mp.target_branchID)
-                branch_ids.add(mp.source_branchID)
-                person_ids.add(mp.registrantID)
-                person_ids.add(mp.merge_reporterID)
-                diff_ids.add(mp.preview_diff_id)
-            if not branch_ids:
-                return
-
-            # Pre-load Person and ValidPersonCache.
-            list(self.store.find(
-                (Person, ValidPersonCache),
-                ValidPersonCache.id == Person.id,
-                Person.id.is_in(person_ids),
-                ))
-
-            # Pre-load PreviewDiffs and Diffs.
-            list(self.store.find(
-                (PreviewDiff, Diff),
-                PreviewDiff.id.is_in(diff_ids),
-                Diff.id == PreviewDiff.diff_id))
-
-            branches = set(
-                self.store.find(Branch, Branch.id.is_in(branch_ids)))
-            self._preloadDataForBranches(branches)
-
+                                target_branch=None, merged_revnos=None,
+                                eager_load=False):
         Target = ClassAlias(Branch, "target")
         extra_tables = list(set(
             self._tables.values() + self._asymmetric_tables.values()))
@@ -367,9 +333,11 @@
         if not eager_load:
             return resultset
         else:
-            return DecoratedResultSet(resultset, pre_iter_hook=do_eager_load)
+            return DecoratedResultSet(
+                resultset,
+                pre_iter_hook=BranchMergeProposal._preloadDataForBMPs)
 
-    def _scopedGetMergeProposals(self, statuses):
+    def _scopedGetMergeProposals(self, statuses, eager_load=False):
         scope_tables = [Branch] + self._tables.values()
         scope_expressions = self._branch_filter_expressions
         select = self.store.using(*scope_tables).find(
@@ -392,10 +360,17 @@
         if statuses is not None:
             expressions.append(
                 BranchMergeProposal.queue_status.is_in(statuses))
-        return self.store.with_(with_expr).using(*tables).find(
+        resultset = self.store.with_(with_expr).using(*tables).find(
             BranchMergeProposal, *expressions)
+        if not eager_load:
+            return resultset
+        else:
+            return DecoratedResultSet(
+                resultset,
+                pre_iter_hook=BranchMergeProposal._preloadDataForBMPs)
 
-    def getMergeProposalsForPerson(self, person, status=None):
+    def getMergeProposalsForPerson(self, person, status=None,
+                                   eager_load=False):
         """See `IBranchCollection`."""
         # We want to limit the proposals to those where the source branch is
         # limited by the defined collection.
@@ -403,31 +378,12 @@
         reviewing = self.getMergeProposalsForReviewer(person, status)
         resultset = owned.union(reviewing)
 
-        def do_eager_load(rows):
-            # Load the related diffs.
-            preview_diffs = load_related(
-                PreviewDiff, rows, ['preview_diff_id'])
-            PreviewDiff.preloadData(preview_diffs)
-            load_related(Diff, preview_diffs, ['diff_id'])
-            # Load related branches.
-            source_branches = load_related(Branch, rows, ['source_branchID'])
-            target_branches = load_related(Branch, rows, ['target_branchID'])
-            load_related(Branch, rows, ['prerequisite_branchID'])
-            # Cache person's data (registrants of the proposal and
-            # owners of the source branches).
-            person_ids = set().union(
-                (proposal.registrantID for proposal in rows),
-                (branch.ownerID for branch in source_branches))
-            list(getUtility(IPersonSet).getPrecachedPersonsFromIDs(
-                person_ids, need_validity=True))
-            # Preload the data for source/target branches.
-            branches = target_branches + source_branches
-            load_related(SourcePackageName, branches, ['sourcepackagenameID'])
-            load_related(DistroSeries, branches, ['distroseriesID'])
-            self._preloadDataForBranches(branches)
-            load_related(Product, branches, ['productID'])
-
-        return DecoratedResultSet(resultset, pre_iter_hook=do_eager_load)
+        if not eager_load:
+            return resultset
+        else:
+            return DecoratedResultSet(
+                resultset,
+                pre_iter_hook=BranchMergeProposal._preloadDataForBMPs)
 
     def getMergeProposalsForReviewer(self, reviewer, status=None):
         """See `IBranchCollection`."""

=== modified file 'lib/lp/code/model/branchmergeproposal.py'
--- lib/lp/code/model/branchmergeproposal.py	2011-09-14 10:19:43 +0000
+++ lib/lp/code/model/branchmergeproposal.py	2011-11-24 09:15:59 +0000
@@ -50,7 +50,10 @@
     SQLBase,
     sqlvalues,
     )
-from canonical.launchpad.interfaces.lpstorm import IMasterStore
+from canonical.launchpad.interfaces.lpstorm import (
+    IMasterStore,
+    IStore,
+    )
 from lp.code.enums import (
     BranchMergeProposalStatus,
     CodeReviewVote,
@@ -88,10 +91,13 @@
     )
 from lp.registry.interfaces.person import (
     IPerson,
+    IPersonSet,
     validate_public_person,
     )
 from lp.registry.interfaces.product import IProduct
 from lp.registry.model.person import Person
+from lp.registry.model.sourcepackagename import SourcePackageName
+from lp.services.database.bulk import load_related
 from lp.services.job.interfaces.job import JobStatus
 from lp.services.job.model.job import Job
 from lp.services.mail.sendmail import validate_message
@@ -904,6 +910,60 @@
         diffs = self.getIncrementalDiffs(ranges)
         return [range_ for range_, diff in zip(ranges, diffs) if diff is None]
 
+    @staticmethod
+    def _preloadDataForBMPs(branch_merge_proposals):
+        # Utility to load the data related to a list of bmps.
+        # Circular imports.
+        from lp.code.model.branch import Branch
+        from lp.code.model.branchcollection import GenericBranchCollection
+        from lp.registry.model.product import Product
+        from lp.registry.model.distroseries import DistroSeries
+
+        branch_ids = set()
+        source_branch_ids = set()
+        person_ids = set()
+        diff_ids = set()
+        for mp in branch_merge_proposals:
+            branch_ids.add(mp.target_branchID)
+            branch_ids.add(mp.prerequisite_branchID)
+            branch_ids.add(mp.source_branchID)
+            source_branch_ids.add(mp.source_branchID)
+            person_ids.add(mp.registrantID)
+            person_ids.add(mp.merge_reporterID)
+            diff_ids.add(mp.preview_diff_id)
+
+        if not branch_ids:
+            return
+
+        store = IStore(BranchMergeProposal)
+
+        # Pre-load PreviewDiffs and Diffs.
+        preview_diffs_and_diffs = list(store.find(
+            (PreviewDiff, Diff),
+            PreviewDiff.id.is_in(diff_ids),
+            Diff.id == PreviewDiff.diff_id))
+        PreviewDiff.preloadData(
+            [preview_diff_and_diff[0] for preview_diff_and_diff
+                in list(preview_diffs_and_diffs)])
+
+        branches = set(
+                store.find(Branch, Branch.id.is_in(branch_ids)))
+        GenericBranchCollection._preloadDataForBranches(branches)
+
+        # Add source branch owners' to the list of pre-loaded persons.
+        for branch in branches:
+            if branch.id in source_branch_ids:
+                person_ids.add(branch.ownerID)
+
+        # Pre-load Person and ValidPersonCache.
+        list(getUtility(IPersonSet).getPrecachedPersonsFromIDs(
+            person_ids, need_validity=True))
+
+        # Pre-load branches' data.
+        load_related(SourcePackageName, branches, ['sourcepackagenameID'])
+        load_related(DistroSeries, branches, ['distroseriesID'])
+        load_related(Product, branches, ['productID'])
+
 
 class BranchMergeProposalGetter:
     """See `IBranchMergeProposalGetter`."""

=== modified file 'lib/lp/translations/model/translationtemplatesbuild.py'
--- lib/lp/translations/model/translationtemplatesbuild.py	2011-11-15 11:08:01 +0000
+++ lib/lp/translations/model/translationtemplatesbuild.py	2011-11-24 09:15:59 +0000
@@ -125,10 +125,9 @@
                 Branch, rows, ['branch_id'])
             load_related(
                 Product, branches, ['productID'])
-            branch_collection = GenericBranchCollection()
             # Preload branches cached associated product series and
             # suite source packages for all the related branches.
-            branch_collection._preloadDataForBranches(branches)
+            GenericBranchCollection._preloadDataForBranches(branches)
 
         resultset = store.find(
             TranslationTemplatesBuild,