Merge lp:~bigkevmcd/offspring/add-queue-wait-time into lp:offspring

Proposed by Kevin McDermott
Status: Merged
Merge reported by: Kevin McDermott
Merged at revision: not available
Proposed branch: lp:~bigkevmcd/offspring/add-queue-wait-time
Merge into: lp:offspring
Prerequisite: lp:~bigkevmcd/offspring/add-queue-metrics
Diff against target: 485 lines (+196/-82)
6 files modified
lib/offspring/web/queuemanager/management/commands/build_metrics.py (+2/-1)
lib/offspring/web/queuemanager/management/commands/tests/test_build_metrics.py (+1/-1)
lib/offspring/web/queuemanager/metrics.py (+68/-31)
lib/offspring/web/queuemanager/models.py (+7/-12)
lib/offspring/web/queuemanager/tests/factory.py (+10/-3)
lib/offspring/web/queuemanager/tests/test_metrics.py (+108/-34)
To merge this branch: bzr merge lp:~bigkevmcd/offspring/add-queue-wait-time
Reviewer Review Type Date Requested Status
Guilherme Salgado Approve
Offspring Committers Pending
Review via email: mp+85489@code.launchpad.net

Description of the change

This refactors the metric calculation logic and adds support for the average queue time.

Still to do...non-scheduled builds (involves adding logic to exclude specific types from the reason).

To post a comment you must log in.
119. By Kevin McDermott

Merge forward with changes to automated request lookups.

120. By Kevin McDermott

Update clarified docstring.

121. By Kevin McDermott

Merge forward.

122. By Kevin McDermott

Merge forward.

123. By Kevin McDermott

Update docstring, and removed some unused code.

124. By Kevin McDermott

Merge forward.

125. By Kevin McDermott

Fix command test to reflect new properties.

126. By Kevin McDermott

Merge forward.

127. By Kevin McDermott

Merge forward.

128. By Kevin McDermott

Merge forward.

129. By Kevin McDermott

Merge forward.

130. By Kevin McDermott

Merge trunk.

Revision history for this message
Guilherme Salgado (salgado) wrote :

This looks quite neat! I was hoping there'd be some template tag in Django to convert a timedetal into a human readable string but I couldn't find anything. I guess you've probably searched for one as well before rolling your own?

Also, in a couple places you create a build result using the object factory, set the requested_at/dispatched_at attributes and then save the object again. It'd be nicer if you changed the factory method to take requested_at/dispatched_at as optional arguments to avoid duplicating it in even more places in the future.

review: Approve
131. By Kevin McDermott

Merge trunk.

132. By Kevin McDermott

Update factory.make_build_result to allow started_at/finished_at etc.

Revision history for this message
Kevin McDermott (bigkevmcd) wrote :

> This looks quite neat! I was hoping there'd be some template tag in Django to
> convert a timedetal into a human readable string but I couldn't find anything.
> I guess you've probably searched for one as well before rolling your own?
Yeah...the normal time one doesn't understand timedeltas (I guess it's relatively rare).

> Also, in a couple places you create a build result using the object factory,
> set the requested_at/dispatched_at attributes and then save the object again.
> It'd be nicer if you changed the factory method to take
> requested_at/dispatched_at as optional arguments to avoid duplicating it in
> even more places in the future.

It's a bit icky...but it works...

I added **kwargs because it would just keep growing...if there are specific things we want to check for, then it's good to move them to the list of named args, otherwise...

    def make_build_result(self, project=None, name=None, result=None,
                          builder=None, requestor=None, **kwargs):
        """
        Create a BuildResult with the given project or a newly created one if
        none is given.
        """
        if name is None:
            name = self.get_unique_string()
        if project is None:
            project = self.make_project()
        requested_at = kwargs.pop("requested_at", None)
        build_result = BuildResult.objects.create(
            project=project, name=name, result=result, builder=builder,
            requestor=requestor, **kwargs)
        # requested_at is auto_now_add, so we can't pass it into the object
        # creation function.
        if requested_at:
            build_result.requested_at = requested_at
            build_result.save()
        return build_result

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/offspring/web/queuemanager/management/commands/build_metrics.py'
2--- lib/offspring/web/queuemanager/management/commands/build_metrics.py 2011-12-14 07:32:32 +0000
3+++ lib/offspring/web/queuemanager/management/commands/build_metrics.py 2012-01-12 08:35:33 +0000
4@@ -10,5 +10,6 @@
5
6 def handle_noargs(self, **options):
7 metrics = get_build_result_metrics()
8- for item, value in metrics.iteritems():
9+
10+ for item, value in metrics:
11 self.stdout.write("%s = %s\n" % (item, value))
12
13=== modified file 'lib/offspring/web/queuemanager/management/commands/tests/test_build_metrics.py'
14--- lib/offspring/web/queuemanager/management/commands/tests/test_build_metrics.py 2011-12-14 08:44:40 +0000
15+++ lib/offspring/web/queuemanager/management/commands/tests/test_build_metrics.py 2012-01-12 08:35:33 +0000
16@@ -17,7 +17,7 @@
17 """
18 build_result_metrics_mock = self.mocker.replace(get_build_result_metrics)
19 build_result_metrics_mock()
20- self.mocker.result({"Description of value": 200})
21+ self.mocker.result([("Description of value", 200)])
22 self.mocker.replay()
23
24 command = Command()
25
26=== modified file 'lib/offspring/web/queuemanager/metrics.py'
27--- lib/offspring/web/queuemanager/metrics.py 2011-12-14 17:21:33 +0000
28+++ lib/offspring/web/queuemanager/metrics.py 2012-01-12 08:35:33 +0000
29@@ -4,13 +4,18 @@
30 from offspring.enums import ProjectBuildStates
31
32
33-def get_average_build_time(project=None, automated=None, period=None):
34+def get_average_time(query_params, columns, project=None, automated=None):
35+
36 """
37- Return the average time taken between BuildResult.started_at and
38- BuildResult.finished_at in seconds for all BuildResults associated with the
39- provided Project.
40-
41- If no Project is supplied, average the build times across all projects.
42+ Return the average time between the two named columns.
43+
44+ The columns must be listed as the start_time and finish_time for the
45+ metric.
46+
47+ If no Project is supplied, average the times across all projects.
48+
49+ query_params: A dictionary of Djanqo query filters.
50+ columns: Columns to be queried, must be start_time, end_time.
51
52 project: A Project to filter BuildResults with, if none supplied, will use
53 all Projects
54@@ -23,54 +28,86 @@
55 # don't run using PostgreSQL...
56 # FIXME: This code should migrate to a manager in order to do the heavy
57 # lifting https://bugs.launchpad.net/offspring/+bug/904334
58- def total_seconds(delta):
59+ def delta_to_seconds(delta):
60 """
61 Return the total amount of time represented by a timedelta.
62 """
63- if delta:
64- return ((delta.microseconds + (delta.seconds + delta.days *
65- 24 * 3600)
66- * 10 ** 6) / 10 ** 6)
67- else:
68- return 0.0
69-
70- query_params = {"finished_at__isnull": False,
71- "result__exact": ProjectBuildStates.SUCCESS}
72+ return ((delta.microseconds + (delta.seconds + delta.days
73+ * 24 * 3600)
74+ * 10 ** 6) / 10 ** 6)
75 if project is not None:
76 query_params["project"] = project
77
78 if automated is not None:
79 query_params["requestor__isnull"] = automated
80- if period is not None:
81- query_params["finished_at__range"] = period
82-
83 build_results = BuildResult.objects.filter(**query_params)
84- time_stamps = build_results.values_list("started_at", "finished_at")
85+ time_stamps = build_results.values_list(*columns)
86 if not time_stamps:
87 return 0.0
88- times = [total_seconds(finished_at - started_at)
89- for (started_at, finished_at) in time_stamps]
90+ times = [delta_to_seconds(end_time - start_time)
91+ for (start_time, end_time) in time_stamps]
92 return float(sum(times)) / len(time_stamps)
93
94
95+def get_average_build_time(project=None, automated=None, period=None):
96+ """
97+ Return the average time taken between BuildResult.started_at and
98+ BuildResult.finished_at in seconds for all BuildResults associated with the
99+ provided Project.
100+
101+ period: A tuple of datetime objects (start_time, end_time)
102+
103+ See get_average_time for the project and automated parameters.
104+ """
105+ query_params = {"finished_at__isnull": False,
106+ "result__exact": ProjectBuildStates.SUCCESS}
107+ if period is not None:
108+ query_params["finished_at__range"] = period
109+ return get_average_time(query_params, ("started_at", "finished_at"),
110+ project=project, automated=automated)
111+
112+
113+def get_average_queue_time(project=None, automated=None, period=None):
114+ """
115+ Return the average time taken between BuildResult.requested_at and
116+ BuildResult.dispatched_at in seconds for all BuildResults associated with the
117+ provided Project.
118+
119+ period: A tuple of datetime objects (start_time, end_time)
120+
121+ See get_average_time for the project and automated parameters.
122+ """
123+ query_params = {}
124+ if period is not None:
125+ query_params["dispatched_at__range"] = period
126+ return get_average_time(query_params, ("requested_at", "dispatched_at"),
127+ project=project, automated=automated)
128+
129+
130 def get_build_result_metrics():
131 """
132 Return some simple metrics, in a format suitable for iterating over and
133 displaying in a command-line tool.
134 """
135- results = {}
136+ results = []
137 start_date = datetime.utcnow()
138
139 seven_days = (start_date - timedelta(days=7), start_date)
140- results["Average scheduled build time (7 days)"] = get_average_build_time(
141- period=seven_days, automated=True)
142-
143 thirty_days = (start_date - timedelta(days=30), start_date)
144- results["Average scheduled build time (30 days)"] = get_average_build_time(
145- period=thirty_days, automated=True)
146-
147 ninety_days = (start_date - timedelta(days=90), start_date)
148- results["Average scheduled build time (90 days)"] = get_average_build_time(
149- period=ninety_days, automated=True)
150+
151+
152+ results.append(("Average scheduled build time (7 days)",
153+ get_average_build_time(period=seven_days, automated=True)))
154+ results.append(("Average scheduled build time (30 days)",
155+ get_average_build_time(period=thirty_days, automated=True)))
156+ results.append(("Average scheduled build time (90 days)",
157+ get_average_build_time(period=ninety_days, automated=True)))
158+ results.append(("Average scheduled queue time (7 days)",
159+ get_average_queue_time(period=seven_days, automated=True)))
160+ results.append(("Average scheduled queue time (30 days)",
161+ get_average_queue_time(period=thirty_days, automated=True)))
162+ results.append(("Average scheduled queue time (90 days)",
163+ get_average_queue_time(period=ninety_days, automated=True)))
164
165 return results
166
167=== modified file 'lib/offspring/web/queuemanager/models.py'
168--- lib/offspring/web/queuemanager/models.py 2011-12-14 14:44:27 +0000
169+++ lib/offspring/web/queuemanager/models.py 2012-01-12 08:35:33 +0000
170@@ -1,17 +1,8 @@
171 # Copyright 2010 Canonical Ltd. This software is licensed under the
172 # GNU Affero General Public License version 3 (see the file LICENSE).
173
174-__all__ = ["Project", "ProjectGroup", "Lexbuilder", "BuildRequest",
175- "BuildResult", "DailyBuildOrder", "LaunchpadProject",
176- "LaunchpadProjectMilestone", "ProjectNotificationSubscription",
177- "Release"]
178-
179-from datetime import (
180- date,
181- datetime,
182- timedelta
183-)
184 import math
185+from datetime import date, datetime, timedelta
186
187 from django.contrib.auth.models import AnonymousUser, User
188 from django.db import (
189@@ -26,6 +17,10 @@
190 from offspring.config import get_configuration
191 from offspring.enums import ProjectBuildStates
192
193+__all__ = ["Project", "ProjectGroup", "Lexbuilder", "BuildRequest",
194+ "BuildResult", "DailyBuildOrder", "LaunchpadProject",
195+ "LaunchpadProjectMilestone", "ProjectNotificationSubscription",
196+ "Release"]
197
198 config = get_configuration()
199
200@@ -85,7 +80,7 @@
201 project_group=self)
202
203 @property
204- def builder(self):
205+ def builder(self):
206 return None
207
208 @property
209@@ -389,7 +384,7 @@
210 requestor = models.ForeignKey(User, db_column="requestor_id", blank=True, null=True, editable=False)
211 reason = models.CharField('request reason', blank=True, null=True, max_length=200, editable=False)
212 requested_at = models.DateTimeField('date created', auto_now_add=True, blank=True, null=True, editable=False)
213- dispatched_at = models.DateTimeField('date dispatched', editable=False, null=True, blank=True)
214+ dispatched_at = models.DateTimeField('date dispatched', editable=False, null=True, blank=True)
215 notes = models.CharField(max_length=200, null=True, blank=True)
216
217 access_relation = 'project'
218
219=== modified file 'lib/offspring/web/queuemanager/tests/factory.py'
220--- lib/offspring/web/queuemanager/tests/factory.py 2011-12-16 10:08:55 +0000
221+++ lib/offspring/web/queuemanager/tests/factory.py 2012-01-12 08:35:33 +0000
222@@ -73,7 +73,7 @@
223 return ProjectGroup.objects.create(name=name, title=title)
224
225 def make_build_result(self, project=None, name=None, result=None,
226- builder=None, requestor=None):
227+ builder=None, requestor=None, **kwargs):
228 """
229 Create a BuildResult with the given project or a newly created one if
230 none is given.
231@@ -82,9 +82,16 @@
232 name = self.get_unique_string()
233 if project is None:
234 project = self.make_project()
235- return BuildResult.objects.create(
236+ requested_at = kwargs.pop("requested_at", None)
237+ build_result = BuildResult.objects.create(
238 project=project, name=name, result=result, builder=builder,
239- requestor=requestor)
240+ requestor=requestor, **kwargs)
241+ # requested_at is auto_now_add, so we can't pass it into the object
242+ # creation function.
243+ if requested_at:
244+ build_result.requested_at = requested_at
245+ build_result.save()
246+ return build_result
247
248 def make_release(self, build=None, name=None, creator=None):
249 """
250
251=== modified file 'lib/offspring/web/queuemanager/tests/test_metrics.py'
252--- lib/offspring/web/queuemanager/tests/test_metrics.py 2011-12-14 17:38:39 +0000
253+++ lib/offspring/web/queuemanager/tests/test_metrics.py 2012-01-12 08:35:33 +0000
254@@ -5,7 +5,8 @@
255 from offspring.enums import ProjectBuildStates
256 from offspring.web.queuemanager.models import User
257 from offspring.web.queuemanager.metrics import (
258- get_average_build_time, get_build_result_metrics)
259+ get_average_build_time, get_average_queue_time,
260+ get_build_result_metrics)
261
262 from offspring.web.queuemanager.tests.factory import factory
263
264@@ -23,10 +24,25 @@
265 finished_at = datetime.combine(build_date, time())
266 for minutes in durations:
267 build_result = factory.make_build_result(
268- project=project, requestor=requestor, result=result)
269- build_result.finished_at = finished_at
270- build_result.started_at = finished_at - timedelta(minutes=minutes)
271- build_result.save()
272+ project=project, requestor=requestor, result=result,
273+ started_at=finished_at - timedelta(minutes=minutes),
274+ finished_at=finished_at)
275+
276+
277+def create_build_results_with_queue_times(project, queue_times, requestor=None,
278+ build_date=date.today()):
279+ """
280+ Create BuildResults with queue times specified.
281+
282+ project: The Project to associate the BuildResults with.
283+ queue_times: A sequence of time periods in minutes.
284+ """
285+ dispatched_at = datetime.combine(build_date, time())
286+ for minutes in queue_times:
287+ build_result = factory.make_build_result(
288+ project=project, requestor=requestor,
289+ requested_at=dispatched_at - timedelta(minutes=minutes),
290+ dispatched_at=dispatched_at)
291
292
293 class BuildResultMetricsTests(TestCase):
294@@ -39,13 +55,12 @@
295 get_average_build_time should return the average time taken to build a
296 project.
297 """
298+ finished_at = datetime.utcnow()
299 build_result = factory.make_build_result(
300- project=self.project, result=ProjectBuildStates.SUCCESS)
301- finished_at = datetime.now()
302- build_result.finished_at = finished_at
303- build_result.started_at = finished_at - timedelta(minutes=10)
304- build_result.save()
305- self.assertEqual(600, get_average_build_time(self.project))
306+ project=self.project, result=ProjectBuildStates.SUCCESS,
307+ finished_at=finished_at,
308+ started_at=finished_at - timedelta(minutes=10))
309+ self.assertEqual(10*60, get_average_build_time(self.project))
310
311 def test_get_average_build_time_only_includes_completed_builds(self):
312 """
313@@ -72,9 +87,9 @@
314 If a build has started, but not yet finished, this we should get 0.0
315 for the average for this entry.
316 """
317- build_result = factory.make_build_result(project=self.project)
318- build_result.started_at = datetime.now() - timedelta(minutes=20)
319- build_result.save()
320+ started_at = datetime.utcnow() - timedelta(minutes=20)
321+ build_result = factory.make_build_result(project=self.project,
322+ started_at=started_at)
323 self.assertEqual(0.0, get_average_build_time(self.project))
324
325 def test_get_average_build_time_multiple_builds(self):
326@@ -82,7 +97,7 @@
327 The time for each BuildResult should be averaged together.
328 """
329 create_build_results_with_durations(self.project, [10, 20])
330- self.assertEqual(15 * 60.0, get_average_build_time(self.project))
331+ self.assertEqual(15*60, get_average_build_time(self.project))
332
333 def test_get_average_build_time_multiple_builds_ignores_unfinished(self):
334 """
335@@ -91,11 +106,11 @@
336 create_build_results_with_durations(self.project, [10, 20])
337
338 # Create an additional unfinished build.
339- build_result = factory.make_build_result(project=self.project)
340- build_result.started_at = datetime.now() - timedelta(minutes=20)
341- build_result.save()
342+ started_at = datetime.utcnow() - timedelta(minutes=20)
343+ build_result = factory.make_build_result(project=self.project,
344+ started_at=started_at)
345
346- self.assertEqual(15 * 60.0, get_average_build_time(self.project))
347+ self.assertEqual(15*60, get_average_build_time(self.project))
348
349 def test_get_average_build_time_ignores_other_projects(self):
350 """
351@@ -104,8 +119,7 @@
352 """
353 create_build_results_with_durations(self.project, [10, 20])
354 create_build_results_with_durations(factory.make_project(), [50, 100])
355-
356- self.assertEqual(15 * 60.0, get_average_build_time(self.project))
357+ self.assertEqual(15*60, get_average_build_time(self.project))
358
359 def test_get_average_build_time_no_project(self):
360 """
361@@ -116,7 +130,7 @@
362 create_build_results_with_durations(
363 factory.make_project(), [50, 100])
364 # 10 + 20 + 50 + 100 = 180 / 4 = 45 minutes...
365- self.assertEqual(45 * 60.0, get_average_build_time())
366+ self.assertEqual(45*60, get_average_build_time())
367
368 def test_get_average_build_time_with_automated_flag(self):
369 """
370@@ -168,8 +182,7 @@
371
372 # From the day after we create our builds 'til today.
373 time_period = (build_date + timedelta(days=1), datetime.utcnow())
374- self.assertEqual(75 * 60.0,
375- get_average_build_time(period=time_period))
376+ self.assertEqual(75*60, get_average_build_time(period=time_period))
377
378 def test_get_average_build_time_filters_by_date_and_automated(self):
379 """
380@@ -189,34 +202,95 @@
381
382 # From the day after we create our builds 'til today.
383 time_period = (build_date + timedelta(days=1), datetime.utcnow())
384- self.assertEqual(40 * 60.0,
385+ self.assertEqual(40*60,
386 get_average_build_time(period=time_period,
387 automated=True))
388
389+
390+class QueueTimeMetricsTests(TestCase):
391+
392+ def setUp(self):
393+ self.project = factory.make_project()
394+
395+ def test_average_queue_time_single_build(self):
396+ """
397+ get_average_queue_time should return the average time a build waits
398+ before being dispatched.
399+ """
400+ dispatched_at = datetime.utcnow()
401+ build_result = factory.make_build_result(
402+ project=self.project, dispatched_at=dispatched_at,
403+ requested_at=dispatched_at - timedelta(minutes=30))
404+ self.assertEqual(30*60, get_average_queue_time(self.project))
405+
406+ def test_average_queue_time_no_builds(self):
407+ """
408+ If there are no builds, we should get an average of 0.0.
409+ """
410+ self.assertEqual(0.0*60, get_average_queue_time(self.project))
411+
412+ def test_average_queue_time_includes_by_date(self):
413+ """
414+ It should be possible to restrict the date queried to a specific time
415+ frame, specifying a period should only include those BuildResults
416+ within that period.
417+ """
418+ build_date = datetime.utcnow() - timedelta(days=30)
419+ create_build_results_with_queue_times(
420+ self.project, [10, 20], build_date=build_date)
421+
422+ create_build_results_with_queue_times(
423+ factory.make_project(), [50, 100],
424+ build_date=build_date + timedelta(days=2))
425+
426+ # From the day after we create our builds 'til today.
427+ time_period = (build_date + timedelta(days=1), datetime.utcnow())
428+ self.assertEqual(75*60,
429+ get_average_queue_time(period=time_period))
430+
431+
432+class GetBuildResultsTests(TestCase):
433+
434+ def setUp(self):
435+ self.project = factory.make_project()
436+
437 def test_get_build_result_metrics(self):
438 """
439- get_build_result_metrics should return a dictionary with the following:
440-
441- "Average scheduled build time (7 days)": value,
442- "Average scheduled build time (30 days)": value,
443- "Average scheduled build time (90 days)": value
444+ get_build_result_metrics should return a list of tuples with the metric
445+ being measured, and the time in minutes.
446 """
447 start_date = datetime.utcnow()
448 # Two this week, average = 10 + 20/2 = 15
449 create_build_results_with_durations(
450 self.project, [10, 20], build_date=start_date)
451
452+ # Two this week, average = 30 + 18/2 = 24.0
453+ create_build_results_with_queue_times(
454+ self.project, [30, 18], build_date=start_date)
455+
456 last_week = start_date - timedelta(days=8)
457 # Two last week, 10 + 10 + 10 + 20/4 = 12.5
458 create_build_results_with_durations(
459 self.project, [10, 10], build_date=last_week)
460
461+ # Two last week, average = 30 + 18 + 14 11/4 = 73
462+ create_build_results_with_queue_times(
463+ self.project, [14, 11], build_date=last_week)
464+
465 last_month = start_date - timedelta(days=35)
466 # Two last month, 10 + 10 + 10 + 20 + 30 + 40/6 = 20
467 create_build_results_with_durations(
468 self.project, [30, 40], build_date=last_month)
469
470- self.assertEqual({"Average scheduled build time (7 days)": 900,
471- "Average scheduled build time (30 days)": 750,
472- "Average scheduled build time (90 days)": 1200},
473- get_build_result_metrics())
474+ # Two last month, average = 30 + 18 + 14 11 + 30 + 20/6 = 82
475+ create_build_results_with_queue_times(
476+ self.project, [30, 20], build_date=last_month)
477+
478+ self.assertEqual(
479+ [("Average scheduled build time (7 days)", 15.0*60),
480+ ("Average scheduled build time (30 days)", 12.5*60),
481+ ("Average scheduled build time (90 days)", 20.0*60),
482+ ("Average scheduled queue time (7 days)", 24.0*60),
483+ ("Average scheduled queue time (30 days)", 18.25*60),
484+ ("Average scheduled queue time (90 days)", 20.5*60)],
485+ get_build_result_metrics())

Subscribers

People subscribed via source and target branches