Merge lp:~lifeless/launchpad/soyuz into lp:launchpad

Proposed by Robert Collins on 2010-11-10
Status: Merged
Approved by: Stuart Bishop on 2010-11-10
Approved revision: no longer in the source branch.
Merged at revision: 11899
Proposed branch: lp:~lifeless/launchpad/soyuz
Merge into: lp:launchpad
Diff against target: 373 lines (+140/-61)
6 files modified
lib/lp/registry/model/distroseries.py (+3/-4)
lib/lp/soyuz/adapters/archivesourcepublication.py (+28/-12)
lib/lp/soyuz/browser/tests/test_archive_packages.py (+86/-1)
lib/lp/soyuz/interfaces/publishing.py (+2/-14)
lib/lp/soyuz/interfaces/sourcepackagerelease.py (+1/-0)
lib/lp/soyuz/model/publishing.py (+20/-30)
To merge this branch: bzr merge lp:~lifeless/launchpad/soyuz
Reviewer Review Type Date Requested Status
Stuart Bishop Approve on 2010-11-10
Steve Kowalik (community) code* 2010-11-10 Needs Fixing on 2010-11-10
Review via email: mp+40499@code.launchpad.net

Commit Message

[r=stub][ui=none][bug=672371] Sanify ppa/+packages to have constant query counts as source packages are added.

Description of the Change

Fix scaling of ppa/+packages to be flat with source packages, distro series and binary packages. I'm not completely confident in the binary package angle as the page is driven by source packages - but its better than nothing.

The key change here is to move an adapter lookup into an eager loaded dataset - the older code which was a problem previously was exacerbated by the underlying changes made to optimise getBuildStatusSummariesForSourceIdsAndArchive. As a result of which I could strip out some code made just for that code path.

To post a comment you must log in.
Steve Kowalik (stevenk) wrote :

Hi,

With in your test, you look to make a PPA, and then 5 lines later, make a different PPA, but assigned to the same variable, and *then* you make a 3rd PPA assigned to the same variable. This is confusing.

You also set a baseline variable, and then immediately after comment about getting the baseline, which makes no sense.

How did you arrive at the numbers of 42 and 26 for the baseline? They smell like magic numbers to me.

review: Needs Fixing (code*)
Robert Collins (lifeless) wrote :

On Wed, Nov 10, 2010 at 6:17 PM, Steve Kowalik <email address hidden> wrote:
> Review: Needs Fixing code*
> Hi,
>
> With in your test, you look to make a PPA, and then 5 lines later, make a different PPA, but assigned to the same variable, and *then* you make a 3rd PPA assigned to the same variable. This is confusing.

It should be two. Doh. Deleting the spurious one.

> You also set a baseline variable, and then immediately after comment about getting the baseline, which makes no sense.

I'll s/Get/Assess/

> How did you arrive at the numbers of 42 and 26 for the baseline? They smell like magic numbers to me.

They are magic numbers. They are they measured numbers that the page takes.

-Rob

Stuart Bishop (stub) :
review: Approve
Stuart Bishop (stub) wrote :

In the name of the senate and peoples of Rome.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/registry/model/distroseries.py'
2--- lib/lp/registry/model/distroseries.py 2010-11-08 12:12:29 +0000
3+++ lib/lp/registry/model/distroseries.py 2010-11-10 05:56:09 +0000
4@@ -984,16 +984,15 @@
5 source_package_ids = [
6 package_name.id for package_name in source_package_names]
7 releases = SourcePackageRelease.select("""
8- SourcePackageName.id IN %s AND
9+ SourcePackageRelease.sourcepackagename IN %s AND
10 SourcePackageRelease.id =
11 SourcePackagePublishingHistory.sourcepackagerelease AND
12 SourcePackagePublishingHistory.id = (
13 SELECT max(spph.id)
14 FROM SourcePackagePublishingHistory spph,
15- SourcePackageRelease spr, SourcePackageName spn
16+ SourcePackageRelease spr
17 WHERE
18- spn.id = SourcePackageName.id AND
19- spr.sourcepackagename = spn.id AND
20+ spr.sourcepackagename = SourcePackageRelease.sourcepackagename AND
21 spph.sourcepackagerelease = spr.id AND
22 spph.archive IN %s AND
23 spph.status IN %s AND
24
25=== modified file 'lib/lp/soyuz/adapters/archivesourcepublication.py'
26--- lib/lp/soyuz/adapters/archivesourcepublication.py 2010-11-07 21:54:38 +0000
27+++ lib/lp/soyuz/adapters/archivesourcepublication.py 2010-11-10 05:56:09 +0000
28@@ -14,11 +14,14 @@
29 'ArchiveSourcePublications',
30 ]
31
32+from collections import defaultdict
33
34 from lazr.delegates import delegates
35 from zope.component import getUtility
36
37 from canonical.launchpad.browser.librarian import ProxiedLibraryFileAlias
38+from canonical.launchpad.interfaces.lpstorm import IStore
39+from lp.registry.model.sourcepackagename import SourcePackageName
40 from lp.registry.model.distroseries import DistroSeries
41 from lp.soyuz.interfaces.publishing import (
42 IPublishingSet,
43@@ -53,11 +56,12 @@
44 """
45 delegates(ISourcePackagePublishingHistory)
46
47- def __init__(self, context, unpublished_builds, builds, changesfile):
48+ def __init__(self, context, unpublished_builds, builds, changesfile, status_summary):
49 self.context = context
50 self._unpublished_builds = unpublished_builds
51 self._builds = builds
52 self._changesfile = changesfile
53+ self._status_summary = status_summary
54
55 @property
56 def sourcepackagerelease(self):
57@@ -84,16 +88,8 @@
58
59 def getStatusSummaryForBuilds(self):
60 """See `ISourcePackagePublishingHistory`."""
61- # XXX Michael Nelson 2009-05-08 bug=373715. It would be nice if
62- # lazr.delegates passed the delegates 'self' for pass-through
63- # methods, then we wouldn't need to proxy this method call via the
64- # IPublishingSet - instead the delegate would call
65- # ISourcePackagePublishingHistory.getStatusSummaryForBuilds() but
66- # using the delegate as self - might not be possible without
67- # duck-typing.
68- return getUtility(
69- IPublishingSet).getBuildStatusSummaryForSourcePublication(self,
70- self.getBuilds)
71+ return self._status_summary
72+
73
74 class ArchiveSourcePublications:
75 """`ArchiveSourcePublication` iterator."""
76@@ -162,19 +158,39 @@
77 return iter(results)
78
79 # Load the extra-information for all source publications.
80+ # All of this code would be better on an object representing a set of
81+ # publications.
82 builds_by_source = self.getBuildsBySource()
83 unpublished_builds_by_source = self.getUnpublishedBuildsBySource()
84 changesfiles_by_source = self.getChangesFileBySource()
85+ # Source package names are used by setNewerDistroSeriesVersions:
86+ # batch load the used source package names.
87+ spn_ids = set()
88+ for spph in self._source_publications:
89+ spn_ids.add(spph.sourcepackagerelease.sourcepackagenameID)
90+ list(IStore(SourcePackageName).find(SourcePackageName,
91+ SourcePackageName.id.is_in(spn_ids)))
92 DistroSeries.setNewerDistroSeriesVersions(self._source_publications)
93+ # Load all the build status summaries at once.
94+ publishing_set = getUtility(IPublishingSet)
95+ archive_pub_ids = defaultdict(list)
96+ for pub in self._source_publications:
97+ archive_pub_ids[pub.archive].append(pub.id)
98+ status_summaries = {}
99+ for archive, pub_ids in archive_pub_ids.items():
100+ status_summaries.update(
101+ publishing_set.getBuildStatusSummariesForSourceIdsAndArchive(
102+ pub_ids, archive))
103
104 # Build the decorated object with the information we have.
105 for pub in self._source_publications:
106 builds = builds_by_source.get(pub, [])
107 unpublished_builds = unpublished_builds_by_source.get(pub, [])
108 changesfile = changesfiles_by_source.get(pub, None)
109+ status_summary = status_summaries[pub.id]
110 complete_pub = ArchiveSourcePublication(
111 pub, unpublished_builds=unpublished_builds, builds=builds,
112- changesfile=changesfile)
113+ changesfile=changesfile, status_summary=status_summary)
114 results.append(complete_pub)
115
116 return iter(results)
117
118=== modified file 'lib/lp/soyuz/browser/tests/test_archive_packages.py'
119--- lib/lp/soyuz/browser/tests/test_archive_packages.py 2010-10-16 15:09:47 +0000
120+++ lib/lp/soyuz/browser/tests/test_archive_packages.py 2010-11-10 05:56:09 +0000
121@@ -9,19 +9,28 @@
122 __all__ = [
123 'TestP3APackages',
124 'TestPPAPackages',
125- 'test_suite',
126 ]
127
128+from testtools.matchers import (
129+ Equals,
130+ LessThan,
131+ MatchesAny,
132+ )
133 from zope.security.interfaces import Unauthorized
134
135+from canonical.launchpad.webapp import canonical_url
136 from canonical.testing.layers import LaunchpadFunctionalLayer
137 from lp.soyuz.browser.archive import ArchiveNavigationMenu
138 from lp.testing import (
139 login,
140 login_person,
141+ person_logged_in,
142 TestCaseWithFactory,
143 )
144+from lp.testing.matchers import HasQueryCount
145+from lp.testing.sampledata import ADMIN_EMAIL
146 from lp.testing.views import create_initialized_view
147+from lp.testing._webservice import QueryCollector
148
149
150 class TestP3APackages(TestCaseWithFactory):
151@@ -113,3 +122,79 @@
152 def test_specified_name_filter_returns_none_on_empty_filter(self):
153 view = self.getPackagesView('field.name_filter=')
154 self.assertIs(None, view.specified_name_filter)
155+
156+ def test_source_query_counts(self):
157+ query_baseline = 42
158+ # Assess the baseline.
159+ collector = QueryCollector()
160+ collector.register()
161+ self.addCleanup(collector.unregister)
162+ ppa = self.factory.makeArchive()
163+ viewer = self.factory.makePerson(password="test")
164+ browser = self.getUserBrowser(user=viewer)
165+ with person_logged_in(viewer):
166+ # The baseline has one package, because otherwise the short-circuit
167+ # prevents the packages iteration happening at all and we're not
168+ # actually measuring scaling appropriately.
169+ self.factory.makeSourcePackagePublishingHistory(archive=ppa)
170+ url = canonical_url(ppa) + "/+packages"
171+ browser.open(url)
172+ self.assertThat(collector, HasQueryCount(LessThan(query_baseline)))
173+ expected_count = collector.count
174+ # We scale with 1 query per distro series because of
175+ # getCurrentSourceReleases.
176+ expected_count += 1
177+ # We need a fuzz of one because if the test is the first to run a
178+ # credentials lookup is done as well (and accrued to the collector).
179+ expected_count += 1
180+ # Use all new objects - avoids caching issues invalidating the gathered
181+ # metrics.
182+ login(ADMIN_EMAIL)
183+ ppa = self.factory.makeArchive()
184+ viewer = self.factory.makePerson(password="test")
185+ browser = self.getUserBrowser(user=viewer)
186+ with person_logged_in(viewer):
187+ for i in range(2):
188+ pkg = self.factory.makeSourcePackagePublishingHistory(
189+ archive=ppa)
190+ self.factory.makeSourcePackagePublishingHistory(archive=ppa,
191+ distroseries=pkg.distroseries)
192+ url = canonical_url(ppa) + "/+packages"
193+ browser.open(url)
194+ self.assertThat(collector, HasQueryCount(LessThan(expected_count)))
195+
196+ def test_binary_query_counts(self):
197+ query_baseline = 26
198+ # Assess the baseline.
199+ collector = QueryCollector()
200+ collector.register()
201+ self.addCleanup(collector.unregister)
202+ ppa = self.factory.makeArchive()
203+ viewer = self.factory.makePerson(password="test")
204+ browser = self.getUserBrowser(user=viewer)
205+ with person_logged_in(viewer):
206+ # The baseline has one package, because otherwise the short-circuit
207+ # prevents the packages iteration happening at all and we're not
208+ # actually measuring scaling appropriately.
209+ self.factory.makeBinaryPackagePublishingHistory(archive=ppa)
210+ url = canonical_url(ppa) + "/+packages"
211+ browser.open(url)
212+ self.assertThat(collector, HasQueryCount(
213+ MatchesAny(LessThan(query_baseline), Equals(query_baseline))))
214+ expected_count = collector.count
215+ # Use all new objects - avoids caching issues invalidating the gathered
216+ # metrics.
217+ login(ADMIN_EMAIL)
218+ ppa = self.factory.makeArchive()
219+ viewer = self.factory.makePerson(password="test")
220+ browser = self.getUserBrowser(user=viewer)
221+ with person_logged_in(viewer):
222+ for i in range(2):
223+ pkg = self.factory.makeBinaryPackagePublishingHistory(
224+ archive=ppa)
225+ self.factory.makeBinaryPackagePublishingHistory(archive=ppa,
226+ distroarchseries=pkg.distroarchseries)
227+ url = canonical_url(ppa) + "/+packages"
228+ browser.open(url)
229+ self.assertThat(collector, HasQueryCount(
230+ MatchesAny(Equals(expected_count), LessThan(expected_count))))
231
232=== modified file 'lib/lp/soyuz/interfaces/publishing.py'
233--- lib/lp/soyuz/interfaces/publishing.py 2010-11-07 22:51:53 +0000
234+++ lib/lp/soyuz/interfaces/publishing.py 2010-11-10 05:56:09 +0000
235@@ -1079,8 +1079,7 @@
236 `IBinaryPackagePublishingHistory`.
237 """
238
239- def getBuildStatusSummariesForSourceIdsAndArchive(source_ids, archive,
240- get_builds=None):
241+ def getBuildStatusSummariesForSourceIdsAndArchive(source_ids, archive):
242 """Return a summary of the build statuses for source publishing ids.
243
244 This method collects all the builds for the provided source package
245@@ -1095,9 +1094,6 @@
246 :param archive: The archive which will be used to filter the source
247 ids.
248 :type archive: `IArchive`
249- :param get_builds: Optional override to replace database querying for
250- build records. This is used in the ArchivePublication adapter code
251- path and should be avoided where possible.
252 :return: A dict consisting of the overall status summaries for the
253 given ids that belong in the archive. For example:
254 {
255@@ -1108,8 +1104,7 @@
256 :rtype: ``dict``.
257 """
258
259- def getBuildStatusSummaryForSourcePublication(source_publication,
260- get_builds=None):
261+ def getBuildStatusSummaryForSourcePublication(source_publication):
262 """Return a summary of the build statuses for this source
263 publication.
264
265@@ -1117,13 +1112,6 @@
266 for details. The call is just proxied here so that it can also be
267 used with an ArchiveSourcePublication passed in as
268 the source_package_pub, allowing the use of the cached results.
269-
270- :param get_builds: An optional callback to replace the database lookup
271- builds. This is used by some adapters as part of a preloading
272- strategy - but the preferred form is to have
273- getBuildStatusSummariesForSourceIdsAndArchive (which this call
274- delegates to) perform the initial and only loading of the build
275- records.
276 """
277
278 def getNearestAncestor(
279
280=== modified file 'lib/lp/soyuz/interfaces/sourcepackagerelease.py'
281--- lib/lp/soyuz/interfaces/sourcepackagerelease.py 2010-08-25 00:29:36 +0000
282+++ lib/lp/soyuz/interfaces/sourcepackagerelease.py 2010-11-10 05:56:09 +0000
283@@ -98,6 +98,7 @@
284 files = Attribute("IBinaryPackageFile entries for this "
285 "sourcepackagerelease")
286 sourcepackagename = Attribute("SourcePackageName table reference")
287+ sourcepackagenameID = Attribute("SourcePackageName id.")
288 upload_distroseries = Attribute("The distroseries in which this package "
289 "was first uploaded in Launchpad")
290 publishings = Attribute("MultipleJoin on SourcepackagePublishing")
291
292=== modified file 'lib/lp/soyuz/model/publishing.py'
293--- lib/lp/soyuz/model/publishing.py 2010-11-09 00:01:48 +0000
294+++ lib/lp/soyuz/model/publishing.py 2010-11-10 05:56:09 +0000
295@@ -1699,41 +1699,32 @@
296 return result_set.one()
297
298 def getBuildStatusSummariesForSourceIdsAndArchive(self, source_ids,
299- archive, get_builds=None):
300+ archive):
301 """See `IPublishingSet`."""
302 # source_ids can be None or an empty sequence.
303 if not source_ids:
304 return {}
305
306 store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
307- if len(source_ids) == 1 and get_builds is not None:
308- # Fake out the DB access for ArchivePublication
309- source_pubs = list(store.find(
310+ build_info = list(
311+ self.getBuildsForSourceIds(source_ids, archive=archive))
312+ source_pubs = set()
313+ found_source_ids = set()
314+ for row in build_info:
315+ source_pubs.add(row[0])
316+ found_source_ids.add(row[0].id)
317+ pubs_without_builds = set(source_ids) - found_source_ids
318+ if pubs_without_builds:
319+ # Add in source pubs for which no builds were found: we may in
320+ # future want to make this a LEFT OUTER JOIN in
321+ # getBuildsForSourceIds but to avoid destabilising other code
322+ # paths while we fix performance, it is just done as a single
323+ # separate query for now.
324+ source_pubs.update(store.find(
325 SourcePackagePublishingHistory,
326- SourcePackagePublishingHistory.id.is_in(source_ids),
327+ SourcePackagePublishingHistory.id.is_in(
328+ pubs_without_builds),
329 SourcePackagePublishingHistory.archive == archive))
330- source = source_pubs[0]
331- build_info = [(source, build, None) for build in get_builds()]
332- else:
333- build_info = list(
334- self.getBuildsForSourceIds(source_ids, archive=archive))
335- source_pubs = set()
336- found_source_ids = set()
337- for row in build_info:
338- source_pubs.add(row[0])
339- found_source_ids.add(row[0].id)
340- pubs_without_builds = set(source_ids) - found_source_ids
341- if pubs_without_builds:
342- # Add in source pubs for which no builds were found: we may in
343- # future want to make this a LEFT OUTER JOIN in
344- # getBuildsForSourceIds but to avoid destabilising other code
345- # paths while we fix performance, it is just done as a single
346- # separate query for now.
347- source_pubs.update(store.find(
348- SourcePackagePublishingHistory,
349- SourcePackagePublishingHistory.id.is_in(
350- pubs_without_builds),
351- SourcePackagePublishingHistory.archive == archive))
352 # For each source_pub found, provide an aggregate summary of its
353 # builds.
354 binarypackages = getUtility(IBinaryPackageBuildSet)
355@@ -1774,8 +1765,7 @@
356
357 return source_build_statuses
358
359- def getBuildStatusSummaryForSourcePublication(self, source_publication,
360- get_builds=None):
361+ def getBuildStatusSummaryForSourcePublication(self, source_publication):
362 """See `ISourcePackagePublishingHistory`.getStatusSummaryForBuilds.
363
364 This is provided here so it can be used by both the SPPH as well
365@@ -1785,7 +1775,7 @@
366 """
367 source_id = source_publication.id
368 return self.getBuildStatusSummariesForSourceIdsAndArchive([source_id],
369- source_publication.archive, get_builds=get_builds)[source_id]
370+ source_publication.archive)[source_id]
371
372 def requestDeletion(self, sources, removed_by, removal_comment=None):
373 """See `IPublishingSet`."""