← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~lifeless/launchpad/bug-661988 into lp:launchpad

 

Robert Collins has proposed merging lp:~lifeless/launchpad/bug-661988 into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  #661988 Timeout on Distribution:+bugs search
  https://bugs.launchpad.net/bugs/661988

For more details, see:
https://code.launchpad.net/~lifeless/launchpad/bug-661988/+merge/48740

Reduce the time taken for distro scope bug searches by 50% by using sequence-of-sets based eager loading rather than wide-query eager loading. As part of making this fit cleanly stop loading assignees during bug task search (they are not rendered so not needed) - which removes an unneeded query grabbing data we don't use. Bug assignees are still catered for for visibility - the assignee eager loading was unnecessary and a pessimism.
-- 
https://code.launchpad.net/~lifeless/launchpad/bug-661988/+merge/48740
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~lifeless/launchpad/bug-661988 into lp:launchpad.
=== modified file 'lib/lp/bugs/configure.zcml'
--- lib/lp/bugs/configure.zcml	2011-01-31 16:58:27 +0000
+++ lib/lp/bugs/configure.zcml	2011-02-06 22:16:20 +0000
@@ -26,11 +26,6 @@
     <lp:help-folder
         folder="help" type="lp.bugs.publisher.BugsLayer" />
 
-    <class class="lp.bugs.model.bugtask.BugTaskResultSet">
-        <allow interface="storm.zope.interfaces.IResultSet" />
-        <allow attributes="__getslice__" />
-    </class>
-
     <class
         class="lp.bugs.model.bugactivity.BugActivity">
         <allow

=== modified file 'lib/lp/bugs/model/bugtask.py'
--- lib/lp/bugs/model/bugtask.py	2011-02-01 15:14:57 +0000
+++ lib/lp/bugs/model/bugtask.py	2011-02-06 22:16:20 +0000
@@ -9,7 +9,6 @@
 
 __all__ = [
     'BugTaskDelta',
-    'BugTaskResultSet',
     'BugTaskToBugAdapter',
     'BugTaskMixin',
     'BugTask',
@@ -426,21 +425,6 @@
             self.bug.id, self.bugtargetdisplayname, self.bug.title)
 
 
-class BugTaskResultSet(DecoratedResultSet):
-    """Decorated results with cached assignees."""
-
-    def __iter__(self, *args, **kwargs):
-        """Iter with caching of assignees.
-
-        Assumes none of the decorators will need to access the assignees or
-        there is not benefit.
-        """
-        bugtasks = list(
-            super(BugTaskResultSet, self).__iter__(*args, **kwargs))
-        BugTaskSet._cache_assignees(bugtasks)
-        return iter(bugtasks)
-
-
 def BugTaskToBugAdapter(bugtask):
     """Adapt an IBugTask to an IBug."""
     return bugtask.bug
@@ -2371,16 +2355,17 @@
                 origin.append(table)
         return origin
 
-    def _search(self, resultrow, prejoins, params, *args):
+    def _search(self, resultrow, prejoins, pre_iter_hook, params, *args):
         """Return a Storm result set for the given search parameters.
 
         :param resultrow: The type of data returned by the query.
         :param prejoins: A sequence of Storm SQL row instances which are
             pre-joined.
+        :param pre_iter_hook: An optional pre-iteration hook used for eager
+            loading bug targets for list views.
         :param params: A BugTaskSearchParams instance.
         :param args: optional additional BugTaskSearchParams instances,
         """
-
         store = IStore(BugTask)
         [query, clauseTables, orderby, bugtask_decorator, join_tables,
         has_duplicate_results] = self.buildQuery(params)
@@ -2400,7 +2385,8 @@
                 decorator = bugtask_decorator
 
             resultset.order_by(orderby)
-            return DecoratedResultSet(resultset, result_decorator=decorator)
+            return DecoratedResultSet(resultset, result_decorator=decorator,
+                pre_iter_hook=pre_iter_hook)
 
         bugtask_fti = SQL('BugTask.fti')
         inner_resultrow = (BugTask, bugtask_fti)
@@ -2439,8 +2425,8 @@
 
         result = store.using(*origin).find(resultrow)
         result.order_by(orderby)
-        return BugTaskResultSet(
-            result, result_decorator=decorator)
+        return DecoratedResultSet(result, result_decorator=decorator,
+            pre_iter_hook=pre_iter_hook)
 
     def search(self, params, *args, **kwargs):
         """See `IBugTaskSet`.
@@ -2459,35 +2445,37 @@
         if _noprejoins:
             prejoins = []
             resultrow = BugTask
+            eager_load = None
         else:
             requested_joins = kwargs.get('prejoins', [])
+            # NB: We could save later work by predicting what sort of targets
+            # we might be interested in here, but as at any point we're dealing
+            # with relatively few results, this is likely to be a small win.
             prejoins = [
-                (Bug, LeftJoin(Bug, BugTask.bug == Bug.id)),
-                (Product, LeftJoin(Product, BugTask.product == Product.id)),
-                (SourcePackageName,
-                 LeftJoin(
-                     SourcePackageName,
-                     BugTask.sourcepackagename == SourcePackageName.id)),
+                (Bug, LeftJoin(Bug, BugTask.bug == Bug.id))
                 ] + requested_joins
-            resultrow = (BugTask, Bug, Product, SourcePackageName)
+            def eager_load(results):
+                product_ids = set([row[0].productID for row in results])
+                product_ids.discard(None)
+                pkgname_ids = set(
+                    [row[0].sourcepackagenameID for row in results])
+                pkgname_ids.discard(None)
+                store = IStore(BugTask)
+                if product_ids:
+                    list(store.find(Product, Product.id.is_in(product_ids)))
+                if pkgname_ids:
+                    list(store.find(SourcePackageName,
+                        SourcePackageName.id.is_in(pkgname_ids)))
+            resultrow = (BugTask, Bug)
             additional_result_objects = [
                 table for table, join in requested_joins
                 if table not in resultrow]
             resultrow = resultrow + tuple(additional_result_objects)
-        return self._search(resultrow, prejoins, params, *args)
+        return self._search(resultrow, prejoins, eager_load, params, *args)
 
     def searchBugIds(self, params):
         """See `IBugTaskSet`."""
-        return self._search(BugTask.bugID, [], params).result_set
-
-    @staticmethod
-    def _cache_assignees(rows):
-        assignee_ids = set(
-            bug_task.assigneeID for bug_task in rows)
-        assignees = getUtility(IPersonSet).getPrecachedPersonsFromIDs(
-            assignee_ids, need_validity=True)
-        # Execute query to load storm cache.
-        list(assignees)
+        return self._search(BugTask.bugID, [], None, params).result_set
 
     def getPrecachedNonConjoinedBugTasks(self, user, milestone):
         """See `IBugTaskSet`."""
@@ -2495,10 +2483,7 @@
             user, milestone=milestone,
             orderby=['status', '-importance', 'id'],
             omit_dupes=True, exclude_conjoined_tasks=True)
-        non_conjoined_slaves = self.search(params)
-
-        return DecoratedResultSet(
-            non_conjoined_slaves, pre_iter_hook=BugTaskSet._cache_assignees)
+        return self.search(params)
 
     def createTask(self, bug, owner, product=None, productseries=None,
                    distribution=None, distroseries=None,

=== modified file 'lib/lp/bugs/tests/test_bugtask_search.py'
--- lib/lp/bugs/tests/test_bugtask_search.py	2011-02-01 15:14:57 +0000
+++ lib/lp/bugs/tests/test_bugtask_search.py	2011-02-06 22:16:20 +0000
@@ -34,10 +34,7 @@
     BugTaskStatus,
     IBugTaskSet,
     )
-from lp.bugs.model.bugtask import (
-    BugTask,
-    BugTaskResultSet,
-    )
+from lp.bugs.model.bugtask import BugTask
 from lp.registry.interfaces.distribution import IDistribution
 from lp.registry.interfaces.distributionsourcepackage import (
     IDistributionSourcePackage,
@@ -968,8 +965,11 @@
             found_tasks = self.runSearch(
                 params,
                 prejoins=[(Person, Join(Person, BugTask.owner == Person.id))])
+            # More than one query may have been performed
+            search_count = recorder.count
+            # Accessing the owner does not trigger more queries.
             found_tasks[0].owner
-            self.assertThat(recorder, HasQueryCount(Equals(1)))
+            self.assertThat(recorder, HasQueryCount(Equals(search_count)))
 
 
 class NoPreloadBugtaskTargets(MultipleParams):
@@ -994,65 +994,6 @@
         return [bugtask.bug.id for bugtask in expected_bugtasks]
 
 
-class TestCachingAssignees(TestCaseWithFactory):
-    """Searching bug tasks should pre-cache the bugtask assignees."""
-
-    layer = LaunchpadFunctionalLayer
-
-    def setUp(self):
-        super(TestCachingAssignees, self).setUp()
-        self.owner = self.factory.makePerson(name="bug-owner")
-        with person_logged_in(self.owner):
-            # Create some bugs with assigned bugtasks.
-            self.bug = self.factory.makeBug(
-                owner=self.owner)
-            self.bug.default_bugtask.transitionToAssignee(
-                self.factory.makePerson())
-
-            for i in xrange(9):
-                bugtask = self.factory.makeBugTask(
-                    bug=self.bug)
-                bugtask.transitionToAssignee(
-                    self.factory.makePerson())
-            self.bug.setPrivate(True, self.owner)
-
-    def _get_bug_tasks(self):
-        """Get the bugtasks for a bug.
-
-        This method is used rather than Bug.bugtasks since the later does
-        prejoining which would spoil the test.
-        """
-        store = Store.of(self.bug)
-        return store.find(
-            BugTask, BugTask.bug == self.bug)
-
-    def test_no_precaching(self):
-        bugtasks = self._get_bug_tasks()
-        Store.of(self.bug).invalidate()
-        with person_logged_in(self.owner):
-            with StormStatementRecorder() as recorder:
-                # Access the assignees to trigger a query.
-                names = [bugtask.assignee.name for bugtask in bugtasks]
-                # With no caching, the number of queries is roughly twice the
-                # number of bugtasks.
-                query_count_floor = len(names) * 2
-                self.assertThat(
-                    recorder, HasQueryCount(Not(LessThan(query_count_floor))))
-
-    def test_precaching(self):
-        bugtasks = self._get_bug_tasks()
-        Store.of(self.bug).invalidate()
-        with person_logged_in(self.owner):
-            with StormStatementRecorder() as recorder:
-                bugtasks = BugTaskResultSet(bugtasks)
-                # Access the assignees to trigger a query if not properly
-                # cached.
-                [bugtask.assignee.name for bugtask in bugtasks]
-                # With caching the number of queries is two, one for the
-                # bugtask and one for all of the assignees at once.
-                self.assertThat(recorder, HasQueryCount(Equals(2)))
-
-
 def test_suite():
     suite = unittest.TestSuite()
     loader = unittest.TestLoader()