Merge lp:~cjwatson/launchpad/front-page-bulk into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 18504
Proposed branch: lp:~cjwatson/launchpad/front-page-bulk
Merge into: lp:launchpad
Diff against target: 192 lines (+57/-10)
4 files modified
lib/lp/app/browser/tests/test_launchpadroot.py (+22/-1)
lib/lp/registry/model/pillar.py (+27/-5)
lib/lp/testing/__init__.py (+4/-1)
lib/lp/testing/_login.py (+4/-3)
To merge this branch: bzr merge lp:~cjwatson/launchpad/front-page-bulk
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+333510@code.launchpad.net

Commit message

Preload pillars and icons in PillarNameSet.featured_projects.

Description of the change

This gets rid of well over half the queries on the front page (about 50 out of 80).

Since this model property has exactly one user, I just made the eager loading mandatory rather than doing anything more clever.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) :
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/app/browser/tests/test_launchpadroot.py'
2--- lib/lp/app/browser/tests/test_launchpadroot.py 2017-10-21 18:14:14 +0000
3+++ lib/lp/app/browser/tests/test_launchpadroot.py 2017-11-10 12:16:02 +0000
4@@ -1,4 +1,4 @@
5-# Copyright 2010-2013 Canonical Ltd. This software is licensed under the
6+# Copyright 2010-2017 Canonical Ltd. This software is licensed under the
7 # GNU Affero General Public License version 3 (see the file LICENSE).
8
9 """Tests related to ILaunchpadRoot."""
10@@ -12,6 +12,7 @@
11
12 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
13 from lp.registry.interfaces.person import IPersonSet
14+from lp.registry.interfaces.pillar import IPillarNameSet
15 from lp.services.beautifulsoup import (
16 BeautifulSoup,
17 SoupStrainer,
18@@ -23,13 +24,16 @@
19 from lp.services.webapp.interfaces import ILaunchpadRoot
20 from lp.testing import (
21 anonymous_logged_in,
22+ login_admin,
23 login_person,
24+ record_two_runs,
25 TestCaseWithFactory,
26 )
27 from lp.testing.layers import (
28 DatabaseFunctionalLayer,
29 LaunchpadFunctionalLayer,
30 )
31+from lp.testing.matchers import HasQueryCount
32 from lp.testing.publication import test_traverse
33 from lp.testing.views import (
34 create_initialized_view,
35@@ -225,3 +229,20 @@
36 parseOnlyThese=SoupStrainer(id='homepage-blogposts'))
37 items = markup.findAll('li', 'news')
38 self.assertEqual(3, len(items))
39+
40+ def test_featured_projects_query_count(self):
41+ def add_featured_projects():
42+ product = self.factory.makeProduct()
43+ project = self.factory.makeProject()
44+ distribution = self.factory.makeDistribution()
45+ for pillar in product, project, distribution:
46+ pillar.icon = self.factory.makeLibraryFileAlias(db_only=True)
47+ getUtility(IPillarNameSet).add_featured_project(pillar)
48+
49+ root = getUtility(ILaunchpadRoot)
50+ user = self.factory.makePerson()
51+ recorder1, recorder2 = record_two_runs(
52+ lambda: create_initialized_view(
53+ root, 'index.html', principal=user)(),
54+ add_featured_projects, 5, login_method=login_admin)
55+ self.assertThat(recorder2, HasQueryCount.byEquality(recorder1))
56
57=== modified file 'lib/lp/registry/model/pillar.py'
58--- lib/lp/registry/model/pillar.py 2016-04-14 05:16:26 +0000
59+++ lib/lp/registry/model/pillar.py 2017-11-10 12:16:02 +0000
60@@ -1,4 +1,4 @@
61-# Copyright 2009 Canonical Ltd. This software is licensed under the
62+# Copyright 2009-2017 Canonical Ltd. This software is licensed under the
63 # GNU Affero General Public License version 3 (see the file LICENSE).
64
65 """Launchpad Pillars share a namespace.
66@@ -8,6 +8,7 @@
67
68 __metaclass__ = type
69
70+from operator import attrgetter
71 import warnings
72
73 from sqlobject import (
74@@ -46,12 +47,15 @@
75 from lp.registry.interfaces.projectgroup import IProjectGroupSet
76 from lp.registry.model.featuredproject import FeaturedProject
77 from lp.services.config import config
78+from lp.services.database.bulk import load_related
79+from lp.services.database.decoratedresultset import DecoratedResultSet
80 from lp.services.database.interfaces import IStore
81 from lp.services.database.sqlbase import (
82 SQLBase,
83 sqlvalues,
84 )
85 from lp.services.helpers import ensure_unicode
86+from lp.services.librarian.model import LibraryFileAlias
87
88
89 __all__ = [
90@@ -269,10 +273,28 @@
91 @property
92 def featured_projects(self):
93 """See `IPillarSet`."""
94-
95- query = "PillarName.id = FeaturedProject.pillar_name"
96- return [pillar_name.pillar for pillar_name in PillarName.select(
97- query, clauseTables=['FeaturedProject'])]
98+ # Circular imports.
99+ from lp.registry.model.distribution import Distribution
100+ from lp.registry.model.product import Product
101+ from lp.registry.model.projectgroup import ProjectGroup
102+
103+ store = IStore(PillarName)
104+ pillar_names = store.find(
105+ PillarName, PillarName.id == FeaturedProject.pillar_name)
106+
107+ def preload_pillars(rows):
108+ pillar_names = (
109+ set(rows).union(load_related(PillarName, rows, ['alias_for'])))
110+ pillars = load_related(Product, pillar_names, ['productID'])
111+ pillars.extend(load_related(
112+ ProjectGroup, pillar_names, ['projectgroupID']))
113+ pillars.extend(load_related(
114+ Distribution, pillar_names, ['distributionID']))
115+ load_related(LibraryFileAlias, pillars, ['iconID'])
116+
117+ return DecoratedResultSet(
118+ pillar_names, result_decorator=attrgetter('pillar'),
119+ pre_iter_hook=preload_pillars)
120
121
122 @implementer(IPillarName)
123
124=== modified file 'lib/lp/testing/__init__.py'
125--- lib/lp/testing/__init__.py 2016-07-07 18:27:16 +0000
126+++ lib/lp/testing/__init__.py 2017-11-10 12:16:02 +0000
127@@ -1,4 +1,4 @@
128-# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
129+# Copyright 2009-2017 Canonical Ltd. This software is licensed under the
130 # GNU Affero General Public License version 3 (see the file LICENSE).
131
132 from __future__ import absolute_import
133@@ -23,6 +23,7 @@
134 'launchpadlib_credentials_for',
135 'launchpadlib_for',
136 'login',
137+ 'login_admin',
138 'login_as',
139 'login_celebrity',
140 'login_person',
141@@ -164,6 +165,7 @@
142 anonymous_logged_in,
143 celebrity_logged_in,
144 login,
145+ login_admin,
146 login_as,
147 login_celebrity,
148 login_person,
149@@ -194,6 +196,7 @@
150 celebrity_logged_in
151 launchpadlib_credentials_for
152 launchpadlib_for
153+login_admin
154 login_as
155 login_celebrity
156 login_person
157
158=== modified file 'lib/lp/testing/_login.py'
159--- lib/lp/testing/_login.py 2013-01-07 03:21:35 +0000
160+++ lib/lp/testing/_login.py 2017-11-10 12:16:02 +0000
161@@ -1,4 +1,4 @@
162-# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
163+# Copyright 2009-2017 Canonical Ltd. This software is licensed under the
164 # GNU Affero General Public License version 3 (see the file LICENSE).
165
166 __metaclass__ = type
167@@ -8,6 +8,7 @@
168 'anonymous_logged_in',
169 'celebrity_logged_in',
170 'login',
171+ 'login_admin',
172 'login_as',
173 'login_celebrity',
174 'login_person',
175@@ -128,7 +129,7 @@
176 return login_as(celeb, participation=participation)
177
178
179-def login_admin(ignored, participation=None):
180+def login_admin(participation=None):
181 """Log in as an admin."""
182 login(ANONYMOUS)
183 admin = getUtility(ILaunchpadCelebrities).admin.teamowner
184@@ -189,7 +190,7 @@
185 @contextmanager
186 def admin_logged_in():
187 # Use teamowner to avoid expensive and noisy team member additions.
188- return _with_login(login_admin, None)
189+ return _with_login(lambda _: login_admin(), None)
190
191
192 with_anonymous_login = decorate_with(person_logged_in, None)