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 | 2021 | """See `IBugTaskSet`.""" | 2021 | """See `IBugTaskSet`.""" |
6 | 2022 | bug_privacy_filter = get_bug_privacy_filter(user) | 2022 | bug_privacy_filter = get_bug_privacy_filter(user) |
7 | 2023 | if bug_privacy_filter != "": | 2023 | if bug_privacy_filter != "": |
9 | 2024 | bug_privacy_filter = ' AND ' + bug_privacy_filter | 2024 | bug_privacy_filter = 'AND ' + bug_privacy_filter |
10 | 2025 | cur = cursor() | 2025 | cur = cursor() |
15 | 2026 | condition = """ | 2026 | |
16 | 2027 | (BugTask.productseries = %s | 2027 | # The union is actually much faster than a LEFT JOIN with the |
17 | 2028 | OR Milestone.productseries = %s) | 2028 | # Milestone table, since postgres optimizes it to perform index |
18 | 2029 | """ % sqlvalues(product_series, product_series) | 2029 | # scans instead of sequential scans on the BugTask table. |
19 | 2030 | query = """ | 2030 | query = """ |
29 | 2031 | SELECT BugTask.status, count(*) | 2031 | SELECT status, count(*) |
30 | 2032 | FROM BugTask | 2032 | FROM ( |
31 | 2033 | JOIN Bug ON BugTask.bug = Bug.id | 2033 | SELECT BugTask.status |
32 | 2034 | LEFT JOIN Milestone ON BugTask.milestone = Milestone.id | 2034 | FROM BugTask |
33 | 2035 | WHERE | 2035 | JOIN Bug ON BugTask.bug = Bug.id |
34 | 2036 | %s | 2036 | WHERE |
35 | 2037 | %s | 2037 | BugTask.productseries = %(series)s |
36 | 2038 | GROUP BY BugTask.status | 2038 | %(privacy)s |
37 | 2039 | """ % (condition, bug_privacy_filter) | 2039 | |
38 | 2040 | UNION ALL | ||
39 | 2041 | |||
40 | 2042 | SELECT BugTask.status | ||
41 | 2043 | FROM BugTask | ||
42 | 2044 | JOIN Bug ON BugTask.bug = Bug.id | ||
43 | 2045 | JOIN Milestone ON BugTask.milestone = Milestone.id | ||
44 | 2046 | WHERE | ||
45 | 2047 | BugTask.productseries IS NULL | ||
46 | 2048 | AND Milestone.productseries = %(series)s | ||
47 | 2049 | %(privacy)s | ||
48 | 2050 | ) AS subquery | ||
49 | 2051 | GROUP BY status | ||
50 | 2052 | """ % dict(series=quote(product_series), | ||
51 | 2053 | privacy=bug_privacy_filter) | ||
52 | 2054 | |||
53 | 2040 | cur.execute(query) | 2055 | cur.execute(query) |
54 | 2041 | return cur.fetchall() | 2056 | return cur.fetchall() |
55 | 2042 | 2057 | ||
56 | 2043 | 2058 | ||
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 | 549 | cached locally and simply returned. | 549 | cached locally and simply returned. |
62 | 550 | """ | 550 | """ |
63 | 551 | 551 | ||
66 | 552 | # These need to be predeclared to avoid delegates taking them | 552 | # `serieses` and `development_focus` need to be declared as class |
67 | 553 | # over. | 553 | # attributes so that this class will not delegate the actual instance |
68 | 554 | # variables to self.product, which would bypass the caching. | ||
69 | 554 | serieses = None | 555 | serieses = None |
70 | 555 | development_focus = None | 556 | development_focus = None |
71 | 556 | delegates(IProduct, 'product') | 557 | delegates(IProduct, 'product') |
72 | @@ -558,17 +559,24 @@ | |||
73 | 558 | def __init__(self, product): | 559 | def __init__(self, product): |
74 | 559 | self.product = product | 560 | self.product = product |
75 | 560 | self.serieses = [] | 561 | self.serieses = [] |
87 | 561 | self.series_by_id = {} | 562 | for series in self.product.serieses: |
88 | 562 | 563 | series_with_releases = SeriesWithReleases(series, parent=self) | |
89 | 563 | def setSeries(self, serieses): | 564 | self.serieses.append(series_with_releases) |
90 | 564 | """Set the serieses to the provided collection.""" | 565 | if self.product.development_focus == series: |
91 | 565 | self.serieses = serieses | 566 | self.development_focus = series_with_releases |
92 | 566 | self.series_by_id = dict( | 567 | |
93 | 567 | (series.id, series) for series in self.serieses) | 568 | # Get all of the releases for all of the serieses in a single |
94 | 568 | 569 | # query. The query sorts the releases properly so we know the | |
95 | 569 | def getSeriesById(self, id): | 570 | # resulting list is sorted correctly. |
96 | 570 | """Look up and return a ProductSeries by id.""" | 571 | series_by_id = dict((series.id, series) for series in self.serieses) |
97 | 571 | return self.series_by_id[id] | 572 | self.release_by_id = {} |
98 | 573 | milestones_and_releases = list( | ||
99 | 574 | self.product.getMilestonesAndReleases()) | ||
100 | 575 | for milestone, release in milestones_and_releases: | ||
101 | 576 | series = series_by_id[milestone.productseries.id] | ||
102 | 577 | release_delegate = ReleaseWithFiles(release, parent=series) | ||
103 | 578 | series.addRelease(release_delegate) | ||
104 | 579 | self.release_by_id[release.id] = release_delegate | ||
105 | 572 | 580 | ||
106 | 573 | 581 | ||
107 | 574 | class SeriesWithReleases: | 582 | class SeriesWithReleases: |
108 | @@ -579,13 +587,17 @@ | |||
109 | 579 | cached locally and simply returned. | 587 | cached locally and simply returned. |
110 | 580 | """ | 588 | """ |
111 | 581 | 589 | ||
114 | 582 | # These need to be predeclared to avoid delegates taking them | 590 | # `parent` and `releases` need to be declared as class attributes so that |
115 | 583 | # over. | 591 | # this class will not delegate the actual instance variables to |
116 | 592 | # self.series, which would bypass the caching for self.releases and would | ||
117 | 593 | # raise an AttributeError for self.parent. | ||
118 | 594 | parent = None | ||
119 | 584 | releases = None | 595 | releases = None |
120 | 585 | delegates(IProductSeries, 'series') | 596 | delegates(IProductSeries, 'series') |
121 | 586 | 597 | ||
123 | 587 | def __init__(self, series): | 598 | def __init__(self, series, parent): |
124 | 588 | self.series = series | 599 | self.series = series |
125 | 600 | self.parent = parent | ||
126 | 589 | self.releases = [] | 601 | self.releases = [] |
127 | 590 | 602 | ||
128 | 591 | def addRelease(self, release): | 603 | def addRelease(self, release): |
129 | @@ -617,17 +629,37 @@ | |||
130 | 617 | cached locally and simply returned. | 629 | cached locally and simply returned. |
131 | 618 | """ | 630 | """ |
132 | 619 | 631 | ||
136 | 620 | # These need to be predeclared to avoid delegates taking them | 632 | # `parent` needs to be declared as class attributes so that |
137 | 621 | # over. | 633 | # this class will not delegate the actual instance variables to |
138 | 622 | files = None | 634 | # self.release, which would raise an AttributeError. |
139 | 635 | parent = None | ||
140 | 623 | delegates(IProductRelease, 'release') | 636 | delegates(IProductRelease, 'release') |
141 | 624 | 637 | ||
143 | 625 | def __init__(self, release): | 638 | def __init__(self, release, parent): |
144 | 626 | self.release = release | 639 | self.release = release |
149 | 627 | self.files = [] | 640 | self.parent = parent |
150 | 628 | 641 | self._files = None | |
151 | 629 | def addFile(self, file): | 642 | |
152 | 630 | self.files.append(file) | 643 | @property |
153 | 644 | def files(self): | ||
154 | 645 | """Cache the release files for all the releases in the product.""" | ||
155 | 646 | if self._files is None: | ||
156 | 647 | # Get all of the files for all of the releases. The query | ||
157 | 648 | # returns all releases sorted properly. | ||
158 | 649 | product = self.parent.parent | ||
159 | 650 | release_delegates = product.release_by_id.values() | ||
160 | 651 | files = getUtility(IProductReleaseSet).getFilesForReleases( | ||
161 | 652 | release_delegates) | ||
162 | 653 | for release_delegate in release_delegates: | ||
163 | 654 | release_delegate._files = [] | ||
164 | 655 | for file in files: | ||
165 | 656 | id = file.productrelease.id | ||
166 | 657 | release_delegate = product.release_by_id[id] | ||
167 | 658 | release_delegate._files.append(file) | ||
168 | 659 | |||
169 | 660 | # self._files was set above, since self is actually in the | ||
170 | 661 | # release_delegates variable. | ||
171 | 662 | return self._files | ||
172 | 631 | 663 | ||
173 | 632 | @property | 664 | @property |
174 | 633 | def name_with_codename(self): | 665 | def name_with_codename(self): |
175 | @@ -653,41 +685,7 @@ | |||
176 | 653 | Decorated classes are created, and they contain cached data | 685 | Decorated classes are created, and they contain cached data |
177 | 654 | obtained with a few queries rather than many iterated queries. | 686 | obtained with a few queries rather than many iterated queries. |
178 | 655 | """ | 687 | """ |
214 | 656 | # Create the decorated product and set the list of series. | 688 | return ProductWithSeries(self.context) |
180 | 657 | original_product = self.context | ||
181 | 658 | |||
182 | 659 | product_with_series = ProductWithSeries(original_product) | ||
183 | 660 | serieses = [] | ||
184 | 661 | for series in original_product.serieses: | ||
185 | 662 | series_with_releases = SeriesWithReleases(series) | ||
186 | 663 | serieses.append(series_with_releases) | ||
187 | 664 | if original_product.development_focus == series: | ||
188 | 665 | product_with_series.development_focus = series_with_releases | ||
189 | 666 | |||
190 | 667 | product_with_series.setSeries(serieses) | ||
191 | 668 | |||
192 | 669 | # Get all of the releases for all of the serieses in a single | ||
193 | 670 | # query. The query sorts the releases properly so we know the | ||
194 | 671 | # resulting list is sorted correctly. | ||
195 | 672 | release_by_id = {} | ||
196 | 673 | milestones_and_releases = list( | ||
197 | 674 | original_product.getMilestonesAndReleases()) | ||
198 | 675 | for milestone, release in milestones_and_releases: | ||
199 | 676 | series = product_with_series.getSeriesById( | ||
200 | 677 | milestone.productseries.id) | ||
201 | 678 | decorated_release = ReleaseWithFiles(release) | ||
202 | 679 | series.addRelease(decorated_release) | ||
203 | 680 | release_by_id[release.id] = decorated_release | ||
204 | 681 | |||
205 | 682 | # Get all of the files for all of the releases. The query | ||
206 | 683 | # returns all releases sorted properly. | ||
207 | 684 | releases = [release for milestone, release in milestones_and_releases] | ||
208 | 685 | files = getUtility(IProductReleaseSet).getFilesForReleases(releases) | ||
209 | 686 | for file in files: | ||
210 | 687 | release = release_by_id[file.productrelease.id] | ||
211 | 688 | release.addFile(file) | ||
212 | 689 | |||
213 | 690 | return product_with_series | ||
215 | 691 | 689 | ||
216 | 692 | def deleteFiles(self, releases): | 690 | def deleteFiles(self, releases): |
217 | 693 | """Delete the selected files from the set of releases. | 691 | """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.