Merge lp:~wallyworld/launchpad/projectgroup-timeout-1016156 into lp:launchpad

Proposed by Ian Booth
Status: Merged
Approved by: Curtis Hovey
Approved revision: no longer in the source branch.
Merged at revision: 16144
Proposed branch: lp:~wallyworld/launchpad/projectgroup-timeout-1016156
Merge into: lp:launchpad
Diff against target: 323 lines (+63/-35)
13 files modified
lib/lp/answers/browser/faqcollection.py (+1/-1)
lib/lp/answers/browser/questiontarget.py (+1/-1)
lib/lp/blueprints/doc/sprint.txt (+2/-0)
lib/lp/blueprints/model/sprint.py (+14/-7)
lib/lp/registry/browser/announcement.py (+9/-5)
lib/lp/registry/browser/project.py (+1/-1)
lib/lp/registry/browser/tests/pillar-views.txt (+2/-0)
lib/lp/registry/browser/tests/projectgroup-views.txt (+6/-3)
lib/lp/registry/doc/projectgroup.txt (+4/-2)
lib/lp/registry/interfaces/projectgroup.py (+2/-7)
lib/lp/registry/model/projectgroup.py (+19/-6)
lib/lp/registry/templates/hasannouncements-portlet-latest.pt (+1/-1)
lib/lp/translations/browser/project.py (+1/-1)
To merge this branch: bzr merge lp:~wallyworld/launchpad/projectgroup-timeout-1016156
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Review via email: mp+129337@code.launchpad.net

Commit message

Rework some entity properties to reduce query counts on the projectgroup index page and others.

Description of the change

== Implementation ==

The entity domain objects had (non-cached) properties which return result sets. So every time the property was referenced via a TAL expression, the query was re-run. I identified some core properties on various classes which are implicated on the project group index page, and also other pillar pages. These were converted to cached properties and listified. The result set creation was moved off to a separate getter if required.

As an example, for one project group index page, query count drops in half from the original number of about 70.

== Tests ==

A generic, internal optimisation, so rely on existing tests.

== Lint ==

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/answers/browser/faqcollection.py
  lib/lp/answers/browser/questiontarget.py
  lib/lp/blueprints/model/sprint.py
  lib/lp/registry/browser/announcement.py
  lib/lp/registry/browser/project.py
  lib/lp/registry/doc/projectgroup.txt
  lib/lp/registry/interfaces/projectgroup.py
  lib/lp/registry/model/projectgroup.py
  lib/lp/registry/templates/hasannouncements-portlet-latest.pt

To post a comment you must log in.
Revision history for this message
Curtis Hovey (sinzui) wrote :

The two changes to /lib/answers looks wrong. I see that we are converting the result set to a list, thus instantiating about 100 FAQs, but we only return 5. The change to questiontarget will instantiate several 1000 questions in the U1 and launchpad project groups. Surely this is slower than how the code it currently written. I think the current codes is issue a single query with a LIMIT and instantiating just the 5 FAQs or questions wanted. I think we want them converted to a list *after* the slice.

review: Needs Information (code)
Revision history for this message
Curtis Hovey (sinzui) wrote :

Oh sorry for the conflicting review status. I don't want this branch to land until you and I agree about what is happening with the slicing in these two cached properties.

Revision history for this message
Ian Booth (wallyworld) wrote :

> The two changes to /lib/answers looks wrong. I see that we are converting the
> result set to a list, thus instantiating about 100 FAQs, but we only return 5.
> The change to questiontarget will instantiate several 1000 questions in the U1
> and launchpad project groups. Surely this is slower than how the code it
> currently written. I think the current codes is issue a single query with a
> LIMIT and instantiating just the 5 FAQs or questions wanted. I think we want
> them converted to a list *after* the slice.

Ah, good pickup. My fat fingers mistyped the final bracket. The 2 cases should be fixed now.

The philosophy I'm using is that all cached properties should return a list (not a generator or result set etc which is re-run each time) since this conflicts with the expected semantics that there is no work in calling the cached property a second time. Hence the listification of the result set.

Revision history for this message
Curtis Hovey (sinzui) wrote :

Thank you.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/answers/browser/faqcollection.py'
2--- lib/lp/answers/browser/faqcollection.py 2012-01-01 02:58:52 +0000
3+++ lib/lp/answers/browser/faqcollection.py 2012-10-14 23:44:21 +0000
4@@ -127,7 +127,7 @@
5 quantity = 5
6 faqs = self.context.searchFAQs(
7 search_text=self.search_text, sort=FAQSort.NEWEST_FIRST)
8- return faqs[:quantity]
9+ return list(faqs[:quantity])
10
11 @safe_action
12 @action(_('Search'), name='search')
13
14=== modified file 'lib/lp/answers/browser/questiontarget.py'
15--- lib/lp/answers/browser/questiontarget.py 2012-07-05 00:47:31 +0000
16+++ lib/lp/answers/browser/questiontarget.py 2012-10-14 23:44:21 +0000
17@@ -167,7 +167,7 @@
18 is used by the +portlet-latestquestions view.
19 """
20 question_collection = IQuestionCollection(self.context)
21- return question_collection.searchQuestions()[:quantity]
22+ return list(question_collection.searchQuestions()[:quantity])
23
24
25 class QuestionCollectionOpenCountView:
26
27=== modified file 'lib/lp/blueprints/doc/sprint.txt'
28--- lib/lp/blueprints/doc/sprint.txt 2012-09-28 14:35:35 +0000
29+++ lib/lp/blueprints/doc/sprint.txt 2012-10-14 23:44:21 +0000
30@@ -91,7 +91,9 @@
31 Flush the updates to the database so we'll see them.
32
33 >>> from lp.services.database.sqlbase import flush_database_updates
34+ >>> from lp.services.propertycache import clear_property_cache
35 >>> flush_database_updates()
36+ >>> clear_property_cache(ubuntu)
37
38 See, there are no ubuntu sprints.
39
40
41=== modified file 'lib/lp/blueprints/model/sprint.py'
42--- lib/lp/blueprints/model/sprint.py 2012-09-27 18:50:38 +0000
43+++ lib/lp/blueprints/model/sprint.py 2012-10-14 23:44:21 +0000
44@@ -60,6 +60,7 @@
45 SQLBase,
46 )
47 from lp.services.database.stormexpr import fti_search
48+from lp.services.propertycache import cachedproperty
49
50
51 class Sprint(SQLBase, HasDriversMixin, HasSpecificationsMixin):
52@@ -361,22 +362,28 @@
53 quote(SprintSpecificationStatus.ACCEPTED))
54 return query, ['Specification', 'SprintSpecification']
55
56- @property
57+ def getSprints(self):
58+ query, tables = self._getBaseQueryAndClauseTablesForQueryingSprints()
59+ return Sprint.select(
60+ query, clauseTables=tables, orderBy='-time_starts', distinct=True)
61+
62+ @cachedproperty
63 def sprints(self):
64 """See IHasSprints."""
65- query, tables = self._getBaseQueryAndClauseTablesForQueryingSprints()
66- return Sprint.select(
67- query, clauseTables=tables, orderBy='-time_starts', distinct=True)
68+ return list(self.getSprints())
69
70- @property
71- def coming_sprints(self):
72- """See IHasSprints."""
73+ def getComingSprings(self):
74 query, tables = self._getBaseQueryAndClauseTablesForQueryingSprints()
75 query += " AND Sprint.time_ends > 'NOW'"
76 return Sprint.select(
77 query, clauseTables=tables, orderBy='time_starts',
78 distinct=True, limit=5)
79
80+ @cachedproperty
81+ def coming_sprints(self):
82+ """See IHasSprints."""
83+ return list(self.getComingSprings())
84+
85 @property
86 def past_sprints(self):
87 """See IHasSprints."""
88
89=== modified file 'lib/lp/registry/browser/announcement.py'
90--- lib/lp/registry/browser/announcement.py 2012-01-01 02:58:52 +0000
91+++ lib/lp/registry/browser/announcement.py 2012-10-14 23:44:21 +0000
92@@ -298,18 +298,22 @@
93 @cachedproperty
94 def announcements(self):
95 published_only = not check_permission('launchpad.Edit', self.context)
96- return self.context.getAnnouncements(
97- limit=None, published_only=published_only)
98+ return list(self.context.getAnnouncements(
99+ limit=None, published_only=published_only))
100
101 @cachedproperty
102 def latest_announcements(self):
103 published_only = not check_permission('launchpad.Edit', self.context)
104- return self.context.getAnnouncements(
105- limit=5, published_only=published_only)
106+ return list(self.context.getAnnouncements(
107+ limit=5, published_only=published_only))
108+
109+ @cachedproperty
110+ def has_announcements(self):
111+ return len(self.latest_announcements) > 0
112
113 @cachedproperty
114 def show_announcements(self):
115- return (self.latest_announcements.count() > 0
116+ return (len(self.latest_announcements) > 0
117 or check_permission('launchpad.Edit', self.context))
118
119 @cachedproperty
120
121=== modified file 'lib/lp/registry/browser/project.py'
122--- lib/lp/registry/browser/project.py 2012-10-08 01:55:25 +0000
123+++ lib/lp/registry/browser/project.py 2012-10-14 23:44:21 +0000
124@@ -390,7 +390,7 @@
125 The number of sub projects can break the preferred layout so the
126 template may want to plan for a long list.
127 """
128- return self.context.products.count() > 10
129+ return len(self.context.products) > 10
130
131 @property
132 def project_group_milestone_tag(self):
133
134=== modified file 'lib/lp/registry/browser/tests/pillar-views.txt'
135--- lib/lp/registry/browser/tests/pillar-views.txt 2012-06-16 13:41:36 +0000
136+++ lib/lp/registry/browser/tests/pillar-views.txt 2012-10-14 23:44:21 +0000
137@@ -216,6 +216,8 @@
138 ... productseries=series,
139 ... language_codes=['es'])
140 >>> product.translations_usage = ServiceUsage.LAUNCHPAD
141+ >>> from lp.services.propertycache import clear_property_cache
142+ >>> clear_property_cache(project_group)
143 >>> project_group.has_translatable()
144 True
145
146
147=== modified file 'lib/lp/registry/browser/tests/projectgroup-views.txt'
148--- lib/lp/registry/browser/tests/projectgroup-views.txt 2012-08-08 07:22:51 +0000
149+++ lib/lp/registry/browser/tests/projectgroup-views.txt 2012-10-14 23:44:21 +0000
150@@ -7,7 +7,7 @@
151
152 >>> projectgroup = factory.makeProject(name='mothership')
153 >>> view = create_view(projectgroup, name='+index')
154- >>> projectgroup.products.count()
155+ >>> len(projectgroup.products)
156 0
157
158 >>> view.has_many_projects
159@@ -23,7 +23,9 @@
160 >>> owner = projectgroup.owner
161 >>> ignored = login_person(owner)
162 >>> add_daughter(projectgroup, 'a')
163- >>> projectgroup.products.count()
164+ >>> from lp.services.propertycache import clear_property_cache
165+ >>> clear_property_cache(projectgroup)
166+ >>> len(projectgroup.products)
167 1
168
169 >>> view = create_view(projectgroup, name='+index')
170@@ -34,7 +36,8 @@
171 projects (10 projects are roughly 2 portlets deep.)
172
173 >>> add_daughter(projectgroup, 'bcdefghijk')
174- >>> projectgroup.products.count()
175+ >>> clear_property_cache(projectgroup)
176+ >>> len(projectgroup.products)
177 11
178
179 >>> view = create_view(projectgroup, name='+index')
180
181=== modified file 'lib/lp/registry/doc/projectgroup.txt'
182--- lib/lp/registry/doc/projectgroup.txt 2012-09-26 20:56:43 +0000
183+++ lib/lp/registry/doc/projectgroup.txt 2012-10-14 23:44:21 +0000
184@@ -131,6 +131,8 @@
185 >>> unlink_source_packages(netapplet)
186 >>> netapplet.active = False
187 >>> flush_database_updates()
188+ >>> from lp.services.propertycache import clear_property_cache
189+ >>> clear_property_cache(gnome)
190 >>> [product.displayname for product in gnome.products]
191 [u'Evolution', u'GNOME Terminal', u'Gnome Applets', u'gnomebaker']
192
193@@ -356,8 +358,8 @@
194 >>> gnome = getUtility(IProjectGroupSet)['gnome']
195 >>> gnome.has_translatable()
196 True
197- >>> translatables = gnome.translatables()
198- >>> translatables.count()
199+ >>> translatables = gnome.translatables
200+ >>> len(translatables)
201 1
202
203 And that translatable product is 'Evolution'.
204
205=== modified file 'lib/lp/registry/interfaces/projectgroup.py'
206--- lib/lp/registry/interfaces/projectgroup.py 2012-10-08 02:42:05 +0000
207+++ lib/lp/registry/interfaces/projectgroup.py 2012-10-14 23:44:21 +0000
208@@ -326,19 +326,14 @@
209 title=u"Search for possible duplicate bugs when a new bug is filed",
210 required=False, readonly=True)
211
212+ translatables = Attribute("Products that are translatable in LP")
213+
214 def getProduct(name):
215 """Get a product with name `name`."""
216
217 def getConfigurableProducts():
218 """Get all products that can be edited by user."""
219
220- def translatables():
221- """Return an iterator over products that are translatable in LP.
222-
223- Only products with IProduct.translations_usage set to
224- ServiceUsage.LAUNCHPAD are considered translatable.
225- """
226-
227 def has_translatable():
228 """Return a boolean showing the existance of translatables products.
229 """
230
231=== modified file 'lib/lp/registry/model/projectgroup.py'
232--- lib/lp/registry/model/projectgroup.py 2012-10-08 02:42:05 +0000
233+++ lib/lp/registry/model/projectgroup.py 2012-10-14 23:44:21 +0000
234@@ -98,6 +98,7 @@
235 sqlvalues,
236 )
237 from lp.services.helpers import shortlist
238+from lp.services.propertycache import cachedproperty
239 from lp.services.webapp.authorization import check_permission
240 from lp.services.worlddata.model.language import Language
241 from lp.translations.enums import TranslationPermission
242@@ -168,11 +169,14 @@
243 """See `IPillar`."""
244 return "Project Group"
245
246- @property
247- def products(self):
248+ def getProducts(self):
249 return Product.selectBy(
250 project=self, active=True, orderBy='displayname')
251
252+ @cachedproperty
253+ def products(self):
254+ return list(self.getProducts())
255+
256 def getProduct(self, name):
257 return Product.selectOneBy(project=self, name=name)
258
259@@ -187,8 +191,12 @@
260 return [self.driver]
261 return []
262
263- def translatables(self):
264- """See `IProjectGroup`."""
265+ def getTranslatables(self):
266+ """Return an iterator over products that are translatable in LP.
267+
268+ Only products with IProduct.translations_usage set to
269+ ServiceUsage.LAUNCHPAD are considered translatable.
270+ """
271 store = Store.of(self)
272 origin = [
273 Product,
274@@ -201,9 +209,14 @@
275 Product.translations_usage == ServiceUsage.LAUNCHPAD,
276 ).config(distinct=True)
277
278+ @cachedproperty
279+ def translatables(self):
280+ """See `IProjectGroup`."""
281+ return list(self.getTranslatables())
282+
283 def has_translatable(self):
284 """See `IProjectGroup`."""
285- return not self.translatables().is_empty()
286+ return len(self.translatables) > 0
287
288 def sharesTranslationsWithOtherSide(self, person, language,
289 sourcepackage=None,
290@@ -383,7 +396,7 @@
291 This is to avoid situations where users try to file bugs against
292 empty project groups (Malone bug #106523).
293 """
294- return self.products.count() != 0
295+ return len(self.products) != 0
296
297 def _getMilestoneCondition(self):
298 """See `HasMilestonesMixin`."""
299
300=== modified file 'lib/lp/registry/templates/hasannouncements-portlet-latest.pt'
301--- lib/lp/registry/templates/hasannouncements-portlet-latest.pt 2009-08-11 04:35:26 +0000
302+++ lib/lp/registry/templates/hasannouncements-portlet-latest.pt 2012-10-14 23:44:21 +0000
303@@ -4,7 +4,7 @@
304 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
305 omit-tag="">
306 <tal:news define="announcements view/latest_announcements;
307- has_announcements announcements/count;
308+ has_announcements view/has_announcements;
309 overview_menu context/menu:overview">
310 <div id="portlet-latest-announcements" class="portlet announcements"
311 tal:condition="view/show_announcements">
312
313=== modified file 'lib/lp/translations/browser/project.py'
314--- lib/lp/translations/browser/project.py 2012-02-28 04:24:19 +0000
315+++ lib/lp/translations/browser/project.py 2012-10-14 23:44:21 +0000
316@@ -52,7 +52,7 @@
317
318 @property
319 def untranslatables(self):
320- translatables = set(self.context.translatables())
321+ translatables = set(self.context.translatables)
322 all_products = set(self.context.products)
323 return list(all_products - translatables)
324