Skip to content

Commit 2bf22df

Browse files
authored
Merge pull request tobami#255 from smarr/performance-all-grid
Allow partial requests of timeline grid
2 parents 6432a76 + 503beb5 commit 2bf22df

5 files changed

Lines changed: 217 additions & 126 deletions

File tree

codespeed/settings.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,17 @@
4040
DEF_TIMELINE_LIMIT = 50 # Default number of revisions to be plotted
4141
# Possible values 10,50,200,1000
4242

43+
TIMELINE_GRID_LIMIT = 30 # Number of benchmarks beyond which the timeline view
44+
# is disabled as default setting. Too many benchmarks make
45+
# the view slow, and put load on the database, which may be
46+
# undeseriable.
47+
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+
4354
#TIMELINE_BRANCHES = True # NOTE: Only the default branch is currently shown
4455
# Get timeline results for specific branches
4556
# 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: 163 additions & 122 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
@@ -20,7 +20,8 @@
2020
Executable, Benchmark, Branch)
2121
from .views_data import (get_default_environment, getbaselineexecutables,
2222
getdefaultexecutable, getcomparisonexes,
23-
get_benchmark_results)
23+
get_benchmark_results, get_num_revs_and_benchmarks,
24+
get_stats_with_defaults)
2425
from .results import save_result, create_report_if_enough_data
2526
from . import commits
2627
from .validators import validate_results_request
@@ -224,6 +225,12 @@ def comparison(request):
224225
'selecteddirection': selecteddirection
225226
})
226227

228+
def get_setting(name, default = None):
229+
if hasattr(settings, name):
230+
return getattr(settings, name)
231+
else:
232+
return default
233+
227234

228235
@require_GET
229236
def gettimelinedata(request):
@@ -251,126 +258,160 @@ def gettimelinedata(request):
251258
except ValueError:
252259
Http404()
253260

254-
benchmarks = []
255-
number_of_revs = int(data.get('revs', 10))
256-
257-
if data['ben'] == 'grid':
258-
benchmarks = Benchmark.objects.all().order_by('name')
259-
number_of_revs = 15
260-
elif data['ben'] == 'show_none':
261-
benchmarks = []
262-
else:
263-
benchmarks = [get_object_or_404(Benchmark, name=data['ben'])]
261+
number_of_revs, benchmarks = get_num_revs_and_benchmarks(data)
264262

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

375416

376417
@require_GET
@@ -437,7 +478,7 @@ def timeline(request):
437478
defaultlast = data['revs']
438479

439480
benchmarks = Benchmark.objects.all()
440-
grid_limit = 30
481+
441482
defaultbenchmark = "grid"
442483
if not len(benchmarks):
443484
return no_data_found(request)
@@ -452,7 +493,7 @@ def timeline(request):
452493
name=settings.DEF_BENCHMARK)
453494
except Benchmark.DoesNotExist:
454495
pass
455-
elif len(benchmarks) >= grid_limit:
496+
elif len(benchmarks) >= get_setting('TIMELINE_GRID_LIMIT', 30):
456497
defaultbenchmark = 'show_none'
457498

458499
if 'ben' in data and data['ben'] != defaultbenchmark:

0 commit comments

Comments
 (0)