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

Proposed by William Grant on 2012-10-02
Status: Merged
Approved by: William Grant on 2012-10-02
Approved revision: no longer in the source branch.
Merged at revision: 16063
Proposed branch: lp:~wgrant/launchpad/bug-1056617
Merge into: lp:launchpad
Diff against target: 255 lines (+72/-92)
3 files modified
lib/lp/registry/model/milestone.py (+42/-56)
lib/lp/registry/model/milestonetag.py (+22/-25)
lib/lp/registry/tests/test_milestone.py (+8/-11)
To merge this branch: bzr merge lp:~wgrant/launchpad/bug-1056617
Reviewer Review Type Date Requested Status
Ian Booth (community) 2012-10-02 Approve on 2012-10-02
Review via email: mp+127412@code.launchpad.net

Commit Message

Fix Milestone.specifications to be fast even when querying workitems.

Description of the Change

Milestone.specifications has been rather slow (~150ms) since workitem filtering was added a few months back. This branch rewords it to use a more indexable (<10ms) UNION, and rewrites ProjectMilestone and ProjectGroupMilestoneTag's implementations to use the new common fast query.

It needs a new index on specification(milestone) to be really fast, but it's still substantially faster without it.

A test failure exposed a bug in the old query. If a blueprint had a deleted workitem targeted to the milestone, it would not be included in the result even if the blueprint itself was milestoned.

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

Looks ok

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/model/milestone.py'
2--- lib/lp/registry/model/milestone.py 2012-08-07 02:31:56 +0000
3+++ lib/lp/registry/model/milestone.py 2012-10-02 04:47:19 +0000
4@@ -28,9 +28,9 @@
5 from storm.expr import (
6 And,
7 Desc,
8- Join,
9 LeftJoin,
10- Or,
11+ Select,
12+ Union,
13 )
14 from storm.locals import Store
15 from storm.zope import IResultSet
16@@ -148,7 +148,36 @@
17
18 @property
19 def specifications(self):
20- raise NotImplementedError
21+ from lp.registry.model.person import Person
22+ store = Store.of(self.target)
23+ origin = [
24+ Specification,
25+ LeftJoin(Person, Specification.assigneeID == Person.id),
26+ ]
27+ milestones = self._milestone_ids_expr
28+
29+ results = store.using(*origin).find(
30+ (Specification, Person),
31+ Specification.id.is_in(
32+ Union(
33+ Select(
34+ Specification.id, tables=[Specification],
35+ where=(Specification.milestoneID.is_in(milestones))),
36+ Select(
37+ SpecificationWorkItem.specification_id,
38+ tables=[SpecificationWorkItem],
39+ where=And(
40+ SpecificationWorkItem.milestone_id.is_in(
41+ milestones),
42+ SpecificationWorkItem.deleted == False)),
43+ all=True)))
44+ results.config(distinct=True)
45+ ordered_results = results.order_by(Desc(Specification.priority),
46+ Specification.definition_status,
47+ Specification.implementation_status,
48+ Specification.title)
49+ mapper = lambda row: row[0]
50+ return DecoratedResultSet(ordered_results, mapper)
51
52 def bugtasks(self, user):
53 """The list of non-conjoined bugtasks targeted to this milestone."""
54@@ -190,30 +219,8 @@
55 code_name = StringCol(dbName='codename', notNull=False, default=None)
56
57 @property
58- def specifications(self):
59- from lp.registry.model.person import Person
60- store = Store.of(self)
61- origin = [
62- Specification,
63- LeftJoin(
64- SpecificationWorkItem,
65- SpecificationWorkItem.specification_id == Specification.id),
66- LeftJoin(Person, Specification.assigneeID == Person.id),
67- ]
68-
69- results = store.using(*origin).find(
70- (Specification, Person),
71- Or(Specification.milestoneID == self.id,
72- SpecificationWorkItem.milestone_id == self.id),
73- Or(SpecificationWorkItem.deleted == None,
74- SpecificationWorkItem.deleted == False))
75- results.config(distinct=True)
76- ordered_results = results.order_by(Desc(Specification.priority),
77- Specification.definition_status,
78- Specification.implementation_status,
79- Specification.title)
80- mapper = lambda row: row[0]
81- return DecoratedResultSet(ordered_results, mapper)
82+ def _milestone_ids_expr(self):
83+ return (self.id,)
84
85 @property
86 def target(self):
87@@ -421,36 +428,15 @@
88 self.summary = None
89
90 @property
91- def specifications(self):
92- """See `IMilestoneData`."""
93- from lp.registry.model.person import Person
94+ def _milestone_ids_expr(self):
95 from lp.registry.model.product import Product
96- store = Store.of(self.target)
97- origin = [
98- Specification,
99- LeftJoin(
100- SpecificationWorkItem,
101- SpecificationWorkItem.specification_id == Specification.id),
102- Join(Milestone,
103- Or(Milestone.id == Specification.milestoneID,
104- Milestone.id == SpecificationWorkItem.milestone_id)),
105- Join(Product, Product.id == Milestone.productID),
106- LeftJoin(Person, Specification.assigneeID == Person.id),
107- ]
108-
109- results = store.using(*origin).find(
110- (Specification, Person),
111- Product.projectID == self.target.id,
112- Milestone.name == self.name,
113- Or(SpecificationWorkItem.deleted == None,
114- SpecificationWorkItem.deleted == False))
115- results.config(distinct=True)
116- ordered_results = results.order_by(Desc(Specification.priority),
117- Specification.definition_status,
118- Specification.implementation_status,
119- Specification.title)
120- mapper = lambda row: row[0]
121- return DecoratedResultSet(ordered_results, mapper)
122+ return Select(
123+ Milestone.id,
124+ tables=[Milestone, Product],
125+ where=And(
126+ Milestone.name == self.name,
127+ Milestone.productID == Product.id,
128+ Product.project == self.target))
129
130 @property
131 def displayname(self):
132
133=== modified file 'lib/lp/registry/model/milestonetag.py'
134--- lib/lp/registry/model/milestonetag.py 2012-09-28 06:25:44 +0000
135+++ lib/lp/registry/model/milestonetag.py 2012-10-02 04:47:19 +0000
136@@ -10,27 +10,25 @@
137 ]
138
139
140-from storm.locals import (
141+from storm.expr import (
142+ And,
143+ Exists,
144+ Select,
145+ )
146+from storm.properties import (
147 DateTime,
148 Int,
149- Reference,
150 Unicode,
151 )
152-from zope.component import getUtility
153+from storm.references import Reference
154 from zope.interface import implements
155
156-from lp.blueprints.model.specification import Specification
157 from lp.registry.interfaces.milestonetag import IProjectGroupMilestoneTag
158 from lp.registry.model.milestone import (
159 Milestone,
160 MilestoneData,
161 )
162 from lp.registry.model.product import Product
163-from lp.services.database.interfaces import (
164- DEFAULT_FLAVOR,
165- IStoreSelector,
166- MAIN_STORE,
167- )
168
169
170 class MilestoneTag(object):
171@@ -80,20 +78,19 @@
172 return self.displayname
173
174 @property
175- def specifications(self):
176- """See IMilestoneData."""
177- store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
178- results = []
179- for tag in self.tags:
180- result = store.find(
181- Specification,
182- Specification.milestone == Milestone.id,
183- Milestone.product == Product.id,
184+ def _milestone_ids_expr(self):
185+ tag_constraints = And(*[
186+ Exists(
187+ Select(
188+ 1, tables=[MilestoneTag],
189+ where=And(
190+ MilestoneTag.milestone_id == Milestone.id,
191+ MilestoneTag.tag == tag)))
192+ for tag in self.tags])
193+ return Select(
194+ Milestone.id,
195+ tables=[Milestone, Product],
196+ where=And(
197+ Milestone.productID == Product.id,
198 Product.project == self.target,
199- MilestoneTag.milestone_id == Milestone.id,
200- MilestoneTag.tag == tag)
201- results.append(result)
202- result = results.pop()
203- for i in results:
204- result = result.intersection(i)
205- return result
206+ tag_constraints))
207
208=== modified file 'lib/lp/registry/tests/test_milestone.py'
209--- lib/lp/registry/tests/test_milestone.py 2012-07-09 08:48:31 +0000
210+++ lib/lp/registry/tests/test_milestone.py 2012-10-02 04:47:19 +0000
211@@ -201,16 +201,14 @@
212 return specification, target_milestone
213
214 def test_milestones_on_product(self):
215- specification, target_milestone = self._create_milestones_on_target(
216+ spec, target_milestone = self._create_milestones_on_target(
217 product=self.factory.makeProduct())
218- self.assertEqual([specification],
219- list(target_milestone.specifications))
220+ self.assertContentEqual([spec], target_milestone.specifications)
221
222 def test_milestones_on_distribution(self):
223- specification, target_milestone = self._create_milestones_on_target(
224+ spec, target_milestone = self._create_milestones_on_target(
225 distribution=self.factory.makeDistribution())
226- self.assertEqual([specification],
227- list(target_milestone.specifications))
228+ self.assertContentEqual([spec], target_milestone.specifications)
229
230 def test_milestones_on_project(self):
231 # A Project (Project Group) milestone contains all specifications
232@@ -218,11 +216,10 @@
233 # a certain name.
234 projectgroup = self.factory.makeProject()
235 product = self.factory.makeProduct(project=projectgroup)
236- specification, target_milestone = self._create_milestones_on_target(
237+ spec, target_milestone = self._create_milestones_on_target(
238 product=product)
239 milestone = projectgroup.getMilestone(name=target_milestone.name)
240- self.assertEqual([specification],
241- list(milestone.specifications))
242+ self.assertContentEqual([spec], milestone.specifications)
243
244 def test_milestones_with_deleted_workitems(self):
245 # Deleted work items do not cause the specification to show up
246@@ -230,7 +227,7 @@
247 milestone = self.factory.makeMilestone(
248 product=self.factory.makeProduct())
249 specification = self.factory.makeSpecification(
250- milestone=milestone, product=milestone.product)
251+ product=milestone.product)
252 self.factory.makeSpecificationWorkItem(
253 specification=specification, milestone=milestone, deleted=True)
254- self.assertEqual([], list(milestone.specifications))
255+ self.assertContentEqual([], milestone.specifications)