Merge lp:~wgrant/launchpad/bug-962912 into lp:launchpad

Proposed by William Grant on 2012-03-26
Status: Merged
Approved by: William Grant on 2012-03-26
Approved revision: no longer in the source branch.
Merged at revision: 15024
Proposed branch: lp:~wgrant/launchpad/bug-962912
Merge into: lp:launchpad
Diff against target: 288 lines (+99/-59)
6 files modified
lib/lp/registry/browser/tests/test_pillar_sharing.py (+1/-1)
lib/lp/services/database/decoratedresultset.py (+44/-10)
lib/lp/services/database/doc/decoratedresultset.txt (+25/-0)
lib/lp/services/database/tests/test_decoratedresultset.py (+0/-20)
lib/lp/services/webapp/batching.py (+13/-11)
lib/lp/soyuz/browser/builder.py (+16/-17)
To merge this branch: bzr merge lp:~wgrant/launchpad/bug-962912
Reviewer Review Type Date Requested Status
Ian Booth (community) 2012-03-26 Approve on 2012-03-26
Review via email: mp+99252@code.launchpad.net

Commit Message

Fix StormRangeFactory to not evaluate DecoratedResultSets twice.

Description of the Change

This branch prevents StormRangeFactory from evaluating a DecoratedResultSet's primary query twice (once for the base values, once for the decorated). I extended DRS to optionally return both the base and the decorated values, and made SRF use that.

To post a comment you must log in.
Ian Booth (wallyworld) wrote :

Yay, less queries. Awesome.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/registry/browser/tests/test_pillar_sharing.py'
2--- lib/lp/registry/browser/tests/test_pillar_sharing.py 2012-03-22 23:21:24 +0000
3+++ lib/lp/registry/browser/tests/test_pillar_sharing.py 2012-03-27 04:04:19 +0000
4@@ -247,7 +247,7 @@
5 view = create_view(self.pillar, name='+sharing')
6 with StormStatementRecorder() as recorder:
7 view.initialize()
8- self.assertThat(recorder, HasQueryCount(LessThan(7)))
9+ self.assertThat(recorder, HasQueryCount(LessThan(6)))
10
11 def test_view_write_enabled_without_feature_flag(self):
12 # Test that sharing_write_enabled is not set without the feature flag.
13
14=== modified file 'lib/lp/services/database/decoratedresultset.py'
15--- lib/lp/services/database/decoratedresultset.py 2011-12-18 14:29:19 +0000
16+++ lib/lp/services/database/decoratedresultset.py 2012-03-27 04:04:19 +0000
17@@ -39,7 +39,7 @@
18 delegates(IResultSet, context='result_set')
19
20 def __init__(self, result_set, result_decorator=None, pre_iter_hook=None,
21- slice_info=False):
22+ slice_info=False, return_both=False):
23 """
24 Wrap `result_set` in a decorator.
25
26@@ -55,23 +55,50 @@
27 :param slice_info: If True pass information about the slice parameters
28 to the result_decorator and pre_iter_hook. any() and similar
29 methods will cause None to be supplied.
30+ :param return_both: If True return both the plain and decorated
31+ values as a tuple.
32 """
33 self.result_set = result_set
34 self.result_decorator = result_decorator
35 self.pre_iter_hook = pre_iter_hook
36 self.slice_info = slice_info
37+ self.config(return_both=return_both)
38+
39+ def _extract_plain_and_result(self, results):
40+ """Extract the plain and normal results from a sub-result.
41+
42+ This gets slightly complicated when there are nested
43+ DecoratedResultSets, as we have to propogate the plain result
44+ all the way up.
45+ """
46+ if not results:
47+ return [], []
48+ elif (zope_isinstance(self.result_set, DecoratedResultSet)
49+ and self.return_both):
50+ assert self.result_set.return_both == self.return_both
51+ return zip(*results)
52+ else:
53+ return results, results
54
55 def decorate_or_none(self, result, row_index=None):
56 """Decorate a result or return None if the result is itself None"""
57+ # If we have a nested DecoratedResultSet we need to propogate
58+ # the plain result.
59+ ([plain], [result]) = self._extract_plain_and_result([result])
60+
61 if result is None:
62- return None
63+ decorated = None
64 else:
65 if self.result_decorator is None:
66- return result
67+ decorated = result
68 elif self.slice_info:
69- return self.result_decorator(result, row_index)
70+ decorated = self.result_decorator(result, row_index)
71 else:
72- return self.result_decorator(result)
73+ decorated = self.result_decorator(result)
74+ if self.return_both:
75+ return (plain, decorated)
76+ else:
77+ return decorated
78
79 def copy(self, *args, **kwargs):
80 """See `IResultSet`.
81@@ -81,13 +108,19 @@
82 new_result_set = self.result_set.copy(*args, **kwargs)
83 return DecoratedResultSet(
84 new_result_set, self.result_decorator, self.pre_iter_hook,
85- self.slice_info)
86+ self.slice_info, self.return_both)
87
88 def config(self, *args, **kwargs):
89 """See `IResultSet`.
90
91 :return: The decorated result set.after updating the config.
92 """
93+ return_both = kwargs.pop('return_both', None)
94+ if return_both is not None:
95+ self.return_both = return_both
96+ if zope_isinstance(self.result_set, DecoratedResultSet):
97+ self.result_set.config(return_both=return_both)
98+
99 self.result_set.config(*args, **kwargs)
100 return self
101
102@@ -106,10 +139,11 @@
103 stop = start + len(results)
104 result_slice = slice(start, stop)
105 if self.pre_iter_hook is not None:
106+ pre_iter_rows = self._extract_plain_and_result(results)[1]
107 if self.slice_info:
108- self.pre_iter_hook(results, result_slice)
109+ self.pre_iter_hook(pre_iter_rows, result_slice)
110 else:
111- self.pre_iter_hook(results)
112+ self.pre_iter_hook(pre_iter_rows)
113 if self.slice_info:
114 start = result_slice.start
115 for offset, value in enumerate(results):
116@@ -173,7 +207,7 @@
117 new_result_set = self.result_set.order_by(*args, **kwargs)
118 return DecoratedResultSet(
119 new_result_set, self.result_decorator, self.pre_iter_hook,
120- self.slice_info)
121+ self.slice_info, self.return_both)
122
123 def get_plain_result_set(self):
124 """Return the plain Storm result set."""
125@@ -195,4 +229,4 @@
126 new_result_set = self.result_set.find(*args, **kwargs)
127 return DecoratedResultSet(
128 new_result_set, self.result_decorator, self.pre_iter_hook,
129- self.slice_info)
130+ self.slice_info, self.return_both)
131
132=== modified file 'lib/lp/services/database/doc/decoratedresultset.txt'
133--- lib/lp/services/database/doc/decoratedresultset.txt 2011-12-18 17:15:11 +0000
134+++ lib/lp/services/database/doc/decoratedresultset.txt 2012-03-27 04:04:19 +0000
135@@ -62,6 +62,31 @@
136 Dist name is: gentoo
137 Dist name is: guadalinex
138
139+It's also possible to ask the set to return both the normal and
140+decorated results:
141+
142+ >>> decorated_result_set.config(return_both=True)
143+ <lp.services.database.decoratedresultset.DecoratedResultSet object at ...>
144+ >>> for dist in decorated_result_set:
145+ ... print dist
146+ (<Distribution 'Debian' (debian)>, u'Dist name is: debian')
147+ (<Distribution 'Gentoo' (gentoo)>, u'Dist name is: gentoo')
148+ ...
149+ (<Distribution 'ubuntutest' (ubuntutest)>, u'Dist name is: ubuntutest')
150+ >>> decorated_result_set.first()
151+ (<Distribution 'Debian' (debian)>, u'Dist name is: debian')
152+
153+This works even if there are multiple levels:
154+
155+ >>> drs_squared = DecoratedResultSet(
156+ ... decorated_result_set, lambda x: len(x)).config(return_both=True)
157+ >>> for dist in drs_squared:
158+ ... print dist
159+ (<Distribution 'Debian' (debian)>, 20)
160+ (<Distribution 'Gentoo' (gentoo)>, 20)
161+ ...
162+ (<Distribution 'ubuntutest' (ubuntutest)>, 24)
163+
164 Some methods of the DecoratedResultSet are not actually decorated and
165 just work like normally:
166
167
168=== removed file 'lib/lp/services/database/tests/test_decoratedresultset.py'
169--- lib/lp/services/database/tests/test_decoratedresultset.py 2011-12-28 17:03:06 +0000
170+++ lib/lp/services/database/tests/test_decoratedresultset.py 1970-01-01 00:00:00 +0000
171@@ -1,20 +0,0 @@
172-# Copyright 2009 Canonical Ltd. This software is licensed under the
173-# GNU Affero General Public License version 3 (see the file LICENSE).
174-
175-"""Test harness for running the decoratedresultset.txt test.
176-
177-All the non-documentation-worthy tests for the DecoratedResultSet class.
178-"""
179-
180-__metaclass__ = type
181-
182-__all__ = []
183-
184-import unittest
185-
186-from lp.testing.layers import LaunchpadZopelessLayer
187-from lp.testing.systemdocs import LayeredDocFileSuite
188-
189-
190-def test_suite():
191- return LayeredDocFileSuite('decoratedresultset.txt', layer=LaunchpadZopelessLayer)
192
193=== modified file 'lib/lp/services/webapp/batching.py'
194--- lib/lp/services/webapp/batching.py 2012-03-21 22:11:31 +0000
195+++ lib/lp/services/webapp/batching.py 2012-03-27 04:04:19 +0000
196@@ -552,6 +552,17 @@
197 result = ProxyFactory(naked_result)
198 return result.config(limit=size)
199
200+ def _get_shadowed_list(self, result):
201+ if zope_isinstance(result, DecoratedResultSet):
202+ items = list(result.copy().config(return_both=True))
203+ if items:
204+ shadow_result, real_result = map(list, zip(*items))
205+ else:
206+ shadow_result = real_result = []
207+ else:
208+ shadow_result = real_result = list(result)
209+ return ShadowedList(real_result, shadow_result)
210+
211 def getSlice(self, size, endpoint_memo='', forwards=True):
212 """See `IRangeFactory`."""
213 if self.empty_resultset:
214@@ -568,21 +579,12 @@
215 result = self.resultset.config(limit=size)
216 else:
217 result = self.getSliceFromMemo(size, parsed_memo)
218- real_result = list(result)
219- if zope_isinstance(result, DecoratedResultSet):
220- shadow_result = list(result.get_plain_result_set())
221- else:
222- shadow_result = real_result
223- return ShadowedList(real_result, shadow_result)
224+ return self._get_shadowed_list(result)
225
226 def getSliceByIndex(self, start, end):
227 """See `IRangeFactory."""
228 sliced = self.resultset[start:end]
229- if zope_isinstance(sliced, DecoratedResultSet):
230- return ShadowedList(
231- list(sliced), list(sliced.get_plain_result_set()))
232- sliced = list(sliced)
233- return ShadowedList(sliced, sliced)
234+ return self._get_shadowed_list(sliced)
235
236 @cachedproperty
237 def rough_length(self):
238
239=== modified file 'lib/lp/soyuz/browser/builder.py'
240--- lib/lp/soyuz/browser/builder.py 2012-01-04 16:39:15 +0000
241+++ lib/lp/soyuz/browser/builder.py 2012-03-27 04:04:19 +0000
242@@ -42,7 +42,6 @@
243 IBuilderSet,
244 )
245 from lp.buildmaster.model.buildqueue import BuildQueue
246-from lp.services.database.decoratedresultset import DecoratedResultSet
247 from lp.services.database.lpstorm import IStore
248 from lp.services.propertycache import (
249 cachedproperty,
250@@ -150,22 +149,22 @@
251 @cachedproperty
252 def builders(self):
253 """All active builders"""
254- def do_eager_load(builders):
255- # Populate builders' currentjob cachedproperty.
256- queues = IStore(BuildQueue).find(
257- BuildQueue,
258- BuildQueue.builderID.is_in(
259- builder.id for builder in builders))
260- queue_builders = dict(
261- (queue.builderID, queue) for queue in queues)
262- for builder in builders:
263- cache = get_property_cache(builder)
264- cache.currentjob = queue_builders.get(builder.id, None)
265- # Prefetch the jobs' data.
266- BuildQueue.preloadSpecificJobData(queues)
267-
268- return list(DecoratedResultSet(
269- list(self.context.getBuilders()), pre_iter_hook=do_eager_load))
270+ builders = list(self.context.getBuilders())
271+
272+ # Populate builders' currentjob cachedproperty.
273+ queues = IStore(BuildQueue).find(
274+ BuildQueue,
275+ BuildQueue.builderID.is_in(
276+ builder.id for builder in builders))
277+ queue_builders = dict(
278+ (queue.builderID, queue) for queue in queues)
279+ for builder in builders:
280+ cache = get_property_cache(builder)
281+ cache.currentjob = queue_builders.get(builder.id, None)
282+ # Prefetch the jobs' data.
283+ BuildQueue.preloadSpecificJobData(queues)
284+
285+ return builders
286
287 @property
288 def number_of_registered_builders(self):