diff --git a/codespeed/admin.py b/codespeed/admin.py index b74ced44..e294860f 100644 --- a/codespeed/admin.py +++ b/codespeed/admin.py @@ -37,9 +37,10 @@ class ExecutableAdmin(admin.ModelAdmin): class BenchmarkAdmin(admin.ModelAdmin): - list_display = ('name', 'benchmark_type', 'description', 'units_title', - 'units', 'lessisbetter', 'default_on_comparison') - list_filter = ('lessisbetter',) + list_display = ('name', 'benchmark_type', 'data_type', 'description', + 'units_title', 'units', 'lessisbetter', + 'default_on_comparison') + list_filter = ('data_type','lessisbetter') ordering = ['name'] search_fields = ('name', 'description') diff --git a/codespeed/migrations/0002_median.py b/codespeed/migrations/0002_median.py new file mode 100644 index 00000000..1f1a6f44 --- /dev/null +++ b/codespeed/migrations/0002_median.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('codespeed', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='benchmark', + name='data_type', + field=models.CharField(default='U', max_length=1, choices=[('U', 'Mean'), ('M', 'Median')]), + ), + migrations.AddField( + model_name='result', + name='q1', + field=models.FloatField(null=True, blank=True), + ), + migrations.AddField( + model_name='result', + name='q3', + field=models.FloatField(null=True, blank=True), + ), + ] diff --git a/codespeed/models.py b/codespeed/models.py index 77e0571e..7794ea8c 100644 --- a/codespeed/models.py +++ b/codespeed/models.py @@ -146,6 +146,10 @@ class Benchmark(models.Model): ('C', 'Cross-project'), ('O', 'Own-project'), ) + D_TYPES = ( + ('U', 'Mean'), + ('M', 'Median'), + ) name = models.CharField(unique=True, max_length=100) parent = models.ForeignKey( @@ -153,6 +157,7 @@ class Benchmark(models.Model): help_text="allows to group benchmarks in hierarchies", null=True, blank=True, default=None) benchmark_type = models.CharField(max_length=1, choices=B_TYPES, default='C') + data_type = models.CharField(max_length=1, choices=D_TYPES, default='U') description = models.CharField(max_length=300, blank=True) units_title = models.CharField(max_length=30, default='Time') units = models.CharField(max_length=20, default='seconds') @@ -188,6 +193,8 @@ class Result(models.Model): std_dev = models.FloatField(blank=True, null=True) val_min = models.FloatField(blank=True, null=True) val_max = models.FloatField(blank=True, null=True) + q1 = models.FloatField(blank=True, null=True) + q3 = models.FloatField(blank=True, null=True) date = models.DateTimeField(blank=True, null=True) revision = models.ForeignKey(Revision, related_name="results") executable = models.ForeignKey(Executable, related_name="results") diff --git a/codespeed/results.py b/codespeed/results.py index 9144ba2b..5bf0faa7 100644 --- a/codespeed/results.py +++ b/codespeed/results.py @@ -122,6 +122,8 @@ def save_result(data): r.std_dev = data.get('std_dev') r.val_min = data.get('min') r.val_max = data.get('max') + r.q1 = data.get('q1') + r.q3 = data.get('q3') r.full_clean() r.save() diff --git a/codespeed/settings.py b/codespeed/settings.py index e2c79dcd..9c298a09 100644 --- a/codespeed/settings.py +++ b/codespeed/settings.py @@ -66,3 +66,5 @@ # COMP_EXECUTABLES = [ # ('myexe', '21df2423ra'), # ('myexe', 'L'),] + +USE_MEDIAN_BANDS = True # True to enable median bands on Timeline view diff --git a/codespeed/static/js/timeline.js b/codespeed/static/js/timeline.js index e9916de2..91ed3d9a 100644 --- a/codespeed/static/js/timeline.js +++ b/codespeed/static/js/timeline.js @@ -24,10 +24,24 @@ function getColor(exe_id) { .data('color'); } +function scaleColorAlpha(color, scale) { + var c = $.jqplot.getColorComponents(color); + c[3] = c[3] * scale; + return 'rgba(' + c[0] +', '+ c[1] +', '+ c[2] +', '+ c[3] + ')'; +} + function shouldPlotEquidistant() { return $("#equidistant").is(':checked'); } +function shouldPlotQuartiles() { + return $("#show_quartile_bands").is(':checked'); +} + +function shouldPlotExtrema() { + return $("#show_extrema_bands").is(':checked'); +} + function getConfiguration() { var config = { exe: readCheckbox("input[name='executable']:checked"), @@ -35,7 +49,9 @@ function getConfiguration() { ben: $("input[name='benchmark']:checked").val(), env: $("input[name='environments']:checked").val(), revs: $("#revisions option:selected").val(), - equid: $("#equidistant").is(':checked') ? "on" : "off" + equid: $("#equidistant").is(':checked') ? "on" : "off", + quarts: $("#show_quartile_bands").is(':checked') ? "on" : "off", + extr: $("#show_extrema_bands").is(':checked') ? "on" : "off" }; var branch = readCheckbox("input[name='branch']:checked"); @@ -53,7 +69,7 @@ function permalinkToChanges(commitid, executableid, environment) { function OnMarkerClickHandler(ev, gridpos, datapos, neighbor, plot) { if($("input[name='benchmark']:checked").val() === "grid") { return false; } if (neighbor) { - var commitid = neighbor.data[3]; + var commitid = neighbor.data[neighbor.data.length-2]; // Get executable ID from the seriesindex array var executableid = seriesindex[neighbor.seriesIndex]; var environment = $("input[name='environments']:checked").val(); @@ -61,17 +77,81 @@ function OnMarkerClickHandler(ev, gridpos, datapos, neighbor, plot) { } } +function getHighlighterConfig(median) { + if (median) { + return { + show: true, + tooltipLocation: 'nw', + yvalues: 7, + formatString:'
date:%s
median:%s
max:%s
Q3:%s
Q1:%s
min:%s
commit:%s
' + }; + } else { + return { + show: true, + tooltipLocation: 'nw', + yvalues: 4, + formatString:'
date:%s
result:%s
std dev:%s
commit:%s
' + }; + } +} + function renderPlot(data) { var plotdata = [], series = [], lastvalues = [];//hopefully the smallest values for determining significant digits. seriesindex = []; + var hiddenSeries = 0; + var median = data['data_type'] === 'M'; for (var branch in data.branches) { // NOTE: Currently, only the "default" branch is shown in the timeline for (var exe_id in data.branches[branch]) { // FIXME if (branch !== "default") { label += " - " + branch; } var label = $("label[for*='executable" + exe_id + "']").html(); - series.push({"label": label, "color": getColor(exe_id)}); + var seriesConfig = { + label: label, + color: getColor(exe_id) + }; + if (median) { + $("span.options.median").css("display", "inline"); + var mins = new Array(); + var maxes = new Array(); + var q1s = new Array(); + var q3s = new Array(); + for (res in data["branches"][branch][exe_id]) { + var date = data["branches"][branch][exe_id][res][0]; + var value = data["branches"][branch][exe_id][res][1]; + var max = data["branches"][branch][exe_id][res][2]; + var q3 = data["branches"][branch][exe_id][res][3]; + var q1 = data["branches"][branch][exe_id][res][4]; + var min = data["branches"][branch][exe_id][res][5]; + if (min !== "") + mins.push([date, min]); + if (max !== "") + maxes.push([date, max]); + if (q1 !== "") + q1s.push([date, q1]); + if (q3 !== "") + q3s.push([date, q3]); + } + var extrema = new Array(mins, maxes); + var quartiles = new Array(q1s, q3s); + if (shouldPlotQuartiles()) { + seriesConfig['rendererOptions'] = {bandData: quartiles}; + } else if (shouldPlotExtrema()) { + seriesConfig['rendererOptions'] = {bandData: extrema}; + } + if (shouldPlotQuartiles() && shouldPlotExtrema()) { + series.push({ + showLabel: false, + showMarker: false, + color: scaleColorAlpha(getColor(exe_id), 0.6), + rendererOptions: {bandData: extrema} + }); + plotdata.push(data.branches[branch][exe_id]); + hiddenSeries++; + } + } + series.push(seriesConfig); seriesindex.push(exe_id); plotdata.push(data.branches[branch][exe_id]); lastvalues.push(data.branches[branch][exe_id][0][1]); @@ -121,15 +201,10 @@ function renderPlot(data) { } }, legend: {show: true, location: 'nw'}, - highlighter: { - show: true, - tooltipLocation: 'nw', - yvalues: 4, - formatString:'
date:%s
result:%s
std dev:%s
commit:%s
' - }, + highlighter: getHighlighterConfig(median), cursor:{show:true, zoom:true, showTooltip:false, clickReset:true} }; - if (series.length > 4) { + if (series.length > 4 + hiddenSeries) { // Move legend outside plot area to unclutter var labels = []; for (var l in series) { @@ -193,6 +268,7 @@ function renderMiniplot(plotid, data) { function render(data) { $("#revisions").attr("disabled", false); $("#equidistant").attr("disabled", false); + $("span.options.median").css("display", "none"); $("#plotgrid").html(""); if(data.error !== "None") { var h = $("#content").height();//get height for error message @@ -254,6 +330,8 @@ function initializeSite(event) { $("input[name='benchmark']" ).change(updateUrl); $("input[name='environments']").change(updateUrl); $("#equidistant" ).change(updateUrl); + $("#show_quartile_bands" ).change(updateUrl); + $("#show_extrema_bands" ).change(updateUrl); } function refreshSite(event) { @@ -307,6 +385,8 @@ function setValuesOfInputFields(event) { $("#baselinecolor").css("background-color", baselineColor); $("#equidistant").prop('checked', valueOrDefault(event.parameters.equid, defaults.equidistant) === "on"); + $("#show_quartile_bands").prop('checked', valueOrDefault(event.parameters.quarts, defaults.quartiles) === "on"); + $("#show_extrema_bands").prop('checked', valueOrDefault(event.parameters.extr, defaults.extrema) === "on"); } function init(def) { diff --git a/codespeed/templates/codespeed/timeline.html b/codespeed/templates/codespeed/timeline.html index b2e87afb..0f4e2a91 100644 --- a/codespeed/templates/codespeed/timeline.html +++ b/codespeed/templates/codespeed/timeline.html @@ -83,6 +83,16 @@ + {% if use_median_bands %} + + + {% endif %}
@@ -115,7 +125,9 @@ branches: [{% for b in branch_list %}"{{ branch }}", {% endfor %}], benchmark: "{{ defaultbenchmark }}", environment: {{ defaultenvironment.id }}, - equidistant: "{{ defaultequid }}" + equidistant: "{{ defaultequid }}", + quartiles: "{{ defaultquarts }}", + extrema: "{{ defaultextr }}" }); }); diff --git a/codespeed/views.py b/codespeed/views.py index 0f715ba3..7d2eac49 100644 --- a/codespeed/views.py +++ b/codespeed/views.py @@ -257,6 +257,7 @@ def gettimelinedata(request): 'benchmark': bench.name, 'benchmark_id': bench.id, 'benchmark_description': bench.description, + 'data_type': bench.data_type, 'units': bench.units, 'lessisbetter': lessisbetter, 'branches': {}, @@ -288,16 +289,37 @@ def gettimelinedata(request): results = [] for res in resultquery: - std_dev = "" - if res.std_dev is not None: - std_dev = res.std_dev - results.append( - [ - res.revision.date.strftime('%Y/%m/%d %H:%M:%S %z'), - res.value, std_dev, - res.revision.get_short_commitid(), branch - ] - ) + if bench.data_type == 'M': + val_min = "" + if res.val_min is not None: + val_min = res.val_min + val_max = "" + if res.val_max is not None: + val_max = res.val_max + q1 = "" + if res.q1 is not None: + q1 = res.q1 + q3 = "" + if res.q3 is not None: + q3 = res.q3 + results.append( + [ + res.revision.date.strftime('%Y/%m/%d %H:%M:%S %z'), + res.value, val_max, q3, q1, val_min, + res.revision.get_short_commitid(), branch + ] + ) + else: + std_dev = "" + if res.std_dev is not None: + std_dev = res.std_dev + results.append( + [ + res.revision.date.strftime('%Y/%m/%d %H:%M:%S %z'), + res.value, std_dev, + res.revision.get_short_commitid(), branch + ] + ) timeline['branches'][branch][executable] = results append = True if baselinerev is not None and append: @@ -424,11 +446,20 @@ def timeline(request): defaultequid = data['equid'] else: defaultequid = "off" + if 'quarts' in data: + defaultquarts = data['quarts'] + else: + defaultquarts = "on" + if 'extr' in data: + defaultextr = data['extr'] + else: + defaultextr = "on" # Information for template executables = {} for proj in Project.objects.filter(track=True): executables[proj] = Executable.objects.filter(project=proj) + use_median_bands = hasattr(settings, 'USE_MEDIAN_BANDS') and settings.USE_MEDIAN_BANDS return render_to_response('codespeed/timeline.html', { 'checkedexecutables': checkedexecutables, 'defaultbaseline': defaultbaseline, @@ -442,7 +473,10 @@ def timeline(request): 'environments': enviros, 'branch_list': branch_list, 'defaultbranch': defaultbranch, - 'defaultequid': defaultequid + 'defaultequid': defaultequid, + 'defaultquarts': defaultquarts, + 'defaultextr': defaultextr, + 'use_median_bands': use_median_bands, }, context_instance=RequestContext(request))