← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~rvb/launchpad/builders-timeout-903827 into lp:launchpad

 

Raphaël Badin has proposed merging lp:~rvb/launchpad/builders-timeout-903827 into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #903827 in Launchpad itself: "https://launchpad.net/builders timeout"
  https://bugs.launchpad.net/launchpad/+bug/903827

For more details, see:
https://code.launchpad.net/~rvb/launchpad/builders-timeout-903827/+merge/86029

This branch caches the various objects related to the builds displayed on the builders' homepage (launchpad.net/builders) prior to displaying the page.

= Summary =
This branch also:
- extracts the methods (previously defined as eager_load inner methods) used to cache the various objects related to the three possible types of build in order to reuse them (the new name for these method is preloadBuildsData).
- changes a few properties into cached properties so that they can be populated in bulk.
- refactors the tests in lib/lp/soyuz/browser/tests/test_builder_views.py to reuse build creation method from there.

= Tests =
./bin/test -vvc test_builder test_builders_binary_package_build_query_count
./bin/test -vvc test_builder test_builders_recipe_build_query_count
./bin/test -vvc test_builder test_builders_translation_template_build_query_count

= Q/A =
No repeated statements should be performed by launchpad.net/builders.
-- 
https://code.launchpad.net/~rvb/launchpad/builders-timeout-903827/+merge/86029
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~rvb/launchpad/builders-timeout-903827 into lp:launchpad.
=== modified file 'lib/lp/buildmaster/interfaces/buildfarmjob.py'
--- lib/lp/buildmaster/interfaces/buildfarmjob.py	2011-12-02 14:33:58 +0000
+++ lib/lp/buildmaster/interfaces/buildfarmjob.py	2011-12-16 12:18:28 +0000
@@ -140,6 +140,13 @@
         has an entry associated with `job`.
         """
 
+    def getByJobs(jobs):
+        """Get the specific `IBuildFarmJob`s for the given `Job`s.
+
+        Invoked on the specific `IBuildFarmJob`-implementing class that
+        has entries associated with `job`s.
+        """
+
     def generateSlaveBuildCookie():
         """Produce a cookie for the slave as a token of the job it's doing.
 

=== modified file 'lib/lp/buildmaster/model/builder.py'
--- lib/lp/buildmaster/model/builder.py	2011-12-13 13:33:04 +0000
+++ lib/lp/buildmaster/model/builder.py	2011-12-16 12:18:28 +0000
@@ -498,7 +498,7 @@
 
     # XXX 2010-08-24 Julian bug=623281
     # This should not be a property!  It's masking a complicated query.
-    @property
+    @cachedproperty
     def currentjob(self):
         """See IBuilder"""
         return getUtility(IBuildQueueSet).getByBuilder(self)

=== modified file 'lib/lp/buildmaster/model/buildfarmjob.py'
--- lib/lp/buildmaster/model/buildfarmjob.py	2011-12-08 11:57:55 +0000
+++ lib/lp/buildmaster/model/buildfarmjob.py	2011-12-16 12:18:28 +0000
@@ -160,12 +160,28 @@
         """
         raise NotImplementedError
 
+    @staticmethod
+    def preloadBuildFarmJobs(jobs):
+        """Preload the build farm jobs to which the given jobs will delegate.
+
+        """
+        pass
+
     @classmethod
     def getByJob(cls, job):
         """See `IBuildFarmJobOld`."""
         store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
         return store.find(cls, cls.job == job).one()
 
+    @classmethod
+    def getByJobs(cls, jobs):
+        """See `IBuildFarmJobOld`.
+        """
+        store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
+        job_ids = [job.id for job in jobs]
+        return store.find(
+            cls, cls.job_id.is_in(job_ids))
+
     def generateSlaveBuildCookie(self):
         """See `IBuildFarmJobOld`."""
         buildqueue = getUtility(IBuildQueueSet).getByJob(self.job)

=== modified file 'lib/lp/buildmaster/model/buildqueue.py'
--- lib/lp/buildmaster/model/buildqueue.py	2011-11-14 08:30:52 +0000
+++ lib/lp/buildmaster/model/buildqueue.py	2011-12-16 12:18:28 +0000
@@ -16,7 +16,9 @@
     datetime,
     timedelta,
     )
+from itertools import groupby
 import logging
+from operator import attrgetter
 
 import pytz
 from sqlobject import (
@@ -56,6 +58,10 @@
     )
 from lp.services.job.interfaces.job import JobStatus
 from lp.services.job.model.job import Job
+from lp.services.propertycache import (
+    cachedproperty,
+    get_property_cache,
+    )
 
 
 def normalize_virtualization(virtualized):
@@ -136,12 +142,34 @@
         """See `IBuildQueue`."""
         return IBuildFarmJobBehavior(self.specific_job)
 
-    @property
+    @cachedproperty
     def specific_job(self):
         """See `IBuildQueue`."""
         specific_class = specific_job_classes()[self.job_type]
         return specific_class.getByJob(self.job)
 
+    @staticmethod
+    def preloadSpecificJobData(queues):
+        key = attrgetter('job_type')
+        specific_jobs_dict = {}
+        for job_type, grouped_queues in groupby(queues, key=key):
+            specific_class = specific_job_classes()[job_type]
+            queue_subset = list(grouped_queues)
+            # We need to preload the build farm jobs early to avoid
+            # the call to _set_build_farm_job to look up BuildFarmBuildJobs
+            # one by one.
+            specific_class.preloadBuildFarmJobs(queue_subset)
+            specific_jobs = specific_class.getByJobs(queue_subset)
+            if len(list(specific_jobs)) == 0:
+                return
+            specific_class.preloadJobsData(specific_jobs)
+            specific_jobs_dict = dict(
+                (specific_job.job, specific_job)
+                    for specific_job in specific_jobs)
+            for queue in queue_subset:
+                cache = get_property_cache(queue)
+                cache.specific_job = specific_jobs_dict[queue.job]
+
     @property
     def date_started(self):
         """See `IBuildQueue`."""

=== modified file 'lib/lp/code/model/sourcepackagerecipebuild.py'
--- lib/lp/code/model/sourcepackagerecipebuild.py	2011-11-14 17:24:36 +0000
+++ lib/lp/code/model/sourcepackagerecipebuild.py	2011-12-16 12:18:28 +0000
@@ -289,6 +289,18 @@
             PackageBuild.build_farm_job_id == build_farm_job.id).one()
 
     @classmethod
+    def preloadBuildsData(cls, builds):
+        # Circular imports.
+        from lp.code.model.sourcepackagerecipe import SourcePackageRecipe
+        package_builds = load_related(
+            PackageBuild, builds, ['package_build_id'])
+        archives = load_related(Archive, package_builds, ['archive_id'])
+        load_related(Person, archives, ['ownerID'])
+        sprs = load_related(
+            SourcePackageRecipe, builds, ['recipe_id'])
+        SourcePackageRecipe.preLoadDataForSourcePackageRecipes(sprs)
+
+    @classmethod
     def getByBuildFarmJobs(cls, build_farm_jobs):
         """See `ISpecificBuildFarmJobSource`."""
         if len(build_farm_jobs) == 0:
@@ -296,20 +308,11 @@
         build_farm_job_ids = [
             build_farm_job.id for build_farm_job in build_farm_jobs]
 
-        def eager_load(rows):
-            # Circular imports.
-            from lp.code.model.sourcepackagerecipe import SourcePackageRecipe
-            package_builds = load_related(
-                PackageBuild, rows, ['package_build_id'])
-            archives = load_related(Archive, package_builds, ['archive_id'])
-            load_related(Person, archives, ['ownerID'])
-            sprs = load_related(
-                SourcePackageRecipe, rows, ['recipe_id'])
-            SourcePackageRecipe.preLoadDataForSourcePackageRecipes(sprs)
         resultset = Store.of(build_farm_jobs[0]).find(cls,
             cls.package_build_id == PackageBuild.id,
             PackageBuild.build_farm_job_id.is_in(build_farm_job_ids))
-        return DecoratedResultSet(resultset, pre_iter_hook=eager_load)
+        return DecoratedResultSet(
+            resultset, pre_iter_hook=cls.preloadBuildsData)
 
     @classmethod
     def getRecentBuilds(cls, requester, recipe, distroseries, _now=None):
@@ -430,6 +433,24 @@
         We override this to provide a delegate specific to package builds."""
         self.build_farm_job = BuildFarmBuildJob(self.build)
 
+    @staticmethod
+    def preloadBuildFarmJobs(jobs):
+        from lp.code.model.sourcepackagerecipebuild import (
+            SourcePackageRecipeBuild,
+            )
+        return list(IStore(SourcePackageRecipeBuildJob).find(
+            SourcePackageRecipeBuild,
+            [SourcePackageRecipeBuildJob.id.is_in([job.id for job in jobs]),
+             SourcePackageRecipeBuildJob.build_id ==
+                 SourcePackageRecipeBuild.id]))
+
+    @classmethod
+    def preloadJobsData(cls, jobs):
+        load_related(Job, jobs, ['job_id'])
+        builds = load_related(
+            SourcePackageRecipeBuild, jobs, ['build_id'])
+        SourcePackageRecipeBuild.preloadBuildsData(builds)
+
     @classmethod
     def new(cls, build, job):
         """See `ISourcePackageRecipeBuildJobSource`."""

=== modified file 'lib/lp/soyuz/browser/builder.py'
--- lib/lp/soyuz/browser/builder.py	2011-09-13 05:23:16 +0000
+++ lib/lp/soyuz/browser/builder.py	2011-12-16 12:18:28 +0000
@@ -30,6 +30,10 @@
 from zope.lifecycleevent import ObjectCreatedEvent
 
 from canonical.launchpad import _
+from canonical.launchpad.components.decoratedresultset import (
+    DecoratedResultSet,
+    )
+from canonical.launchpad.interfaces.lpstorm import IStore
 from canonical.launchpad.webapp import (
     ApplicationMenu,
     canonical_url,
@@ -53,7 +57,11 @@
     IBuilder,
     IBuilderSet,
     )
-from lp.services.propertycache import cachedproperty
+from lp.buildmaster.model.buildqueue import BuildQueue
+from lp.services.propertycache import (
+    cachedproperty,
+    get_property_cache,
+    )
 from lp.soyuz.browser.build import (
     BuildNavigationMixin,
     BuildRecordsView,
@@ -144,7 +152,22 @@
     @cachedproperty
     def builders(self):
         """All active builders"""
-        return list(self.context.getBuilders())
+        def do_eager_load(builders):
+            # Populate builders' currentjob cachedproperty.
+            queues = IStore(BuildQueue).find(
+                BuildQueue,
+                BuildQueue.builderID.is_in(
+                    builder.id for builder in builders))
+            queue_builders = dict(
+                (queue.builderID, queue) for queue in queues)
+            for builder in builders:
+                cache = get_property_cache(builder)
+                cache.currentjob = queue_builders.get(builder.id, None)
+            # Prefetch the jobs' data.
+            BuildQueue.preloadSpecificJobData(queues)
+
+        return list(DecoratedResultSet(
+            list(self.context.getBuilders()), pre_iter_hook=do_eager_load))
 
     @property
     def number_of_registered_builders(self):

=== modified file 'lib/lp/soyuz/browser/tests/test_builder.py'
--- lib/lp/soyuz/browser/tests/test_builder.py	2011-05-03 04:46:02 +0000
+++ lib/lp/soyuz/browser/tests/test_builder.py	2011-12-16 12:18:28 +0000
@@ -1,14 +1,32 @@
-# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# Copyright 2010-2011 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Tests for the lp.soyuz.browser.builder module."""
 
 __metaclass__ = type
 
+from testtools.matchers import Equals
+from zope.component import getUtility
+from zope.security.proxy import removeSecurityProxy
+
 from canonical.launchpad.webapp import canonical_url
-from canonical.testing.layers import DatabaseFunctionalLayer
-from lp.testing import TestCaseWithFactory
+from canonical.testing.layers import (
+    DatabaseFunctionalLayer,
+    LaunchpadFunctionalLayer,
+    )
+from lp.buildmaster.interfaces.builder import IBuilderSet
+from lp.buildmaster.interfaces.buildqueue import IBuildQueueSet
+from lp.soyuz.browser.tests.test_builder_views import BuildCreationMixin
+from lp.testing import (
+    record_two_runs,
+    TestCaseWithFactory,
+    )
+from lp.testing.matchers import HasQueryCount
 from lp.testing.publication import test_traverse
+from lp.testing.views import create_initialized_view
+from lp.translations.interfaces.translationtemplatesbuildjob import (
+    ITranslationTemplatesBuildJobSource,
+    )
 
 
 class TestBuildersNavigation(TestCaseWithFactory):
@@ -38,3 +56,52 @@
         self.assertEqual(
             canonical_url(build),
             request.response.getHeader('location'))
+
+
+def builders_homepage_render():
+    builders = getUtility(IBuilderSet)
+    create_initialized_view(builders, "+index").render()
+
+
+class TestBuildersHomepage(TestCaseWithFactory, BuildCreationMixin):
+
+    layer = LaunchpadFunctionalLayer
+
+    def test_builders_binary_package_build_query_count(self):
+        def create_build():
+            build = self.createBinaryPackageBuild()
+            queue = build.queueBuild()
+            queue.markAsBuilding(build.builder)
+
+        recorder1, recorder2 = record_two_runs(
+            builders_homepage_render, create_build, 2)
+
+        self.assertThat(recorder2, HasQueryCount(Equals(recorder1.count)))
+
+    def test_builders_recipe_build_query_count(self):
+        def create_build():
+            build = self.createRecipeBuildWithBuilder()
+            queue = build.queueBuild()
+            queue.markAsBuilding(build.builder)
+
+        recorder1, recorder2 = record_two_runs(
+            builders_homepage_render, create_build, 2)
+
+        self.assertThat(recorder2, HasQueryCount(Equals(recorder1.count)))
+
+    def test_builders_translation_template_build_query_count(self):
+        def create_build():
+            jobset = getUtility(ITranslationTemplatesBuildJobSource)
+            branch = self.factory.makeBranch()
+            specific_job = jobset.create(branch)
+            queueset = getUtility(IBuildQueueSet)
+            # Using rSP is required to get the job id.
+            naked_job = removeSecurityProxy(specific_job.job)
+            job_id = naked_job.id
+            queue = queueset.get(job_id)
+            queue.markAsBuilding(self.factory.makeBuilder())
+
+        recorder1, recorder2 = record_two_runs(
+            builders_homepage_render, create_build, 2)
+
+        self.assertThat(recorder2, HasQueryCount(Equals(recorder1.count)))

=== modified file 'lib/lp/soyuz/browser/tests/test_builder_views.py'
--- lib/lp/soyuz/browser/tests/test_builder_views.py	2011-12-09 10:30:27 +0000
+++ lib/lp/soyuz/browser/tests/test_builder_views.py	2011-12-16 12:18:28 +0000
@@ -18,10 +18,7 @@
 from canonical.launchpad.ftests import login
 from canonical.launchpad.webapp.servers import LaunchpadTestRequest
 from canonical.testing.layers import LaunchpadFunctionalLayer
-from lp.buildmaster.enums import (
-    BuildFarmJobType,
-    BuildStatus,
-    )
+from lp.buildmaster.enums import BuildFarmJobType
 from lp.buildmaster.interfaces.buildfarmjob import (
     IBuildFarmJobSource,
     InconsistentBuildFarmJobError,
@@ -159,28 +156,25 @@
             getSpecificJobs, [build_farm_job])
 
 
-class TestBuilderHistoryView(TestCaseWithFactory):
-
-    layer = LaunchpadFunctionalLayer
-
-    nb_objects = 2
-
-    def setUp(self):
-        super(TestBuilderHistoryView, self).setUp()
-        self.builder = self.factory.makeBuilder()
-
-    def createTranslationTemplateBuildWithBuilder(self):
+class BuildCreationMixin(object):
+
+    def createTranslationTemplateBuildWithBuilder(self, builder=None):
+        if builder is None:
+            builder = self.factory.makeBuilder()
         build_farm_job_source = getUtility(IBuildFarmJobSource)
         build_farm_job = build_farm_job_source.new(
             BuildFarmJobType.TRANSLATIONTEMPLATESBUILD)
         source = getUtility(ITranslationTemplatesBuildSource)
         branch = self.factory.makeBranch()
         build = source.create(build_farm_job, branch)
-        removeSecurityProxy(build).builder = self.builder
+        removeSecurityProxy(build).builder = builder
         self.addFakeBuildLog(build)
         return build
 
-    def createRecipeBuildWithBuilder(self, private_branch=False):
+    def createRecipeBuildWithBuilder(self, private_branch=False,
+                                     builder=None):
+        if builder is None:
+            builder = self.factory.makeBuilder()
         branch2 = self.factory.makeAnyBranch()
         branch1 = self.factory.makeAnyBranch()
         build = self.factory.makeSourcePackageRecipeBuild(
@@ -191,7 +185,7 @@
                 branch1.setPrivate(
                     True, getUtility(IPersonSet).getByEmail(ADMIN_EMAIL))
         Store.of(build).flush()
-        removeSecurityProxy(build).builder = self.builder
+        removeSecurityProxy(build).builder = builder
         self.addFakeBuildLog(build)
         return build
 
@@ -201,19 +195,31 @@
         import transaction
         transaction.commit()
 
-    def createBinaryPackageBuild(self, in_ppa=False):
+    def createBinaryPackageBuild(self, in_ppa=False, builder=None):
+        if builder is None:
+            builder = self.factory.makeBuilder()
         archive = None
         if in_ppa:
             archive = self.factory.makeArchive()
-        build = self.factory.makeBinaryPackageBuild(
-            archive=archive, status=BuildStatus.FULLYBUILT)
+        build = self.factory.makeBinaryPackageBuild(archive=archive)
         naked_build = removeSecurityProxy(build)
-        naked_build.builder = self.builder
+        naked_build.builder = builder
         naked_build.date_started = self.factory.getUniqueDate()
         naked_build.date_finished = self.factory.getUniqueDate()
         self.addFakeBuildLog(build)
         return build
 
+
+class TestBuilderHistoryView(TestCaseWithFactory, BuildCreationMixin):
+
+    layer = LaunchpadFunctionalLayer
+
+    nb_objects = 2
+
+    def setUp(self):
+        super(TestBuilderHistoryView, self).setUp()
+        self.builder = self.factory.makeBuilder()
+
     def test_build_history_queries_count_view_recipe_builds(self):
         # The builder's history view creation (i.e. the call to
         # view.setupBuildList) issues a constant number of queries
@@ -221,7 +227,8 @@
         def builder_history_render():
             create_initialized_view(self.builder, '+history').render()
         recorder1, recorder2 = record_two_runs(
-            builder_history_render, self.createRecipeBuildWithBuilder,
+            builder_history_render,
+            partial(self.createRecipeBuildWithBuilder, builder=self.builder),
             self.nb_objects)
 
         # XXX: rvb 2011-11-14 bug=890326: The only query remaining is the
@@ -237,7 +244,8 @@
         def builder_history_render():
             create_initialized_view(self.builder, '+history').render()
         recorder1, recorder2 = record_two_runs(
-            builder_history_render, self.createBinaryPackageBuild,
+            builder_history_render,
+            partial(self.createBinaryPackageBuild, builder=self.builder),
             self.nb_objects)
 
         self.assertThat(recorder2, HasQueryCount(Equals(recorder1.count)))
@@ -248,7 +256,7 @@
         def builder_history_render():
             create_initialized_view(self.builder, '+history').render()
         createBinaryPackageBuildInPPA = partial(
-            self.createBinaryPackageBuild, in_ppa=True)
+            self.createBinaryPackageBuild, in_ppa=True, builder=self.builder)
         recorder1, recorder2 = record_two_runs(
             builder_history_render, createBinaryPackageBuildInPPA,
             self.nb_objects)
@@ -262,21 +270,26 @@
             create_initialized_view(self.builder, '+history').render()
         recorder1, recorder2 = record_two_runs(
             builder_history_render,
-            self.createTranslationTemplateBuildWithBuilder, self.nb_objects)
+            partial(
+                self.createTranslationTemplateBuildWithBuilder,
+                builder=self.builder),
+            self.nb_objects)
 
         self.assertThat(recorder2, HasQueryCount(Equals(recorder1.count)))
 
     def test_build_history_private_build_view(self):
-        self.createRecipeBuildWithBuilder()
-        self.createRecipeBuildWithBuilder(private_branch=True)
+        self.createRecipeBuildWithBuilder(builder=self.builder)
+        self.createRecipeBuildWithBuilder(
+            private_branch=True, builder=self.builder)
         view = create_initialized_view(self.builder, '+history')
         view.setupBuildList()
 
         self.assertIn(None, view.complete_builds)
 
     def test_build_history_private_build_display(self):
-        self.createRecipeBuildWithBuilder()
-        self.createRecipeBuildWithBuilder(private_branch=True)
+        self.createRecipeBuildWithBuilder(builder=self.builder)
+        self.createRecipeBuildWithBuilder(
+            private_branch=True, builder=self.builder)
         view = create_initialized_view(self.builder, '+history')
         private_build_icon_matcher = soupmatchers.HTMLContains(
             soupmatchers.Tag(

=== modified file 'lib/lp/soyuz/interfaces/binarypackagebuild.py'
--- lib/lp/soyuz/interfaces/binarypackagebuild.py	2011-10-31 14:02:47 +0000
+++ lib/lp/soyuz/interfaces/binarypackagebuild.py	2011-12-16 12:18:28 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 # pylint: disable-msg=E0211,E0213
@@ -426,6 +426,11 @@
         asserted to not be None/empty.
         """
 
+    def preloadBuildsData(builds):
+        """Prefetch the data related to the builds.
+
+        """
+
 
 class IBuildRescoreForm(Interface):
     """Form for rescoring a build."""

=== modified file 'lib/lp/soyuz/model/binarypackagebuild.py'
--- lib/lp/soyuz/model/binarypackagebuild.py	2011-11-28 10:28:54 +0000
+++ lib/lp/soyuz/model/binarypackagebuild.py	2011-12-16 12:18:28 +0000
@@ -867,6 +867,31 @@
             return None
         return resulting_tuple[0]
 
+    def preloadBuildsData(self, builds):
+        # Circular imports.
+        from lp.soyuz.model.distroarchseries import (
+            DistroArchSeries
+            )
+        from lp.registry.model.distroseries import (
+            DistroSeries
+            )
+        from lp.registry.model.distribution import (
+            Distribution
+            )
+        from lp.soyuz.model.archive import Archive
+        from lp.registry.model.person import Person
+        self._prefetchBuildData(builds)
+        distro_arch_series = load_related(
+            DistroArchSeries, builds, ['distro_arch_series_id'])
+        package_builds = load_related(
+            PackageBuild, builds, ['package_build_id'])
+        archives = load_related(Archive, package_builds, ['archive_id'])
+        load_related(Person, archives, ['ownerID'])
+        distroseries = load_related(
+            DistroSeries, distro_arch_series, ['distroseriesID'])
+        load_related(
+            Distribution, distroseries, ['distributionID'])
+
     def getByBuildFarmJobs(self, build_farm_jobs):
         """See `ISpecificBuildFarmJobSource`."""
         if len(build_farm_jobs) == 0:
@@ -875,36 +900,13 @@
         build_farm_job_ids = [
             build_farm_job.id for build_farm_job in build_farm_jobs]
 
-        def eager_load(rows):
-            # Circular imports.
-            from lp.soyuz.model.distroarchseries import (
-                DistroArchSeries
-                )
-            from lp.registry.model.distroseries import (
-                DistroSeries
-                )
-            from lp.registry.model.distribution import (
-                Distribution
-                )
-            from lp.soyuz.model.archive import Archive
-            from lp.registry.model.person import Person
-            self._prefetchBuildData(rows)
-            distro_arch_series = load_related(
-                DistroArchSeries, rows, ['distro_arch_series_id'])
-            package_builds = load_related(
-                PackageBuild, rows, ['package_build_id'])
-            archives = load_related(Archive, package_builds, ['archive_id'])
-            load_related(Person, archives, ['ownerID'])
-            distroseries = load_related(
-                DistroSeries, distro_arch_series, ['distroseriesID'])
-            load_related(
-                Distribution, distroseries, ['distributionID'])
         resultset = Store.of(build_farm_jobs[0]).using(*clause_tables).find(
             BinaryPackageBuild,
             BinaryPackageBuild.package_build == PackageBuild.id,
             PackageBuild.build_farm_job == BuildFarmJob.id,
             BuildFarmJob.id.is_in(build_farm_job_ids))
-        return DecoratedResultSet(resultset, pre_iter_hook=eager_load)
+        return DecoratedResultSet(
+            resultset, pre_iter_hook=self.preloadBuildsData)
 
     def getPendingBuildsForArchSet(self, archseries):
         """See `IBinaryPackageBuildSet`."""

=== modified file 'lib/lp/soyuz/model/buildpackagejob.py'
--- lib/lp/soyuz/model/buildpackagejob.py	2011-07-06 19:31:32 +0000
+++ lib/lp/soyuz/model/buildpackagejob.py	2011-12-16 12:18:28 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
@@ -19,10 +19,12 @@
 from zope.interface import implements
 
 from canonical.database.sqlbase import sqlvalues
+from canonical.launchpad.interfaces.lpstorm import IStore
 from lp.buildmaster.enums import BuildStatus
+from lp.buildmaster.interfaces.builder import IBuilderSet
 from lp.buildmaster.model.buildfarmjob import BuildFarmJobOldDerived
 from lp.registry.interfaces.pocket import PackagePublishingPocket
-from lp.buildmaster.interfaces.builder import IBuilderSet
+from lp.services.database.bulk import load_related
 from lp.soyuz.enums import (
     ArchivePurpose,
     PackagePublishingStatus,
@@ -36,7 +38,6 @@
     SCORE_BY_POCKET,
     SCORE_BY_URGENCY,
     )
-
 from lp.soyuz.model.buildfarmbuildjob import BuildFarmBuildJob
 
 
@@ -63,6 +64,14 @@
         We override this to provide a delegate specific to package builds."""
         self.build_farm_job = BuildFarmBuildJob(self.build)
 
+    @staticmethod
+    def preloadBuildFarmJobs(jobs):
+        from lp.soyuz.model.binarypackagebuild import BinaryPackageBuild
+        return list(IStore(BinaryPackageBuild).find(
+            BinaryPackageBuild,
+            [BuildPackageJob.id.is_in([job.id for job in jobs]),
+             BuildPackageJob.build_id == BinaryPackageBuild.id]))
+
     def score(self):
         """See `IBuildPackageJob`."""
         # Define a table we'll use to calculate the score based on the time
@@ -157,6 +166,14 @@
         """See `IBuildFarmJob`."""
         return self.build.is_virtualized
 
+    @classmethod
+    def preloadJobsData(cls, jobs):
+        from lp.soyuz.model.binarypackagebuild import BinaryPackageBuild
+        from lp.services.job.model.job import Job
+        load_related(Job, jobs, ['job_id'])
+        builds = load_related(BinaryPackageBuild, jobs, ['build_id'])
+        getUtility(IBinaryPackageBuildSet).preloadBuildsData(list(builds))
+
     @staticmethod
     def addCandidateSelectionCriteria(processor, virtualized):
         """See `IBuildFarmJob`."""

=== modified file 'lib/lp/translations/model/translationtemplatesbuildjob.py'
--- lib/lp/translations/model/translationtemplatesbuildjob.py	2011-05-27 21:12:25 +0000
+++ lib/lp/translations/model/translationtemplatesbuildjob.py	2011-12-16 12:18:28 +0000
@@ -1,4 +1,4 @@
-# Copyright 2010 Canonical Ltd. This software is licensed under the
+# Copyright 2010-2011 Canonical Ltd. This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
@@ -40,6 +40,7 @@
     BranchJobDerived,
     BranchJobType,
     )
+from lp.services.database.bulk import load_related
 from lp.translations.interfaces.translationtemplatesbuild import (
     ITranslationTemplatesBuildSource,
     )
@@ -226,6 +227,31 @@
             return cls(branch_job)
 
     @classmethod
+    def getByJobs(cls, jobs):
+        """See `IBuildFarmJob`.
+
+        Overridden here to search via a BranchJob, rather than a Job.
+        """
+        store = IStore(BranchJob)
+        job_ids = [job.id for job in jobs]
+        branch_jobs = store.find(
+            BranchJob, BranchJob.jobID.is_in(job_ids))
+        return [cls(branch_job) for branch_job in branch_jobs]
+
+    @classmethod
+    def preloadJobsData(cls, jobs):
+        # Circular imports.
+        from lp.code.model.branch import Branch
+        from lp.registry.model.product import Product
+        from lp.code.model.branchcollection import GenericBranchCollection
+        from lp.services.job.model.job import Job
+        contexts = [job.context for job in jobs]
+        load_related(Job, contexts, ['jobID'])
+        branches = load_related(Branch, contexts, ['branchID'])
+        GenericBranchCollection.preloadDataForBranches(branches)
+        load_related(Product, branches, ['productID'])
+
+    @classmethod
     def getByBranch(cls, branch):
         """See `ITranslationTemplatesBuildJobSource`."""
         store = IStore(BranchJob)