Merge lp:~adeuring/launchpad/bug-518746-faster-retrieval-of-patch-age into lp:launchpad/db-devel

Proposed by Abel Deuring on 2010-02-09
Status: Merged
Merged at revision: not available
Proposed branch: lp:~adeuring/launchpad/bug-518746-faster-retrieval-of-patch-age
Merge into: lp:launchpad/db-devel
Diff against target: 227 lines (+75/-40)
6 files modified
lib/lp/bugs/browser/bugtarget.py (+0/-17)
lib/lp/bugs/configure.zcml (+1/-0)
lib/lp/bugs/doc/bug.txt (+39/-15)
lib/lp/bugs/interfaces/bug.py (+2/-0)
lib/lp/bugs/model/bug.py (+26/-1)
lib/lp/bugs/templates/bugtarget-patches.pt (+7/-7)
To merge this branch: bzr merge lp:~adeuring/launchpad/bug-518746-faster-retrieval-of-patch-age
Reviewer Review Type Date Requested Status
Brad Crittenden (community) code 2010-02-09 Approve on 2010-02-09
Review via email: mp+18936@code.launchpad.net
To post a comment you must log in.
Abel Deuring (adeuring) wrote :
Download full text (3.6 KiB)

This branch fixes timeout OOPSes that consistenly occur on the new +patches view, for nearly all targets. Examples: https://staging.launchpad.net/ubuntu/+patches , https://bugs.staging.launchpad.net/~adeuring/+patches

The OOPS reports show that ca. 15 seconds are spent with ca 30-40 queries which retrieve the bug attachments for a given bug. Each of these queries needs 300..500 milliseconds; they are issued for each of the up to 50 bug tasks we display on the +patches page.

The queries are issued by the method BugsPatchesView.youngestPatch(), which looks for the most recently added patch attachment by iterating over Bug.attachments.

The fix is quite straightforward: We don't need to retrieve all bug attachments of a bug, if we can directly find the one we are interested in using a "better" SQL query.

So I added a property Bug.latest_patch, which does just that: return the most recentl added patch attachment.

As noted in a comment in model/bug.py, the result is in very rare cases not necessarily exact, but I think the inaccuracy does not matter in this case. The alternative would have been to query over the joined tables BugAttachment and Message, and to sort by Message.datecreated, which would be much slower, since Message.datecreated is not indexed.

Chex already EXPLAIN ANALYZEd the new query on staging: It needs ca 40 milliseconds -- not blindingly fast, but, with a total execution time of ca 2 seconds for 50 bugtasks, "good enough" for the +patches view, I think.

tests:
./bin/test -vvv -t patches-view.txt
./bin/test -vvv -t doc/bug.txt

= Launchpad lint =

Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.

Linting changed files:
  lib/lp/bugs/configure.zcml
  lib/lp/bugs/browser/bugtarget.py
  lib/lp/bugs/doc/bug.txt
  lib/lp/bugs/interfaces/bug.py
  lib/lp/bugs/model/bug.py
  lib/lp/bugs/templates/bugtarget-patches.pt

== Pylint notices ==

lib/lp/bugs/browser/bugtarget.py
    333: [C0322, FileBugViewBase.submit_bug_action] Operator not preceded by a space
    failure=handleSubmitBugFailure)
    ^
    def submit_bug_action(self, action, data):
    511: [C0322, FileBugViewBase.this_is_my_bug_action] Operator not preceded by a space
    name="this_is_my_bug", failure=handleSubmitBugFailure)
    ^
    def this_is_my_bug_action(self, action, data):
    883: [C0322, FileBugGuidedView.continue_action] Operator not preceded by a space
    validator="validate_no_dupe_found")
    ^
    def continue_action(self, action, data):

lib/lp/bugs/interfaces/bug.py
    50: [F0401] Unable to import 'lazr.restful.declarations' (No module named restful)
  ...

Read more...

Abel Deuring (adeuring) wrote :
Download full text (4.0 KiB)

sorry copy&paste error with the lint output... The complete lint message:

= Launchpad lint =

Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.

Linting changed files:
  lib/lp/bugs/configure.zcml
  lib/lp/bugs/browser/bugtarget.py
  lib/lp/bugs/doc/bug.txt
  lib/lp/bugs/interfaces/bug.py
  lib/lp/bugs/model/bug.py
  lib/lp/bugs/templates/bugtarget-patches.pt

== Pylint notices ==

lib/lp/bugs/browser/bugtarget.py
    333: [C0322, FileBugViewBase.submit_bug_action] Operator not preceded by a space
    failure=handleSubmitBugFailure)
    ^
    def submit_bug_action(self, action, data):
    511: [C0322, FileBugViewBase.this_is_my_bug_action] Operator not preceded by a space
    name="this_is_my_bug", failure=handleSubmitBugFailure)
    ^
    def this_is_my_bug_action(self, action, data):
    883: [C0322, FileBugGuidedView.continue_action] Operator not preceded by a space
    validator="validate_no_dupe_found")
    ^
    def continue_action(self, action, data):

lib/lp/bugs/interfaces/bug.py
    50: [F0401] Unable to import 'lazr.restful.declarations' (No module named restful)
    56: [F0401] Unable to import 'lazr.restful.fields' (No module named restful)
    57: [F0401] Unable to import 'lazr.restful.interface' (No module named restful)
    501: [C0322, IBug.addAttachment] Operator not preceded by a space
    comment=Text(), filename=TextLine(), is_patch=Bool(),
    ^
    content_type=TextLine(), description=Text())
    @export_factory_operation(IBugAttachment, [])
    def addAttachment(owner, data, comment, filename, is_patch=False,
    content_type=None, description=None):
    636: [C0322, IBug.getNominations] Operator not preceded by a space
    nominations=List(
    ^
    title=_("Nominations to search through."),
    value_type=Reference(schema=Interface), # IBugNomination
    required=False))
    @operation_returns_collection_of(Interface) # IBugNomination
    @export_read_operation()
    def getNominations(target=None, nominations=None):
    716: [C0322, IBug.markUserAffected] Operator not preceded by a space
    required=False, default=True))
    ^
    @call_with(user=REQUEST_USER)
    @export_write_operation()
    def markUserAffected(user, affected=True):
    731: [C0322, IBug.setCommentVisibility] Operator not preceded by a space
    required=True),
    ^
    visible=Bool(title=_('Show this comment?'), required=True))
    @call_with(user=REQUEST_USER)
    @export_write_operation()
    def setCommentVisibility(user, comment_number, visible):
    743: [C0322, IBug.linkHWSubmission] Operator not preceded by a space
    Interface, title=_(...

Read more...

Brad Crittenden (bac) wrote :

Hi Abel,

Thanks for the nice work to solve those timeouts. Here are some suggestions to improve the branch.

* I think in bug.txt you are missing one test: when there are multiple patches and the latest patch is deleted. In that case I assume the next oldest is shown, correct? Having that demonstrated in the test would be great. It'll also make the property regression-proof in the event someone decided to cache the latest patch rather than doing a query.

* s/The most recently patch of this bug./The most recent patch of this bug./

* s/datecrated/datecreated/

* In your template I'd discourage the use of the one letter variable name 'p' (though I recognized you inherited it.)

review: Approve (code)
Abel Deuring (adeuring) wrote :

Hi Brad,

thanks for your review!

On 09.02.2010 19:59, Brad Crittenden wrote:
> Review: Approve code
> Hi Abel,
>
> Thanks for the nice work to solve those timeouts. Here are some suggestions to improve the branch.
>
> * I think in bug.txt you are missing one test: when there are multiple patches and the latest patch is deleted. In that case I assume the next oldest is shown, correct? Having that demonstrated in the test would be great. It'll also make the property regression-proof in the event someone decided to cache the latest patch rather than doing a query.

Good idea, I extended the tests a bit.

>
> * s/The most recently patch of this bug./The most recent patch of this bug./

Fixed.

>
> * s/datecrated/datecreated/

Fixed.

>
> * In your template I'd discourage the use of the one letter variable name 'p' (though I recognized you inherited it.)

Fixed.

Aebl

Abel Deuring (adeuring) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

Hi Brad,

thanks for your review!

On 09.02.2010 19:59, Brad Crittenden wrote:
> Review: Approve code
> Hi Abel,
>
> Thanks for the nice work to solve those timeouts. Here are some suggestions to improve the branch.
>
> * I think in bug.txt you are missing one test: when there are multiple patches and the latest patch is deleted. In that case I assume the next oldest is shown, correct? Having that demonstrated in the test would be great. It'll also make the property regression-proof in the event someone decided to cache the latest patch rather than doing a query.

Good idea, I extended the tests a bit.

>
> * s/The most recently patch of this bug./The most recent patch of this bug./
>

Fixed.

> * s/datecrated/datecreated/

Fixed.

>
> * In your template I'd discourage the use of the one letter variable name 'p' (though I recognized you inherited it.)

Fixed.

Abel

-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org

iD8DBQFLccKIekBPhm8NrtARAh5cAJoDGSel+wQGQZj4eoWzYdZdpu1hPwCfTloc
kpwtKELVUwXrt9A2onyz6dc=
=iC3D
-----END PGP SIGNATURE-----

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/bugs/browser/bugtarget.py'
2--- lib/lp/bugs/browser/bugtarget.py 2010-02-04 03:12:35 +0000
3+++ lib/lp/bugs/browser/bugtarget.py 2010-02-09 20:16:22 +0000
4@@ -1278,23 +1278,6 @@
5 else:
6 return None
7
8- def youngestPatch(self, bug):
9- """Return the youngest patch attached to a bug, else error."""
10- youngest = None
11- # Loop over bugtasks, gathering youngest patch for each's bug.
12- for attachment in bug.attachments:
13- if attachment.is_patch:
14- if youngest is None:
15- youngest = attachment
16- elif (attachment.message.datecreated >
17- youngest.message.datecreated):
18- youngest = attachment
19- if youngest is None:
20- # This is the patches view, so every bug under
21- # consideration should have at least one patch attachment.
22- raise AssertionError("bug %i has no patch attachments" % bug.id)
23- return youngest
24-
25 def patchAge(self, patch):
26 """Return a timedelta object for the age of a patch attachment."""
27 now = datetime.now(timezone('UTC'))
28
29=== modified file 'lib/lp/bugs/configure.zcml'
30--- lib/lp/bugs/configure.zcml 2010-02-03 18:57:40 +0000
31+++ lib/lp/bugs/configure.zcml 2010-02-09 20:16:22 +0000
32@@ -576,6 +576,7 @@
33 personIsSubscribedToDuplicate
34 heat
35 has_patches
36+ latest_patch
37 latest_patch_uploaded"/>
38 <require
39 permission="launchpad.View"
40
41=== modified file 'lib/lp/bugs/doc/bug.txt'
42--- lib/lp/bugs/doc/bug.txt 2010-02-02 16:48:42 +0000
43+++ lib/lp/bugs/doc/bug.txt 2010-02-09 20:16:22 +0000
44@@ -1639,16 +1639,18 @@
45 True
46
47
48-Date when the most recent patch was added
49------------------------------------------
50-
51-If a bug has attachments of type BugAttachmentType.PATCH, the property
52-Bug.latest_patch_uploaded is set to the time when the latest patch was
53-uploaded.
54-
55-If a bug has no attachments, latest_patch_uploaded is None.
56+Most recently added patch
57+-------------------------
58+
59+Bug.latest_patch provides the most recently added bug attachment of
60+type BugAttachmentType.PATCH; the property Bug.latest_patch_uploaded
61+is set to the time when the latest patch was uploaded.
62+
63+If a bug has no attachments, both properties are None.
64
65 >>> bug = factory.makeBug()
66+ >>> print bug.latest_patch
67+ None
68 >>> print bug.latest_patch_uploaded
69 None
70
71@@ -1667,37 +1669,59 @@
72
73 If we declare the existing attachment to be a patch,
74 latest_patch_uploaded is set to the date_created value of the Message
75-record for this attachment.
76+record for this attachment, and we can access the attachment via
77+Bug.latest_patch.
78
79 >>> from lp.bugs.interfaces.bugattachment import BugAttachmentType
80 >>> attachment_1.type = BugAttachmentType.PATCH
81 >>> transaction.commit()
82 >>> date_message_1_created = bug.attachments[0].message.datecreated
83+ >>> print bug.latest_patch == attachment_1
84+ True
85 >>> print bug.latest_patch_uploaded == date_message_1_created
86 True
87
88 If we add another attachment, this time declared to be a patch
89-at creation time, the new value of bug.latest_patch_uploaded will
90-change to the value of message.datecreated for this new attachment.
91+at creation time, we can access this attachment via Bug.latest_patch,
92+and the new value of bug.latest_patch_uploaded will change to the value
93+of message.datecreated for this new attachment.
94
95 >>> attachment_2 = factory.makeBugAttachment(bug, is_patch=True)
96 >>> transaction.commit()
97 >>> date_message_2_created = bug.attachments[1].message.datecreated
98+ >>> print bug.latest_patch == attachment_2
99+ True
100 >>> print bug.latest_patch_uploaded == date_message_2_created
101 True
102
103-If we say that attachment_1 is not a patch, the value of
104-bug.latest_patch_uploaded does not change.
105+If we say that attachment_1 is not a patch, the values of
106+bug.latest_patch and bug.latest_patch_uploaded does not change.
107
108 >>> attachment_1.type = BugAttachmentType.UNSPECIFIED
109 >>> transaction.commit()
110+ >>> print bug.latest_patch == attachment_2
111+ True
112 >>> print bug.latest_patch_uploaded == date_message_2_created
113 True
114
115-And if we delete attachment_2, bug.latest_patch_uploaded is again None,
116-because the bug has no longer any attachments that are patches.
117+If we declare attachment_1 again to be a patch and if we delete
118+attachment_2, bug.latest_patch references again attachment_1, and
119+bug.bug.latest_patch_uploaded is its creation time.
120
121+ >>> attachment_1.type = BugAttachmentType.PATCH
122 >>> attachment_2.removeFromBug(user=bug.owner)
123 >>> transaction.commit()
124+ >>> print bug.latest_patch == attachment_1
125+ True
126+ >>> print bug.latest_patch_uploaded == date_message_1_created
127+ True
128+
129+If we delete attachment_1 too, bug.latest_patch and
130+bug.latest_patch_uploaded are again None.
131+
132+ >>> attachment_1.removeFromBug(user=bug.owner)
133+ >>> transaction.commit()
134+ >>> print bug.latest_patch
135+ None
136 >>> print bug.latest_patch_uploaded
137 None
138
139=== modified file 'lib/lp/bugs/interfaces/bug.py'
140--- lib/lp/bugs/interfaces/bug.py 2010-02-03 18:57:40 +0000
141+++ lib/lp/bugs/interfaces/bug.py 2010-02-09 20:16:22 +0000
142@@ -351,6 +351,8 @@
143 title=_('Date when the most recent patch was uploaded.'),
144 required=False, readonly=True))
145
146+ latest_patch = Attribute("The most recent patch of this bug.")
147+
148 @operation_parameters(
149 subject=optional_message_subject_field(),
150 content=copy_field(IMessage['content']))
151
152=== modified file 'lib/lp/bugs/model/bug.py'
153--- lib/lp/bugs/model/bug.py 2010-02-03 18:57:40 +0000
154+++ lib/lp/bugs/model/bug.py 2010-02-09 20:16:22 +0000
155@@ -32,7 +32,7 @@
156 from sqlobject import SQLMultipleJoin, SQLRelatedJoin
157 from sqlobject import SQLObjectNotFound
158 from storm.expr import (
159- And, Count, Func, In, LeftJoin, Not, Select, SQLRaw, Union)
160+ And, Count, Func, In, LeftJoin, Max, Not, Select, SQLRaw, Union)
161 from storm.store import EmptyResultSet, Store
162
163 from lazr.lifecycle.event import (
164@@ -261,6 +261,31 @@
165 latest_patch_uploaded = UtcDateTimeCol(default=None)
166
167 @property
168+ def latest_patch(self):
169+ """See `IBug`."""
170+ # We want to retrieve the most recently added bug attachment
171+ # that is of type BugAttachmentType.PATCH. In order to find
172+ # this attachment, we should in theory sort by
173+ # BugAttachment.message.datecreated. Since we don't have
174+ # an index for Message.datecreated, such a query would be
175+ # quite slow. We search instead for the BugAttachment with
176+ # the largest ID for a given bug. This is "nearly" equivalent
177+ # to searching the record with the maximum value of
178+ # message.datecreated: The only exception is the rare case when
179+ # two BugAttachment records are simultaneuosly added to the same
180+ # bug, where bug_attachment_1.id < bug_attachment_2.id, while
181+ # the Message record for bug_attachment_2 is created before
182+ # the Message record for bug_attachment_1. The difference of
183+ # the datecreated values of the Message records is in this case
184+ # probably smaller than one second and the selection of the
185+ # "most recent" patch anyway somewhat arbitrary.
186+ return Store.of(self).find(
187+ BugAttachment, BugAttachment.id == Select(
188+ Max(BugAttachment.id),
189+ And(BugAttachment.bug == self.id,
190+ BugAttachment.type == BugAttachmentType.PATCH))).one()
191+
192+ @property
193 def comment_count(self):
194 """See `IBug`."""
195 return self.message_count - 1
196
197=== modified file 'lib/lp/bugs/templates/bugtarget-patches.pt'
198--- lib/lp/bugs/templates/bugtarget-patches.pt 2010-02-03 15:37:11 +0000
199+++ lib/lp/bugs/templates/bugtarget-patches.pt 2010-02-09 20:16:22 +0000
200@@ -93,20 +93,20 @@
201 <a tal:attributes="href patch_task/target/fmt:url"
202 tal:content="patch_task/target/name"></a></td>
203 <td class="patches-view-cell"
204- tal:define="p python:view.youngestPatch(patch_task.bug);
205- age python:view.patchAge(p)"
206+ tal:define="patch patch_task/bug/latest_patch;
207+ age python:view.patchAge(patch)"
208 tal:attributes="id string:patch-cell-${repeat/patch_task/index}">
209- <a tal:attributes="href p/libraryfile/http_url"
210+ <a tal:attributes="href patch/libraryfile/http_url"
211 tal:content="age/fmt:approximateduration/use-digits"></a>
212 <div class="popupTitle"
213 tal:attributes="id string:patch-popup-${repeat/patch_task/index};">
214- <p tal:define="submitter p/message/owner">
215+ <p tal:define="submitter patch/message/owner">
216 <strong>From:</strong>
217 <a tal:replace="structure submitter/fmt:link"></a><br/>
218 <strong>Link:</strong>
219- <a tal:attributes="href p/libraryfile/http_url"
220- tal:content="p/libraryfile/filename"></a></p>
221- <p tal:content="string:${p/title}"></p>
222+ <a tal:attributes="href patch/libraryfile/http_url"
223+ tal:content="patch/libraryfile/filename"></a></p>
224+ <p tal:content="string:${patch/title}"></p>
225 </div>
226 <script type="text/javascript" tal:content="string:
227 YUI().use('base', 'node', 'event', function(Y) {

Subscribers

People subscribed via source and target branches

to status/vote changes: