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
=== modified file 'lib/lp/answers/browser/faqcollection.py'
--- lib/lp/answers/browser/faqcollection.py 2012-01-01 02:58:52 +0000
+++ lib/lp/answers/browser/faqcollection.py 2012-10-14 23:44:21 +0000
@@ -127,7 +127,7 @@
127 quantity = 5127 quantity = 5
128 faqs = self.context.searchFAQs(128 faqs = self.context.searchFAQs(
129 search_text=self.search_text, sort=FAQSort.NEWEST_FIRST)129 search_text=self.search_text, sort=FAQSort.NEWEST_FIRST)
130 return faqs[:quantity]130 return list(faqs[:quantity])
131131
132 @safe_action132 @safe_action
133 @action(_('Search'), name='search')133 @action(_('Search'), name='search')
134134
=== modified file 'lib/lp/answers/browser/questiontarget.py'
--- lib/lp/answers/browser/questiontarget.py 2012-07-05 00:47:31 +0000
+++ lib/lp/answers/browser/questiontarget.py 2012-10-14 23:44:21 +0000
@@ -167,7 +167,7 @@
167 is used by the +portlet-latestquestions view.167 is used by the +portlet-latestquestions view.
168 """168 """
169 question_collection = IQuestionCollection(self.context)169 question_collection = IQuestionCollection(self.context)
170 return question_collection.searchQuestions()[:quantity]170 return list(question_collection.searchQuestions()[:quantity])
171171
172172
173class QuestionCollectionOpenCountView:173class QuestionCollectionOpenCountView:
174174
=== modified file 'lib/lp/blueprints/doc/sprint.txt'
--- lib/lp/blueprints/doc/sprint.txt 2012-09-28 14:35:35 +0000
+++ lib/lp/blueprints/doc/sprint.txt 2012-10-14 23:44:21 +0000
@@ -91,7 +91,9 @@
91Flush the updates to the database so we'll see them.91Flush the updates to the database so we'll see them.
9292
93 >>> from lp.services.database.sqlbase import flush_database_updates93 >>> from lp.services.database.sqlbase import flush_database_updates
94 >>> from lp.services.propertycache import clear_property_cache
94 >>> flush_database_updates()95 >>> flush_database_updates()
96 >>> clear_property_cache(ubuntu)
9597
96See, there are no ubuntu sprints.98See, there are no ubuntu sprints.
9799
98100
=== modified file 'lib/lp/blueprints/model/sprint.py'
--- lib/lp/blueprints/model/sprint.py 2012-09-27 18:50:38 +0000
+++ lib/lp/blueprints/model/sprint.py 2012-10-14 23:44:21 +0000
@@ -60,6 +60,7 @@
60 SQLBase,60 SQLBase,
61 )61 )
62from lp.services.database.stormexpr import fti_search62from lp.services.database.stormexpr import fti_search
63from lp.services.propertycache import cachedproperty
6364
6465
65class Sprint(SQLBase, HasDriversMixin, HasSpecificationsMixin):66class Sprint(SQLBase, HasDriversMixin, HasSpecificationsMixin):
@@ -361,22 +362,28 @@
361 quote(SprintSpecificationStatus.ACCEPTED))362 quote(SprintSpecificationStatus.ACCEPTED))
362 return query, ['Specification', 'SprintSpecification']363 return query, ['Specification', 'SprintSpecification']
363364
364 @property365 def getSprints(self):
366 query, tables = self._getBaseQueryAndClauseTablesForQueryingSprints()
367 return Sprint.select(
368 query, clauseTables=tables, orderBy='-time_starts', distinct=True)
369
370 @cachedproperty
365 def sprints(self):371 def sprints(self):
366 """See IHasSprints."""372 """See IHasSprints."""
367 query, tables = self._getBaseQueryAndClauseTablesForQueryingSprints()373 return list(self.getSprints())
368 return Sprint.select(
369 query, clauseTables=tables, orderBy='-time_starts', distinct=True)
370374
371 @property375 def getComingSprings(self):
372 def coming_sprints(self):
373 """See IHasSprints."""
374 query, tables = self._getBaseQueryAndClauseTablesForQueryingSprints()376 query, tables = self._getBaseQueryAndClauseTablesForQueryingSprints()
375 query += " AND Sprint.time_ends > 'NOW'"377 query += " AND Sprint.time_ends > 'NOW'"
376 return Sprint.select(378 return Sprint.select(
377 query, clauseTables=tables, orderBy='time_starts',379 query, clauseTables=tables, orderBy='time_starts',
378 distinct=True, limit=5)380 distinct=True, limit=5)
379381
382 @cachedproperty
383 def coming_sprints(self):
384 """See IHasSprints."""
385 return list(self.getComingSprings())
386
380 @property387 @property
381 def past_sprints(self):388 def past_sprints(self):
382 """See IHasSprints."""389 """See IHasSprints."""
383390
=== modified file 'lib/lp/registry/browser/announcement.py'
--- lib/lp/registry/browser/announcement.py 2012-01-01 02:58:52 +0000
+++ lib/lp/registry/browser/announcement.py 2012-10-14 23:44:21 +0000
@@ -298,18 +298,22 @@
298 @cachedproperty298 @cachedproperty
299 def announcements(self):299 def announcements(self):
300 published_only = not check_permission('launchpad.Edit', self.context)300 published_only = not check_permission('launchpad.Edit', self.context)
301 return self.context.getAnnouncements(301 return list(self.context.getAnnouncements(
302 limit=None, published_only=published_only)302 limit=None, published_only=published_only))
303303
304 @cachedproperty304 @cachedproperty
305 def latest_announcements(self):305 def latest_announcements(self):
306 published_only = not check_permission('launchpad.Edit', self.context)306 published_only = not check_permission('launchpad.Edit', self.context)
307 return self.context.getAnnouncements(307 return list(self.context.getAnnouncements(
308 limit=5, published_only=published_only)308 limit=5, published_only=published_only))
309
310 @cachedproperty
311 def has_announcements(self):
312 return len(self.latest_announcements) > 0
309313
310 @cachedproperty314 @cachedproperty
311 def show_announcements(self):315 def show_announcements(self):
312 return (self.latest_announcements.count() > 0316 return (len(self.latest_announcements) > 0
313 or check_permission('launchpad.Edit', self.context))317 or check_permission('launchpad.Edit', self.context))
314318
315 @cachedproperty319 @cachedproperty
316320
=== modified file 'lib/lp/registry/browser/project.py'
--- lib/lp/registry/browser/project.py 2012-10-08 01:55:25 +0000
+++ lib/lp/registry/browser/project.py 2012-10-14 23:44:21 +0000
@@ -390,7 +390,7 @@
390 The number of sub projects can break the preferred layout so the390 The number of sub projects can break the preferred layout so the
391 template may want to plan for a long list.391 template may want to plan for a long list.
392 """392 """
393 return self.context.products.count() > 10393 return len(self.context.products) > 10
394394
395 @property395 @property
396 def project_group_milestone_tag(self):396 def project_group_milestone_tag(self):
397397
=== modified file 'lib/lp/registry/browser/tests/pillar-views.txt'
--- lib/lp/registry/browser/tests/pillar-views.txt 2012-06-16 13:41:36 +0000
+++ lib/lp/registry/browser/tests/pillar-views.txt 2012-10-14 23:44:21 +0000
@@ -216,6 +216,8 @@
216 ... productseries=series,216 ... productseries=series,
217 ... language_codes=['es'])217 ... language_codes=['es'])
218 >>> product.translations_usage = ServiceUsage.LAUNCHPAD218 >>> product.translations_usage = ServiceUsage.LAUNCHPAD
219 >>> from lp.services.propertycache import clear_property_cache
220 >>> clear_property_cache(project_group)
219 >>> project_group.has_translatable()221 >>> project_group.has_translatable()
220 True222 True
221223
222224
=== modified file 'lib/lp/registry/browser/tests/projectgroup-views.txt'
--- lib/lp/registry/browser/tests/projectgroup-views.txt 2012-08-08 07:22:51 +0000
+++ lib/lp/registry/browser/tests/projectgroup-views.txt 2012-10-14 23:44:21 +0000
@@ -7,7 +7,7 @@
77
8 >>> projectgroup = factory.makeProject(name='mothership')8 >>> projectgroup = factory.makeProject(name='mothership')
9 >>> view = create_view(projectgroup, name='+index')9 >>> view = create_view(projectgroup, name='+index')
10 >>> projectgroup.products.count()10 >>> len(projectgroup.products)
11 011 0
1212
13 >>> view.has_many_projects13 >>> view.has_many_projects
@@ -23,7 +23,9 @@
23 >>> owner = projectgroup.owner23 >>> owner = projectgroup.owner
24 >>> ignored = login_person(owner)24 >>> ignored = login_person(owner)
25 >>> add_daughter(projectgroup, 'a')25 >>> add_daughter(projectgroup, 'a')
26 >>> projectgroup.products.count()26 >>> from lp.services.propertycache import clear_property_cache
27 >>> clear_property_cache(projectgroup)
28 >>> len(projectgroup.products)
27 129 1
2830
29 >>> view = create_view(projectgroup, name='+index')31 >>> view = create_view(projectgroup, name='+index')
@@ -34,7 +36,8 @@
34projects (10 projects are roughly 2 portlets deep.)36projects (10 projects are roughly 2 portlets deep.)
3537
36 >>> add_daughter(projectgroup, 'bcdefghijk')38 >>> add_daughter(projectgroup, 'bcdefghijk')
37 >>> projectgroup.products.count()39 >>> clear_property_cache(projectgroup)
40 >>> len(projectgroup.products)
38 1141 11
3942
40 >>> view = create_view(projectgroup, name='+index')43 >>> view = create_view(projectgroup, name='+index')
4144
=== modified file 'lib/lp/registry/doc/projectgroup.txt'
--- lib/lp/registry/doc/projectgroup.txt 2012-09-26 20:56:43 +0000
+++ lib/lp/registry/doc/projectgroup.txt 2012-10-14 23:44:21 +0000
@@ -131,6 +131,8 @@
131 >>> unlink_source_packages(netapplet)131 >>> unlink_source_packages(netapplet)
132 >>> netapplet.active = False132 >>> netapplet.active = False
133 >>> flush_database_updates()133 >>> flush_database_updates()
134 >>> from lp.services.propertycache import clear_property_cache
135 >>> clear_property_cache(gnome)
134 >>> [product.displayname for product in gnome.products]136 >>> [product.displayname for product in gnome.products]
135 [u'Evolution', u'GNOME Terminal', u'Gnome Applets', u'gnomebaker']137 [u'Evolution', u'GNOME Terminal', u'Gnome Applets', u'gnomebaker']
136138
@@ -356,8 +358,8 @@
356 >>> gnome = getUtility(IProjectGroupSet)['gnome']358 >>> gnome = getUtility(IProjectGroupSet)['gnome']
357 >>> gnome.has_translatable()359 >>> gnome.has_translatable()
358 True360 True
359 >>> translatables = gnome.translatables()361 >>> translatables = gnome.translatables
360 >>> translatables.count()362 >>> len(translatables)
361 1363 1
362364
363And that translatable product is 'Evolution'.365And that translatable product is 'Evolution'.
364366
=== modified file 'lib/lp/registry/interfaces/projectgroup.py'
--- lib/lp/registry/interfaces/projectgroup.py 2012-10-08 02:42:05 +0000
+++ lib/lp/registry/interfaces/projectgroup.py 2012-10-14 23:44:21 +0000
@@ -326,19 +326,14 @@
326 title=u"Search for possible duplicate bugs when a new bug is filed",326 title=u"Search for possible duplicate bugs when a new bug is filed",
327 required=False, readonly=True)327 required=False, readonly=True)
328328
329 translatables = Attribute("Products that are translatable in LP")
330
329 def getProduct(name):331 def getProduct(name):
330 """Get a product with name `name`."""332 """Get a product with name `name`."""
331333
332 def getConfigurableProducts():334 def getConfigurableProducts():
333 """Get all products that can be edited by user."""335 """Get all products that can be edited by user."""
334336
335 def translatables():
336 """Return an iterator over products that are translatable in LP.
337
338 Only products with IProduct.translations_usage set to
339 ServiceUsage.LAUNCHPAD are considered translatable.
340 """
341
342 def has_translatable():337 def has_translatable():
343 """Return a boolean showing the existance of translatables products.338 """Return a boolean showing the existance of translatables products.
344 """339 """
345340
=== modified file 'lib/lp/registry/model/projectgroup.py'
--- lib/lp/registry/model/projectgroup.py 2012-10-08 02:42:05 +0000
+++ lib/lp/registry/model/projectgroup.py 2012-10-14 23:44:21 +0000
@@ -98,6 +98,7 @@
98 sqlvalues,98 sqlvalues,
99 )99 )
100from lp.services.helpers import shortlist100from lp.services.helpers import shortlist
101from lp.services.propertycache import cachedproperty
101from lp.services.webapp.authorization import check_permission102from lp.services.webapp.authorization import check_permission
102from lp.services.worlddata.model.language import Language103from lp.services.worlddata.model.language import Language
103from lp.translations.enums import TranslationPermission104from lp.translations.enums import TranslationPermission
@@ -168,11 +169,14 @@
168 """See `IPillar`."""169 """See `IPillar`."""
169 return "Project Group"170 return "Project Group"
170171
171 @property172 def getProducts(self):
172 def products(self):
173 return Product.selectBy(173 return Product.selectBy(
174 project=self, active=True, orderBy='displayname')174 project=self, active=True, orderBy='displayname')
175175
176 @cachedproperty
177 def products(self):
178 return list(self.getProducts())
179
176 def getProduct(self, name):180 def getProduct(self, name):
177 return Product.selectOneBy(project=self, name=name)181 return Product.selectOneBy(project=self, name=name)
178182
@@ -187,8 +191,12 @@
187 return [self.driver]191 return [self.driver]
188 return []192 return []
189193
190 def translatables(self):194 def getTranslatables(self):
191 """See `IProjectGroup`."""195 """Return an iterator over products that are translatable in LP.
196
197 Only products with IProduct.translations_usage set to
198 ServiceUsage.LAUNCHPAD are considered translatable.
199 """
192 store = Store.of(self)200 store = Store.of(self)
193 origin = [201 origin = [
194 Product,202 Product,
@@ -201,9 +209,14 @@
201 Product.translations_usage == ServiceUsage.LAUNCHPAD,209 Product.translations_usage == ServiceUsage.LAUNCHPAD,
202 ).config(distinct=True)210 ).config(distinct=True)
203211
212 @cachedproperty
213 def translatables(self):
214 """See `IProjectGroup`."""
215 return list(self.getTranslatables())
216
204 def has_translatable(self):217 def has_translatable(self):
205 """See `IProjectGroup`."""218 """See `IProjectGroup`."""
206 return not self.translatables().is_empty()219 return len(self.translatables) > 0
207220
208 def sharesTranslationsWithOtherSide(self, person, language,221 def sharesTranslationsWithOtherSide(self, person, language,
209 sourcepackage=None,222 sourcepackage=None,
@@ -383,7 +396,7 @@
383 This is to avoid situations where users try to file bugs against396 This is to avoid situations where users try to file bugs against
384 empty project groups (Malone bug #106523).397 empty project groups (Malone bug #106523).
385 """398 """
386 return self.products.count() != 0399 return len(self.products) != 0
387400
388 def _getMilestoneCondition(self):401 def _getMilestoneCondition(self):
389 """See `HasMilestonesMixin`."""402 """See `HasMilestonesMixin`."""
390403
=== modified file 'lib/lp/registry/templates/hasannouncements-portlet-latest.pt'
--- lib/lp/registry/templates/hasannouncements-portlet-latest.pt 2009-08-11 04:35:26 +0000
+++ lib/lp/registry/templates/hasannouncements-portlet-latest.pt 2012-10-14 23:44:21 +0000
@@ -4,7 +4,7 @@
4 xmlns:i18n="http://xml.zope.org/namespaces/i18n"4 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
5 omit-tag="">5 omit-tag="">
6<tal:news define="announcements view/latest_announcements;6<tal:news define="announcements view/latest_announcements;
7 has_announcements announcements/count;7 has_announcements view/has_announcements;
8 overview_menu context/menu:overview">8 overview_menu context/menu:overview">
9<div id="portlet-latest-announcements" class="portlet announcements"9<div id="portlet-latest-announcements" class="portlet announcements"
10 tal:condition="view/show_announcements">10 tal:condition="view/show_announcements">
1111
=== modified file 'lib/lp/translations/browser/project.py'
--- lib/lp/translations/browser/project.py 2012-02-28 04:24:19 +0000
+++ lib/lp/translations/browser/project.py 2012-10-14 23:44:21 +0000
@@ -52,7 +52,7 @@
5252
53 @property53 @property
54 def untranslatables(self):54 def untranslatables(self):
55 translatables = set(self.context.translatables())55 translatables = set(self.context.translatables)
56 all_products = set(self.context.products)56 all_products = set(self.context.products)
57 return list(all_products - translatables)57 return list(all_products - translatables)
5858