Merge lp:~cjwatson/launchpad/preload-git-landing-targets-candidates into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 18285
Proposed branch: lp:~cjwatson/launchpad/preload-git-landing-targets-candidates
Merge into: lp:launchpad
Prerequisite: lp:~cjwatson/launchpad/preload-branch-landing-targets
Diff against target: 541 lines (+268/-48)
9 files modified
lib/lp/_schema_circular_imports.py (+6/-4)
lib/lp/code/browser/gitref.py (+4/-2)
lib/lp/code/browser/tests/test_gitref.py (+36/-0)
lib/lp/code/interfaces/gitref.py (+40/-16)
lib/lp/code/interfaces/gitrepository.py (+40/-16)
lib/lp/code/model/gitref.py (+20/-10)
lib/lp/code/model/gitrepository.py (+20/-0)
lib/lp/code/model/tests/test_gitref.py (+51/-0)
lib/lp/code/model/tests/test_gitrepository.py (+51/-0)
To merge this branch: bzr merge lp:~cjwatson/launchpad/preload-git-landing-targets-candidates
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+310655@code.launchpad.net

Commit message

Preload {GitRepository,GitRef}.{landing_candidates,landing_targets}.

Description of the change

This is in line with similar handling in Branch.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/_schema_circular_imports.py'
2--- lib/lp/_schema_circular_imports.py 2016-11-11 15:10:55 +0000
3+++ lib/lp/_schema_circular_imports.py 2016-11-11 15:10:56 +0000
4@@ -566,8 +566,10 @@
5 IGitRef, 'createMergeProposal', 'merge_target', IGitRef)
6 patch_plain_parameter_type(
7 IGitRef, 'createMergeProposal', 'merge_prerequisite', IGitRef)
8-patch_collection_property(IGitRef, 'landing_targets', IBranchMergeProposal)
9-patch_collection_property(IGitRef, 'landing_candidates', IBranchMergeProposal)
10+patch_collection_property(
11+ IGitRef, '_api_landing_targets', IBranchMergeProposal)
12+patch_collection_property(
13+ IGitRef, '_api_landing_candidates', IBranchMergeProposal)
14 patch_collection_property(IGitRef, 'dependent_landings', IBranchMergeProposal)
15 patch_entry_return_type(IGitRef, 'createMergeProposal', IBranchMergeProposal)
16 patch_collection_return_type(
17@@ -581,9 +583,9 @@
18 patch_entry_return_type(IGitRepository, 'getSubscription', IGitSubscription)
19 patch_reference_property(IGitRepository, 'code_import', ICodeImport)
20 patch_collection_property(
21- IGitRepository, 'landing_targets', IBranchMergeProposal)
22+ IGitRepository, '_api_landing_targets', IBranchMergeProposal)
23 patch_collection_property(
24- IGitRepository, 'landing_candidates', IBranchMergeProposal)
25+ IGitRepository, '_api_landing_candidates', IBranchMergeProposal)
26 patch_collection_property(
27 IGitRepository, 'dependent_landings', IBranchMergeProposal)
28
29
30=== modified file 'lib/lp/code/browser/gitref.py'
31--- lib/lp/code/browser/gitref.py 2016-10-14 16:16:18 +0000
32+++ lib/lp/code/browser/gitref.py 2016-11-11 15:10:56 +0000
33@@ -133,12 +133,14 @@
34 @cachedproperty
35 def landing_targets(self):
36 """Return a filtered list of landing targets."""
37- return latest_proposals_for_each_branch(self.context.landing_targets)
38+ targets = self.context.getPrecachedLandingTargets(self.user)
39+ return latest_proposals_for_each_branch(targets)
40
41 @cachedproperty
42 def landing_candidates(self):
43 """Return a decorated list of landing candidates."""
44- return [proposal for proposal in self.context.landing_candidates
45+ candidates = self.context.getPrecachedLandingCandidates(self.user)
46+ return [proposal for proposal in candidates
47 if check_permission("launchpad.View", proposal)]
48
49 def _getBranchCountText(self, count):
50
51=== modified file 'lib/lp/code/browser/tests/test_gitref.py'
52--- lib/lp/code/browser/tests/test_gitref.py 2016-09-07 11:12:58 +0000
53+++ lib/lp/code/browser/tests/test_gitref.py 2016-11-11 15:10:56 +0000
54@@ -12,6 +12,8 @@
55 from BeautifulSoup import BeautifulSoup
56 import pytz
57 import soupmatchers
58+from storm.store import Store
59+from testtools.matchers import Equals
60 from zope.component import getUtility
61 from zope.security.proxy import removeSecurityProxy
62
63@@ -23,9 +25,11 @@
64 from lp.testing import (
65 admin_logged_in,
66 BrowserTestCase,
67+ StormStatementRecorder,
68 )
69 from lp.testing.dbuser import dbuser
70 from lp.testing.layers import LaunchpadFunctionalLayer
71+from lp.testing.matchers import HasQueryCount
72 from lp.testing.pages import (
73 extract_text,
74 find_tags_by_class,
75@@ -220,3 +224,35 @@
76 soupmatchers.Tag(
77 'all commits link', 'a', text='All commits',
78 attrs={'href': expected_url}))))
79+
80+ def test_query_count_landing_candidates(self):
81+ project = self.factory.makeProduct()
82+ [ref] = self.factory.makeGitRefs(target=project)
83+ for i in range(10):
84+ self.factory.makeBranchMergeProposalForGit(target_ref=ref)
85+ [source] = self.factory.makeGitRefs(target=project)
86+ [prereq] = self.factory.makeGitRefs(target=project)
87+ self.factory.makeBranchMergeProposalForGit(
88+ source_ref=source, target_ref=ref, prerequisite_ref=prereq)
89+ Store.of(ref).flush()
90+ Store.of(ref).invalidate()
91+ view = create_view(ref, '+index')
92+ with StormStatementRecorder() as recorder:
93+ view.landing_candidates
94+ self.assertThat(recorder, HasQueryCount(Equals(11)))
95+
96+ def test_query_count_landing_targets(self):
97+ project = self.factory.makeProduct()
98+ [ref] = self.factory.makeGitRefs(target=project)
99+ for i in range(10):
100+ self.factory.makeBranchMergeProposalForGit(source_ref=ref)
101+ [target] = self.factory.makeGitRefs(target=project)
102+ [prereq] = self.factory.makeGitRefs(target=project)
103+ self.factory.makeBranchMergeProposalForGit(
104+ source_ref=ref, target_ref=target, prerequisite_ref=prereq)
105+ Store.of(ref).flush()
106+ Store.of(ref).invalidate()
107+ view = create_view(ref, '+index')
108+ with StormStatementRecorder() as recorder:
109+ view.landing_targets
110+ self.assertThat(recorder, HasQueryCount(Equals(11)))
111
112=== modified file 'lib/lp/code/interfaces/gitref.py'
113--- lib/lp/code/interfaces/gitref.py 2016-11-09 17:18:21 +0000
114+++ lib/lp/code/interfaces/gitref.py 2016-11-11 15:10:56 +0000
115@@ -221,22 +221,34 @@
116 and their subscriptions.
117 """
118
119- landing_targets = exported(CollectionField(
120- title=_("Landing targets"),
121- description=_(
122- "A collection of the merge proposals where this reference is the "
123- "source."),
124- readonly=True,
125- # Really IBranchMergeProposal, patched in _schema_circular_imports.py.
126- value_type=Reference(Interface)))
127- landing_candidates = exported(CollectionField(
128- title=_("Landing candidates"),
129- description=_(
130- "A collection of the merge proposals where this reference is the "
131- "target."),
132- readonly=True,
133- # Really IBranchMergeProposal, patched in _schema_circular_imports.py.
134- value_type=Reference(Interface)))
135+ landing_targets = Attribute(
136+ "A collection of the merge proposals where this reference is "
137+ "the source.")
138+ _api_landing_targets = exported(
139+ CollectionField(
140+ title=_("Landing targets"),
141+ description=_(
142+ "A collection of the merge proposals where this reference is "
143+ "the source."),
144+ readonly=True,
145+ # Really IBranchMergeProposal, patched in
146+ # _schema_circular_imports.py.
147+ value_type=Reference(Interface)),
148+ exported_as="landing_targets")
149+ landing_candidates = Attribute(
150+ "A collection of the merge proposals where this reference is "
151+ "the target.")
152+ _api_landing_candidates = exported(
153+ CollectionField(
154+ title=_("Landing candidates"),
155+ description=_(
156+ "A collection of the merge proposals where this reference is "
157+ "the target."),
158+ readonly=True,
159+ # Really IBranchMergeProposal, patched in
160+ # _schema_circular_imports.py.
161+ value_type=Reference(Interface)),
162+ exported_as="landing_candidates")
163 dependent_landings = exported(CollectionField(
164 title=_("Dependent landings"),
165 description=_(
166@@ -246,6 +258,18 @@
167 # Really IBranchMergeProposal, patched in _schema_circular_imports.py.
168 value_type=Reference(Interface)))
169
170+ def getPrecachedLandingTargets(user):
171+ """Return precached landing targets.
172+
173+ Target and prerequisite repositories are preloaded.
174+ """
175+
176+ def getPrecachedLandingCandidates(user):
177+ """Return precached landing candidates.
178+
179+ Source and prerequisite repositories are preloaded.
180+ """
181+
182 # XXX cjwatson 2015-04-16: Rename in line with landing_targets above
183 # once we have a better name.
184 def addLandingTarget(registrant, merge_target, merge_prerequisite=None,
185
186=== modified file 'lib/lp/code/interfaces/gitrepository.py'
187--- lib/lp/code/interfaces/gitrepository.py 2016-10-14 15:08:36 +0000
188+++ lib/lp/code/interfaces/gitrepository.py 2016-11-11 15:10:56 +0000
189@@ -476,22 +476,34 @@
190 and their subscriptions.
191 """
192
193- landing_targets = exported(CollectionField(
194- title=_("Landing targets"),
195- description=_(
196- "A collection of the merge proposals where this repository is the "
197- "source."),
198- readonly=True,
199- # Really IBranchMergeProposal, patched in _schema_circular_imports.py.
200- value_type=Reference(Interface)))
201- landing_candidates = exported(CollectionField(
202- title=_("Landing candidates"),
203- description=_(
204- "A collection of the merge proposals where this repository is the "
205- "target."),
206- readonly=True,
207- # Really IBranchMergeProposal, patched in _schema_circular_imports.py.
208- value_type=Reference(Interface)))
209+ landing_targets = Attribute(
210+ "A collection of the merge proposals where this repository is "
211+ "the source.")
212+ _api_landing_targets = exported(
213+ CollectionField(
214+ title=_("Landing targets"),
215+ description=_(
216+ "A collection of the merge proposals where this repository is "
217+ "the source."),
218+ readonly=True,
219+ # Really IBranchMergeProposal, patched in
220+ # _schema_circular_imports.py.
221+ value_type=Reference(Interface)),
222+ exported_as="landing_targets")
223+ landing_candidates = Attribute(
224+ "A collection of the merge proposals where this repository is "
225+ "the target.")
226+ _api_landing_candidates = exported(
227+ CollectionField(
228+ title=_("Landing candidates"),
229+ description=_(
230+ "A collection of the merge proposals where this repository is "
231+ "the target."),
232+ readonly=True,
233+ # Really IBranchMergeProposal, patched in
234+ # _schema_circular_imports.py.
235+ value_type=Reference(Interface)),
236+ exported_as="landing_candidates")
237 dependent_landings = exported(CollectionField(
238 title=_("Dependent landings"),
239 description=_(
240@@ -501,6 +513,18 @@
241 # Really IBranchMergeProposal, patched in _schema_circular_imports.py.
242 value_type=Reference(Interface)))
243
244+ def getPrecachedLandingTargets(user):
245+ """Return precached landing targets.
246+
247+ Target and prerequisite repositories are preloaded.
248+ """
249+
250+ def getPrecachedLandingCandidates(user):
251+ """Return precached landing candidates.
252+
253+ Source and prerequisite repositories are preloaded.
254+ """
255+
256 def getMergeProposalByID(id):
257 """Return this repository's merge proposal with this id, or None."""
258
259
260=== modified file 'lib/lp/code/model/gitref.py'
261--- lib/lp/code/model/gitref.py 2016-11-09 17:18:21 +0000
262+++ lib/lp/code/model/gitref.py 2016-11-11 15:10:56 +0000
263@@ -8,6 +8,7 @@
264 ]
265
266 from datetime import datetime
267+from functools import partial
268 import json
269 from urllib import quote_plus
270 from urlparse import urlsplit
271@@ -51,7 +52,6 @@
272 BranchMergeProposalGetter,
273 )
274 from lp.services.config import config
275-from lp.services.database.bulk import load_related
276 from lp.services.database.constants import UTC_NOW
277 from lp.services.database.decoratedresultset import DecoratedResultSet
278 from lp.services.database.enumcol import EnumCol
279@@ -59,6 +59,7 @@
280 from lp.services.database.stormbase import StormBase
281 from lp.services.features import getFeatureFlag
282 from lp.services.memcache.interfaces import IMemcacheClient
283+from lp.services.webapp.interfaces import ILaunchBag
284
285
286 class GitRefMixin:
287@@ -194,25 +195,34 @@
288 BranchMergeProposal.source_git_repository == self.repository,
289 BranchMergeProposal.source_git_path == self.path)
290
291+ def getPrecachedLandingTargets(self, user):
292+ """See `IGitRef`."""
293+ loader = partial(BranchMergeProposal.preloadDataForBMPs, user=user)
294+ return DecoratedResultSet(self.landing_targets, pre_iter_hook=loader)
295+
296+ @property
297+ def _api_landing_targets(self):
298+ return self.getPrecachedLandingTargets(getUtility(ILaunchBag).user)
299+
300 @property
301 def landing_candidates(self):
302 """See `IGitRef`."""
303- # Circular import.
304- from lp.code.model.gitrepository import GitRepository
305-
306- result = Store.of(self).find(
307+ return Store.of(self).find(
308 BranchMergeProposal,
309 BranchMergeProposal.target_git_repository == self.repository,
310 BranchMergeProposal.target_git_path == self.path,
311 Not(BranchMergeProposal.queue_status.is_in(
312 BRANCH_MERGE_PROPOSAL_FINAL_STATES)))
313
314- def eager_load(rows):
315- load_related(
316- GitRepository, rows,
317- ["source_git_repositoryID", "prerequisite_git_repositoryID"])
318+ def getPrecachedLandingCandidates(self, user):
319+ """See `IGitRef`."""
320+ loader = partial(BranchMergeProposal.preloadDataForBMPs, user=user)
321+ return DecoratedResultSet(
322+ self.landing_candidates, pre_iter_hook=loader)
323
324- return DecoratedResultSet(result, pre_iter_hook=eager_load)
325+ @property
326+ def _api_landing_candidates(self):
327+ return self.getPrecachedLandingCandidates(getUtility(ILaunchBag).user)
328
329 @property
330 def dependent_landings(self):
331
332=== modified file 'lib/lp/code/model/gitrepository.py'
333--- lib/lp/code/model/gitrepository.py 2016-10-14 15:08:58 +0000
334+++ lib/lp/code/model/gitrepository.py 2016-11-11 15:10:56 +0000
335@@ -155,6 +155,7 @@
336 get_property_cache,
337 )
338 from lp.services.webapp.authorization import available_with_permission
339+from lp.services.webapp.interfaces import ILaunchBag
340 from lp.services.webhooks.interfaces import IWebhookSet
341 from lp.services.webhooks.model import WebhookTargetMixin
342 from lp.snappy.interfaces.snap import ISnapSet
343@@ -887,6 +888,15 @@
344 BranchMergeProposal,
345 BranchMergeProposal.source_git_repository == self)
346
347+ def getPrecachedLandingTargets(self, user):
348+ """See `IGitRef`."""
349+ loader = partial(BranchMergeProposal.preloadDataForBMPs, user=user)
350+ return DecoratedResultSet(self.landing_targets, pre_iter_hook=loader)
351+
352+ @property
353+ def _api_landing_targets(self):
354+ return self.getPrecachedLandingTargets(getUtility(ILaunchBag).user)
355+
356 def getActiveLandingTargets(self, paths):
357 """Merge proposals not in final states where these refs are source."""
358 return Store.of(self).find(
359@@ -905,6 +915,16 @@
360 Not(BranchMergeProposal.queue_status.is_in(
361 BRANCH_MERGE_PROPOSAL_FINAL_STATES)))
362
363+ def getPrecachedLandingCandidates(self, user):
364+ """See `IGitRef`."""
365+ loader = partial(BranchMergeProposal.preloadDataForBMPs, user=user)
366+ return DecoratedResultSet(
367+ self.landing_candidates, pre_iter_hook=loader)
368+
369+ @property
370+ def _api_landing_candidates(self):
371+ return self.getPrecachedLandingCandidates(getUtility(ILaunchBag).user)
372+
373 def getActiveLandingCandidates(self, paths):
374 """Merge proposals not in final states where these refs are target."""
375 return Store.of(self).find(
376
377=== modified file 'lib/lp/code/model/tests/test_gitref.py'
378--- lib/lp/code/model/tests/test_gitref.py 2016-09-07 11:12:58 +0000
379+++ lib/lp/code/model/tests/test_gitref.py 2016-11-11 15:10:56 +0000
380@@ -18,6 +18,7 @@
381 EndsWith,
382 Equals,
383 Is,
384+ LessThan,
385 MatchesListwise,
386 MatchesStructure,
387 )
388@@ -36,6 +37,7 @@
389 ANONYMOUS,
390 api_url,
391 person_logged_in,
392+ record_two_runs,
393 TestCaseWithFactory,
394 verifyObject,
395 )
396@@ -43,6 +45,7 @@
397 DatabaseFunctionalLayer,
398 LaunchpadFunctionalLayer,
399 )
400+from lp.testing.matchers import HasQueryCount
401 from lp.testing.pages import webservice_for_person
402
403
404@@ -461,6 +464,30 @@
405 self.assertThat(
406 landing_candidates["entries"][0]["self_link"], EndsWith(bmp_url))
407
408+ def test_landing_candidates_constant_queries(self):
409+ project = self.factory.makeProduct()
410+ with person_logged_in(project.owner):
411+ [trunk] = self.factory.makeGitRefs(target=project)
412+ trunk_url = api_url(trunk)
413+ webservice = webservice_for_person(
414+ project.owner, permission=OAuthPermission.WRITE_PRIVATE)
415+
416+ def create_mp():
417+ with admin_logged_in():
418+ [ref] = self.factory.makeGitRefs(
419+ target=project,
420+ information_type=InformationType.PRIVATESECURITY)
421+ self.factory.makeBranchMergeProposalForGit(
422+ source_ref=ref, target_ref=trunk)
423+
424+ def list_mps():
425+ webservice.get(trunk_url + "/landing_candidates")
426+
427+ list_mps()
428+ recorder1, recorder2 = record_two_runs(list_mps, create_mp, 2)
429+ self.assertThat(recorder1, HasQueryCount(LessThan(30)))
430+ self.assertThat(recorder2, HasQueryCount.byEquality(recorder1))
431+
432 def test_landing_targets(self):
433 bmp_db = self.factory.makeBranchMergeProposalForGit()
434 with person_logged_in(bmp_db.registrant):
435@@ -476,6 +503,30 @@
436 self.assertThat(
437 landing_targets["entries"][0]["self_link"], EndsWith(bmp_url))
438
439+ def test_landing_targets_constant_queries(self):
440+ project = self.factory.makeProduct()
441+ with person_logged_in(project.owner):
442+ [source] = self.factory.makeGitRefs(target=project)
443+ source_url = api_url(source)
444+ webservice = webservice_for_person(
445+ project.owner, permission=OAuthPermission.WRITE_PRIVATE)
446+
447+ def create_mp():
448+ with admin_logged_in():
449+ [ref] = self.factory.makeGitRefs(
450+ target=project,
451+ information_type=InformationType.PRIVATESECURITY)
452+ self.factory.makeBranchMergeProposalForGit(
453+ source_ref=source, target_ref=ref)
454+
455+ def list_mps():
456+ webservice.get(source_url + "/landing_targets")
457+
458+ list_mps()
459+ recorder1, recorder2 = record_two_runs(list_mps, create_mp, 2)
460+ self.assertThat(recorder1, HasQueryCount(LessThan(30)))
461+ self.assertThat(recorder2, HasQueryCount.byEquality(recorder1))
462+
463 def test_dependent_landings(self):
464 [ref] = self.factory.makeGitRefs()
465 bmp_db = self.factory.makeBranchMergeProposalForGit(
466
467=== modified file 'lib/lp/code/model/tests/test_gitrepository.py'
468--- lib/lp/code/model/tests/test_gitrepository.py 2016-10-14 16:16:18 +0000
469+++ lib/lp/code/model/tests/test_gitrepository.py 2016-11-11 15:10:56 +0000
470@@ -20,6 +20,7 @@
471 from storm.store import Store
472 from testtools.matchers import (
473 EndsWith,
474+ LessThan,
475 MatchesSetwise,
476 MatchesStructure,
477 )
478@@ -2800,6 +2801,31 @@
479 self.assertThat(
480 landing_candidates["entries"][0]["self_link"], EndsWith(bmp_url))
481
482+ def test_landing_candidates_constant_queries(self):
483+ project = self.factory.makeProduct()
484+ with person_logged_in(project.owner):
485+ repository = self.factory.makeGitRepository(target=project)
486+ repository_url = api_url(repository)
487+ webservice = webservice_for_person(
488+ project.owner, permission=OAuthPermission.WRITE_PRIVATE)
489+
490+ def create_mp():
491+ with admin_logged_in():
492+ [target] = self.factory.makeGitRefs(repository=repository)
493+ [source] = self.factory.makeGitRefs(
494+ target=project,
495+ information_type=InformationType.PRIVATESECURITY)
496+ self.factory.makeBranchMergeProposalForGit(
497+ source_ref=source, target_ref=target)
498+
499+ def list_mps():
500+ webservice.get(repository_url + "/landing_candidates")
501+
502+ list_mps()
503+ recorder1, recorder2 = record_two_runs(list_mps, create_mp, 2)
504+ self.assertThat(recorder1, HasQueryCount(LessThan(40)))
505+ self.assertThat(recorder2, HasQueryCount.byEquality(recorder1))
506+
507 def test_landing_targets(self):
508 bmp_db = self.factory.makeBranchMergeProposalForGit()
509 with person_logged_in(bmp_db.registrant):
510@@ -2815,6 +2841,31 @@
511 self.assertThat(
512 landing_targets["entries"][0]["self_link"], EndsWith(bmp_url))
513
514+ def test_landing_targets_constant_queries(self):
515+ project = self.factory.makeProduct()
516+ with person_logged_in(project.owner):
517+ repository = self.factory.makeGitRepository(target=project)
518+ repository_url = api_url(repository)
519+ webservice = webservice_for_person(
520+ project.owner, permission=OAuthPermission.WRITE_PRIVATE)
521+
522+ def create_mp():
523+ with admin_logged_in():
524+ [source] = self.factory.makeGitRefs(repository=repository)
525+ [target] = self.factory.makeGitRefs(
526+ target=project,
527+ information_type=InformationType.PRIVATESECURITY)
528+ self.factory.makeBranchMergeProposalForGit(
529+ source_ref=source, target_ref=target)
530+
531+ def list_mps():
532+ webservice.get(repository_url + "/landing_targets")
533+
534+ list_mps()
535+ recorder1, recorder2 = record_two_runs(list_mps, create_mp, 2)
536+ self.assertThat(recorder1, HasQueryCount(LessThan(30)))
537+ self.assertThat(recorder2, HasQueryCount.byEquality(recorder1))
538+
539 def test_dependent_landings(self):
540 [ref] = self.factory.makeGitRefs()
541 bmp_db = self.factory.makeBranchMergeProposalForGit(