Merge lp:~rvb/launchpad/branches-timeout-bug-827935 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: 14225
Proposed branch: lp:~rvb/launchpad/branches-timeout-bug-827935
Merge into: lp:launchpad
Diff against target: 420 lines (+227/-15)
6 files modified
lib/lp/code/browser/branchlisting.py (+83/-4)
lib/lp/code/browser/tests/test_branchlisting.py (+100/-3)
lib/lp/code/interfaces/branchcollection.py (+4/-1)
lib/lp/code/model/branchcollection.py (+10/-6)
lib/lp/code/templates/person-codesummary.pt (+26/-1)
lib/lp/services/features/flags.py (+4/-0)
To merge this branch: bzr merge lp:~rvb/launchpad/branches-timeout-bug-827935
Reviewer Review Type Date Requested Status
Graham Binns (community) code Approve
Review via email: mp+80875@code.launchpad.net

Commit message

[r=gmb][bug=827935] Add a new simplified menu branch without the counts.

Description of the change

This branch adds a new "simplified" branch menu. The new menu does not include the number of {owned,registered,subscribed} branches which is the main reason why the page https://code.launchpad.net/~ubuntu-branches times out. The display of the new menu is protected by a feature flag so that we will be able to see (with real data) if the performance boost is worth the simplification of the design.

= Details =

The performance for this page was analysed by lifeless, jtv and myself (https://lp-oops.canonical.com/oops.py/?oopsid=OOPS-2124AO73). Obviously, only the teams/persons with a very large number of owned branches is a problem (~ubuntu-branches has ~300k branches). As usual, most of the time is SQL time (~10s out of a total of ~11s for the whole page rendering).

After trying to see if the individual queries could be improved lifeless suggested this —rather dramatic— approach because it was clear that the performance problems were coming from the count queries used to display the number of {owned,registered,subscribed} branches: one of the count queries is taking ~3s and the others ~1s.

= UI =

The new UI will look like this: http://people.canonical.com/~rvb/new_menu_branches.png where the old menu looks like this http://people.canonical.com/~rvb/prev_menu_branches.png.

On the bright side, this will unify the branch menu looks with the bug menu looks (this is how the bug menu looks atm http://people.canonical.com/~rvb/current_bug_menu.png).

= Testing =

Right now, the display of this new menu is protected by a feature flag: code.simplified_branches_menu.enabled. My goal is to:
a) make sure that the performance boost is worth the change
b) ask the list to see if everyone is okay with the change

Also, I've recoded the testing done in lib/lp/code/stories/branches/xx-person-branches.txt so if we decide that this can land for real (i.e. without the FF), this doctest can be removed.

To post a comment you must log in.
Revision history for this message
Graham Binns (gmb) wrote :

Hi Raphaël,

Nice branch! I've got a few comments but they're all cosmetic really. Otherwise, r=me.

[1]

62 +
63 + if self.simplified_branch_menu:
64 + return (
65 + self.registered_branch_not_empty or
66 + self.owned_branch_not_empty or
67 + self.subscribed_branch_not_empty or
68 + self.active_review_not_empty
69 + )
70 + else:
71 + return (self.owned_branch_count or
72 + self.registered_branch_count or
73 + self.subscribed_branch_count or
74 + self.active_review_count)

Slightly incosistent formatting here: The form you've used from
64-69 (newline after the opening parenthesis) is the correct one, though
the closing paren should go on the last line as you've done on line 74.

[2]

89 + @cachedproperty
90 + def owned_branch_not_empty(self):
91 + """False if the number of branches owned by self.person is zero."""
92 + return not self._getCountCollection().ownedBy(self.person).is_empty()

I get the feeling this should be named "owned_branches_not_empty" since
you're talking about a collection but the property name suggests you're
talking about a single branch.

[3]

94 + @cachedproperty
95 + def subscribed_branch_not_empty(self):

... And by the same token this should be subscribed_branches_not_empty.

[4]

103 + @cachedproperty
104 + def active_review_not_empty(self):

active_reviews_not_empty

[5]

224 + registered_branches_matcher = soupmatchers.HTMLContains(
225 + soupmatchers.Tag(
226 + 'Registered link', 'a', text='Registered branches',
227 + attrs={'href': 'http://launchpad.dev/~barney'
228 + '/+registeredbranches'}))
229 +

By convention, if you're going to declare a class attribute, you should
do it at the top of the class, since otherwise the reader (i.e. your
humble bearded reviewer) gets a bit confused :).

review: Approve (code)
Revision history for this message
Raphaël Badin (rvb) wrote :

> [1]
>
[...]
> Slightly incosistent formatting here: The form you've used from
> 64-69 (newline after the opening parenthesis) is the correct one, though
> the closing paren should go on the last line as you've done on line 74.

Right, I've only written the first part, but I should have harmonized it with the copied code indeed.
Fixed.

> [2]
> [3]
> [4]
> [5]

Right. Done.

> 224 + registered_branches_matcher = soupmatchers.HTMLContains(
> 225 + soupmatchers.Tag(
> 226 + 'Registered link', 'a', text='Registered branches',
> 227 + attrs={'href': 'http://launchpad.dev/~barney'
> 228 + '/+registeredbranches'}))
> 229 +
>
> By convention, if you're going to declare a class attribute, you should
> do it at the top of the class, since otherwise the reader (i.e. your
> humble bearded reviewer) gets a bit confused :).

Okay. I thought it might be a good idea to declare it near where it's used but I suppose it's much better to have a sane default for these.

Thanks for the review Graham!

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/code/browser/branchlisting.py'
2--- lib/lp/code/browser/branchlisting.py 2011-10-14 02:12:06 +0000
3+++ lib/lp/code/browser/branchlisting.py 2011-11-01 14:15:30 +0000
4@@ -31,6 +31,7 @@
5 ]
6
7 from operator import attrgetter
8+import urlparse
9
10 from lazr.delegates import delegates
11 from lazr.enum import (
12@@ -41,7 +42,6 @@
13 Asc,
14 Desc,
15 )
16-import urlparse
17 from z3c.ptcompat import ViewPageTemplateFile
18 from zope.component import getUtility
19 from zope.formlib import form
20@@ -135,6 +135,7 @@
21 from lp.registry.interfaces.sourcepackage import ISourcePackageFactory
22 from lp.registry.model.sourcepackage import SourcePackage
23 from lp.services.browser_helpers import get_plural_text
24+from lp.services.features import getFeatureFlag
25 from lp.services.propertycache import cachedproperty
26
27
28@@ -850,7 +851,9 @@
29 usedfor = IPerson
30 facet = 'branches'
31 links = ['registered', 'owned', 'subscribed', 'addbranch',
32- 'active_reviews', 'mergequeues']
33+ 'active_reviews', 'mergequeues',
34+ 'simplified_subscribed', 'simplified_registered',
35+ 'simplified_owned', 'simplified_active_reviews']
36 extra_attributes = [
37 'active_review_count',
38 'owned_branch_count',
39@@ -858,6 +861,7 @@
40 'show_summary',
41 'subscribed_branch_count',
42 'mergequeue_count',
43+ 'simplified_branches_menu',
44 ]
45
46 def _getCountCollection(self):
47@@ -881,13 +885,88 @@
48 """
49 return self.context
50
51+ @cachedproperty
52+ def has_branches(self):
53+ """Should the template show the summary view with the links."""
54+
55 @property
56 def show_summary(self):
57 """Should the template show the summary view with the links."""
58- return (self.owned_branch_count or
59+
60+ if self.simplified_branches_menu:
61+ return (
62+ self.registered_branches_not_empty or
63+ self.owned_branches_not_empty or
64+ self.subscribed_branches_not_empty or
65+ self.active_reviews_not_empty
66+ )
67+ else:
68+ return (
69+ self.owned_branch_count or
70 self.registered_branch_count or
71 self.subscribed_branch_count or
72- self.active_review_count)
73+ self.active_review_count
74+ )
75+
76+ @cachedproperty
77+ def simplified_branches_menu(self):
78+ return getFeatureFlag('code.simplified_branches_menu.enabled')
79+
80+ @cachedproperty
81+ def registered_branches_not_empty(self):
82+ """False if the number of branches registered by self.person
83+ is zero.
84+ """
85+ return (
86+ not self._getCountCollection().registeredBy(
87+ self.person).is_empty())
88+
89+ @cachedproperty
90+ def owned_branches_not_empty(self):
91+ """False if the number of branches owned by self.person is zero."""
92+ return not self._getCountCollection().ownedBy(self.person).is_empty()
93+
94+ @cachedproperty
95+ def subscribed_branches_not_empty(self):
96+ """False if the number of branches subscribed to by self.person
97+ is zero.
98+ """
99+ return (
100+ not self._getCountCollection().subscribedBy(
101+ self.person).is_empty())
102+
103+ @cachedproperty
104+ def active_reviews_not_empty(self):
105+ """Return the number of active reviews for self.person's branches."""
106+ active_reviews = PersonActiveReviewsView(self.context, self.request)
107+ return not active_reviews.getProposals().is_empty()
108+
109+ def simplified_owned(self):
110+ return Link(
111+ canonical_url(self.context, rootsite='code'),
112+ 'Owned branches',
113+ enabled=self.owned_branches_not_empty)
114+
115+ def simplified_registered(self):
116+ person_is_individual = (not self.person.is_team)
117+ return Link(
118+ '+registeredbranches',
119+ 'Registered branches',
120+ enabled=(
121+ person_is_individual and
122+ self.registered_branches_not_empty))
123+
124+ def simplified_subscribed(self):
125+ return Link(
126+ '+subscribedbranches',
127+ 'Subscribed branches',
128+ enabled=self.subscribed_branches_not_empty)
129+
130+ def simplified_active_reviews(self):
131+ return Link(
132+ '+activereviews',
133+ 'Active reviews',
134+ enabled=self.active_reviews_not_empty)
135
136 @cachedproperty
137 def registered_branch_count(self):
138
139=== modified file 'lib/lp/code/browser/tests/test_branchlisting.py'
140--- lib/lp/code/browser/tests/test_branchlisting.py 2011-10-14 02:12:06 +0000
141+++ lib/lp/code/browser/tests/test_branchlisting.py 2011-11-01 14:15:30 +0000
142@@ -11,18 +11,22 @@
143 import re
144
145 from lazr.uri import URI
146+import soupmatchers
147 from storm.expr import (
148 Asc,
149 Desc,
150 )
151+from testtools.matchers import Not
152 from zope.component import getUtility
153
154 from canonical.launchpad.testing.pages import (
155 extract_text,
156+ find_main_content,
157 find_tag_by_id,
158- find_main_content)
159+ )
160 from canonical.launchpad.webapp import canonical_url
161 from canonical.launchpad.webapp.servers import LaunchpadTestRequest
162+from canonical.testing import LaunchpadFunctionalLayer
163 from canonical.testing.layers import DatabaseFunctionalLayer
164 from lp.code.browser.branchlisting import (
165 BranchListingSort,
166@@ -31,7 +35,10 @@
167 PersonProductSubscribedBranchesView,
168 SourcePackageBranchesView,
169 )
170-from lp.code.enums import BranchVisibilityRule
171+from lp.code.enums import (
172+ BranchMergeProposalStatus,
173+ BranchVisibilityRule,
174+ )
175 from lp.code.model.branch import Branch
176 from lp.code.model.seriessourcepackagebranch import (
177 SeriesSourcePackageBranchSet,
178@@ -59,8 +66,8 @@
179 COMMERCIAL_ADMIN_EMAIL,
180 )
181 from lp.testing.views import (
182+ create_initialized_view,
183 create_view,
184- create_initialized_view,
185 )
186
187
188@@ -257,6 +264,96 @@
189 self._test_batch_template(self.barney)
190
191
192+SIMPLIFIED_BRANCHES_MENU_FLAG = {
193+ 'code.simplified_branches_menu.enabled': 'on'}
194+
195+
196+class TestSimplifiedPersonOwnedBranchesView(TestCaseWithFactory):
197+
198+ layer = LaunchpadFunctionalLayer
199+
200+ registered_branches_matcher = soupmatchers.HTMLContains(
201+ soupmatchers.Tag(
202+ 'Registered link', 'a', text='Registered branches',
203+ attrs={'href': 'http://launchpad.dev/~barney'
204+ '/+registeredbranches'}))
205+
206+ def setUp(self):
207+ TestCaseWithFactory.setUp(self)
208+ self.user = self.factory.makePerson()
209+ self.person = self.factory.makePerson(name='barney')
210+ self.team = self.factory.makeTeam(owner=self.person)
211+ self.product = self.factory.makeProduct(name='bambam')
212+
213+ def get_branch_list_page(self, page_name='+branches', user=None):
214+ if user is None:
215+ user = self.person
216+ with FeatureFixture(SIMPLIFIED_BRANCHES_MENU_FLAG):
217+ with person_logged_in(self.user):
218+ return create_initialized_view(
219+ user, page_name, rootsite='code',
220+ principal=self.user)()
221+
222+ def test_branch_list_h1(self):
223+ page = self.get_branch_list_page()
224+ h1_matcher = soupmatchers.HTMLContains(
225+ soupmatchers.Tag(
226+ 'Title', 'h1', text='Bazaar branches owned by Barney'))
227+ self.assertThat(page, h1_matcher)
228+
229+ def test_branch_list_empty(self):
230+ page = self.get_branch_list_page()
231+ empty_message_matcher = soupmatchers.HTMLContains(
232+ soupmatchers.Tag(
233+ 'Empty message', 'p',
234+ text='There are no branches related to Barney '
235+ 'in Launchpad today.'))
236+ self.assertThat(page, empty_message_matcher)
237+ self.assertThat(page, Not(self.registered_branches_matcher))
238+
239+ def test_branch_list_registered_link(self):
240+ self.factory.makeAnyBranch(owner=self.person)
241+ page = self.get_branch_list_page()
242+ self.assertThat(page, self.registered_branches_matcher)
243+
244+ def test_branch_list_owned_link(self):
245+ owned_branches_matcher = soupmatchers.HTMLContains(
246+ soupmatchers.Tag(
247+ 'Owned link', 'a', text='Owned branches',
248+ attrs={'href': 'http://code.launchpad.dev/~barney'}))
249+ self.factory.makeAnyBranch(owner=self.person)
250+ page = self.get_branch_list_page('+subscribedbranches')
251+ self.assertThat(page, owned_branches_matcher)
252+
253+ def test_branch_list_subscribed_link(self):
254+ subscribed_branches_matcher = soupmatchers.HTMLContains(
255+ soupmatchers.Tag(
256+ 'Subscribed link', 'a', text='Subscribed branches',
257+ attrs={'href': 'http://launchpad.dev/~barney'
258+ '/+subscribedbranches'}))
259+ self.factory.makeAnyBranch(owner=self.person)
260+ page = self.get_branch_list_page()
261+ self.assertThat(page, subscribed_branches_matcher)
262+
263+ def test_branch_list_activereviews_link(self):
264+ active_review_matcher = soupmatchers.HTMLContains(
265+ soupmatchers.Tag(
266+ 'Active reviews link', 'a', text='Active reviews',
267+ attrs={'href': 'http://launchpad.dev/~barney'
268+ '/+activereviews'}))
269+ branch = self.factory.makeAnyBranch(owner=self.person)
270+ self.factory.makeBranchMergeProposal(
271+ target_branch=branch, registrant=self.person,
272+ set_state=BranchMergeProposalStatus.NEEDS_REVIEW)
273+ page = self.get_branch_list_page()
274+ self.assertThat(page, active_review_matcher)
275+
276+ def test_branch_list_no_registered_link_team(self):
277+ self.factory.makeAnyBranch(owner=self.person)
278+ page = self.get_branch_list_page(user=self.team)
279+ self.assertThat(page, Not(self.registered_branches_matcher))
280+
281+
282 class TestSourcePackageBranchesView(TestCaseWithFactory):
283
284 layer = DatabaseFunctionalLayer
285
286=== modified file 'lib/lp/code/interfaces/branchcollection.py'
287--- lib/lp/code/interfaces/branchcollection.py 2011-08-25 20:37:02 +0000
288+++ lib/lp/code/interfaces/branchcollection.py 2011-11-01 14:15:30 +0000
289@@ -1,4 +1,4 @@
290-# Copyright 2009 Canonical Ltd. This software is licensed under the
291+# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
292 # GNU Affero General Public License version 3 (see the file LICENSE).
293
294 # pylint: disable-msg=E0211, E0213
295@@ -50,6 +50,9 @@
296 def count():
297 """The number of branches in this collection."""
298
299+ def is_empty():
300+ """Is this collection empty?"""
301+
302 def ownerCounts():
303 """Return the number of different branch owners.
304
305
306=== modified file 'lib/lp/code/model/branchcollection.py'
307--- lib/lp/code/model/branchcollection.py 2011-10-09 23:35:43 +0000
308+++ lib/lp/code/model/branchcollection.py 2011-11-01 14:15:30 +0000
309@@ -1,4 +1,4 @@
310-# Copyright 2009 Canonical Ltd. This software is licensed under the
311+# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
312 # GNU Affero General Public License version 3 (see the file LICENSE).
313
314 """Implementations of `IBranchCollection`."""
315@@ -33,31 +33,31 @@
316 DecoratedResultSet,
317 )
318 from canonical.launchpad.interfaces.lpstorm import IStore
319+from canonical.launchpad.searchbuilder import any
320 from canonical.launchpad.webapp.interfaces import (
321 DEFAULT_FLAVOR,
322 IStoreSelector,
323 MAIN_STORE,
324 )
325-from canonical.launchpad.searchbuilder import any
326 from canonical.launchpad.webapp.vocabulary import CountableIterator
327 from lp.bugs.interfaces.bugtask import (
328+ BugTaskSearchParams,
329 IBugTaskSet,
330- BugTaskSearchParams,
331 )
332 from lp.bugs.interfaces.bugtaskfilter import filter_bugtasks_by_context
333 from lp.bugs.model.bugbranch import BugBranch
334 from lp.bugs.model.bugtask import BugTask
335+from lp.code.enums import BranchMergeProposalStatus
336 from lp.code.interfaces.branch import user_has_special_branch_access
337 from lp.code.interfaces.branchcollection import (
338 IBranchCollection,
339 InvalidFilter,
340 )
341+from lp.code.interfaces.branchlookup import IBranchLookup
342+from lp.code.interfaces.codehosting import LAUNCHPAD_SERVICES
343 from lp.code.interfaces.seriessourcepackagebranch import (
344 IFindOfficialBranchLinks,
345 )
346-from lp.code.enums import BranchMergeProposalStatus
347-from lp.code.interfaces.branchlookup import IBranchLookup
348-from lp.code.interfaces.codehosting import LAUNCHPAD_SERVICES
349 from lp.code.model.branch import Branch
350 from lp.code.model.branchmergeproposal import BranchMergeProposal
351 from lp.code.model.branchsubscription import BranchSubscription
352@@ -132,6 +132,10 @@
353 """See `IBranchCollection`."""
354 return self.getBranches(eager_load=False).count()
355
356+ def is_empty(self):
357+ """See `IBranchCollection`."""
358+ return self.getBranches(eager_load=False).is_empty()
359+
360 def ownerCounts(self):
361 """See `IBranchCollection`."""
362 is_team = Person.teamowner != None
363
364=== modified file 'lib/lp/code/templates/person-codesummary.pt'
365--- lib/lp/code/templates/person-codesummary.pt 2010-11-18 12:05:34 +0000
366+++ lib/lp/code/templates/person-codesummary.pt 2011-11-01 14:15:30 +0000
367@@ -8,7 +8,7 @@
368 features request/features"
369 tal:condition="menu/show_summary">
370
371- <table>
372+ <table tal:condition="not: menu/simplified_branches_menu">
373 <tr class="code-links">
374 <td class="code-count" tal:content="menu/owned_branch_count">100</td>
375 <td tal:content="structure menu/owned/render"
376@@ -34,4 +34,29 @@
377 />
378 </tr>
379 </table>
380+
381+ <table tal:condition="menu/simplified_branches_menu">
382+ <tr class="code-links"
383+ tal:condition="menu/simplified_owned/enabled">
384+ <td tal:content="structure menu/simplified_owned/render" />
385+ </tr>
386+ <tr class="code-links"
387+ tal:condition="menu/simplified_registered/enabled">
388+ <td tal:content="structure menu/simplified_registered/render" />
389+ </tr>
390+ <tr class="code-links"
391+ tal:condition="menu/simplified_subscribed/enabled">
392+ <td tal:content="structure menu/simplified_subscribed/render" />
393+ </tr>
394+ <tr class="code-links"
395+ tal:condition="menu/simplified_active_reviews/enabled">
396+ <td tal:content="structure menu/simplified_active_reviews/render" />
397+ </tr>
398+ <tr tal:condition="features/code.branchmergequeue" id="mergequeue-counts">
399+ <td class="code-count" tal:content="menu/mergequeue_count">5</td>
400+ <td tal:condition="menu"
401+ tal:content="structure menu/mergequeues/render"
402+ />
403+ </tr>
404+ </table>
405 </div>
406
407=== modified file 'lib/lp/services/features/flags.py'
408--- lib/lp/services/features/flags.py 2011-10-27 01:11:39 +0000
409+++ lib/lp/services/features/flags.py 2011-11-01 14:15:30 +0000
410@@ -64,6 +64,10 @@
411 'boolean',
412 'Shows incremental diffs on merge proposals.',
413 ''),
414+ ('code.simplified_branches_menu.enabled',
415+ 'boolean',
416+ ('Display a simplified version of the branch menu (omit the counts).'),
417+ ''),
418 ('hard_timeout',
419 'float',
420 'Sets the hard request timeout in milliseconds.',