launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #21194
[Merge] lp:~cjwatson/launchpad/preload-git-landing-targets-candidates into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/preload-git-landing-targets-candidates into lp:launchpad with lp:~cjwatson/launchpad/preload-branch-landing-targets as a prerequisite.
Commit message:
Preload {GitRepository,GitRef}.{landing_candidates,landing_targets}.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/preload-git-landing-targets-candidates/+merge/310655
This is in line with similar handling in Branch.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/preload-git-landing-targets-candidates into lp:launchpad.
=== modified file 'lib/lp/_schema_circular_imports.py'
--- lib/lp/_schema_circular_imports.py 2016-11-11 15:10:55 +0000
+++ lib/lp/_schema_circular_imports.py 2016-11-11 15:10:56 +0000
@@ -566,8 +566,10 @@
IGitRef, 'createMergeProposal', 'merge_target', IGitRef)
patch_plain_parameter_type(
IGitRef, 'createMergeProposal', 'merge_prerequisite', IGitRef)
-patch_collection_property(IGitRef, 'landing_targets', IBranchMergeProposal)
-patch_collection_property(IGitRef, 'landing_candidates', IBranchMergeProposal)
+patch_collection_property(
+ IGitRef, '_api_landing_targets', IBranchMergeProposal)
+patch_collection_property(
+ IGitRef, '_api_landing_candidates', IBranchMergeProposal)
patch_collection_property(IGitRef, 'dependent_landings', IBranchMergeProposal)
patch_entry_return_type(IGitRef, 'createMergeProposal', IBranchMergeProposal)
patch_collection_return_type(
@@ -581,9 +583,9 @@
patch_entry_return_type(IGitRepository, 'getSubscription', IGitSubscription)
patch_reference_property(IGitRepository, 'code_import', ICodeImport)
patch_collection_property(
- IGitRepository, 'landing_targets', IBranchMergeProposal)
+ IGitRepository, '_api_landing_targets', IBranchMergeProposal)
patch_collection_property(
- IGitRepository, 'landing_candidates', IBranchMergeProposal)
+ IGitRepository, '_api_landing_candidates', IBranchMergeProposal)
patch_collection_property(
IGitRepository, 'dependent_landings', IBranchMergeProposal)
=== modified file 'lib/lp/code/browser/gitref.py'
--- lib/lp/code/browser/gitref.py 2016-10-14 16:16:18 +0000
+++ lib/lp/code/browser/gitref.py 2016-11-11 15:10:56 +0000
@@ -133,12 +133,14 @@
@cachedproperty
def landing_targets(self):
"""Return a filtered list of landing targets."""
- return latest_proposals_for_each_branch(self.context.landing_targets)
+ targets = self.context.getPrecachedLandingTargets(self.user)
+ return latest_proposals_for_each_branch(targets)
@cachedproperty
def landing_candidates(self):
"""Return a decorated list of landing candidates."""
- return [proposal for proposal in self.context.landing_candidates
+ candidates = self.context.getPrecachedLandingCandidates(self.user)
+ return [proposal for proposal in candidates
if check_permission("launchpad.View", proposal)]
def _getBranchCountText(self, count):
=== modified file 'lib/lp/code/browser/tests/test_gitref.py'
--- lib/lp/code/browser/tests/test_gitref.py 2016-09-07 11:12:58 +0000
+++ lib/lp/code/browser/tests/test_gitref.py 2016-11-11 15:10:56 +0000
@@ -12,6 +12,8 @@
from BeautifulSoup import BeautifulSoup
import pytz
import soupmatchers
+from storm.store import Store
+from testtools.matchers import Equals
from zope.component import getUtility
from zope.security.proxy import removeSecurityProxy
@@ -23,9 +25,11 @@
from lp.testing import (
admin_logged_in,
BrowserTestCase,
+ StormStatementRecorder,
)
from lp.testing.dbuser import dbuser
from lp.testing.layers import LaunchpadFunctionalLayer
+from lp.testing.matchers import HasQueryCount
from lp.testing.pages import (
extract_text,
find_tags_by_class,
@@ -220,3 +224,35 @@
soupmatchers.Tag(
'all commits link', 'a', text='All commits',
attrs={'href': expected_url}))))
+
+ def test_query_count_landing_candidates(self):
+ project = self.factory.makeProduct()
+ [ref] = self.factory.makeGitRefs(target=project)
+ for i in range(10):
+ self.factory.makeBranchMergeProposalForGit(target_ref=ref)
+ [source] = self.factory.makeGitRefs(target=project)
+ [prereq] = self.factory.makeGitRefs(target=project)
+ self.factory.makeBranchMergeProposalForGit(
+ source_ref=source, target_ref=ref, prerequisite_ref=prereq)
+ Store.of(ref).flush()
+ Store.of(ref).invalidate()
+ view = create_view(ref, '+index')
+ with StormStatementRecorder() as recorder:
+ view.landing_candidates
+ self.assertThat(recorder, HasQueryCount(Equals(11)))
+
+ def test_query_count_landing_targets(self):
+ project = self.factory.makeProduct()
+ [ref] = self.factory.makeGitRefs(target=project)
+ for i in range(10):
+ self.factory.makeBranchMergeProposalForGit(source_ref=ref)
+ [target] = self.factory.makeGitRefs(target=project)
+ [prereq] = self.factory.makeGitRefs(target=project)
+ self.factory.makeBranchMergeProposalForGit(
+ source_ref=ref, target_ref=target, prerequisite_ref=prereq)
+ Store.of(ref).flush()
+ Store.of(ref).invalidate()
+ view = create_view(ref, '+index')
+ with StormStatementRecorder() as recorder:
+ view.landing_targets
+ self.assertThat(recorder, HasQueryCount(Equals(11)))
=== modified file 'lib/lp/code/interfaces/gitref.py'
--- lib/lp/code/interfaces/gitref.py 2016-11-09 17:18:21 +0000
+++ lib/lp/code/interfaces/gitref.py 2016-11-11 15:10:56 +0000
@@ -221,22 +221,34 @@
and their subscriptions.
"""
- landing_targets = exported(CollectionField(
- title=_("Landing targets"),
- description=_(
- "A collection of the merge proposals where this reference is the "
- "source."),
- readonly=True,
- # Really IBranchMergeProposal, patched in _schema_circular_imports.py.
- value_type=Reference(Interface)))
- landing_candidates = exported(CollectionField(
- title=_("Landing candidates"),
- description=_(
- "A collection of the merge proposals where this reference is the "
- "target."),
- readonly=True,
- # Really IBranchMergeProposal, patched in _schema_circular_imports.py.
- value_type=Reference(Interface)))
+ landing_targets = Attribute(
+ "A collection of the merge proposals where this reference is "
+ "the source.")
+ _api_landing_targets = exported(
+ CollectionField(
+ title=_("Landing targets"),
+ description=_(
+ "A collection of the merge proposals where this reference is "
+ "the source."),
+ readonly=True,
+ # Really IBranchMergeProposal, patched in
+ # _schema_circular_imports.py.
+ value_type=Reference(Interface)),
+ exported_as="landing_targets")
+ landing_candidates = Attribute(
+ "A collection of the merge proposals where this reference is "
+ "the target.")
+ _api_landing_candidates = exported(
+ CollectionField(
+ title=_("Landing candidates"),
+ description=_(
+ "A collection of the merge proposals where this reference is "
+ "the target."),
+ readonly=True,
+ # Really IBranchMergeProposal, patched in
+ # _schema_circular_imports.py.
+ value_type=Reference(Interface)),
+ exported_as="landing_candidates")
dependent_landings = exported(CollectionField(
title=_("Dependent landings"),
description=_(
@@ -246,6 +258,18 @@
# Really IBranchMergeProposal, patched in _schema_circular_imports.py.
value_type=Reference(Interface)))
+ def getPrecachedLandingTargets(user):
+ """Return precached landing targets.
+
+ Target and prerequisite repositories are preloaded.
+ """
+
+ def getPrecachedLandingCandidates(user):
+ """Return precached landing candidates.
+
+ Source and prerequisite repositories are preloaded.
+ """
+
# XXX cjwatson 2015-04-16: Rename in line with landing_targets above
# once we have a better name.
def addLandingTarget(registrant, merge_target, merge_prerequisite=None,
=== modified file 'lib/lp/code/interfaces/gitrepository.py'
--- lib/lp/code/interfaces/gitrepository.py 2016-10-14 15:08:36 +0000
+++ lib/lp/code/interfaces/gitrepository.py 2016-11-11 15:10:56 +0000
@@ -476,22 +476,34 @@
and their subscriptions.
"""
- landing_targets = exported(CollectionField(
- title=_("Landing targets"),
- description=_(
- "A collection of the merge proposals where this repository is the "
- "source."),
- readonly=True,
- # Really IBranchMergeProposal, patched in _schema_circular_imports.py.
- value_type=Reference(Interface)))
- landing_candidates = exported(CollectionField(
- title=_("Landing candidates"),
- description=_(
- "A collection of the merge proposals where this repository is the "
- "target."),
- readonly=True,
- # Really IBranchMergeProposal, patched in _schema_circular_imports.py.
- value_type=Reference(Interface)))
+ landing_targets = Attribute(
+ "A collection of the merge proposals where this repository is "
+ "the source.")
+ _api_landing_targets = exported(
+ CollectionField(
+ title=_("Landing targets"),
+ description=_(
+ "A collection of the merge proposals where this repository is "
+ "the source."),
+ readonly=True,
+ # Really IBranchMergeProposal, patched in
+ # _schema_circular_imports.py.
+ value_type=Reference(Interface)),
+ exported_as="landing_targets")
+ landing_candidates = Attribute(
+ "A collection of the merge proposals where this repository is "
+ "the target.")
+ _api_landing_candidates = exported(
+ CollectionField(
+ title=_("Landing candidates"),
+ description=_(
+ "A collection of the merge proposals where this repository is "
+ "the target."),
+ readonly=True,
+ # Really IBranchMergeProposal, patched in
+ # _schema_circular_imports.py.
+ value_type=Reference(Interface)),
+ exported_as="landing_candidates")
dependent_landings = exported(CollectionField(
title=_("Dependent landings"),
description=_(
@@ -501,6 +513,18 @@
# Really IBranchMergeProposal, patched in _schema_circular_imports.py.
value_type=Reference(Interface)))
+ def getPrecachedLandingTargets(user):
+ """Return precached landing targets.
+
+ Target and prerequisite repositories are preloaded.
+ """
+
+ def getPrecachedLandingCandidates(user):
+ """Return precached landing candidates.
+
+ Source and prerequisite repositories are preloaded.
+ """
+
def getMergeProposalByID(id):
"""Return this repository's merge proposal with this id, or None."""
=== modified file 'lib/lp/code/model/gitref.py'
--- lib/lp/code/model/gitref.py 2016-11-09 17:18:21 +0000
+++ lib/lp/code/model/gitref.py 2016-11-11 15:10:56 +0000
@@ -8,6 +8,7 @@
]
from datetime import datetime
+from functools import partial
import json
from urllib import quote_plus
from urlparse import urlsplit
@@ -51,7 +52,6 @@
BranchMergeProposalGetter,
)
from lp.services.config import config
-from lp.services.database.bulk import load_related
from lp.services.database.constants import UTC_NOW
from lp.services.database.decoratedresultset import DecoratedResultSet
from lp.services.database.enumcol import EnumCol
@@ -59,6 +59,7 @@
from lp.services.database.stormbase import StormBase
from lp.services.features import getFeatureFlag
from lp.services.memcache.interfaces import IMemcacheClient
+from lp.services.webapp.interfaces import ILaunchBag
class GitRefMixin:
@@ -194,25 +195,34 @@
BranchMergeProposal.source_git_repository == self.repository,
BranchMergeProposal.source_git_path == self.path)
+ def getPrecachedLandingTargets(self, user):
+ """See `IGitRef`."""
+ loader = partial(BranchMergeProposal.preloadDataForBMPs, user=user)
+ return DecoratedResultSet(self.landing_targets, pre_iter_hook=loader)
+
+ @property
+ def _api_landing_targets(self):
+ return self.getPrecachedLandingTargets(getUtility(ILaunchBag).user)
+
@property
def landing_candidates(self):
"""See `IGitRef`."""
- # Circular import.
- from lp.code.model.gitrepository import GitRepository
-
- result = Store.of(self).find(
+ return Store.of(self).find(
BranchMergeProposal,
BranchMergeProposal.target_git_repository == self.repository,
BranchMergeProposal.target_git_path == self.path,
Not(BranchMergeProposal.queue_status.is_in(
BRANCH_MERGE_PROPOSAL_FINAL_STATES)))
- def eager_load(rows):
- load_related(
- GitRepository, rows,
- ["source_git_repositoryID", "prerequisite_git_repositoryID"])
+ def getPrecachedLandingCandidates(self, user):
+ """See `IGitRef`."""
+ loader = partial(BranchMergeProposal.preloadDataForBMPs, user=user)
+ return DecoratedResultSet(
+ self.landing_candidates, pre_iter_hook=loader)
- return DecoratedResultSet(result, pre_iter_hook=eager_load)
+ @property
+ def _api_landing_candidates(self):
+ return self.getPrecachedLandingCandidates(getUtility(ILaunchBag).user)
@property
def dependent_landings(self):
=== modified file 'lib/lp/code/model/gitrepository.py'
--- lib/lp/code/model/gitrepository.py 2016-10-14 15:08:58 +0000
+++ lib/lp/code/model/gitrepository.py 2016-11-11 15:10:56 +0000
@@ -155,6 +155,7 @@
get_property_cache,
)
from lp.services.webapp.authorization import available_with_permission
+from lp.services.webapp.interfaces import ILaunchBag
from lp.services.webhooks.interfaces import IWebhookSet
from lp.services.webhooks.model import WebhookTargetMixin
from lp.snappy.interfaces.snap import ISnapSet
@@ -887,6 +888,15 @@
BranchMergeProposal,
BranchMergeProposal.source_git_repository == self)
+ def getPrecachedLandingTargets(self, user):
+ """See `IGitRef`."""
+ loader = partial(BranchMergeProposal.preloadDataForBMPs, user=user)
+ return DecoratedResultSet(self.landing_targets, pre_iter_hook=loader)
+
+ @property
+ def _api_landing_targets(self):
+ return self.getPrecachedLandingTargets(getUtility(ILaunchBag).user)
+
def getActiveLandingTargets(self, paths):
"""Merge proposals not in final states where these refs are source."""
return Store.of(self).find(
@@ -905,6 +915,16 @@
Not(BranchMergeProposal.queue_status.is_in(
BRANCH_MERGE_PROPOSAL_FINAL_STATES)))
+ def getPrecachedLandingCandidates(self, user):
+ """See `IGitRef`."""
+ loader = partial(BranchMergeProposal.preloadDataForBMPs, user=user)
+ return DecoratedResultSet(
+ self.landing_candidates, pre_iter_hook=loader)
+
+ @property
+ def _api_landing_candidates(self):
+ return self.getPrecachedLandingCandidates(getUtility(ILaunchBag).user)
+
def getActiveLandingCandidates(self, paths):
"""Merge proposals not in final states where these refs are target."""
return Store.of(self).find(
=== modified file 'lib/lp/code/model/tests/test_gitref.py'
--- lib/lp/code/model/tests/test_gitref.py 2016-09-07 11:12:58 +0000
+++ lib/lp/code/model/tests/test_gitref.py 2016-11-11 15:10:56 +0000
@@ -18,6 +18,7 @@
EndsWith,
Equals,
Is,
+ LessThan,
MatchesListwise,
MatchesStructure,
)
@@ -36,6 +37,7 @@
ANONYMOUS,
api_url,
person_logged_in,
+ record_two_runs,
TestCaseWithFactory,
verifyObject,
)
@@ -43,6 +45,7 @@
DatabaseFunctionalLayer,
LaunchpadFunctionalLayer,
)
+from lp.testing.matchers import HasQueryCount
from lp.testing.pages import webservice_for_person
@@ -461,6 +464,30 @@
self.assertThat(
landing_candidates["entries"][0]["self_link"], EndsWith(bmp_url))
+ def test_landing_candidates_constant_queries(self):
+ project = self.factory.makeProduct()
+ with person_logged_in(project.owner):
+ [trunk] = self.factory.makeGitRefs(target=project)
+ trunk_url = api_url(trunk)
+ webservice = webservice_for_person(
+ project.owner, permission=OAuthPermission.WRITE_PRIVATE)
+
+ def create_mp():
+ with admin_logged_in():
+ [ref] = self.factory.makeGitRefs(
+ target=project,
+ information_type=InformationType.PRIVATESECURITY)
+ self.factory.makeBranchMergeProposalForGit(
+ source_ref=ref, target_ref=trunk)
+
+ def list_mps():
+ webservice.get(trunk_url + "/landing_candidates")
+
+ list_mps()
+ recorder1, recorder2 = record_two_runs(list_mps, create_mp, 2)
+ self.assertThat(recorder1, HasQueryCount(LessThan(30)))
+ self.assertThat(recorder2, HasQueryCount.byEquality(recorder1))
+
def test_landing_targets(self):
bmp_db = self.factory.makeBranchMergeProposalForGit()
with person_logged_in(bmp_db.registrant):
@@ -476,6 +503,30 @@
self.assertThat(
landing_targets["entries"][0]["self_link"], EndsWith(bmp_url))
+ def test_landing_targets_constant_queries(self):
+ project = self.factory.makeProduct()
+ with person_logged_in(project.owner):
+ [source] = self.factory.makeGitRefs(target=project)
+ source_url = api_url(source)
+ webservice = webservice_for_person(
+ project.owner, permission=OAuthPermission.WRITE_PRIVATE)
+
+ def create_mp():
+ with admin_logged_in():
+ [ref] = self.factory.makeGitRefs(
+ target=project,
+ information_type=InformationType.PRIVATESECURITY)
+ self.factory.makeBranchMergeProposalForGit(
+ source_ref=source, target_ref=ref)
+
+ def list_mps():
+ webservice.get(source_url + "/landing_targets")
+
+ list_mps()
+ recorder1, recorder2 = record_two_runs(list_mps, create_mp, 2)
+ self.assertThat(recorder1, HasQueryCount(LessThan(30)))
+ self.assertThat(recorder2, HasQueryCount.byEquality(recorder1))
+
def test_dependent_landings(self):
[ref] = self.factory.makeGitRefs()
bmp_db = self.factory.makeBranchMergeProposalForGit(
=== modified file 'lib/lp/code/model/tests/test_gitrepository.py'
--- lib/lp/code/model/tests/test_gitrepository.py 2016-10-14 16:16:18 +0000
+++ lib/lp/code/model/tests/test_gitrepository.py 2016-11-11 15:10:56 +0000
@@ -20,6 +20,7 @@
from storm.store import Store
from testtools.matchers import (
EndsWith,
+ LessThan,
MatchesSetwise,
MatchesStructure,
)
@@ -2800,6 +2801,31 @@
self.assertThat(
landing_candidates["entries"][0]["self_link"], EndsWith(bmp_url))
+ def test_landing_candidates_constant_queries(self):
+ project = self.factory.makeProduct()
+ with person_logged_in(project.owner):
+ repository = self.factory.makeGitRepository(target=project)
+ repository_url = api_url(repository)
+ webservice = webservice_for_person(
+ project.owner, permission=OAuthPermission.WRITE_PRIVATE)
+
+ def create_mp():
+ with admin_logged_in():
+ [target] = self.factory.makeGitRefs(repository=repository)
+ [source] = self.factory.makeGitRefs(
+ target=project,
+ information_type=InformationType.PRIVATESECURITY)
+ self.factory.makeBranchMergeProposalForGit(
+ source_ref=source, target_ref=target)
+
+ def list_mps():
+ webservice.get(repository_url + "/landing_candidates")
+
+ list_mps()
+ recorder1, recorder2 = record_two_runs(list_mps, create_mp, 2)
+ self.assertThat(recorder1, HasQueryCount(LessThan(40)))
+ self.assertThat(recorder2, HasQueryCount.byEquality(recorder1))
+
def test_landing_targets(self):
bmp_db = self.factory.makeBranchMergeProposalForGit()
with person_logged_in(bmp_db.registrant):
@@ -2815,6 +2841,31 @@
self.assertThat(
landing_targets["entries"][0]["self_link"], EndsWith(bmp_url))
+ def test_landing_targets_constant_queries(self):
+ project = self.factory.makeProduct()
+ with person_logged_in(project.owner):
+ repository = self.factory.makeGitRepository(target=project)
+ repository_url = api_url(repository)
+ webservice = webservice_for_person(
+ project.owner, permission=OAuthPermission.WRITE_PRIVATE)
+
+ def create_mp():
+ with admin_logged_in():
+ [source] = self.factory.makeGitRefs(repository=repository)
+ [target] = self.factory.makeGitRefs(
+ target=project,
+ information_type=InformationType.PRIVATESECURITY)
+ self.factory.makeBranchMergeProposalForGit(
+ source_ref=source, target_ref=target)
+
+ def list_mps():
+ webservice.get(repository_url + "/landing_targets")
+
+ list_mps()
+ recorder1, recorder2 = record_two_runs(list_mps, create_mp, 2)
+ self.assertThat(recorder1, HasQueryCount(LessThan(30)))
+ self.assertThat(recorder2, HasQueryCount.byEquality(recorder1))
+
def test_dependent_landings(self):
[ref] = self.factory.makeGitRefs()
bmp_db = self.factory.makeBranchMergeProposalForGit(
Follow ups