Merge lp:~rvb/launchpad/builders-timeout-bug-887078-eager-load 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: 14329
Proposed branch: lp:~rvb/launchpad/builders-timeout-bug-887078-eager-load
Merge into: lp:launchpad
Prerequisite: lp:~rvb/launchpad/builders-timeout-bug-887078-use-getSpecificJobs
Diff against target: 267 lines (+138/-29)
4 files modified
lib/lp/buildmaster/tests/test_buildfarmjob.py (+15/-5)
lib/lp/code/model/sourcepackagerecipebuild.py (+28/-1)
lib/lp/soyuz/browser/tests/test_builder_views.py (+59/-22)
lib/lp/soyuz/model/binarypackagebuild.py (+36/-1)
To merge this branch: bzr merge lp:~rvb/launchpad/builders-timeout-bug-887078-eager-load
Reviewer Review Type Date Requested Status
Gavin Panella (community) Approve
Review via email: mp+82008@code.launchpad.net

Commit message

Eager load data in getByBuildFarmJobs.

Description of the change

The ultimate goal is to make the number of queries issued by builder:+history independent of the number of builds. We have 3 types of builds displayed on these pages: BinaryPackageBuild, SourcePackageRecipeBuild and TranslationTemplatesBuild.

This branch focuses on the elements of type BinaryPackageBuild and SourcePackageRecipeBuild. It eager loads data when the build farm jobs are fetched by getByBuildFarmJobs (BinaryPackageBuildSet and SourcePackageRecipeBuild).

- BinaryPackageBuild: the number of queries is now independent of the number of BinaryPackageBuilds \o/.
- SourcePackageRecipeBuild: I've improved things seriously here but each new SourcePackageRecipeBuild displayed generates 2 additional queries: this is because I've no idea how to fetch what SourcePackageRecipe._recipe_data fetches in bulk so that it's properly cached by Storm. Also note that the test test_build_history_queries_count_view_recipe_builds does not tests the entire page generation but only the data preparation that is done by the view class. If we manage to cache SourcePackageRecipe._recipe_data somehow then more work could be done to make the whole page rendering issue a constant number of queries.

= Tests =

./bin/test -vvc test_builder_views test_build_history_queries_count_view_recipe_builds
./bin/test -vvc test_builder_views test_build_history_queries_count_binary_package_builds

= Q/A =

None.

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote :

\o/

[1]

+ flush_database_updates()
         with StormStatementRecorder() as recorder:

I end up doing this every time. I'll put a branch in ec2 that calls
flush_database_updates() in StormStatementRecorder.__enter__().

[2]

+ build_farm_jobs.extend([build.build_farm_job for build in builds2])

No need for the intermediate list, though I guess it does little harm.

[3]

+ # rvb 2011-11-11: Each build issues 2 new queries.
+ # This is because of the way SourcePackageRecipe._recipe_data
+ # fetches SourcePackageRecipeData. I've no idea how to prefetch
+ # this.

I guess this is work in progress, but should probably be XXX'd
anyway. What do you think?

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

Thanks for the review.

> I end up doing this every time. I'll put a branch in ec2 that calls
> flush_database_updates() in StormStatementRecorder.__enter__().

Good idea ;)

> [2]
>
> + build_farm_jobs.extend([build.build_farm_job for build in builds2])
>
> No need for the intermediate list, though I guess it does little harm.

Okay.

> [3]
>
> + # rvb 2011-11-11: Each build issues 2 new queries.
> + # This is because of the way SourcePackageRecipe._recipe_data
> + # fetches SourcePackageRecipeData. I've no idea how to prefetch
> + # this.
>
> I guess this is work in progress, but should probably be XXX'd
> anyway. What do you think?

I'll keep it as is but I'll work on a branch to fix this right away.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/buildmaster/tests/test_buildfarmjob.py'
2--- lib/lp/buildmaster/tests/test_buildfarmjob.py 2011-11-10 15:11:52 +0000
3+++ lib/lp/buildmaster/tests/test_buildfarmjob.py 2011-11-14 09:09:41 +0000
4@@ -248,11 +248,14 @@
5 Store.of(sprb).flush()
6 return sprb
7
8+ def createBinaryPackageBuild(self):
9+ build = self.factory.makeBinaryPackageBuild()
10+ return build
11+
12 def createBuilds(self):
13 builds = []
14- for i in xrange(10):
15- # We don't create binary package builds because the test
16- # would be really heavy to setup.
17+ for i in xrange(2):
18+ builds.append(self.createBinaryPackageBuild())
19 builds.append(self.createTranslationTemplateBuild())
20 builds.append(self.createSourcePackageRecipeBuild())
21 return builds
22@@ -278,13 +281,20 @@
23 self.build_farm_job_set.getSpecificJobs([]))
24
25 def test_getSpecificJobs_sql_queries_count(self):
26- # getSpecificJobs issues one query for each build type.
27+ # getSpecificJobs issues a constant number of queries.
28 builds = self.createBuilds()
29 build_farm_jobs = [build.build_farm_job for build in builds]
30+ flush_database_updates()
31 with StormStatementRecorder() as recorder:
32 self.build_farm_job_set.getSpecificJobs(
33 build_farm_jobs)
34- self.assertThat(recorder, HasQueryCount(Equals(2)))
35+ builds2 = self.createBuilds()
36+ build_farm_jobs.extend([build.build_farm_job for build in builds2])
37+ flush_database_updates()
38+ with StormStatementRecorder() as recorder2:
39+ self.build_farm_job_set.getSpecificJobs(
40+ build_farm_jobs)
41+ self.assertThat(recorder, HasQueryCount(Equals(recorder2.count)))
42
43 def test_getSpecificJobs_no_specific_job(self):
44 build_farm_job_source = getUtility(IBuildFarmJobSource)
45
46=== modified file 'lib/lp/code/model/sourcepackagerecipebuild.py'
47--- lib/lp/code/model/sourcepackagerecipebuild.py 2011-11-09 11:36:44 +0000
48+++ lib/lp/code/model/sourcepackagerecipebuild.py 2011-11-14 09:09:41 +0000
49@@ -35,6 +35,9 @@
50
51 from canonical.database.constants import UTC_NOW
52 from canonical.launchpad.browser.librarian import ProxiedLibraryFileAlias
53+from canonical.launchpad.components.decoratedresultset import (
54+ DecoratedResultSet,
55+ )
56 from canonical.launchpad.interfaces.lpstorm import (
57 IMasterStore,
58 IStore,
59@@ -63,10 +66,17 @@
60 from lp.code.mail.sourcepackagerecipebuild import (
61 SourcePackageRecipeBuildMailer,
62 )
63+from lp.code.model.branch import Branch
64 from lp.code.model.sourcepackagerecipedata import SourcePackageRecipeData
65 from lp.registry.interfaces.pocket import PackagePublishingPocket
66+from lp.registry.model.person import Person
67+from lp.services.database.bulk import (
68+ load_referencing,
69+ load_related,
70+ )
71 from lp.services.job.model.job import Job
72 from lp.soyuz.interfaces.archive import CannotUploadToArchive
73+from lp.soyuz.model.archive import Archive
74 from lp.soyuz.model.binarypackagebuild import BinaryPackageBuild
75 from lp.soyuz.model.buildfarmbuildjob import BuildFarmBuildJob
76 from lp.soyuz.model.sourcepackagerelease import SourcePackageRelease
77@@ -289,9 +299,26 @@
78 return EmptyResultSet()
79 build_farm_job_ids = [
80 build_farm_job.id for build_farm_job in build_farm_jobs]
81- return Store.of(build_farm_jobs[0]).find(cls,
82+
83+ def eager_load(rows):
84+ # Circular imports.
85+ from lp.code.model.sourcepackagerecipe import SourcePackageRecipe
86+ package_builds = load_related(
87+ PackageBuild, rows, ['package_build_id'])
88+ load_related(
89+ Archive, package_builds, ['archive_id'])
90+ sprs = load_related(
91+ SourcePackageRecipe, rows, ['recipe_id'])
92+ sprds = load_referencing(
93+ SourcePackageRecipeData, sprs, ['sourcepackage_recipe_id'])
94+ load_related(
95+ Branch, sprds, ['base_branch_id'])
96+ load_related(
97+ Person, rows, ['requester_id'])
98+ resultset = Store.of(build_farm_jobs[0]).find(cls,
99 cls.package_build_id == PackageBuild.id,
100 PackageBuild.build_farm_job_id.is_in(build_farm_job_ids))
101+ return DecoratedResultSet(resultset, pre_iter_hook=eager_load)
102
103 @classmethod
104 def getRecentBuilds(cls, requester, recipe, distroseries, _now=None):
105
106=== modified file 'lib/lp/soyuz/browser/tests/test_builder_views.py'
107--- lib/lp/soyuz/browser/tests/test_builder_views.py 2011-11-10 15:14:00 +0000
108+++ lib/lp/soyuz/browser/tests/test_builder_views.py 2011-11-14 09:09:41 +0000
109@@ -7,6 +7,7 @@
110 from testtools.matchers import Equals
111 from zope.security.proxy import removeSecurityProxy
112
113+from canonical.database.sqlbase import flush_database_caches
114 from canonical.launchpad.ftests import login
115 from canonical.launchpad.webapp.servers import LaunchpadTestRequest
116 from canonical.testing.layers import LaunchpadFunctionalLayer
117@@ -62,33 +63,69 @@
118
119 layer = LaunchpadFunctionalLayer
120
121- def createRecipeBuildWithBuilder(self, builder=None):
122+ nb_objects = 2
123+
124+ def setUp(self):
125+ super(TestBuilderHistoryView, self).setUp()
126+ self.builder = self.factory.makeBuilder()
127+
128+ def createRecipeBuildWithBuilder(self):
129 build = self.factory.makeSourcePackageRecipeBuild()
130 Store.of(build).flush()
131- if builder is None:
132- builder = self.factory.makeBuilder()
133- removeSecurityProxy(build).builder = builder
134- return build
135-
136- def test_build_history_queries_count(self):
137- # The number of queries issued by setupBuildList is not dependent
138- # on the number of builds.
139- builder = self.factory.makeBuilder()
140- self.createRecipeBuildWithBuilder(builder)
141- self.createRecipeBuildWithBuilder(builder)
142- # Record how many queries are issued when setupBuildList is
143- # called with 2 builds.
144+ removeSecurityProxy(build).builder = self.builder
145+ return build
146+
147+ def createBinaryPackageBuild(self):
148+ build = self.factory.makeBinaryPackageBuild()
149+ removeSecurityProxy(build).builder = self.builder
150+ return build
151+
152+ def _record_queries_count(self, tested_method, item_creator):
153+ # A simple helper that returns the two storm statement recorders
154+ # obtained when running tested_method with {nb_objects} items creater
155+ # (using item_creator) and then with {nb_objects}*2 items created.
156+ for i in range(self.nb_objects):
157+ item_creator()
158+ # Record how many queries are issued when tested_method is
159+ # called with {nb_objects} items created.
160+ flush_database_caches()
161 with StormStatementRecorder() as recorder1:
162- view = create_initialized_view(builder, '+history')
163- view.setupBuildList()
164- self.assertEqual(2, len(view.complete_builds))
165- # Create two more builds.
166- self.createRecipeBuildWithBuilder(builder)
167- self.createRecipeBuildWithBuilder(builder)
168+ tested_method()
169+ # Create {nb_objects} more items.
170+ for i in range(self.nb_objects):
171+ item_creator()
172 # Record again the number of queries issued.
173+ flush_database_caches()
174 with StormStatementRecorder() as recorder2:
175- view = create_initialized_view(builder, '+history')
176+ tested_method()
177+ return recorder1, recorder2
178+
179+ def test_build_history_queries_count_view_recipe_builds(self):
180+ # The builder's history view creation (i.e. the call to
181+ # view.setupBuildList) issues a constant number of queries
182+ # when recipe builds are displayed.
183+ def call_setupBuildList():
184+ view = create_initialized_view(self.builder, '+history')
185 view.setupBuildList()
186- self.assertEqual(4, len(view.complete_builds))
187+ recorder1, recorder2 = self._record_queries_count(
188+ call_setupBuildList,
189+ self.createRecipeBuildWithBuilder)
190+
191+ # rvb 2011-11-11: Each build issues 2 new queries.
192+ # This is because of the way SourcePackageRecipe._recipe_data
193+ # fetches SourcePackageRecipeData. I've no idea how to prefetch
194+ # this.
195+ self.assertThat(
196+ recorder2,
197+ HasQueryCount(Equals(recorder1.count + 2 * self.nb_objects)))
198+
199+ def test_build_history_queries_count_binary_package_builds(self):
200+ # Rendering to builder's history issues a constant number of queries
201+ # when binary builds are displayed.
202+ def call_history_render():
203+ create_initialized_view(self.builder, '+history').render()
204+ recorder1, recorder2 = self._record_queries_count(
205+ call_history_render,
206+ self.createBinaryPackageBuild)
207
208 self.assertThat(recorder2, HasQueryCount(Equals(recorder1.count)))
209
210=== modified file 'lib/lp/soyuz/model/binarypackagebuild.py'
211--- lib/lp/soyuz/model/binarypackagebuild.py 2011-11-11 20:34:43 +0000
212+++ lib/lp/soyuz/model/binarypackagebuild.py 2011-11-14 09:09:41 +0000
213@@ -78,6 +78,7 @@
214 PackageBuild,
215 PackageBuildDerived,
216 )
217+from lp.services.database.bulk import load_related
218 from lp.services.job.model.job import Job
219 from lp.services.mail.sendmail import (
220 format_address,
221@@ -873,11 +874,45 @@
222 clause_tables = (BinaryPackageBuild, PackageBuild, BuildFarmJob)
223 build_farm_job_ids = [
224 build_farm_job.id for build_farm_job in build_farm_jobs]
225- return Store.of(build_farm_jobs[0]).using(*clause_tables).find(
226+
227+ def eager_load(rows):
228+ # Circular imports.
229+ from lp.registry.model.sourcepackagename import (
230+ SourcePackageName
231+ )
232+ from lp.soyuz.model.sourcepackagerelease import (
233+ SourcePackageRelease
234+ )
235+ from lp.soyuz.model.distroarchseries import (
236+ DistroArchSeries
237+ )
238+ from lp.registry.model.distroseries import (
239+ DistroSeries
240+ )
241+ from lp.registry.model.distribution import (
242+ Distribution
243+ )
244+ from lp.soyuz.model.archive import Archive
245+ sprs = load_related(
246+ SourcePackageRelease, rows, ['source_package_release_id'])
247+ load_related(
248+ SourcePackageName, sprs, ['sourcepackagenameID'])
249+ distro_arch_series = load_related(
250+ DistroArchSeries, rows, ['distro_arch_series_id'])
251+ package_builds = load_related(
252+ PackageBuild, rows, ['package_build_id'])
253+ load_related(
254+ Archive, package_builds, ['archive_id'])
255+ distroseries = load_related(
256+ DistroSeries, distro_arch_series, ['distroseriesID'])
257+ load_related(
258+ Distribution, distroseries, ['distributionID'])
259+ resultset = Store.of(build_farm_jobs[0]).using(*clause_tables).find(
260 BinaryPackageBuild,
261 BinaryPackageBuild.package_build == PackageBuild.id,
262 PackageBuild.build_farm_job == BuildFarmJob.id,
263 BuildFarmJob.id.is_in(build_farm_job_ids))
264+ return DecoratedResultSet(resultset, pre_iter_hook=eager_load)
265
266 def getPendingBuildsForArchSet(self, archseries):
267 """See `IBinaryPackageBuildSet`."""