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
1diff --git a/lib/lp/registry/doc/vocabularies.txt b/lib/lp/registry/doc/vocabularies.txt
2index 307eaa1..7e4d4d1 100644
3--- a/lib/lp/registry/doc/vocabularies.txt
4+++ b/lib/lp/registry/doc/vocabularies.txt
5@@ -327,33 +327,6 @@ membership is public.
6 LookupError:...
7
8
9-PersonTeamParticipations
10-........................
11-
12-This vocabulary contains all the teams a person participates in. Either
13-through direct or indirect participations.
14-
15- >>> sample_person = person_set.getByName('name12')
16- >>> [membership.team.name
17- ... for membership in sample_person.team_memberships]
18- [u'hwdb-team', u'landscape-developers', u'launchpad-users', u'name20']
19-
20- >>> [team.name for team in sample_person.teams_participated_in]
21- [u'hwdb-team', u'landscape-developers', u'launchpad-users', u'name18',
22- u'name20']
23-
24- >>> sample_person_teams_vocabulary = get_naked_vocab(
25- ... sample_person, 'PersonTeamParticipations')
26-
27- >>> for term in sample_person_teams_vocabulary:
28- ... print "%s: %s (%s)" % (term.token, term.title, term.value.name)
29- hwdb-team: HWDB Team (hwdb-team)
30- landscape-developers: Landscape Developers (landscape-developers)
31- launchpad-users: Launchpad Users (launchpad-users)
32- name18: Ubuntu Gnome Team (name18)
33- name20: Warty Security Team (name20)
34-
35-
36 Milestone
37 .........
38
39diff --git a/lib/lp/registry/tests/test_user_vocabularies.py b/lib/lp/registry/tests/test_user_vocabularies.py
40index a35ff21..77a4a25 100644
41--- a/lib/lp/registry/tests/test_user_vocabularies.py
42+++ b/lib/lp/registry/tests/test_user_vocabularies.py
43@@ -1,4 +1,4 @@
44-# Copyright 2009 Canonical Ltd. This software is licensed under the
45+# Copyright 2009-2020 Canonical Ltd. This software is licensed under the
46 # GNU Affero General Public License version 3 (see the file LICENSE).
47
48 """Test the user vocabularies."""
49@@ -19,9 +19,11 @@ from lp.testing import (
50 ANONYMOUS,
51 login,
52 login_person,
53+ record_two_runs,
54 TestCaseWithFactory,
55 )
56 from lp.testing.layers import DatabaseFunctionalLayer
57+from lp.testing.matchers import HasQueryCount
58
59
60 class TestUserTeamsParticipationPlusSelfVocabulary(TestCaseWithFactory):
61@@ -67,6 +69,26 @@ class TestUserTeamsParticipationPlusSelfVocabulary(TestCaseWithFactory):
62 login_person(user)
63 self.assertEqual([user], self._vocabTermValues())
64
65+ def test_user_no_private_teams_query_count(self):
66+ # Checking membership of private teams doesn't inflate the
67+ # vocabulary's query count.
68+ user = self.factory.makePerson()
69+ team_owner = self.factory.makePerson()
70+
71+ def make_private_team():
72+ team = self.factory.makeTeam(
73+ owner=team_owner, visibility=PersonVisibility.PRIVATE)
74+ team.addMember(person=user, reviewer=team_owner)
75+
76+ def expand_vocabulary():
77+ login_person(user)
78+ self.assertEqual([user], self._vocabTermValues())
79+
80+ recorder1, recorder2 = record_two_runs(
81+ expand_vocabulary, make_private_team, 5,
82+ login_method=lambda: login_person(team_owner))
83+ self.assertThat(recorder2, HasQueryCount.byEquality(recorder1))
84+
85 def test_indirect_team_membership(self):
86 # Indirect team membership is shown.
87 user = self.factory.makePerson()
88@@ -101,6 +123,28 @@ class TestAllUserTeamsParticipationPlusSelfVocabulary(TestCaseWithFactory):
89 login_person(team_owner)
90 self.assertEqual([team_owner, team], self._vocabTermValues())
91
92+ def test_user_no_private_teams_query_count(self):
93+ # Checking membership of private teams doesn't inflate the
94+ # vocabulary's query count.
95+ user = self.factory.makePerson()
96+ team_owner = self.factory.makePerson()
97+ teams = []
98+
99+ def make_private_team():
100+ team = self.factory.makeTeam(
101+ owner=team_owner, visibility=PersonVisibility.PRIVATE)
102+ team.addMember(person=user, reviewer=team_owner)
103+ teams.append(team)
104+
105+ def expand_vocabulary():
106+ login_person(user)
107+ self.assertContentEqual([user] + teams, self._vocabTermValues())
108+
109+ recorder1, recorder2 = record_two_runs(
110+ expand_vocabulary, make_private_team, 5,
111+ login_method=lambda: login_person(team_owner))
112+ self.assertThat(recorder2, HasQueryCount.byEquality(recorder1))
113+
114 def test_only_exclusive_teams_for_series_branches(self):
115 # For series branches, only exclusive teams are permitted in the vocab.
116 branch = self.factory.makeBranch()
117diff --git a/lib/lp/registry/vocabularies.py b/lib/lp/registry/vocabularies.py
118index d7ef98d..78780a4 100644
119--- a/lib/lp/registry/vocabularies.py
120+++ b/lib/lp/registry/vocabularies.py
121@@ -1,4 +1,4 @@
122-# Copyright 2009-2016 Canonical Ltd. This software is licensed under the
123+# Copyright 2009-2020 Canonical Ltd. This software is licensed under the
124 # GNU Affero General Public License version 3 (see the file LICENSE).
125
126 """Vocabularies for content objects.
127@@ -44,7 +44,6 @@ __all__ = [
128 'MilestoneWithDateExpectedVocabulary',
129 'NewPillarGranteeVocabulary',
130 'NonMergedPeopleAndTeamsVocabulary',
131- 'person_team_participations_vocabulary_factory',
132 'PersonAccountToMergeVocabulary',
133 'PersonActiveMembershipVocabulary',
134 'ProductReleaseVocabulary',
135@@ -199,7 +198,10 @@ from lp.services.propertycache import (
136 cachedproperty,
137 get_property_cache,
138 )
139-from lp.services.webapp.authorization import check_permission
140+from lp.services.webapp.authorization import (
141+ check_permission,
142+ precache_permission_for_objects,
143+ )
144 from lp.services.webapp.interfaces import ILaunchBag
145 from lp.services.webapp.publisher import nearest
146 from lp.services.webapp.vocabulary import (
147@@ -401,25 +403,38 @@ class UserTeamsParticipationVocabulary(SQLObjectVocabularyBase):
148 return SimpleTerm(obj, obj.name, obj.unique_displayname)
149
150 def __iter__(self):
151- kw = {}
152- if self._orderBy:
153- kw['orderBy'] = self._orderBy
154 launchbag = getUtility(ILaunchBag)
155 if launchbag.user:
156 user = launchbag.user
157- for team in user.teams_participated_in:
158- if ((team.visibility == PersonVisibility.PUBLIC
159- or self.INCLUDE_PRIVATE_TEAM) and
160- (team.membership_policy in EXCLUSIVE_TEAM_POLICY
161- or not self.EXCLUSIVE_TEAMS_ONLY)):
162- yield self.toTerm(team)
163+ clauses = [
164+ Person.id == TeamParticipation.teamID,
165+ TeamParticipation.person == user,
166+ Person.teamowner != None,
167+ ]
168+ if not self.INCLUDE_PRIVATE_TEAM:
169+ clauses.append(Person.visibility == PersonVisibility.PUBLIC)
170+ if self.EXCLUSIVE_TEAMS_ONLY:
171+ clauses.append(
172+ Person.membership_policy.is_in(EXCLUSIVE_TEAM_POLICY))
173+ teams = list(
174+ IStore(Person).find(Person, *clauses).order_by(
175+ Person._storm_sortingColumns))
176+ # Users can view all the teams they belong to.
177+ precache_permission_for_objects(
178+ None, 'launchpad.LimitedView', teams)
179+ for team in teams:
180+ yield self.toTerm(team)
181
182 def getTermByToken(self, token):
183 """See `IVocabularyTokenized`."""
184 launchbag = getUtility(ILaunchBag)
185 if launchbag.user:
186 user = launchbag.user
187- for team in user.teams_participated_in:
188+ teams = list(user.teams_participated_in)
189+ # Users can view all the teams they belong to.
190+ precache_permission_for_objects(
191+ None, 'launchpad.LimitedView', teams)
192+ for team in teams:
193 if team.name == token:
194 return self.getTerm(team)
195 raise LookupError(token)
196@@ -1056,21 +1071,6 @@ class ActiveMailingListVocabulary(FilteredVocabularyBase):
197 return CountableIterator(results.count(), results, self.toTerm)
198
199
200-def person_term(person):
201- """Return a SimpleTerm for the `Person`."""
202- return SimpleTerm(person, person.name, title=person.displayname)
203-
204-
205-def person_team_participations_vocabulary_factory(context):
206- """Return a SimpleVocabulary containing the teams a person
207- participate in.
208- """
209- assert context is not None
210- person = IPerson(context)
211- return SimpleVocabulary([
212- person_term(team) for team in person.teams_participated_in])
213-
214-
215 class UserTeamsParticipationPlusSelfVocabulary(
216 UserTeamsParticipationVocabulary):
217 """A vocabulary containing the public teams that the logged
218diff --git a/lib/lp/registry/vocabularies.zcml b/lib/lp/registry/vocabularies.zcml
219index 057cad4..4e2e488 100644
220--- a/lib/lp/registry/vocabularies.zcml
221+++ b/lib/lp/registry/vocabularies.zcml
222@@ -184,15 +184,6 @@
223
224
225 <securedutility
226- name="PersonTeamParticipations"
227- component="lp.registry.vocabularies.person_team_participations_vocabulary_factory"
228- provides="zope.schema.interfaces.IVocabularyFactory"
229- >
230- <allow interface="zope.schema.interfaces.IVocabularyFactory"/>
231- </securedutility>
232-
233-
234- <securedutility
235 name="Product"
236 component="lp.registry.vocabularies.ProductVocabulary"
237 provides="zope.schema.interfaces.IVocabularyFactory"

Subscribers

People subscribed via source and target branches

to status/vote changes: