Merge lp:~edwin-grubbs/launchpad/bug-436974-series-timeouts into lp:launchpad

Proposed by Edwin Grubbs
Status: Merged
Approved by: Edwin Grubbs
Approved revision: no longer in the source branch.
Merged at revision: not available
Proposed branch: lp:~edwin-grubbs/launchpad/bug-436974-series-timeouts
Merge into: lp:launchpad
Diff against target: 217 lines
2 files modified
lib/lp/bugs/model/bugtask.py (+29/-14)
lib/lp/registry/browser/product.py (+57/-59)
To merge this branch: bzr merge lp:~edwin-grubbs/launchpad/bug-436974-series-timeouts
Reviewer Review Type Date Requested Status
Michael Nelson (community) code Approve
Review via email: mp+13397@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Edwin Grubbs (edwin-grubbs) wrote :

Summary
-------

Optimize the $productseries/+series page.

Implementation details
----------------------

Eliminated a query for product release files by making it lazy load,
since that data is not used on the +series page.

Improved the query for BugTask.getStatusCountsForProductSeries(), which
is called for every active series in the product.

Tests
-----

./bin/test -vv -t xx-productseries-series.txt

Demo and Q/A
------------

* Open http://launchpad.dev/bzr/+series
  * This should not timeout on staging or edge.

Revision history for this message
Michael Nelson (michael.nelson) wrote :
Download full text (10.5 KiB)

Hi Edwin,

I've marked this as Needs Fixing because there are a few things that look like oversights - but I might be wrong. Other than that, just a few comments. See below.

Thanks!

> === modified file 'lib/lp/bugs/model/bugtask.py'
> --- lib/lp/bugs/model/bugtask.py 2009-10-01 19:50:54 +0000
> +++ lib/lp/bugs/model/bugtask.py 2009-10-15 09:32:02 +0000
> @@ -2021,22 +2021,37 @@
> """See `IBugTaskSet`."""
> bug_privacy_filter = get_bug_privacy_filter(user)
> if bug_privacy_filter != "":
> - bug_privacy_filter = ' AND ' + bug_privacy_filter
> + bug_privacy_filter = 'AND ' + bug_privacy_filter
> cur = cursor()
> - condition = """
> - (BugTask.productseries = %s
> - OR Milestone.productseries = %s)
> - """ % sqlvalues(product_series, product_series)
> +
> + # The union is actually much faster than a LEFT JOIN with the
> + # Milestone table, since postgres optimizes it to perform index
> + # scans instead of sequential scans on the BugTask table.

Wow - was there any way you were able to verify that locally? That's
really interesting.

> query = """
> - SELECT BugTask.status, count(*)
> - FROM BugTask
> - JOIN Bug ON BugTask.bug = Bug.id
> - LEFT JOIN Milestone ON BugTask.milestone = Milestone.id
> - WHERE
> - %s
> - %s
> - GROUP BY BugTask.status
> - """ % (condition, bug_privacy_filter)
> + SELECT status, count(*)
> + FROM (
> + SELECT BugTask.status
> + FROM BugTask
> + JOIN Bug ON BugTask.bug = Bug.id
> + WHERE
> + BugTask.productseries = %(series)s
> + %(privacy)s
> +
> + UNION ALL
> +
> + SELECT BugTask.status
> + FROM BugTask
> + JOIN Bug ON BugTask.bug = Bug.id
> + JOIN Milestone ON BugTask.milestone = Milestone.id
> + WHERE
> + BugTask.productseries IS NULL
> + AND Milestone.productseries = %(series)s
> + %(privacy)s
> + ) AS subquery
> + GROUP BY status
> + """ % dict(series=quote(product_series),
> + privacy=bug_privacy_filter)
> +
> cur.execute(query)
> return cur.fetchall()
>
>
> === modified file 'lib/lp/registry/browser/product.py'
> --- lib/lp/registry/browser/product.py 2009-10-08 15:54:09 +0000
> +++ lib/lp/registry/browser/product.py 2009-10-15 09:32:02 +0000
> @@ -558,14 +558,28 @@
> def __init__(self, product):
> self.product = product
> self.serieses = []
> - self.series_by_id = {}
> + for series in self.product.serieses:
> + series_with_releases = SeriesWithReleases(series, parent=self)
> + self.serieses.append(series_with_releases)
> + if self.product.development_focus == series:
> + self.development_focus = series_with_releases
>
> - def...

review: Needs Fixing (code)
Revision history for this message
Edwin Grubbs (edwin-grubbs) wrote :
Download full text (17.4 KiB)

Hi Michael,

Thanks for the review. Incremental diff included.

On Thu, Oct 15, 2009 at 5:51 AM, Michael Nelson
<email address hidden> wrote:
> Review: Needs Fixing code
> Hi Edwin,
>
> I've marked this as Needs Fixing because there are a few things that look like oversights - but I might be wrong. Other than that, just a few comments. See below.
>
> Thanks!
>
>> === modified file 'lib/lp/bugs/model/bugtask.py'
>> --- lib/lp/bugs/model/bugtask.py      2009-10-01 19:50:54 +0000
>> +++ lib/lp/bugs/model/bugtask.py      2009-10-15 09:32:02 +0000
>> @@ -2021,22 +2021,37 @@
>>          """See `IBugTaskSet`."""
>>          bug_privacy_filter = get_bug_privacy_filter(user)
>>          if bug_privacy_filter != "":
>> -            bug_privacy_filter = ' AND ' + bug_privacy_filter
>> +            bug_privacy_filter = 'AND ' + bug_privacy_filter
>>          cur = cursor()
>> -        condition = """
>> -            (BugTask.productseries = %s
>> -                 OR Milestone.productseries = %s)
>> -            """ % sqlvalues(product_series, product_series)
>> +
>> +        # The union is actually much faster than a LEFT JOIN with the
>> +        # Milestone table, since postgres optimizes it to perform index
>> +        # scans instead of sequential scans on the BugTask table.
>
> Wow - was there any way you were able to verify that locally? That's
> really interesting.

It actually runs a little bit slower locally, but on staging's db it runs 50
to 170 times as fast. I can show you the output of EXPLAIN ANALYZE if
you are interested.

>>          query = """
>> -            SELECT BugTask.status, count(*)
>> -            FROM BugTask
>> -                JOIN Bug ON BugTask.bug = Bug.id
>> -                LEFT JOIN Milestone ON BugTask.milestone = Milestone.id
>> -            WHERE
>> -                %s
>> -                %s
>> -            GROUP BY BugTask.status
>> -            """ % (condition, bug_privacy_filter)
>> +            SELECT status, count(*)
>> +            FROM (
>> +                SELECT BugTask.status
>> +                FROM BugTask
>> +                    JOIN Bug ON BugTask.bug = Bug.id
>> +                WHERE
>> +                    BugTask.productseries = %(series)s
>> +                    %(privacy)s
>> +
>> +                UNION ALL
>> +
>> +                SELECT BugTask.status
>> +                FROM BugTask
>> +                    JOIN Bug ON BugTask.bug = Bug.id
>> +                    JOIN Milestone ON BugTask.milestone = Milestone.id
>> +                WHERE
>> +                    BugTask.productseries IS NULL
>> +                    AND Milestone.productseries = %(series)s
>> +                    %(privacy)s
>> +                ) AS subquery
>> +            GROUP BY status
>> +            """ % dict(series=quote(product_series),
>> +                       privacy=bug_privacy_filter)
>> +
>>          cur.execute(query)
>>          return cur.fetchall()
>>
>>
>> === modified file 'lib/lp/registry/browser/product.py'
>> --- lib/lp/registry/browser/product.py        2009-10-08 15:54:09 +0000
>> +++ lib/lp/registry/browser/product.py        2009-10-15 09:32:02 +0000
>> @@ -558,14 +558,28 @@
>>      def __...

Revision history for this message
Michael Nelson (michael.nelson) wrote :

> Hi Michael,
>
> Thanks for the review. Incremental diff included.
>

Thanks for making me laugh with your comments :)

Just a typo in your comment below. Great work!

> === modified file 'lib/lp/registry/browser/product.py'
> --- lib/lp/registry/browser/product.py 2009-10-13 02:44:40 +0000
> +++ lib/lp/registry/browser/product.py 2009-10-15 15:38:50 +0000
> @@ -643,22 +640,25 @@
> self.parent = parent
> self._files = None
>
> - @cachedproperty
> + @property
> def files(self):
> + """Cache the release files for all the releases in the product."""
> if self._files is None:
> # Get all of the files for all of the releases. The query
> # returns all releases sorted properly.
> product = self.parent.parent
> - decorated_releases = product.release_by_id.values()
> + release_delegates = product.release_by_id.values()
> files = getUtility(IProductReleaseSet).getFilesForReleases(
> - decorated_releases)
> - for decorated_release in decorated_releases:
> - decorated_release._files = []
> + release_delegates)
> + for release_delegate in release_delegates:
> + release_delegate._files = []
> for file in files:
> id = file.productrelease.id
> - decorated_release = product.release_by_id[id]
> - decorated_release._files.append(file)
> + release_delegate = product.release_by_id[id]
> + release_delegate._files.append(file)
>
> + # self._files was set above, since self is actually in the
> + # decorated_releases variable.

Should be release_delegates now ^^^^ to avoid confusion :)

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/bugs/model/bugtask.py'
2--- lib/lp/bugs/model/bugtask.py 2009-10-01 19:50:54 +0000
3+++ lib/lp/bugs/model/bugtask.py 2009-10-16 14:08:13 +0000
4@@ -2021,22 +2021,37 @@
5 """See `IBugTaskSet`."""
6 bug_privacy_filter = get_bug_privacy_filter(user)
7 if bug_privacy_filter != "":
8- bug_privacy_filter = ' AND ' + bug_privacy_filter
9+ bug_privacy_filter = 'AND ' + bug_privacy_filter
10 cur = cursor()
11- condition = """
12- (BugTask.productseries = %s
13- OR Milestone.productseries = %s)
14- """ % sqlvalues(product_series, product_series)
15+
16+ # The union is actually much faster than a LEFT JOIN with the
17+ # Milestone table, since postgres optimizes it to perform index
18+ # scans instead of sequential scans on the BugTask table.
19 query = """
20- SELECT BugTask.status, count(*)
21- FROM BugTask
22- JOIN Bug ON BugTask.bug = Bug.id
23- LEFT JOIN Milestone ON BugTask.milestone = Milestone.id
24- WHERE
25- %s
26- %s
27- GROUP BY BugTask.status
28- """ % (condition, bug_privacy_filter)
29+ SELECT status, count(*)
30+ FROM (
31+ SELECT BugTask.status
32+ FROM BugTask
33+ JOIN Bug ON BugTask.bug = Bug.id
34+ WHERE
35+ BugTask.productseries = %(series)s
36+ %(privacy)s
37+
38+ UNION ALL
39+
40+ SELECT BugTask.status
41+ FROM BugTask
42+ JOIN Bug ON BugTask.bug = Bug.id
43+ JOIN Milestone ON BugTask.milestone = Milestone.id
44+ WHERE
45+ BugTask.productseries IS NULL
46+ AND Milestone.productseries = %(series)s
47+ %(privacy)s
48+ ) AS subquery
49+ GROUP BY status
50+ """ % dict(series=quote(product_series),
51+ privacy=bug_privacy_filter)
52+
53 cur.execute(query)
54 return cur.fetchall()
55
56
57=== modified file 'lib/lp/registry/browser/product.py'
58--- lib/lp/registry/browser/product.py 2009-10-08 15:54:09 +0000
59+++ lib/lp/registry/browser/product.py 2009-10-16 14:08:13 +0000
60@@ -549,8 +549,9 @@
61 cached locally and simply returned.
62 """
63
64- # These need to be predeclared to avoid delegates taking them
65- # over.
66+ # `serieses` and `development_focus` need to be declared as class
67+ # attributes so that this class will not delegate the actual instance
68+ # variables to self.product, which would bypass the caching.
69 serieses = None
70 development_focus = None
71 delegates(IProduct, 'product')
72@@ -558,17 +559,24 @@
73 def __init__(self, product):
74 self.product = product
75 self.serieses = []
76- self.series_by_id = {}
77-
78- def setSeries(self, serieses):
79- """Set the serieses to the provided collection."""
80- self.serieses = serieses
81- self.series_by_id = dict(
82- (series.id, series) for series in self.serieses)
83-
84- def getSeriesById(self, id):
85- """Look up and return a ProductSeries by id."""
86- return self.series_by_id[id]
87+ for series in self.product.serieses:
88+ series_with_releases = SeriesWithReleases(series, parent=self)
89+ self.serieses.append(series_with_releases)
90+ if self.product.development_focus == series:
91+ self.development_focus = series_with_releases
92+
93+ # Get all of the releases for all of the serieses in a single
94+ # query. The query sorts the releases properly so we know the
95+ # resulting list is sorted correctly.
96+ series_by_id = dict((series.id, series) for series in self.serieses)
97+ self.release_by_id = {}
98+ milestones_and_releases = list(
99+ self.product.getMilestonesAndReleases())
100+ for milestone, release in milestones_and_releases:
101+ series = series_by_id[milestone.productseries.id]
102+ release_delegate = ReleaseWithFiles(release, parent=series)
103+ series.addRelease(release_delegate)
104+ self.release_by_id[release.id] = release_delegate
105
106
107 class SeriesWithReleases:
108@@ -579,13 +587,17 @@
109 cached locally and simply returned.
110 """
111
112- # These need to be predeclared to avoid delegates taking them
113- # over.
114+ # `parent` and `releases` need to be declared as class attributes so that
115+ # this class will not delegate the actual instance variables to
116+ # self.series, which would bypass the caching for self.releases and would
117+ # raise an AttributeError for self.parent.
118+ parent = None
119 releases = None
120 delegates(IProductSeries, 'series')
121
122- def __init__(self, series):
123+ def __init__(self, series, parent):
124 self.series = series
125+ self.parent = parent
126 self.releases = []
127
128 def addRelease(self, release):
129@@ -617,17 +629,37 @@
130 cached locally and simply returned.
131 """
132
133- # These need to be predeclared to avoid delegates taking them
134- # over.
135- files = None
136+ # `parent` needs to be declared as class attributes so that
137+ # this class will not delegate the actual instance variables to
138+ # self.release, which would raise an AttributeError.
139+ parent = None
140 delegates(IProductRelease, 'release')
141
142- def __init__(self, release):
143+ def __init__(self, release, parent):
144 self.release = release
145- self.files = []
146-
147- def addFile(self, file):
148- self.files.append(file)
149+ self.parent = parent
150+ self._files = None
151+
152+ @property
153+ def files(self):
154+ """Cache the release files for all the releases in the product."""
155+ if self._files is None:
156+ # Get all of the files for all of the releases. The query
157+ # returns all releases sorted properly.
158+ product = self.parent.parent
159+ release_delegates = product.release_by_id.values()
160+ files = getUtility(IProductReleaseSet).getFilesForReleases(
161+ release_delegates)
162+ for release_delegate in release_delegates:
163+ release_delegate._files = []
164+ for file in files:
165+ id = file.productrelease.id
166+ release_delegate = product.release_by_id[id]
167+ release_delegate._files.append(file)
168+
169+ # self._files was set above, since self is actually in the
170+ # release_delegates variable.
171+ return self._files
172
173 @property
174 def name_with_codename(self):
175@@ -653,41 +685,7 @@
176 Decorated classes are created, and they contain cached data
177 obtained with a few queries rather than many iterated queries.
178 """
179- # Create the decorated product and set the list of series.
180- original_product = self.context
181-
182- product_with_series = ProductWithSeries(original_product)
183- serieses = []
184- for series in original_product.serieses:
185- series_with_releases = SeriesWithReleases(series)
186- serieses.append(series_with_releases)
187- if original_product.development_focus == series:
188- product_with_series.development_focus = series_with_releases
189-
190- product_with_series.setSeries(serieses)
191-
192- # Get all of the releases for all of the serieses in a single
193- # query. The query sorts the releases properly so we know the
194- # resulting list is sorted correctly.
195- release_by_id = {}
196- milestones_and_releases = list(
197- original_product.getMilestonesAndReleases())
198- for milestone, release in milestones_and_releases:
199- series = product_with_series.getSeriesById(
200- milestone.productseries.id)
201- decorated_release = ReleaseWithFiles(release)
202- series.addRelease(decorated_release)
203- release_by_id[release.id] = decorated_release
204-
205- # Get all of the files for all of the releases. The query
206- # returns all releases sorted properly.
207- releases = [release for milestone, release in milestones_and_releases]
208- files = getUtility(IProductReleaseSet).getFilesForReleases(releases)
209- for file in files:
210- release = release_by_id[file.productrelease.id]
211- release.addFile(file)
212-
213- return product_with_series
214+ return ProductWithSeries(self.context)
215
216 def deleteFiles(self, releases):
217 """Delete the selected files from the set of releases.