Merge ~cjwatson/launchpad:faster-teams-vocab into launchpad:master

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: 822f59a58c5c23d977b39835995b3d49f4abd73c
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~cjwatson/launchpad:faster-teams-vocab
Merge into: launchpad:master
Diff against target: 237 lines (+73/-65)
4 files modified
lib/lp/registry/doc/vocabularies.txt (+0/-27)
lib/lp/registry/tests/test_user_vocabularies.py (+45/-1)
lib/lp/registry/vocabularies.py (+28/-28)
lib/lp/registry/vocabularies.zcml (+0/-9)
Reviewer Review Type Date Requested Status
Thiago F. Pappacena (community) Approve
Review via email: mp+389909@code.launchpad.net

Commit message

Optimise UserTeamsParticipationVocabulary

Description of the change

UserTeamsParticipationVocabulary (or, more specifically, AllUserTeamsParticipationPlusSelfVocabulary) is very slow for people who are members of many private teams, doing one TeamParticipation query for each such team.

Optimise this in two ways: firstly, we can fetch fewer teams from the database to begin with; secondly, we can precache the permission check, since a user can always view teams they belong to.

To post a comment you must log in.
Revision history for this message
Thiago F. Pappacena (pappacena) wrote :

LGTM

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/lib/lp/registry/doc/vocabularies.txt b/lib/lp/registry/doc/vocabularies.txt
index 307eaa1..7e4d4d1 100644
--- a/lib/lp/registry/doc/vocabularies.txt
+++ b/lib/lp/registry/doc/vocabularies.txt
@@ -327,33 +327,6 @@ membership is public.
327 LookupError:...327 LookupError:...
328328
329329
330PersonTeamParticipations
331........................
332
333This vocabulary contains all the teams a person participates in. Either
334through direct or indirect participations.
335
336 >>> sample_person = person_set.getByName('name12')
337 >>> [membership.team.name
338 ... for membership in sample_person.team_memberships]
339 [u'hwdb-team', u'landscape-developers', u'launchpad-users', u'name20']
340
341 >>> [team.name for team in sample_person.teams_participated_in]
342 [u'hwdb-team', u'landscape-developers', u'launchpad-users', u'name18',
343 u'name20']
344
345 >>> sample_person_teams_vocabulary = get_naked_vocab(
346 ... sample_person, 'PersonTeamParticipations')
347
348 >>> for term in sample_person_teams_vocabulary:
349 ... print "%s: %s (%s)" % (term.token, term.title, term.value.name)
350 hwdb-team: HWDB Team (hwdb-team)
351 landscape-developers: Landscape Developers (landscape-developers)
352 launchpad-users: Launchpad Users (launchpad-users)
353 name18: Ubuntu Gnome Team (name18)
354 name20: Warty Security Team (name20)
355
356
357Milestone330Milestone
358.........331.........
359332
diff --git a/lib/lp/registry/tests/test_user_vocabularies.py b/lib/lp/registry/tests/test_user_vocabularies.py
index a35ff21..77a4a25 100644
--- a/lib/lp/registry/tests/test_user_vocabularies.py
+++ b/lib/lp/registry/tests/test_user_vocabularies.py
@@ -1,4 +1,4 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009-2020 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Test the user vocabularies."""4"""Test the user vocabularies."""
@@ -19,9 +19,11 @@ from lp.testing import (
19 ANONYMOUS,19 ANONYMOUS,
20 login,20 login,
21 login_person,21 login_person,
22 record_two_runs,
22 TestCaseWithFactory,23 TestCaseWithFactory,
23 )24 )
24from lp.testing.layers import DatabaseFunctionalLayer25from lp.testing.layers import DatabaseFunctionalLayer
26from lp.testing.matchers import HasQueryCount
2527
2628
27class TestUserTeamsParticipationPlusSelfVocabulary(TestCaseWithFactory):29class TestUserTeamsParticipationPlusSelfVocabulary(TestCaseWithFactory):
@@ -67,6 +69,26 @@ class TestUserTeamsParticipationPlusSelfVocabulary(TestCaseWithFactory):
67 login_person(user)69 login_person(user)
68 self.assertEqual([user], self._vocabTermValues())70 self.assertEqual([user], self._vocabTermValues())
6971
72 def test_user_no_private_teams_query_count(self):
73 # Checking membership of private teams doesn't inflate the
74 # vocabulary's query count.
75 user = self.factory.makePerson()
76 team_owner = self.factory.makePerson()
77
78 def make_private_team():
79 team = self.factory.makeTeam(
80 owner=team_owner, visibility=PersonVisibility.PRIVATE)
81 team.addMember(person=user, reviewer=team_owner)
82
83 def expand_vocabulary():
84 login_person(user)
85 self.assertEqual([user], self._vocabTermValues())
86
87 recorder1, recorder2 = record_two_runs(
88 expand_vocabulary, make_private_team, 5,
89 login_method=lambda: login_person(team_owner))
90 self.assertThat(recorder2, HasQueryCount.byEquality(recorder1))
91
70 def test_indirect_team_membership(self):92 def test_indirect_team_membership(self):
71 # Indirect team membership is shown.93 # Indirect team membership is shown.
72 user = self.factory.makePerson()94 user = self.factory.makePerson()
@@ -101,6 +123,28 @@ class TestAllUserTeamsParticipationPlusSelfVocabulary(TestCaseWithFactory):
101 login_person(team_owner)123 login_person(team_owner)
102 self.assertEqual([team_owner, team], self._vocabTermValues())124 self.assertEqual([team_owner, team], self._vocabTermValues())
103125
126 def test_user_no_private_teams_query_count(self):
127 # Checking membership of private teams doesn't inflate the
128 # vocabulary's query count.
129 user = self.factory.makePerson()
130 team_owner = self.factory.makePerson()
131 teams = []
132
133 def make_private_team():
134 team = self.factory.makeTeam(
135 owner=team_owner, visibility=PersonVisibility.PRIVATE)
136 team.addMember(person=user, reviewer=team_owner)
137 teams.append(team)
138
139 def expand_vocabulary():
140 login_person(user)
141 self.assertContentEqual([user] + teams, self._vocabTermValues())
142
143 recorder1, recorder2 = record_two_runs(
144 expand_vocabulary, make_private_team, 5,
145 login_method=lambda: login_person(team_owner))
146 self.assertThat(recorder2, HasQueryCount.byEquality(recorder1))
147
104 def test_only_exclusive_teams_for_series_branches(self):148 def test_only_exclusive_teams_for_series_branches(self):
105 # For series branches, only exclusive teams are permitted in the vocab.149 # For series branches, only exclusive teams are permitted in the vocab.
106 branch = self.factory.makeBranch()150 branch = self.factory.makeBranch()
diff --git a/lib/lp/registry/vocabularies.py b/lib/lp/registry/vocabularies.py
index d7ef98d..78780a4 100644
--- a/lib/lp/registry/vocabularies.py
+++ b/lib/lp/registry/vocabularies.py
@@ -1,4 +1,4 @@
1# Copyright 2009-2016 Canonical Ltd. This software is licensed under the1# Copyright 2009-2020 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Vocabularies for content objects.4"""Vocabularies for content objects.
@@ -44,7 +44,6 @@ __all__ = [
44 'MilestoneWithDateExpectedVocabulary',44 'MilestoneWithDateExpectedVocabulary',
45 'NewPillarGranteeVocabulary',45 'NewPillarGranteeVocabulary',
46 'NonMergedPeopleAndTeamsVocabulary',46 'NonMergedPeopleAndTeamsVocabulary',
47 'person_team_participations_vocabulary_factory',
48 'PersonAccountToMergeVocabulary',47 'PersonAccountToMergeVocabulary',
49 'PersonActiveMembershipVocabulary',48 'PersonActiveMembershipVocabulary',
50 'ProductReleaseVocabulary',49 'ProductReleaseVocabulary',
@@ -199,7 +198,10 @@ from lp.services.propertycache import (
199 cachedproperty,198 cachedproperty,
200 get_property_cache,199 get_property_cache,
201 )200 )
202from lp.services.webapp.authorization import check_permission201from lp.services.webapp.authorization import (
202 check_permission,
203 precache_permission_for_objects,
204 )
203from lp.services.webapp.interfaces import ILaunchBag205from lp.services.webapp.interfaces import ILaunchBag
204from lp.services.webapp.publisher import nearest206from lp.services.webapp.publisher import nearest
205from lp.services.webapp.vocabulary import (207from lp.services.webapp.vocabulary import (
@@ -401,25 +403,38 @@ class UserTeamsParticipationVocabulary(SQLObjectVocabularyBase):
401 return SimpleTerm(obj, obj.name, obj.unique_displayname)403 return SimpleTerm(obj, obj.name, obj.unique_displayname)
402404
403 def __iter__(self):405 def __iter__(self):
404 kw = {}
405 if self._orderBy:
406 kw['orderBy'] = self._orderBy
407 launchbag = getUtility(ILaunchBag)406 launchbag = getUtility(ILaunchBag)
408 if launchbag.user:407 if launchbag.user:
409 user = launchbag.user408 user = launchbag.user
410 for team in user.teams_participated_in:409 clauses = [
411 if ((team.visibility == PersonVisibility.PUBLIC410 Person.id == TeamParticipation.teamID,
412 or self.INCLUDE_PRIVATE_TEAM) and411 TeamParticipation.person == user,
413 (team.membership_policy in EXCLUSIVE_TEAM_POLICY412 Person.teamowner != None,
414 or not self.EXCLUSIVE_TEAMS_ONLY)):413 ]
415 yield self.toTerm(team)414 if not self.INCLUDE_PRIVATE_TEAM:
415 clauses.append(Person.visibility == PersonVisibility.PUBLIC)
416 if self.EXCLUSIVE_TEAMS_ONLY:
417 clauses.append(
418 Person.membership_policy.is_in(EXCLUSIVE_TEAM_POLICY))
419 teams = list(
420 IStore(Person).find(Person, *clauses).order_by(
421 Person._storm_sortingColumns))
422 # Users can view all the teams they belong to.
423 precache_permission_for_objects(
424 None, 'launchpad.LimitedView', teams)
425 for team in teams:
426 yield self.toTerm(team)
416427
417 def getTermByToken(self, token):428 def getTermByToken(self, token):
418 """See `IVocabularyTokenized`."""429 """See `IVocabularyTokenized`."""
419 launchbag = getUtility(ILaunchBag)430 launchbag = getUtility(ILaunchBag)
420 if launchbag.user:431 if launchbag.user:
421 user = launchbag.user432 user = launchbag.user
422 for team in user.teams_participated_in:433 teams = list(user.teams_participated_in)
434 # Users can view all the teams they belong to.
435 precache_permission_for_objects(
436 None, 'launchpad.LimitedView', teams)
437 for team in teams:
423 if team.name == token:438 if team.name == token:
424 return self.getTerm(team)439 return self.getTerm(team)
425 raise LookupError(token)440 raise LookupError(token)
@@ -1056,21 +1071,6 @@ class ActiveMailingListVocabulary(FilteredVocabularyBase):
1056 return CountableIterator(results.count(), results, self.toTerm)1071 return CountableIterator(results.count(), results, self.toTerm)
10571072
10581073
1059def person_term(person):
1060 """Return a SimpleTerm for the `Person`."""
1061 return SimpleTerm(person, person.name, title=person.displayname)
1062
1063
1064def person_team_participations_vocabulary_factory(context):
1065 """Return a SimpleVocabulary containing the teams a person
1066 participate in.
1067 """
1068 assert context is not None
1069 person = IPerson(context)
1070 return SimpleVocabulary([
1071 person_term(team) for team in person.teams_participated_in])
1072
1073
1074class UserTeamsParticipationPlusSelfVocabulary(1074class UserTeamsParticipationPlusSelfVocabulary(
1075 UserTeamsParticipationVocabulary):1075 UserTeamsParticipationVocabulary):
1076 """A vocabulary containing the public teams that the logged1076 """A vocabulary containing the public teams that the logged
diff --git a/lib/lp/registry/vocabularies.zcml b/lib/lp/registry/vocabularies.zcml
index 057cad4..4e2e488 100644
--- a/lib/lp/registry/vocabularies.zcml
+++ b/lib/lp/registry/vocabularies.zcml
@@ -184,15 +184,6 @@
184184
185185
186 <securedutility186 <securedutility
187 name="PersonTeamParticipations"
188 component="lp.registry.vocabularies.person_team_participations_vocabulary_factory"
189 provides="zope.schema.interfaces.IVocabularyFactory"
190 >
191 <allow interface="zope.schema.interfaces.IVocabularyFactory"/>
192 </securedutility>
193
194
195 <securedutility
196 name="Product"187 name="Product"
197 component="lp.registry.vocabularies.ProductVocabulary"188 component="lp.registry.vocabularies.ProductVocabulary"
198 provides="zope.schema.interfaces.IVocabularyFactory"189 provides="zope.schema.interfaces.IVocabularyFactory"

Subscribers

People subscribed via source and target branches

to status/vote changes: