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 %}
Permalink
@@ -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))