Skip to content

Commit 05d1b26

Browse files
committed
Refactor timeline grid to stream data and partition it
The page size is configurable as a setting. Streaming is used to be able to already send data to the client while more database query my still need to be performed. timeline.js will keep requesting data as long as the nextBenchmarks field indicates an indicates (!== false). The plotgrid is only reset on the first request. Signed-off-by: Stefan Marr <git@stefan-marr.de>
1 parent 35fbdcd commit 05d1b26

4 files changed

Lines changed: 179 additions & 107 deletions

File tree

codespeed/settings.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@
4545
# the view slow, and put load on the database, which may be
4646
# undeseriable.
4747

48+
TIMELINE_GRID_PAGING = 4 # Number of benchmarks to be send in one grid request
49+
# May be adjusted to improve the performance of the timeline grid view.
50+
# If a large number of benchmarks is in the system,
51+
# and the database is not fast, it can take a long time
52+
# to send all results.
53+
4854
#TIMELINE_BRANCHES = True # NOTE: Only the default branch is currently shown
4955
# Get timeline results for specific branches
5056
# Set to False if you want timeline plots and results only for trunk.

codespeed/static/js/timeline.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -291,18 +291,26 @@ function render(data) {
291291
$("#revisions").attr("disabled", false);
292292
$("#equidistant").attr("disabled", false);
293293
$("span.options.median").css("display", "none");
294-
$("#plotgrid").html("");
294+
if (data.first !== false) {
295+
$("#plotgrid").html("");
296+
}
295297
if(data.error !== "None") {
296298
var h = $("#content").height();//get height for error message
297299
$("#plotgrid").html(getLoadText(data.error, h));
298300
return 1;
299301
} else if ($("input[name='benchmark']:checked").val() === "show_none") {
300302
var h = $("#content").height();//get height for error message
301303
$("#plotgrid").html(getLoadText("Please select a benchmark on the left", h));
302-
} else if (data.timelines.length === 0) {
304+
} else if (data.timelines.length === 0 && data.first !== false) {
303305
var h = $("#content").height();//get height for error message
304306
$("#plotgrid").html(getLoadText("No data available", h));
305-
} else if ($("input[name='benchmark']:checked").val() === "grid"){
307+
} else if ($("input[name='benchmark']:checked").val() === "grid") {
308+
if (data.nextBenchmarks !== false) {
309+
var config = getConfiguration();
310+
config.nextBenchmarks = data.nextBenchmarks;
311+
$.getJSON("json/", config, render);
312+
}
313+
306314
//Render Grid of plots
307315
$("#revisions").attr("disabled",true);
308316
$("#equidistant").attr("disabled", true);

codespeed/tests/test_views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@ def test_gettimelinedata(self):
353353
}
354354
response = self.client.get(path, data)
355355
self.assertEquals(response.status_code, 200)
356-
responsedata = json.loads(response.content.decode())
356+
responsedata = json.loads(response.getvalue().decode())
357357

358358
self.assertEquals(
359359
responsedata['error'], "None", "there should be no errors")

codespeed/views.py

Lines changed: 161 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
from django.conf import settings
99
from django.core.urlresolvers import reverse
1010
from django.core.exceptions import ValidationError, ObjectDoesNotExist
11-
from django.http import HttpResponse, Http404, HttpResponseBadRequest,\
12-
HttpResponseNotFound
11+
from django.http import HttpResponse, Http404, HttpResponseBadRequest, \
12+
HttpResponseNotFound, StreamingHttpResponse
1313
from django.db.models import F
1414
from django.shortcuts import get_object_or_404, render_to_response
1515
from django.views.decorators.http import require_GET, require_POST
@@ -257,115 +257,173 @@ def gettimelinedata(request):
257257
except ValueError:
258258
Http404()
259259

260-
benchmarks = []
261-
number_of_revs = int(data.get('revs', 10))
260+
number_of_revs, benchmarks = get_num_revs_and_benchmarks(data)
262261

262+
baseline_rev = None
263+
baseline_exe = None
264+
if data.get('base') not in (None, 'none', 'undefined'):
265+
exe_id, rev_id = data['base'].split("+")
266+
baseline_rev = Revision.objects.get(id=rev_id)
267+
baseline_exe = Executable.objects.get(id=exe_id)
268+
269+
next_benchmarks = data.get('nextBenchmarks', False)
270+
if next_benchmarks is not False:
271+
next_benchmarks = int(next_benchmarks)
272+
273+
resp = StreamingHttpResponse(stream_timeline(baseline_exe, baseline_rev, benchmarks, data,
274+
environment, executables, number_of_revs,
275+
next_benchmarks),
276+
content_type='application/json')
277+
return resp
278+
279+
280+
def stream_timeline(baseline_exe, baseline_rev, benchmarks, data, environment, executables,
281+
number_of_revs, next_benchmarks):
282+
yield '{"timelines": ['
283+
num_results = {"results": 0}
284+
num_benchmark = 0
285+
transmitted_benchmarks = 0
286+
timeline_grid_paging = get_setting('TIMELINE_GRID_PAGING', 10)
287+
288+
for bench in benchmarks:
289+
if transmitted_benchmarks + 1 > timeline_grid_paging:
290+
# don't send more results than configured
291+
break
292+
293+
num_benchmark += 1
294+
295+
if not next_benchmarks or num_benchmark > next_benchmarks:
296+
result = get_timeline_for_benchmark(baseline_exe, baseline_rev, bench, environment,
297+
executables, number_of_revs, num_results)
298+
if result != "":
299+
transmitted_benchmarks += 1
300+
yield result
301+
302+
if not next_benchmarks or (next_benchmarks < len(benchmarks)
303+
and transmitted_benchmarks > 0):
304+
next_page = ', "nextBenchmarks": ' + str(num_benchmark)
305+
else:
306+
next_page = ', "nextBenchmarks": false'
307+
308+
if next_benchmarks:
309+
not_first = ', "first": false'
310+
else:
311+
not_first = ', "first": true'
312+
313+
if num_results['results'] == 0 and data['ben'] != 'show_none' and not next_benchmarks:
314+
yield ']' + not_first + next_page + ', "error":"No data found for the selected options"}\n'
315+
else:
316+
yield ']' + not_first + next_page + ', "error":"None"}\n'
317+
318+
319+
def get_timeline_for_benchmark(baseline_exe, baseline_rev, bench, environment, executables,
320+
number_of_revs, num_results):
321+
lessisbetter = bench.lessisbetter and ' (less is better)' or ' (more is better)'
322+
timeline = {
323+
'benchmark': bench.name,
324+
'benchmark_id': bench.id,
325+
'benchmark_description': bench.description,
326+
'data_type': bench.data_type,
327+
'units': bench.units,
328+
'lessisbetter': lessisbetter,
329+
'branches': {},
330+
'baseline': "None",
331+
}
332+
append = False
333+
for branch in Branch.objects.filter(
334+
project__track=True, name=F('project__default_branch')):
335+
# For now, we'll only work with default branches
336+
for executable in executables:
337+
if executable.project != branch.project:
338+
continue
339+
340+
resultquery = Result.objects.filter(
341+
benchmark=bench
342+
).filter(
343+
environment=environment
344+
).filter(
345+
executable=executable
346+
).filter(
347+
revision__branch=branch
348+
).select_related(
349+
"revision"
350+
).order_by('-revision__date')[:number_of_revs]
351+
if not len(resultquery):
352+
continue
353+
timeline['branches'].setdefault(branch.name, {})
354+
355+
results = []
356+
for res in resultquery:
357+
if bench.data_type == 'M':
358+
q1, q3, val_max, val_min = get_stats_with_defaults(res)
359+
results.append(
360+
[
361+
res.revision.date.strftime('%Y/%m/%d %H:%M:%S %z'),
362+
res.value, val_max, q3, q1, val_min,
363+
res.revision.get_short_commitid(), res.revision.tag, branch.name
364+
]
365+
)
366+
else:
367+
std_dev = ""
368+
if res.std_dev is not None:
369+
std_dev = res.std_dev
370+
results.append(
371+
[
372+
res.revision.date.strftime('%Y/%m/%d %H:%M:%S %z'),
373+
res.value, std_dev,
374+
res.revision.get_short_commitid(), res.revision.tag, branch.name
375+
]
376+
)
377+
timeline['branches'][branch.name][executable.id] = results
378+
append = True
379+
if baseline_rev is not None and append:
380+
try:
381+
baselinevalue = Result.objects.get(
382+
executable=baseline_exe,
383+
benchmark=bench,
384+
revision=baseline_rev,
385+
environment=environment
386+
).value
387+
except Result.DoesNotExist:
388+
timeline['baseline'] = "None"
389+
else:
390+
# determine start and end revision (x axis)
391+
# from longest data series
392+
results = []
393+
for branch in timeline['branches']:
394+
for exe in timeline['branches'][branch]:
395+
if len(timeline['branches'][branch][exe]) > len(results):
396+
results = timeline['branches'][branch][exe]
397+
end = results[0][0]
398+
start = results[len(results) - 1][0]
399+
timeline['baseline'] = [
400+
[str(start), baselinevalue],
401+
[str(end), baselinevalue]
402+
]
403+
if append:
404+
old_num_results = num_results['results']
405+
json_str = json.dumps(timeline)
406+
num_results['results'] = old_num_results + len(timeline)
407+
408+
if old_num_results > 0:
409+
return "," + json_str
410+
else:
411+
return json_str
412+
else:
413+
return ""
414+
415+
416+
def get_num_revs_and_benchmarks(data):
263417
if data['ben'] == 'grid':
264418
benchmarks = Benchmark.objects.all().order_by('name')
265419
number_of_revs = 15
266420
elif data['ben'] == 'show_none':
267421
benchmarks = []
422+
number_of_revs = int(data.get('revs', 10))
268423
else:
269424
benchmarks = [get_object_or_404(Benchmark, name=data['ben'])]
270-
271-
baselinerev = None
272-
baselineexe = None
273-
if data.get('base') not in (None, 'none', 'undefined'):
274-
exeid, revid = data['base'].split("+")
275-
baselinerev = Revision.objects.get(id=revid)
276-
baselineexe = Executable.objects.get(id=exeid)
277-
for bench in benchmarks:
278-
lessisbetter = bench.lessisbetter and ' (less is better)' or ' (more is better)'
279-
timeline = {
280-
'benchmark': bench.name,
281-
'benchmark_id': bench.id,
282-
'benchmark_description': bench.description,
283-
'data_type': bench.data_type,
284-
'units': bench.units,
285-
'lessisbetter': lessisbetter,
286-
'branches': {},
287-
'baseline': "None",
288-
}
289-
append = False
290-
for branch in Branch.objects.filter(
291-
project__track=True, name=F('project__default_branch')):
292-
# For now, we'll only work with default branches
293-
for executable in executables:
294-
if executable.project != branch.project:
295-
continue
296-
297-
resultquery = Result.objects.filter(
298-
benchmark=bench
299-
).filter(
300-
environment=environment
301-
).filter(
302-
executable=executable
303-
).filter(
304-
revision__branch=branch
305-
).select_related(
306-
"revision"
307-
).order_by('-revision__date')[:number_of_revs]
308-
if not len(resultquery):
309-
continue
310-
timeline['branches'].setdefault(branch.name, {})
311-
312-
results = []
313-
for res in resultquery:
314-
if bench.data_type == 'M':
315-
q1, q3, val_max, val_min = get_stats_with_defaults(res)
316-
results.append(
317-
[
318-
res.revision.date.strftime('%Y/%m/%d %H:%M:%S %z'),
319-
res.value, val_max, q3, q1, val_min,
320-
res.revision.get_short_commitid(), res.revision.tag, branch.name
321-
]
322-
)
323-
else:
324-
std_dev = ""
325-
if res.std_dev is not None:
326-
std_dev = res.std_dev
327-
results.append(
328-
[
329-
res.revision.date.strftime('%Y/%m/%d %H:%M:%S %z'),
330-
res.value, std_dev,
331-
res.revision.get_short_commitid(), res.revision.tag, branch.name
332-
]
333-
)
334-
timeline['branches'][branch.name][executable.id] = results
335-
append = True
336-
337-
if baselinerev is not None and append:
338-
try:
339-
baselinevalue = Result.objects.get(
340-
executable=baselineexe,
341-
benchmark=bench,
342-
revision=baselinerev,
343-
environment=environment
344-
).value
345-
except Result.DoesNotExist:
346-
timeline['baseline'] = "None"
347-
else:
348-
# determine start and end revision (x axis)
349-
# from longest data series
350-
results = []
351-
for branch in timeline['branches']:
352-
for exe in timeline['branches'][branch]:
353-
if len(timeline['branches'][branch][exe]) > len(results):
354-
results = timeline['branches'][branch][exe]
355-
end = results[0][0]
356-
start = results[len(results) - 1][0]
357-
timeline['baseline'] = [
358-
[str(start), baselinevalue],
359-
[str(end), baselinevalue]
360-
]
361-
362-
if append:
363-
timeline_list['timelines'].append(timeline)
364-
365-
if not len(timeline_list['timelines']) and data['ben'] != 'show_none':
366-
response = 'No data found for the selected options'
367-
timeline_list['error'] = response
368-
return HttpResponse(json.dumps(timeline_list))
425+
number_of_revs = int(data.get('revs', 10))
426+
return number_of_revs, benchmarks
369427

370428

371429
def get_stats_with_defaults(res):

0 commit comments

Comments
 (0)