Merge lp:~edwin-grubbs/launchpad/bug-436974-series-timeouts into lp:launchpad
- bug-436974-series-timeouts
- Merge into devel
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Michael Nelson (community) | code | Approve | |
Review via email: mp+13397@code.launchpad.net |
Commit message
Description of the change
Edwin Grubbs (edwin-grubbs) wrote : | # |
Michael Nelson (michael.nelson) wrote : | # |
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/
> --- lib/lp/
> +++ lib/lp/
> @@ -2021,22 +2021,37 @@
> """See `IBugTaskSet`."""
> bug_privacy_filter = get_bug_
> if bug_privacy_filter != "":
> - bug_privacy_filter = ' AND ' + bug_privacy_filter
> + bug_privacy_filter = 'AND ' + bug_privacy_filter
> cur = cursor()
> - condition = """
> - (BugTask.
> - OR Milestone.
> - """ % sqlvalues(
> +
> + # 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.
> + %(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.
> + AND Milestone.
> + %(privacy)s
> + ) AS subquery
> + GROUP BY status
> + """ % dict(series=
> + privacy=
> +
> cur.execute(query)
> return cur.fetchall()
>
>
> === modified file 'lib/lp/
> --- lib/lp/
> +++ lib/lp/
> @@ -558,14 +558,28 @@
> def __init__(self, product):
> self.product = product
> self.serieses = []
> - self.series_by_id = {}
> + for series in self.product.
> + series_
> + self.serieses.
> + if self.product.
> + self.developmen
>
> - def...
Edwin Grubbs (edwin-grubbs) wrote : | # |
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/
>> --- lib/lp/
>> +++ lib/lp/
>> @@ -2021,22 +2021,37 @@
>> """See `IBugTaskSet`."""
>> bug_privacy_filter = get_bug_
>> if bug_privacy_filter != "":
>> - bug_privacy_filter = ' AND ' + bug_privacy_filter
>> + bug_privacy_filter = 'AND ' + bug_privacy_filter
>> cur = cursor()
>> - condition = """
>> - (BugTask.
>> - OR Milestone.
>> - """ % sqlvalues(
>> +
>> + # 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.
>> + %(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.
>> + AND Milestone.
>> + %(privacy)s
>> + ) AS subquery
>> + GROUP BY status
>> + """ % dict(series=
>> + privacy=
>> +
>> cur.execute(query)
>> return cur.fetchall()
>>
>>
>> === modified file 'lib/lp/
>> --- lib/lp/
>> +++ lib/lp/
>> @@ -558,14 +558,28 @@
>> def __...
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/
> --- lib/lp/
> +++ lib/lp/
> @@ -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_delegates = product.
> files = getUtility(
> - decorated_releases)
> - for decorated_release in decorated_releases:
> - decorated_
> + release_delegates)
> + for release_delegate in release_delegates:
> + release_
> for file in files:
> id = file.productrel
> - decorated_release = product.
> - decorated_
> + release_delegate = product.
> + release_
>
> + # self._files was set above, since self is actually in the
> + # decorated_releases variable.
Should be release_delegates now ^^^^ to avoid confusion :)
Preview Diff
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. |
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. getStatusCounts ForProductSerie s(), which
is called for every active series in the product.
Tests
-----
./bin/test -vv -t xx-productserie s-series. txt
Demo and Q/A
------------
* Open http:// launchpad. dev/bzr/ +series
* This should not timeout on staging or edge.