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
1=== modified file 'lib/lp/code/browser/tests/test_branchmergeproposallisting.py'
2--- lib/lp/code/browser/tests/test_branchmergeproposallisting.py 2011-11-23 14:36:15 +0000
3+++ lib/lp/code/browser/tests/test_branchmergeproposallisting.py 2011-11-25 22:09:49 +0000
4@@ -449,7 +449,9 @@
5 self.assertThat(recorder2, HasQueryCount(Equals(recorder1.count)))
6
7 def createProductBMP(self, product):
8- bmp = self.factory.makeBranchMergeProposal(product=product)
9+ target_branch = self.factory.makeStackedOnBranchChain(product=product)
10+ bmp = self.factory.makeBranchMergeProposal(
11+ product=product, target_branch=target_branch)
12 self.setupBMP(bmp)
13 return bmp
14
15
16=== modified file 'lib/lp/code/model/branchcollection.py'
17--- lib/lp/code/model/branchcollection.py 2011-11-24 12:36:57 +0000
18+++ lib/lp/code/model/branchcollection.py 2011-11-25 22:09:49 +0000
19@@ -9,6 +9,10 @@
20 ]
21
22 from collections import defaultdict
23+from functools import partial
24+from operator import (
25+ attrgetter,
26+ )
27
28 from lazr.restful.utils import safe_hasattr
29 from storm.expr import (
30@@ -29,6 +33,7 @@
31 from zope.component import getUtility
32 from zope.interface import implements
33
34+from canonical.database.sqlbase import quote
35 from canonical.launchpad.components.decoratedresultset import (
36 DecoratedResultSet,
37 )
38@@ -122,6 +127,7 @@
39 if exclude_from_search is None:
40 exclude_from_search = []
41 self._exclude_from_search = exclude_from_search
42+ self._user = None
43
44 def count(self):
45 """See `IBranchCollection`."""
46@@ -218,6 +224,43 @@
47 With("candidate_branches", SQL("SELECT id from scope_branches"))]
48
49 @staticmethod
50+ def preloadVisibleStackedOnBranches(branches, user=None):
51+ """Preload the chains of stacked on branches related to the given list
52+ of branches. Only the branches visible for the given user are
53+ preloaded/returned.
54+
55+ """
56+ if len(branches) == 0:
57+ return
58+ store = IStore(Branch)
59+ result = store.execute("""
60+ WITH RECURSIVE stacked_on_branches_ids AS (
61+ SELECT column1 as id FROM (VALUES %s) AS temp
62+ UNION
63+ SELECT DISTINCT branch.stacked_on
64+ FROM stacked_on_branches_ids, Branch AS branch
65+ WHERE
66+ branch.id = stacked_on_branches_ids.id AND
67+ branch.stacked_on IS NOT NULL
68+ )
69+ SELECT id from stacked_on_branches_ids
70+ """ % ', '.join(
71+ ["(%s)" % quote(id)
72+ for id in map(attrgetter('id'), branches)]))
73+ branch_ids = [res[0] for res in result.get_all()]
74+ # Not really sure this is useful: if a given branch is visible by a
75+ # user, then I think it means that the whole chain of branches on
76+ # which is is stacked on is visible by this user
77+ expressions = [Branch.id.is_in(branch_ids)]
78+ if user is None:
79+ collection = AnonymousBranchCollection(
80+ branch_filter_expressions=expressions)
81+ else:
82+ collection = VisibleBranchCollection(
83+ user=user, branch_filter_expressions=expressions)
84+ return list(collection.getBranches())
85+
86+ @staticmethod
87 def preloadDataForBranches(branches):
88 """Preload branches cached associated product series and
89 suite source packages."""
90@@ -291,8 +334,9 @@
91 for_branches is not None or
92 target_branch is not None or
93 merged_revnos is not None):
94- return self._naiveGetMergeProposals(statuses, for_branches,
95- target_branch, merged_revnos, eager_load=eager_load)
96+ return self._naiveGetMergeProposals(
97+ statuses, for_branches, target_branch, merged_revnos,
98+ eager_load=eager_load)
99 else:
100 # When examining merge proposals in a scope, this is a moderately
101 # effective set of constrained queries. It is not effective when
102@@ -333,9 +377,9 @@
103 if not eager_load:
104 return resultset
105 else:
106- return DecoratedResultSet(
107- resultset,
108- pre_iter_hook=BranchMergeProposal.preloadDataForBMPs)
109+ loader = partial(
110+ BranchMergeProposal.preloadDataForBMPs, user=self._user)
111+ return DecoratedResultSet(resultset, pre_iter_hook=loader)
112
113 def _scopedGetMergeProposals(self, statuses, eager_load=False):
114 scope_tables = [Branch] + self._tables.values()
115@@ -365,9 +409,9 @@
116 if not eager_load:
117 return resultset
118 else:
119- return DecoratedResultSet(
120- resultset,
121- pre_iter_hook=BranchMergeProposal.preloadDataForBMPs)
122+ loader = partial(
123+ BranchMergeProposal.preloadDataForBMPs, user=self._user)
124+ return DecoratedResultSet(resultset, pre_iter_hook=loader)
125
126 def getMergeProposalsForPerson(self, person, status=None,
127 eager_load=False):
128@@ -381,9 +425,9 @@
129 if not eager_load:
130 return resultset
131 else:
132- return DecoratedResultSet(
133- resultset,
134- pre_iter_hook=BranchMergeProposal.preloadDataForBMPs)
135+ loader = partial(
136+ BranchMergeProposal.preloadDataForBMPs, user=self._user)
137+ return DecoratedResultSet(resultset, pre_iter_hook=loader)
138
139 def getMergeProposalsForReviewer(self, reviewer, status=None):
140 """See `IBranchCollection`."""
141
142=== modified file 'lib/lp/code/model/branchmergeproposal.py'
143--- lib/lp/code/model/branchmergeproposal.py 2011-11-24 13:26:16 +0000
144+++ lib/lp/code/model/branchmergeproposal.py 2011-11-25 22:09:49 +0000
145@@ -911,7 +911,7 @@
146 return [range_ for range_, diff in zip(ranges, diffs) if diff is None]
147
148 @staticmethod
149- def preloadDataForBMPs(branch_merge_proposals):
150+ def preloadDataForBMPs(branch_merge_proposals, user):
151 # Utility to load the data related to a list of bmps.
152 # Circular imports.
153 from lp.code.model.branch import Branch
154@@ -933,9 +933,8 @@
155 "target_branchID", "prerequisite_branchID",
156 "source_branchID"))
157 # The stacked on branches are used to check branch visibility.
158- # This is only temporary because we should fetch the whole chain
159- # of stacked on branches instead of only the first level.
160- load_related(Branch, branches, ["stacked_onID"])
161+ GenericBranchCollection.preloadVisibleStackedOnBranches(
162+ branches, user)
163
164 if len(branches) == 0:
165 return
166@@ -951,8 +950,6 @@
167 [preview_diff_and_diff[0] for preview_diff_and_diff
168 in preview_diffs_and_diffs])
169
170- GenericBranchCollection.preloadDataForBranches(branches)
171-
172 # Add source branch owners' to the list of pre-loaded persons.
173 person_ids.update(
174 branch.ownerID for branch in branches
175@@ -966,6 +963,7 @@
176 load_related(SourcePackageName, branches, ['sourcepackagenameID'])
177 load_related(DistroSeries, branches, ['distroseriesID'])
178 load_related(Product, branches, ['productID'])
179+ GenericBranchCollection.preloadDataForBranches(branches)
180
181
182 class BranchMergeProposalGetter:
183
184=== modified file 'lib/lp/code/model/tests/test_branchcollection.py'
185--- lib/lp/code/model/tests/test_branchcollection.py 2011-10-10 00:03:00 +0000
186+++ lib/lp/code/model/tests/test_branchcollection.py 2011-11-25 22:09:49 +0000
187@@ -151,6 +151,51 @@
188 self.store, [Branch.product == branch.product])
189 self.assertEqual(1, collection.count())
190
191+ def test_preloadVisibleStackedOnBranches_visible_private_branches(self):
192+ person = self.factory.makePerson()
193+ branch_number = 2
194+ depth = 3
195+ # Create private branches person can see.
196+ branches = []
197+ for i in range(branch_number):
198+ branches.append(
199+ self.factory.makeStackedOnBranchChain(
200+ owner=person, private=True, depth=depth))
201+ with person_logged_in(person):
202+ all_branches = (
203+ GenericBranchCollection.preloadVisibleStackedOnBranches(
204+ branches, person))
205+ self.assertEqual(len(all_branches), branch_number * depth)
206+
207+ def test_preloadVisibleStackedOnBranches_anon_public_branches(self):
208+ branch_number = 2
209+ depth = 3
210+ # Create public branches.
211+ branches = []
212+ for i in range(branch_number):
213+ branches.append(
214+ self.factory.makeStackedOnBranchChain(
215+ private=False, depth=depth))
216+ all_branches = (
217+ GenericBranchCollection.preloadVisibleStackedOnBranches(branches))
218+ self.assertEqual(len(all_branches), branch_number * depth)
219+
220+ def test_preloadVisibleStackedOnBranches_non_anon_public_branches(self):
221+ person = self.factory.makePerson()
222+ branch_number = 2
223+ depth = 3
224+ # Create public branches.
225+ branches = []
226+ for i in range(branch_number):
227+ branches.append(
228+ self.factory.makeStackedOnBranchChain(
229+ owner=person, private=False, depth=depth))
230+ with person_logged_in(person):
231+ all_branches = (
232+ GenericBranchCollection.preloadVisibleStackedOnBranches(
233+ branches, person))
234+ self.assertEqual(len(all_branches), branch_number * depth)
235+
236
237 class TestBranchCollectionFilters(TestCaseWithFactory):
238
239
240=== modified file 'lib/lp/testing/factory.py'
241--- lib/lp/testing/factory.py 2011-11-22 22:07:39 +0000
242+++ lib/lp/testing/factory.py 2011-11-25 22:09:49 +0000
243@@ -1084,6 +1084,12 @@
244 owner=owner, name=name, title=title, time_zone=time_zone,
245 time_starts=time_starts, time_ends=time_ends, summary=summary)
246
247+ def makeStackedOnBranchChain(self, depth=5, **kwargs):
248+ branch = None
249+ for i in xrange(depth):
250+ branch = self.makeAnyBranch(stacked_on=branch, **kwargs)
251+ return branch
252+
253 def makeBranch(self, branch_type=None, owner=None,
254 name=None, product=_DEFAULT, url=_DEFAULT, registrant=None,
255 private=False, stacked_on=None, sourcepackage=None,