Merge lp:~rvb/launchpad/activereviews-bug-867941-eagerload3 into lp:launchpad

Proposed by Raphaël Badin
Status: Merged
Approved by: Raphaël Badin
Approved revision: no longer in the source branch.
Merged at revision: 14390
Proposed branch: lp:~rvb/launchpad/activereviews-bug-867941-eagerload3
Merge into: lp:launchpad
Prerequisite: lp:~rvb/launchpad/activereviews-bug-893702
Diff against target: 255 lines (+113/-18)
5 files modified
lib/lp/code/browser/tests/test_branchmergeproposallisting.py (+3/-1)
lib/lp/code/model/branchcollection.py (+55/-11)
lib/lp/code/model/branchmergeproposal.py (+4/-6)
lib/lp/code/model/tests/test_branchcollection.py (+45/-0)
lib/lp/testing/factory.py (+6/-0)
To merge this branch: bzr merge lp:~rvb/launchpad/activereviews-bug-867941-eagerload3
Reviewer Review Type Date Requested Status
Brad Crittenden (community) code Approve
Review via email: mp+83389@code.launchpad.net

Commit message

[r=bac][bug=867941] Preload the chain of stacked on branches.

Description of the change

This branch is (hopefully) the last one in a series aimed at improving the performance of +activereviews. This branch improves the preloading of the chain of stacked on branches. It uses a recursive CTE to fetch in one query all the branches on which the branches to be displayed in {person,product}:activereviews are stacked on.

Two things I'd be happy to have a reviewer's option about:
- since the list of branches provided to preloadVisibleStackedOnBranches is a list of visible branches, is it necessary to make sure that all the returned branches are also visible? I think that If a branch is stacked on a non visible branch then it won't be visible ifself.
- I would be happy to avoid using the name 'column1' (which is the default postgresql name for columns created by VALUES expressions but is not a sql standard) in 'SELECT column1 as id FROM (VALUES %s) AS temp[...]' but I could not find a way to do this. Here is a working query if you are wanting to give a try at modifying it: http://paste.ubuntu.com/749381/.

=Tests=
(changed)
/bin/test -vvc test_branchmergeproposallisting test_product_activereviews_query_count
(added)
/bin/test -vvc test_branchcollection test_preloadVisibleStackedOnBranches_visible_private_branches
/bin/test -vvc test_branchcollection test_preloadVisibleStackedOnBranches_anon_public_branches
/bin/test -vvc test_branchcollection test_preloadVisibleStackedOnBranches_non_anon_public_branches

=Q/A=
https://code.qastaging.launchpad.net/~ubuntu-branches/+activereviews should not issue a single repeated query.

To post a comment you must log in.
Revision history for this message
Brad Crittenden (bac) wrote :

Hi Raphaël,

* For createStackedOnChain(self, product) it might be nice to add another argument for the number of branches, defaulting to 5.

* Can branch_ids = map(itemgetter(0), result.get_all()) be rewritten as
           branch_ids = [res[0] for res in result.get_all()] ? If so, all things being equal, I think it is more readable.

* Nice use of 'partial'. I learned something new!

* Actually now that I see createBranchChain it looks like createdStackedOnChain is redundant. Care to promote createBranchChain to the factory.py and re-use it?

* As to your question about visibility of stacked on branches, if you can write a test to prove your assertion then I'd be happy for you to delete the extra step. But the test must be there to ensure we don't have a regression that will allow private branches to leak.

* Sorry I have no ideas on your 'column1' question. Perhaps you can ask stub later.

review: Approve
Revision history for this message
Brad Crittenden (bac) :
review: Approve (code)
Revision history for this message
Raphaël Badin (rvb) wrote :

Thanks for the review Brad!

> * Can branch_ids = map(itemgetter(0), result.get_all()) be rewritten as
> branch_ids = [res[0] for res in result.get_all()] ? If so, all
> things being equal, I think it is more readable.

True. Although you will admit that using map/itemgetter is cooler ;).

> * Actually now that I see createBranchChain it looks like
> createdStackedOnChain is redundant. Care to promote createBranchChain to the
> factory.py and re-use it?

Good idea.

> * As to your question about visibility of stacked on branches, if you can
> write a test to prove your assertion then I'd be happy for you to delete the
> extra step. But the test must be there to ensure we don't have a regression
> that will allow private branches to leak.

Good point, I'll keep the assertion for now.

> * Sorry I have no ideas on your 'column1' question. Perhaps you can ask stub
> later.

I suppose it'd good enough to be landed as is but I'll ask stub/jtv.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/code/browser/tests/test_branchmergeproposallisting.py'
--- lib/lp/code/browser/tests/test_branchmergeproposallisting.py 2011-11-23 14:36:15 +0000
+++ lib/lp/code/browser/tests/test_branchmergeproposallisting.py 2011-11-25 22:09:49 +0000
@@ -449,7 +449,9 @@
449 self.assertThat(recorder2, HasQueryCount(Equals(recorder1.count)))449 self.assertThat(recorder2, HasQueryCount(Equals(recorder1.count)))
450450
451 def createProductBMP(self, product):451 def createProductBMP(self, product):
452 bmp = self.factory.makeBranchMergeProposal(product=product)452 target_branch = self.factory.makeStackedOnBranchChain(product=product)
453 bmp = self.factory.makeBranchMergeProposal(
454 product=product, target_branch=target_branch)
453 self.setupBMP(bmp)455 self.setupBMP(bmp)
454 return bmp456 return bmp
455457
456458
=== modified file 'lib/lp/code/model/branchcollection.py'
--- lib/lp/code/model/branchcollection.py 2011-11-24 12:36:57 +0000
+++ lib/lp/code/model/branchcollection.py 2011-11-25 22:09:49 +0000
@@ -9,6 +9,10 @@
9 ]9 ]
1010
11from collections import defaultdict11from collections import defaultdict
12from functools import partial
13from operator import (
14 attrgetter,
15 )
1216
13from lazr.restful.utils import safe_hasattr17from lazr.restful.utils import safe_hasattr
14from storm.expr import (18from storm.expr import (
@@ -29,6 +33,7 @@
29from zope.component import getUtility33from zope.component import getUtility
30from zope.interface import implements34from zope.interface import implements
3135
36from canonical.database.sqlbase import quote
32from canonical.launchpad.components.decoratedresultset import (37from canonical.launchpad.components.decoratedresultset import (
33 DecoratedResultSet,38 DecoratedResultSet,
34 )39 )
@@ -122,6 +127,7 @@
122 if exclude_from_search is None:127 if exclude_from_search is None:
123 exclude_from_search = []128 exclude_from_search = []
124 self._exclude_from_search = exclude_from_search129 self._exclude_from_search = exclude_from_search
130 self._user = None
125131
126 def count(self):132 def count(self):
127 """See `IBranchCollection`."""133 """See `IBranchCollection`."""
@@ -218,6 +224,43 @@
218 With("candidate_branches", SQL("SELECT id from scope_branches"))]224 With("candidate_branches", SQL("SELECT id from scope_branches"))]
219225
220 @staticmethod226 @staticmethod
227 def preloadVisibleStackedOnBranches(branches, user=None):
228 """Preload the chains of stacked on branches related to the given list
229 of branches. Only the branches visible for the given user are
230 preloaded/returned.
231
232 """
233 if len(branches) == 0:
234 return
235 store = IStore(Branch)
236 result = store.execute("""
237 WITH RECURSIVE stacked_on_branches_ids AS (
238 SELECT column1 as id FROM (VALUES %s) AS temp
239 UNION
240 SELECT DISTINCT branch.stacked_on
241 FROM stacked_on_branches_ids, Branch AS branch
242 WHERE
243 branch.id = stacked_on_branches_ids.id AND
244 branch.stacked_on IS NOT NULL
245 )
246 SELECT id from stacked_on_branches_ids
247 """ % ', '.join(
248 ["(%s)" % quote(id)
249 for id in map(attrgetter('id'), branches)]))
250 branch_ids = [res[0] for res in result.get_all()]
251 # Not really sure this is useful: if a given branch is visible by a
252 # user, then I think it means that the whole chain of branches on
253 # which is is stacked on is visible by this user
254 expressions = [Branch.id.is_in(branch_ids)]
255 if user is None:
256 collection = AnonymousBranchCollection(
257 branch_filter_expressions=expressions)
258 else:
259 collection = VisibleBranchCollection(
260 user=user, branch_filter_expressions=expressions)
261 return list(collection.getBranches())
262
263 @staticmethod
221 def preloadDataForBranches(branches):264 def preloadDataForBranches(branches):
222 """Preload branches cached associated product series and265 """Preload branches cached associated product series and
223 suite source packages."""266 suite source packages."""
@@ -291,8 +334,9 @@
291 for_branches is not None or334 for_branches is not None or
292 target_branch is not None or335 target_branch is not None or
293 merged_revnos is not None):336 merged_revnos is not None):
294 return self._naiveGetMergeProposals(statuses, for_branches,337 return self._naiveGetMergeProposals(
295 target_branch, merged_revnos, eager_load=eager_load)338 statuses, for_branches, target_branch, merged_revnos,
339 eager_load=eager_load)
296 else:340 else:
297 # When examining merge proposals in a scope, this is a moderately341 # When examining merge proposals in a scope, this is a moderately
298 # effective set of constrained queries. It is not effective when342 # effective set of constrained queries. It is not effective when
@@ -333,9 +377,9 @@
333 if not eager_load:377 if not eager_load:
334 return resultset378 return resultset
335 else:379 else:
336 return DecoratedResultSet(380 loader = partial(
337 resultset,381 BranchMergeProposal.preloadDataForBMPs, user=self._user)
338 pre_iter_hook=BranchMergeProposal.preloadDataForBMPs)382 return DecoratedResultSet(resultset, pre_iter_hook=loader)
339383
340 def _scopedGetMergeProposals(self, statuses, eager_load=False):384 def _scopedGetMergeProposals(self, statuses, eager_load=False):
341 scope_tables = [Branch] + self._tables.values()385 scope_tables = [Branch] + self._tables.values()
@@ -365,9 +409,9 @@
365 if not eager_load:409 if not eager_load:
366 return resultset410 return resultset
367 else:411 else:
368 return DecoratedResultSet(412 loader = partial(
369 resultset,413 BranchMergeProposal.preloadDataForBMPs, user=self._user)
370 pre_iter_hook=BranchMergeProposal.preloadDataForBMPs)414 return DecoratedResultSet(resultset, pre_iter_hook=loader)
371415
372 def getMergeProposalsForPerson(self, person, status=None,416 def getMergeProposalsForPerson(self, person, status=None,
373 eager_load=False):417 eager_load=False):
@@ -381,9 +425,9 @@
381 if not eager_load:425 if not eager_load:
382 return resultset426 return resultset
383 else:427 else:
384 return DecoratedResultSet(428 loader = partial(
385 resultset,429 BranchMergeProposal.preloadDataForBMPs, user=self._user)
386 pre_iter_hook=BranchMergeProposal.preloadDataForBMPs)430 return DecoratedResultSet(resultset, pre_iter_hook=loader)
387431
388 def getMergeProposalsForReviewer(self, reviewer, status=None):432 def getMergeProposalsForReviewer(self, reviewer, status=None):
389 """See `IBranchCollection`."""433 """See `IBranchCollection`."""
390434
=== modified file 'lib/lp/code/model/branchmergeproposal.py'
--- lib/lp/code/model/branchmergeproposal.py 2011-11-24 13:26:16 +0000
+++ lib/lp/code/model/branchmergeproposal.py 2011-11-25 22:09:49 +0000
@@ -911,7 +911,7 @@
911 return [range_ for range_, diff in zip(ranges, diffs) if diff is None]911 return [range_ for range_, diff in zip(ranges, diffs) if diff is None]
912912
913 @staticmethod913 @staticmethod
914 def preloadDataForBMPs(branch_merge_proposals):914 def preloadDataForBMPs(branch_merge_proposals, user):
915 # Utility to load the data related to a list of bmps.915 # Utility to load the data related to a list of bmps.
916 # Circular imports.916 # Circular imports.
917 from lp.code.model.branch import Branch917 from lp.code.model.branch import Branch
@@ -933,9 +933,8 @@
933 "target_branchID", "prerequisite_branchID",933 "target_branchID", "prerequisite_branchID",
934 "source_branchID"))934 "source_branchID"))
935 # The stacked on branches are used to check branch visibility.935 # The stacked on branches are used to check branch visibility.
936 # This is only temporary because we should fetch the whole chain936 GenericBranchCollection.preloadVisibleStackedOnBranches(
937 # of stacked on branches instead of only the first level.937 branches, user)
938 load_related(Branch, branches, ["stacked_onID"])
939938
940 if len(branches) == 0:939 if len(branches) == 0:
941 return940 return
@@ -951,8 +950,6 @@
951 [preview_diff_and_diff[0] for preview_diff_and_diff950 [preview_diff_and_diff[0] for preview_diff_and_diff
952 in preview_diffs_and_diffs])951 in preview_diffs_and_diffs])
953952
954 GenericBranchCollection.preloadDataForBranches(branches)
955
956 # Add source branch owners' to the list of pre-loaded persons.953 # Add source branch owners' to the list of pre-loaded persons.
957 person_ids.update(954 person_ids.update(
958 branch.ownerID for branch in branches955 branch.ownerID for branch in branches
@@ -966,6 +963,7 @@
966 load_related(SourcePackageName, branches, ['sourcepackagenameID'])963 load_related(SourcePackageName, branches, ['sourcepackagenameID'])
967 load_related(DistroSeries, branches, ['distroseriesID'])964 load_related(DistroSeries, branches, ['distroseriesID'])
968 load_related(Product, branches, ['productID'])965 load_related(Product, branches, ['productID'])
966 GenericBranchCollection.preloadDataForBranches(branches)
969967
970968
971class BranchMergeProposalGetter:969class BranchMergeProposalGetter:
972970
=== modified file 'lib/lp/code/model/tests/test_branchcollection.py'
--- lib/lp/code/model/tests/test_branchcollection.py 2011-10-10 00:03:00 +0000
+++ lib/lp/code/model/tests/test_branchcollection.py 2011-11-25 22:09:49 +0000
@@ -151,6 +151,51 @@
151 self.store, [Branch.product == branch.product])151 self.store, [Branch.product == branch.product])
152 self.assertEqual(1, collection.count())152 self.assertEqual(1, collection.count())
153153
154 def test_preloadVisibleStackedOnBranches_visible_private_branches(self):
155 person = self.factory.makePerson()
156 branch_number = 2
157 depth = 3
158 # Create private branches person can see.
159 branches = []
160 for i in range(branch_number):
161 branches.append(
162 self.factory.makeStackedOnBranchChain(
163 owner=person, private=True, depth=depth))
164 with person_logged_in(person):
165 all_branches = (
166 GenericBranchCollection.preloadVisibleStackedOnBranches(
167 branches, person))
168 self.assertEqual(len(all_branches), branch_number * depth)
169
170 def test_preloadVisibleStackedOnBranches_anon_public_branches(self):
171 branch_number = 2
172 depth = 3
173 # Create public branches.
174 branches = []
175 for i in range(branch_number):
176 branches.append(
177 self.factory.makeStackedOnBranchChain(
178 private=False, depth=depth))
179 all_branches = (
180 GenericBranchCollection.preloadVisibleStackedOnBranches(branches))
181 self.assertEqual(len(all_branches), branch_number * depth)
182
183 def test_preloadVisibleStackedOnBranches_non_anon_public_branches(self):
184 person = self.factory.makePerson()
185 branch_number = 2
186 depth = 3
187 # Create public branches.
188 branches = []
189 for i in range(branch_number):
190 branches.append(
191 self.factory.makeStackedOnBranchChain(
192 owner=person, private=False, depth=depth))
193 with person_logged_in(person):
194 all_branches = (
195 GenericBranchCollection.preloadVisibleStackedOnBranches(
196 branches, person))
197 self.assertEqual(len(all_branches), branch_number * depth)
198
154199
155class TestBranchCollectionFilters(TestCaseWithFactory):200class TestBranchCollectionFilters(TestCaseWithFactory):
156201
157202
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2011-11-22 22:07:39 +0000
+++ lib/lp/testing/factory.py 2011-11-25 22:09:49 +0000
@@ -1084,6 +1084,12 @@
1084 owner=owner, name=name, title=title, time_zone=time_zone,1084 owner=owner, name=name, title=title, time_zone=time_zone,
1085 time_starts=time_starts, time_ends=time_ends, summary=summary)1085 time_starts=time_starts, time_ends=time_ends, summary=summary)
10861086
1087 def makeStackedOnBranchChain(self, depth=5, **kwargs):
1088 branch = None
1089 for i in xrange(depth):
1090 branch = self.makeAnyBranch(stacked_on=branch, **kwargs)
1091 return branch
1092
1087 def makeBranch(self, branch_type=None, owner=None,1093 def makeBranch(self, branch_type=None, owner=None,
1088 name=None, product=_DEFAULT, url=_DEFAULT, registrant=None,1094 name=None, product=_DEFAULT, url=_DEFAULT, registrant=None,
1089 private=False, stacked_on=None, sourcepackage=None,1095 private=False, stacked_on=None, sourcepackage=None,