Skip to content

Commit b1ca8fa

Browse files
authored
Merge pull request #203 from str4d/median
Add support for displaying median benchmarks
2 parents a85b782 + 325a1fd commit b1ca8fa

File tree

8 files changed

+192
-25
lines changed

8 files changed

+192
-25
lines changed

codespeed/admin.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,10 @@ class ExecutableAdmin(admin.ModelAdmin):
3737

3838

3939
class BenchmarkAdmin(admin.ModelAdmin):
40-
list_display = ('name', 'benchmark_type', 'description', 'units_title',
41-
'units', 'lessisbetter', 'default_on_comparison')
42-
list_filter = ('lessisbetter',)
40+
list_display = ('name', 'benchmark_type', 'data_type', 'description',
41+
'units_title', 'units', 'lessisbetter',
42+
'default_on_comparison')
43+
list_filter = ('data_type','lessisbetter')
4344
ordering = ['name']
4445
search_fields = ('name', 'description')
4546

codespeed/migrations/0002_median.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# -*- coding: utf-8 -*-
2+
from __future__ import unicode_literals
3+
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('codespeed', '0001_initial'),
11+
]
12+
13+
operations = [
14+
migrations.AddField(
15+
model_name='benchmark',
16+
name='data_type',
17+
field=models.CharField(default='U', max_length=1, choices=[('U', 'Mean'), ('M', 'Median')]),
18+
),
19+
migrations.AddField(
20+
model_name='result',
21+
name='q1',
22+
field=models.FloatField(null=True, blank=True),
23+
),
24+
migrations.AddField(
25+
model_name='result',
26+
name='q3',
27+
field=models.FloatField(null=True, blank=True),
28+
),
29+
]

codespeed/models.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,13 +146,18 @@ class Benchmark(models.Model):
146146
('C', 'Cross-project'),
147147
('O', 'Own-project'),
148148
)
149+
D_TYPES = (
150+
('U', 'Mean'),
151+
('M', 'Median'),
152+
)
149153

150154
name = models.CharField(unique=True, max_length=100)
151155
parent = models.ForeignKey(
152156
'self', verbose_name="parent",
153157
help_text="allows to group benchmarks in hierarchies",
154158
null=True, blank=True, default=None)
155159
benchmark_type = models.CharField(max_length=1, choices=B_TYPES, default='C')
160+
data_type = models.CharField(max_length=1, choices=D_TYPES, default='U')
156161
description = models.CharField(max_length=300, blank=True)
157162
units_title = models.CharField(max_length=30, default='Time')
158163
units = models.CharField(max_length=20, default='seconds')
@@ -188,6 +193,8 @@ class Result(models.Model):
188193
std_dev = models.FloatField(blank=True, null=True)
189194
val_min = models.FloatField(blank=True, null=True)
190195
val_max = models.FloatField(blank=True, null=True)
196+
q1 = models.FloatField(blank=True, null=True)
197+
q3 = models.FloatField(blank=True, null=True)
191198
date = models.DateTimeField(blank=True, null=True)
192199
revision = models.ForeignKey(Revision, related_name="results")
193200
executable = models.ForeignKey(Executable, related_name="results")

codespeed/results.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@ def save_result(data):
122122
r.std_dev = data.get('std_dev')
123123
r.val_min = data.get('min')
124124
r.val_max = data.get('max')
125+
r.q1 = data.get('q1')
126+
r.q3 = data.get('q3')
125127

126128
r.full_clean()
127129
r.save()

codespeed/settings.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,5 @@
6666
# COMP_EXECUTABLES = [
6767
# ('myexe', '21df2423ra'),
6868
# ('myexe', 'L'),]
69+
70+
USE_MEDIAN_BANDS = True # True to enable median bands on Timeline view

codespeed/static/js/timeline.js

Lines changed: 90 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,34 @@ function getColor(exe_id) {
2424
.data('color');
2525
}
2626

27+
function scaleColorAlpha(color, scale) {
28+
var c = $.jqplot.getColorComponents(color);
29+
c[3] = c[3] * scale;
30+
return 'rgba(' + c[0] +', '+ c[1] +', '+ c[2] +', '+ c[3] + ')';
31+
}
32+
2733
function shouldPlotEquidistant() {
2834
return $("#equidistant").is(':checked');
2935
}
3036

37+
function shouldPlotQuartiles() {
38+
return $("#show_quartile_bands").is(':checked');
39+
}
40+
41+
function shouldPlotExtrema() {
42+
return $("#show_extrema_bands").is(':checked');
43+
}
44+
3145
function getConfiguration() {
3246
var config = {
3347
exe: readCheckbox("input[name='executable']:checked"),
3448
base: $("#baseline option:selected").val(),
3549
ben: $("input[name='benchmark']:checked").val(),
3650
env: $("input[name='environments']:checked").val(),
3751
revs: $("#revisions option:selected").val(),
38-
equid: $("#equidistant").is(':checked') ? "on" : "off"
52+
equid: $("#equidistant").is(':checked') ? "on" : "off",
53+
quarts: $("#show_quartile_bands").is(':checked') ? "on" : "off",
54+
extr: $("#show_extrema_bands").is(':checked') ? "on" : "off"
3955
};
4056

4157
var branch = readCheckbox("input[name='branch']:checked");
@@ -53,25 +69,89 @@ function permalinkToChanges(commitid, executableid, environment) {
5369
function OnMarkerClickHandler(ev, gridpos, datapos, neighbor, plot) {
5470
if($("input[name='benchmark']:checked").val() === "grid") { return false; }
5571
if (neighbor) {
56-
var commitid = neighbor.data[3];
72+
var commitid = neighbor.data[neighbor.data.length-2];
5773
// Get executable ID from the seriesindex array
5874
var executableid = seriesindex[neighbor.seriesIndex];
5975
var environment = $("input[name='environments']:checked").val();
6076
permalinkToChanges(commitid, executableid, environment);
6177
}
6278
}
6379

80+
function getHighlighterConfig(median) {
81+
if (median) {
82+
return {
83+
show: true,
84+
tooltipLocation: 'nw',
85+
yvalues: 7,
86+
formatString:'<table class="jqplot-highlighter"> <tr><td>date:</td><td>%s</td></tr> <tr><td>median:</td><td>%s</td></tr> <tr><td>max:</td><td>%s</td></tr> <tr><td>Q3:</td><td>%s</td></tr> <tr><td>Q1:</td><td>%s</td></tr> <tr><td>min:</td><td>%s</td></tr> <tr><td>commit:</td><td>%s</td></tr></table>'
87+
};
88+
} else {
89+
return {
90+
show: true,
91+
tooltipLocation: 'nw',
92+
yvalues: 4,
93+
formatString:'<table class="jqplot-highlighter"> <tr><td>date:</td><td>%s</td></tr> <tr><td>result:</td><td>%s</td></tr> <tr><td>std dev:</td><td>%s</td></tr> <tr><td>commit:</td><td>%s</td></tr></table>'
94+
};
95+
}
96+
}
97+
6498
function renderPlot(data) {
6599
var plotdata = [],
66100
series = [],
67101
lastvalues = [];//hopefully the smallest values for determining significant digits.
68102
seriesindex = [];
103+
var hiddenSeries = 0;
104+
var median = data['data_type'] === 'M';
69105
for (var branch in data.branches) {
70106
// NOTE: Currently, only the "default" branch is shown in the timeline
71107
for (var exe_id in data.branches[branch]) {
72108
// FIXME if (branch !== "default") { label += " - " + branch; }
73109
var label = $("label[for*='executable" + exe_id + "']").html();
74-
series.push({"label": label, "color": getColor(exe_id)});
110+
var seriesConfig = {
111+
label: label,
112+
color: getColor(exe_id)
113+
};
114+
if (median) {
115+
$("span.options.median").css("display", "inline");
116+
var mins = new Array();
117+
var maxes = new Array();
118+
var q1s = new Array();
119+
var q3s = new Array();
120+
for (res in data["branches"][branch][exe_id]) {
121+
var date = data["branches"][branch][exe_id][res][0];
122+
var value = data["branches"][branch][exe_id][res][1];
123+
var max = data["branches"][branch][exe_id][res][2];
124+
var q3 = data["branches"][branch][exe_id][res][3];
125+
var q1 = data["branches"][branch][exe_id][res][4];
126+
var min = data["branches"][branch][exe_id][res][5];
127+
if (min !== "")
128+
mins.push([date, min]);
129+
if (max !== "")
130+
maxes.push([date, max]);
131+
if (q1 !== "")
132+
q1s.push([date, q1]);
133+
if (q3 !== "")
134+
q3s.push([date, q3]);
135+
}
136+
var extrema = new Array(mins, maxes);
137+
var quartiles = new Array(q1s, q3s);
138+
if (shouldPlotQuartiles()) {
139+
seriesConfig['rendererOptions'] = {bandData: quartiles};
140+
} else if (shouldPlotExtrema()) {
141+
seriesConfig['rendererOptions'] = {bandData: extrema};
142+
}
143+
if (shouldPlotQuartiles() && shouldPlotExtrema()) {
144+
series.push({
145+
showLabel: false,
146+
showMarker: false,
147+
color: scaleColorAlpha(getColor(exe_id), 0.6),
148+
rendererOptions: {bandData: extrema}
149+
});
150+
plotdata.push(data.branches[branch][exe_id]);
151+
hiddenSeries++;
152+
}
153+
}
154+
series.push(seriesConfig);
75155
seriesindex.push(exe_id);
76156
plotdata.push(data.branches[branch][exe_id]);
77157
lastvalues.push(data.branches[branch][exe_id][0][1]);
@@ -121,15 +201,10 @@ function renderPlot(data) {
121201
}
122202
},
123203
legend: {show: true, location: 'nw'},
124-
highlighter: {
125-
show: true,
126-
tooltipLocation: 'nw',
127-
yvalues: 4,
128-
formatString:'<table class="jqplot-highlighter"> <tr><td>date:</td><td>%s</td></tr> <tr><td>result:</td><td>%s</td></tr> <tr><td>std dev:</td><td>%s</td></tr> <tr><td>commit:</td><td>%s</td></tr></table>'
129-
},
204+
highlighter: getHighlighterConfig(median),
130205
cursor:{show:true, zoom:true, showTooltip:false, clickReset:true}
131206
};
132-
if (series.length > 4) {
207+
if (series.length > 4 + hiddenSeries) {
133208
// Move legend outside plot area to unclutter
134209
var labels = [];
135210
for (var l in series) {
@@ -193,6 +268,7 @@ function renderMiniplot(plotid, data) {
193268
function render(data) {
194269
$("#revisions").attr("disabled", false);
195270
$("#equidistant").attr("disabled", false);
271+
$("span.options.median").css("display", "none");
196272
$("#plotgrid").html("");
197273
if(data.error !== "None") {
198274
var h = $("#content").height();//get height for error message
@@ -254,6 +330,8 @@ function initializeSite(event) {
254330
$("input[name='benchmark']" ).change(updateUrl);
255331
$("input[name='environments']").change(updateUrl);
256332
$("#equidistant" ).change(updateUrl);
333+
$("#show_quartile_bands" ).change(updateUrl);
334+
$("#show_extrema_bands" ).change(updateUrl);
257335
}
258336

259337
function refreshSite(event) {
@@ -307,6 +385,8 @@ function setValuesOfInputFields(event) {
307385

308386
$("#baselinecolor").css("background-color", baselineColor);
309387
$("#equidistant").prop('checked', valueOrDefault(event.parameters.equid, defaults.equidistant) === "on");
388+
$("#show_quartile_bands").prop('checked', valueOrDefault(event.parameters.quarts, defaults.quartiles) === "on");
389+
$("#show_extrema_bands").prop('checked', valueOrDefault(event.parameters.extr, defaults.extrema) === "on");
310390
}
311391

312392
function init(def) {

codespeed/templates/codespeed/timeline.html

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,16 @@
8383
<input id="equidistant" name="equidistant" type="checkbox" />
8484
<label for="equidistant">Equidistant</label>
8585
</span>
86+
{% if use_median_bands %}
87+
<span class="options median" title="Shows quartile bands in the plots" style="display: none">
88+
<input id="show_quartile_bands" type="checkbox" name="show_quartile_bands" checked="checked"/>
89+
<label for="show_quartile_bands">Show quartile bands</label>
90+
</span>
91+
<span class="options median" title="Shows extrema bands in the plots" style="display: none">
92+
<input id="show_extrema_bands" type="checkbox" name="show_extrema_bands" checked="checked"/>
93+
<label for="show_extrema_bands">Show extrema bands</label>
94+
</span>
95+
{% endif %}
8696
<a id="permalink" href="#">Permalink</a>
8797
</div>
8898
<div id="content" class="clearfix">
@@ -115,7 +125,9 @@
115125
branches: [{% for b in branch_list %}"{{ branch }}", {% endfor %}],
116126
benchmark: "{{ defaultbenchmark }}",
117127
environment: {{ defaultenvironment.id }},
118-
equidistant: "{{ defaultequid }}"
128+
equidistant: "{{ defaultequid }}",
129+
quartiles: "{{ defaultquarts }}",
130+
extrema: "{{ defaultextr }}"
119131
});
120132
});
121133
</script>

codespeed/views.py

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ def gettimelinedata(request):
257257
'benchmark': bench.name,
258258
'benchmark_id': bench.id,
259259
'benchmark_description': bench.description,
260+
'data_type': bench.data_type,
260261
'units': bench.units,
261262
'lessisbetter': lessisbetter,
262263
'branches': {},
@@ -288,16 +289,37 @@ def gettimelinedata(request):
288289

289290
results = []
290291
for res in resultquery:
291-
std_dev = ""
292-
if res.std_dev is not None:
293-
std_dev = res.std_dev
294-
results.append(
295-
[
296-
res.revision.date.strftime('%Y/%m/%d %H:%M:%S %z'),
297-
res.value, std_dev,
298-
res.revision.get_short_commitid(), branch
299-
]
300-
)
292+
if bench.data_type == 'M':
293+
val_min = ""
294+
if res.val_min is not None:
295+
val_min = res.val_min
296+
val_max = ""
297+
if res.val_max is not None:
298+
val_max = res.val_max
299+
q1 = ""
300+
if res.q1 is not None:
301+
q1 = res.q1
302+
q3 = ""
303+
if res.q3 is not None:
304+
q3 = res.q3
305+
results.append(
306+
[
307+
res.revision.date.strftime('%Y/%m/%d %H:%M:%S %z'),
308+
res.value, val_max, q3, q1, val_min,
309+
res.revision.get_short_commitid(), branch
310+
]
311+
)
312+
else:
313+
std_dev = ""
314+
if res.std_dev is not None:
315+
std_dev = res.std_dev
316+
results.append(
317+
[
318+
res.revision.date.strftime('%Y/%m/%d %H:%M:%S %z'),
319+
res.value, std_dev,
320+
res.revision.get_short_commitid(), branch
321+
]
322+
)
301323
timeline['branches'][branch][executable] = results
302324
append = True
303325
if baselinerev is not None and append:
@@ -424,11 +446,20 @@ def timeline(request):
424446
defaultequid = data['equid']
425447
else:
426448
defaultequid = "off"
449+
if 'quarts' in data:
450+
defaultquarts = data['quarts']
451+
else:
452+
defaultquarts = "on"
453+
if 'extr' in data:
454+
defaultextr = data['extr']
455+
else:
456+
defaultextr = "on"
427457

428458
# Information for template
429459
executables = {}
430460
for proj in Project.objects.filter(track=True):
431461
executables[proj] = Executable.objects.filter(project=proj)
462+
use_median_bands = hasattr(settings, 'USE_MEDIAN_BANDS') and settings.USE_MEDIAN_BANDS
432463
return render_to_response('codespeed/timeline.html', {
433464
'checkedexecutables': checkedexecutables,
434465
'defaultbaseline': defaultbaseline,
@@ -442,7 +473,10 @@ def timeline(request):
442473
'environments': enviros,
443474
'branch_list': branch_list,
444475
'defaultbranch': defaultbranch,
445-
'defaultequid': defaultequid
476+
'defaultequid': defaultequid,
477+
'defaultquarts': defaultquarts,
478+
'defaultextr': defaultextr,
479+
'use_median_bands': use_median_bands,
446480
}, context_instance=RequestContext(request))
447481

448482

0 commit comments

Comments
 (0)