Skip to content

Commit 7affd83

Browse files
Add the 'makeimage' service
This adds a service for generating chart images based on the latest benchmark results using the matplotlib module. The URL for this service is /sample_project/makeimage/. The image can be directly embedded in any HTML document using <img src="host_name/sample_project/makeimage/?...> or simply accessed using the same URL. The mandatory GET params are: env - the environment's name proj - the project's name branch -the branch's name exe - the executable's name ben - the benchmark's name The following params are optional: revs - positive integer, number of results the image will display. revs=10 is the implicit value representing that the latest 10 results will be displayed on the chart relative - if '1' or 'yes', instead of displaying absolute values, the first value from the series is used as a baseline and the chart will display percentual increments from that baseline base_commit - specifies a commit name to be used as a baseline instead of the first value of the series base_env - the environment's name to be used for the base_commit instead of env base_proj - the project's name to be used for the base_commit instead of proj base_exe - the executable's name to be use for the base_commit instead of exe
1 parent 9b7e94b commit 7affd83

3 files changed

Lines changed: 247 additions & 1 deletion

File tree

codespeed/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
url(r'^timeline/json/$', 'gettimelinedata', name='gettimelinedata'),
2424
url(r'^comparison/$', 'comparison', name='comparison'),
2525
url(r'^comparison/json/$', 'getcomparisondata', name='getcomparisondata'),
26+
url(r'^makeimage/$', 'makeimage', name='makeimage'),
2627
)
2728

2829
urlpatterns += patterns('codespeed.views',

codespeed/views.py

Lines changed: 245 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33

44
import json
55
import logging
6+
import django
67

78
from django.core.urlresolvers import reverse
8-
from django.http import HttpResponse, Http404, HttpResponseBadRequest
9+
from django.http import HttpResponse, Http404, HttpResponseBadRequest,\
10+
HttpResponseNotFound
911
from django.shortcuts import get_object_or_404, render_to_response
1012
from django.views.decorators.http import require_GET, require_POST
1113
from django.views.decorators.csrf import csrf_exempt
@@ -19,6 +21,12 @@
1921
from .results import save_result, create_report_if_enough_data
2022
from . import commits
2123

24+
import cStringIO
25+
from matplotlib.figure import Figure
26+
from matplotlib.ticker import FormatStrFormatter
27+
from matplotlib.backends.backend_agg import FigureCanvasAgg
28+
29+
2230
logger = logging.getLogger(__name__)
2331

2432

@@ -731,3 +739,239 @@ def add_json_results(request):
731739
logger.debug("add_json_results: completed")
732740

733741
return HttpResponse("All result data saved successfully", status=202)
742+
743+
744+
def django_has_content_type():
745+
return (django.VERSION[0] > 1 or
746+
(django.VERSION[0] == 1 and django.VERSION[1] >= 6))
747+
748+
749+
def validate_results_request(data):
750+
"""
751+
Validates that a result request dictionary has all needed parameters
752+
753+
It returns a tuple
754+
"", False when no errors where found
755+
Error_message, True when there is an error
756+
"""
757+
mandatory_data = [
758+
'env',
759+
'proj',
760+
'branch',
761+
'exe',
762+
'ben',
763+
]
764+
765+
for key in mandatory_data:
766+
if key not in data:
767+
return 'Key "' + key + '" missing from GET request', True
768+
elif data[key] == '':
769+
return 'Value for key "' + key + '" empty in GET request', True
770+
771+
# Check that 'revs' is the correct format (if it exists)
772+
if 'revs' in data:
773+
try:
774+
rev_value = int(data['revs'])
775+
except:
776+
return 'Value for key "revs" is not an integer', True
777+
if rev_value <= 0:
778+
return 'Value for key "revs" should be a strictly positive '\
779+
'integer', True
780+
781+
return '', False
782+
783+
784+
def get_benchmark_results(data):
785+
try:
786+
environment = Environment.objects.get(name=data['env'])
787+
project = Project.objects.get(name=data['proj'])
788+
executable = Executable.objects.get(name=data['exe'], project=project)
789+
branch = Branch.objects.get(name=data['branch'], project=project)
790+
benchmark = Benchmark.objects.get(name=data['ben'])
791+
except Environment.DoesNotExist:
792+
return None, HttpResponseNotFound(
793+
'Environment "' + data['env'] + '" does not exist!')
794+
except Project.DoesNotExist:
795+
return None, HttpResponseNotFound(
796+
'Project "' + data['proj'] + '" does not exist!')
797+
except Executable.DoesNotExist:
798+
return None, HttpResponseNotFound(
799+
'Executable "' + data['exe'] + '" does not exist!')
800+
except Branch.DoesNotExist:
801+
return None, HttpResponseNotFound(
802+
'Branch "' + data['branch'] + '" does not exist!')
803+
except Benchmark.DoesNotExist:
804+
return None, HttpResponseNotFound(
805+
'Benchmark "' + data['ben'] + '" does not exist!')
806+
807+
number_of_revs = int(data.get('revs', 10))
808+
809+
baseline_commit_name = (data['base_commit'] if 'base_commit' in data
810+
else None)
811+
relative_results = (
812+
('relative' in data and data['relative'] in ['1', 'yes']) or
813+
baseline_commit_name is not None)
814+
815+
result_query = Result.objects.filter(
816+
benchmark=benchmark
817+
).filter(
818+
environment=environment
819+
).filter(
820+
executable=executable
821+
).filter(
822+
revision__project=project
823+
).filter(
824+
revision__branch=branch
825+
).select_related(
826+
"revision"
827+
).order_by('-date')[:number_of_revs]
828+
829+
if len(result_query) == 0:
830+
return None, HttpResponseNotFound("No results were found!")
831+
832+
result_list = [item for item in result_query]
833+
result_list.reverse()
834+
835+
if relative_results:
836+
ref_value = result_list[0].value
837+
838+
if baseline_commit_name is not None:
839+
baseline_env = environment
840+
baseline_proj = project
841+
baseline_exe = executable
842+
baseline_branch = branch
843+
844+
try:
845+
if 'base_env' in data:
846+
baseline_env = Environment.objects.get(name=data['base_env'])
847+
if 'base_proj' in data:
848+
baseline_proj = Project.objects.get(name=data['base_proj'])
849+
if 'base_exe' in data:
850+
baseline_exe = Executable.objects.get(name=data['base_exe'],
851+
project=baseline_proj)
852+
if 'base_branch' in data:
853+
baseline_branch = Branch.objects.get(name=data['base_branch'],
854+
project=baseline_proj)
855+
except Environment.DoesNotExist:
856+
return None, HttpResponseNotFound(
857+
'Baseline environment "' + data['base_env'] +
858+
'" does not exist!')
859+
except Project.DoesNotExist:
860+
return None, HttpResponseNotFound(
861+
'Baseline project "' + data['base_proj'] +
862+
'" does not exist!')
863+
except Executable.DoesNotExist:
864+
return None, HttpResponseNotFound(
865+
'Baseline executable "' + data['base_exe'] +
866+
'"does not exist!')
867+
except Branch.DoesNotExist:
868+
return None, HttpResponseNotFound(
869+
'Baseline branch "' + data['base_branch'] +
870+
'" does not exist!')
871+
872+
base_data = Result.objects.get(
873+
benchmark=benchmark,
874+
environment=baseline_env,
875+
executable=baseline_exe,
876+
revision__project=baseline_proj,
877+
revision__branch=baseline_branch,
878+
revision__commitid=baseline_commit_name)
879+
880+
if base_data:
881+
ref_value = base_data.value
882+
else:
883+
return None, HttpResponseNotFound(
884+
'Result for revision "' + baseline_commit +
885+
'" does not exist !')
886+
887+
if relative_results:
888+
for element in result_list:
889+
element.value = (100 * (element.value - ref_value)) / ref_value
890+
891+
return {
892+
'environment': environment,
893+
'project': project,
894+
'executable': executable,
895+
'branch': branch,
896+
'benchmark': benchmark,
897+
'results': result_list,
898+
'relative': relative_results,
899+
}, None
900+
901+
902+
@require_GET
903+
def makeimage(request):
904+
data = request.GET
905+
906+
err_msg, err = validate_results_request(data)
907+
908+
if err:
909+
return HttpResponseBadRequest(err_msg)
910+
911+
result_data, error = get_benchmark_results(data)
912+
913+
if error is not None:
914+
return error
915+
916+
canvas_width = int(data['width']) if 'width' in data else 600
917+
canvas_height = int(data['height']) if 'height' in data else 500
918+
919+
canvas_width = max(canvas_width, 400)
920+
canvas_height = max(canvas_height, 300)
921+
922+
values = [element.value for element in result_data['results']]
923+
924+
max_value = max(values)
925+
min_value = min(values)
926+
value_range = max_value - min_value
927+
range_increment = 0.05 * abs(value_range)
928+
929+
fig = Figure(figsize=(canvas_width / 100, canvas_height / 100), dpi=100)
930+
ax = fig.add_axes([.1, .15, .85, .75])
931+
ax.set_ylim(min_value - range_increment, max_value + range_increment)
932+
933+
xax = range(0, len(values))
934+
yax = values
935+
936+
ax.set_xticks(xax)
937+
ax.set_xticklabels([element.date.strftime('%d %b') for element in
938+
result_data['results']], rotation=75)
939+
ax.set_title(result_data['benchmark'].name)
940+
941+
if result_data['relative']:
942+
ax.yaxis.set_major_formatter(FormatStrFormatter('%.3f%%'))
943+
944+
font_sizes = [16, 16]
945+
dimensions = [canvas_width, canvas_height]
946+
947+
for idx, value in enumerate(dimensions):
948+
if value < 500:
949+
font_sizes[idx] = 8
950+
elif value < 1000:
951+
font_sizes[idx] = 12
952+
953+
for item in ax.get_yticklabels():
954+
item.set_fontsize(font_sizes[0])
955+
956+
for item in ax.get_xticklabels():
957+
item.set_fontsize(font_sizes[1])
958+
959+
ax.title.set_fontsize(font_sizes[1] + 4)
960+
961+
ax.scatter(xax, yax)
962+
ax.plot(xax, yax)
963+
964+
canvas = FigureCanvasAgg(fig)
965+
buf = cStringIO.StringIO()
966+
canvas.print_png(buf)
967+
buf_data = buf.getvalue()
968+
969+
if django_has_content_type():
970+
response = HttpResponse(content=buf_data, content_type='image/png')
971+
else:
972+
response = HttpResponse(content=buf_data, mimetype='image/png')
973+
974+
response['Content-Length'] = len(buf_data)
975+
response['Content-Disposition'] = 'attachment; filename=image.png'
976+
977+
return response

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
Django>=1.6,<1.9
22
isodate>=0.4.7,<0.6
3+
matplotlib>=1.4.3

0 commit comments

Comments
 (0)