Merge lp:~lifeless/launchpad/bug-618367 into lp:launchpad

Proposed by Robert Collins
Status: Merged
Approved by: Paul Hummer
Approved revision: no longer in the source branch.
Merged at revision: 11558
Proposed branch: lp:~lifeless/launchpad/bug-618367
Merge into: lp:launchpad
Diff against target: 227 lines (+89/-54)
5 files modified
lib/lp/blueprints/browser/specificationtarget.py (+6/-0)
lib/lp/blueprints/browser/tests/test_specificationtarget.py (+46/-12)
lib/lp/blueprints/model/specification.py (+28/-39)
lib/lp/blueprints/templates/specificationtarget-assignments.pt (+4/-2)
lib/lp/services/database/prejoin.py (+5/-1)
To merge this branch: bzr merge lp:~lifeless/launchpad/bug-618367
Reviewer Review Type Date Requested Status
Paul Hummer (community) Approve
Review via email: mp+35489@code.launchpad.net

Commit message

Batch +assignments and tune the specs sql to cater for storm handling of very wide queries.

Description of the change

Second stab at addressing /ubuntu/+assignments.

This branch does two things:
 - it uses a slightly different preloading strategy which avoids some sometimes slow stuff in storm
 - it batches with a default of 500 (we have 3000 specs on that page)

This is one of the slowest pages we have now (\o/).

The only downside I can see here is that the sorted columns on the table are not brilliantly integrated into the batching, but I don't know of any workaround for that.

To post a comment you must log in.
Revision history for this message
Paul Hummer (rockstar) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/blueprints/browser/specificationtarget.py'
2--- lib/lp/blueprints/browser/specificationtarget.py 2010-08-24 10:45:57 +0000
3+++ lib/lp/blueprints/browser/specificationtarget.py 2010-09-15 00:12:50 +0000
4@@ -326,6 +326,12 @@
5 return self.context.specifications(filter=filter)
6
7 @cachedproperty
8+ def specs_batched(self):
9+ navigator = BatchNavigator(self.specs, self.request, size=500)
10+ navigator.setHeadings('specification', 'specifications')
11+ return navigator
12+
13+ @cachedproperty
14 def spec_count(self):
15 return self.specs.count()
16
17
18=== modified file 'lib/lp/blueprints/browser/tests/test_specificationtarget.py'
19--- lib/lp/blueprints/browser/tests/test_specificationtarget.py 2010-08-20 20:31:18 +0000
20+++ lib/lp/blueprints/browser/tests/test_specificationtarget.py 2010-09-15 00:12:50 +0000
21@@ -3,8 +3,8 @@
22
23 __metaclass__ = type
24
25-import unittest
26-
27+from canonical.launchpad.webapp.batching import BatchNavigator
28+from canonical.launchpad.testing.pages import find_tag_by_id
29 from canonical.testing.layers import DatabaseFunctionalLayer
30 from lp.blueprints.interfaces.specificationtarget import (
31 IHasSpecifications,
32@@ -15,7 +15,10 @@
33 login_person,
34 TestCaseWithFactory,
35 )
36-from lp.testing.views import create_view
37+from lp.testing.views import (
38+ create_view,
39+ create_initialized_view,
40+ )
41
42
43 class TestRegisterABlueprintButtonView(TestCaseWithFactory):
44@@ -86,12 +89,43 @@
45 self.assertFalse(
46 '<div id="involvement" class="portlet involvement">' in view())
47
48-
49-def test_suite():
50- suite = unittest.TestSuite()
51- suite.addTest(unittest.TestLoader().loadTestsFromName(__name__))
52- return suite
53-
54-
55-if __name__ == '__main__':
56- unittest.TextTestRunner().run(test_suite())
57+ def test_specs_batch(self):
58+ # Some pages turn up in very large contexts and patch. E.g.
59+ # Distro:+assignments which uses SpecificationAssignmentsView, a
60+ # subclass.
61+ person = self.factory.makePerson()
62+ view = create_initialized_view(person, name='+assignments')
63+ self.assertIsInstance(view.specs_batched, BatchNavigator)
64+
65+ def test_batch_headings(self):
66+ person = self.factory.makePerson()
67+ view = create_initialized_view(person, name='+assignments')
68+ navigator = view.specs_batched
69+ self.assertEqual('specification', navigator._singular_heading)
70+ self.assertEqual('specifications', navigator._plural_heading)
71+
72+ def test_batch_size(self):
73+ # Because +assignments is meant to provide an overview, we default to
74+ # 500 as the default batch size.
75+ person = self.factory.makePerson()
76+ view = create_initialized_view(person, name='+assignments')
77+ navigator = view.specs_batched
78+ self.assertEqual(500, view.specs_batched.default_size)
79+
80+
81+class TestAssignments(TestCaseWithFactory):
82+
83+ layer = DatabaseFunctionalLayer
84+
85+ def test_assignments_are_batched(self):
86+ product = self.factory.makeProduct()
87+ spec1 = self.factory.makeSpecification(product=product)
88+ spec2 = self.factory.makeSpecification(product=product)
89+ view = create_initialized_view(product, name='+assignments',
90+ query_string="batch=1")
91+ content = view.render()
92+ self.assertEqual('next',
93+ find_tag_by_id(content, 'upper-batch-nav-batchnav-next')['class'])
94+ self.assertEqual('next',
95+ find_tag_by_id(content, 'lower-batch-nav-batchnav-next')['class'])
96+
97
98=== modified file 'lib/lp/blueprints/model/specification.py'
99--- lib/lp/blueprints/model/specification.py 2010-08-31 19:38:30 +0000
100+++ lib/lp/blueprints/model/specification.py 2010-09-15 00:12:50 +0000
101@@ -713,50 +713,39 @@
102 """
103 # Circular import.
104 from lp.registry.model.person import Person
105- assignee = ClassAlias(Person, "assignee")
106- approver = ClassAlias(Person, "approver")
107- drafter = ClassAlias(Person, "drafter")
108- origin = [Specification,
109- LeftJoin(assignee, Specification.assignee==assignee.id),
110- LeftJoin(approver, Specification.approver==approver.id),
111- LeftJoin(drafter, Specification.drafter==drafter.id),
112- ]
113- columns = [Specification, assignee, approver, drafter]
114- for alias in (assignee, approver, drafter):
115- validity_info = Person._validity_queries(person_table=alias)
116+ def cache_people(rows):
117+ # Find the people we need:
118+ person_ids = set()
119+ for spec in rows:
120+ person_ids.add(spec.assigneeID)
121+ person_ids.add(spec.approverID)
122+ person_ids.add(spec.drafterID)
123+ person_ids.discard(None)
124+ if not person_ids:
125+ return
126+ # Query those people
127+ origin = [Person]
128+ columns = [Person]
129+ validity_info = Person._validity_queries()
130 origin.extend(validity_info["joins"])
131 columns.extend(validity_info["tables"])
132- # We only need one decorators list: all the decorators will be
133- # bound the same, and having a single list lets us more easily call
134- # the right one.
135 decorators = validity_info["decorators"]
136- columns = tuple(columns)
137- results = Store.of(self).using(*origin).find(
138- columns,
139+ personset = Store.of(self).using(*origin).find(
140+ tuple(columns),
141+ Person.id.is_in(person_ids),
142+ )
143+ for row in personset:
144+ person = row[0]
145+ index = 1
146+ for decorator in decorators:
147+ column = row[index]
148+ index += 1
149+ decorator(person, column)
150+ results = Store.of(self).find(
151+ Specification,
152 SQL(query),
153 )
154- def cache_person(person, row, start_index):
155- """apply caching decorators to person."""
156- index = start_index
157- for decorator in decorators:
158- column = row[index]
159- index += 1
160- decorator(person, column)
161- return index
162- def cache_validity(row):
163- # Assignee
164- person = row[1]
165- # After drafter
166- index = 4
167- index = cache_person(person, row, index)
168- # approver
169- person = row[2]
170- index = cache_person(person, row, index)
171- # drafter
172- person = row[3]
173- index = cache_person(person, row, index)
174- return row[0]
175- return DecoratedResultSet(results, cache_validity)
176+ return DecoratedResultSet(results, pre_iter_hook=cache_people)
177
178 @property
179 def valid_specifications(self):
180
181=== modified file 'lib/lp/blueprints/templates/specificationtarget-assignments.pt'
182--- lib/lp/blueprints/templates/specificationtarget-assignments.pt 2009-09-22 15:02:41 +0000
183+++ lib/lp/blueprints/templates/specificationtarget-assignments.pt 2010-09-15 00:12:50 +0000
184@@ -31,6 +31,7 @@
185 sign off on the specification.
186 </p>
187
188+ <tal:navigation replace="structure view/specs_batched/@@+navigation-links-upper" />
189 <table class="listing sortable" id="work">
190 <thead>
191 <tr>
192@@ -43,8 +44,8 @@
193 <th>Approver</th>
194 </tr>
195 </thead>
196- <tbody class="lesser">
197- <tr tal:repeat="spec view/specs">
198+ <tbody class="lesser" tal:define="specs view/specs_batched/currentBatch">
199+ <tr tal:repeat="spec specs">
200 <td>
201 <span class="sortkey" tal:content="spec/priority/sortkey" />
202 <span tal:content="spec/priority/title"
203@@ -91,6 +92,7 @@
204 </tr>
205 </tbody>
206 </table>
207+ <tal:navigation replace="structure view/specs_batched/@@+navigation-links-lower" />
208 </div>
209
210 </div>
211
212=== modified file 'lib/lp/services/database/prejoin.py'
213--- lib/lp/services/database/prejoin.py 2010-08-20 20:31:18 +0000
214+++ lib/lp/services/database/prejoin.py 2010-09-15 00:12:50 +0000
215@@ -1,7 +1,11 @@
216 # Copyright 2009 Canonical Ltd. This software is licensed under the
217 # GNU Affero General Public License version 3 (see the file LICENSE).
218
219-"""Prejoin for Storm."""
220+"""Prejoin for Storm.
221+
222+XXX bug=632150 This is duplicated with DecoratedResultSet. please use that.
223+RBC 20100907.
224+"""
225
226 __metaclass__ = type
227 __all__ = ['prejoin']