Merge lp:~abentley/launchpad/estimate-duration into lp:launchpad

Proposed by Aaron Bentley
Status: Merged
Merged at revision: 10920
Proposed branch: lp:~abentley/launchpad/estimate-duration
Merge into: lp:launchpad
Diff against target: 130 lines (+51/-9)
4 files modified
lib/lp/code/model/sourcepackagerecipe.py (+14/-1)
lib/lp/code/model/sourcepackagerecipebuild.py (+4/-2)
lib/lp/code/model/tests/test_sourcepackagerecipe.py (+22/-2)
lib/lp/code/model/tests/test_sourcepackagerecipebuild.py (+11/-4)
To merge this branch: bzr merge lp:~abentley/launchpad/estimate-duration
Reviewer Review Type Date Requested Status
Paul Hummer (community) code Approve
Review via email: mp+25553@code.launchpad.net

Commit message

Provide a data-based estimateDuration.

Description of the change

= Summary =
Fix bug #507764: Need proper SourcePackageRecipeBuild.estimateDuration()

== Proposed fix ==
For sourcepackagerecipes with one or more builds which has a duration, use the
median of those durations. The status of the these builds is not considered,
so if builds of this recipe always fail, this will estimate how long it will
take to fail.

Using the median means that extreme values will not unduly influence the
estimate.

If there are no builds with durations, the estimate is 10 minutes, which is a
reasonable value for small packages.

== Pre-implementation notes ==
bigjools impressed the importance of this bug upon me. Mid-implementation with
rockstar re: status

== Implementation details ==
This implements SourcePackageRecipe.getMedianBuildDuration as the underlying
calculation, and then implements estimateDuration on top of it.

Where the number of values is even, this is not a true median, because it takes
the lower-middle value instead of combining the lower-middle and higher-middle
values.

There are some small drive-by fixes, as well.

== Tests ==
bin/test -t estimateDuration -t getMedian test_sourcepackagerecipe

== Demo and Q/A ==
Request a build. The estimated duration should start at 10 minutes.
Request a second build. The estimated duration should be the actual previous
duration.

= 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/code/model/tests/test_sourcepackagerecipebuild.py
  lib/lp/code/model/tests/test_sourcepackagerecipe.py
  lib/lp/code/model/sourcepackagerecipebuild.py
  lib/lp/code/model/sourcepackagerecipe.py

To post a comment you must log in.
Revision history for this message
Paul Hummer (rockstar) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/code/model/sourcepackagerecipe.py'
2--- lib/lp/code/model/sourcepackagerecipe.py 2010-05-10 15:50:23 +0000
3+++ lib/lp/code/model/sourcepackagerecipe.py 2010-05-18 19:27:28 +0000
4@@ -20,7 +20,7 @@
5 from zope.interface import classProvides, implements
6
7 from canonical.database.datetimecol import UtcDateTimeCol
8-from canonical.launchpad.interfaces.lpstorm import IMasterStore
9+from canonical.launchpad.interfaces.lpstorm import IMasterStore, IStore
10
11 from lp.code.interfaces.sourcepackagerecipe import (
12 ISourcePackageRecipe, ISourcePackageRecipeSource,
13@@ -176,3 +176,16 @@
14 *clauses)
15 result.order_by(Desc(SourcePackageRecipeBuild.datebuilt))
16 return result
17+
18+ def getMedianBuildDuration(self):
19+ """Return the median duration of builds of this recipe."""
20+ store = IStore(self)
21+ result = store.find(
22+ SourcePackageRecipeBuild.buildduration,
23+ SourcePackageRecipeBuild.recipe==self.id,
24+ SourcePackageRecipeBuild.buildduration != None)
25+ result.order_by(Desc(SourcePackageRecipeBuild.buildduration))
26+ count = result.count()
27+ if count == 0:
28+ return None
29+ return result[count/2]
30
31=== modified file 'lib/lp/code/model/sourcepackagerecipebuild.py'
32--- lib/lp/code/model/sourcepackagerecipebuild.py 2010-05-14 21:24:48 +0000
33+++ lib/lp/code/model/sourcepackagerecipebuild.py 2010-05-18 19:27:28 +0000
34@@ -214,8 +214,10 @@
35
36 def estimateDuration(self):
37 """See `IBuildBase`."""
38- # XXX: wgrant 2010-01-19 bug=507764: Need proper implementation.
39- return datetime.timedelta(minutes=2)
40+ median = self.recipe.getMedianBuildDuration()
41+ if median is not None:
42+ return median
43+ return datetime.timedelta(minutes=10)
44
45 def verifySuccessfulUpload(self):
46 return self.source_package_release is not None
47
48=== modified file 'lib/lp/code/model/tests/test_sourcepackagerecipe.py'
49--- lib/lp/code/model/tests/test_sourcepackagerecipe.py 2010-05-17 19:02:37 +0000
50+++ lib/lp/code/model/tests/test_sourcepackagerecipe.py 2010-05-18 19:27:28 +0000
51@@ -7,7 +7,7 @@
52
53 __metaclass__ = type
54
55-from datetime import datetime
56+from datetime import datetime, timedelta
57 import textwrap
58 import unittest
59
60@@ -25,7 +25,8 @@
61
62 from canonical.launchpad.webapp.authorization import check_permission
63 from lp.soyuz.interfaces.archive import (
64- ArchiveDisabled, ArchivePurpose, CannotUploadToArchive, InvalidPocketForPPA)
65+ ArchiveDisabled, ArchivePurpose, CannotUploadToArchive,
66+ InvalidPocketForPPA)
67 from lp.buildmaster.interfaces.buildqueue import IBuildQueue
68 from lp.buildmaster.model.buildqueue import BuildQueue
69 from lp.code.interfaces.sourcepackagerecipe import (
70@@ -319,6 +320,25 @@
71 # Show no database constraints were violated
72 Store.of(recipe).flush()
73
74+ def test_getMedianBuildDuration(self):
75+ recipe = removeSecurityProxy(self.factory.makeSourcePackageRecipe())
76+ self.assertIs(None, recipe.getMedianBuildDuration())
77+ build = removeSecurityProxy(
78+ self.factory.makeSourcePackageRecipeBuild(recipe=recipe))
79+ build.buildduration = timedelta(minutes=10)
80+ self.assertEqual(
81+ timedelta(minutes=10), recipe.getMedianBuildDuration())
82+ def addBuild(minutes):
83+ build = removeSecurityProxy(
84+ self.factory.makeSourcePackageRecipeBuild(recipe=recipe))
85+ build.buildduration = timedelta(minutes=minutes)
86+ addBuild(20)
87+ self.assertEqual(
88+ timedelta(minutes=10), recipe.getMedianBuildDuration())
89+ addBuild(11)
90+ self.assertEqual(
91+ timedelta(minutes=11), recipe.getMedianBuildDuration())
92+
93
94 class TestRecipeBranchRoundTripping(TestCaseWithFactory):
95
96
97=== modified file 'lib/lp/code/model/tests/test_sourcepackagerecipebuild.py'
98--- lib/lp/code/model/tests/test_sourcepackagerecipebuild.py 2010-05-17 17:40:32 +0000
99+++ lib/lp/code/model/tests/test_sourcepackagerecipebuild.py 2010-05-18 19:27:28 +0000
100@@ -78,8 +78,8 @@
101 self.assertProvides(bq.specific_job, ISourcePackageRecipeBuildJob)
102 self.assertEqual(True, bq.virtualized)
103
104- # The processor for SourcePackageRecipeBuilds should not be None. They
105- # do require specific environments.
106+ # The processor for SourcePackageRecipeBuilds should not be None.
107+ # They do require specific environments.
108 self.assertNotEqual(None, bq.processor)
109 self.assertEqual(
110 spb.distroseries.nominatedarchindep.default_processor,
111@@ -136,10 +136,17 @@
112 self.assertFalse(check_permission('launchpad.View', build))
113
114 def test_estimateDuration(self):
115- # The duration estimate is currently hard-coded as two minutes.
116+ # If there are no successful builds, estimate 10 minutes.
117 spb = self.makeSourcePackageRecipeBuild()
118 self.assertEqual(
119- datetime.timedelta(minutes=2), spb.estimateDuration())
120+ datetime.timedelta(minutes=10), spb.estimateDuration())
121+ for minutes in [20, 5, 1]:
122+ build = removeSecurityProxy(
123+ self.factory.makeSourcePackageRecipeBuild(recipe=spb.recipe))
124+ build.buildduration = datetime.timedelta(minutes=minutes)
125+ self.assertEqual(
126+ datetime.timedelta(minutes=5), spb.estimateDuration())
127+
128
129 def test_datestarted(self):
130 """Datestarted is taken from job if not specified in the build.