Skip to content

Commit 39f9f32

Browse files
authored
Merge pull request tobami#209 from catalin-manciu/image_gen_api
Add the 'makeimage' service
2 parents a9edb75 + 53d33b0 commit 39f9f32

7 files changed

Lines changed: 245 additions & 5 deletions

File tree

codespeed/images.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
from io import BytesIO
2+
from matplotlib.figure import Figure
3+
from matplotlib.ticker import FormatStrFormatter
4+
from matplotlib.backends.backend_agg import FigureCanvasAgg
5+
6+
DEF_CHART_W = 600
7+
DEF_CHART_H = 500
8+
9+
MIN_CHART_W = 400
10+
MIN_CHART_H = 300
11+
12+
13+
def gen_image_from_results(result_data, width, height):
14+
canvas_width = width if width is not None else DEF_CHART_W
15+
canvas_height = height if height is not None else DEF_CHART_H
16+
17+
canvas_width = max(canvas_width, MIN_CHART_W)
18+
canvas_height = max(canvas_height, MIN_CHART_H)
19+
20+
values = [element.value for element in result_data['results']]
21+
22+
max_value = max(values)
23+
min_value = min(values)
24+
value_range = max_value - min_value
25+
range_increment = 0.05 * abs(value_range)
26+
27+
fig = Figure(figsize=(canvas_width / 100, canvas_height / 100), dpi=100)
28+
ax = fig.add_axes([.1, .15, .85, .75])
29+
ax.set_ylim(min_value - range_increment, max_value + range_increment)
30+
31+
xax = range(0, len(values))
32+
yax = values
33+
34+
ax.set_xticks(xax)
35+
ax.set_xticklabels([element.date.strftime('%d %b') for element in
36+
result_data['results']], rotation=75)
37+
ax.set_title(result_data['benchmark'].name)
38+
39+
if result_data['relative']:
40+
ax.yaxis.set_major_formatter(FormatStrFormatter('%.2f%%'))
41+
42+
font_sizes = [16, 16]
43+
dimensions = [canvas_width, canvas_height]
44+
45+
for idx, value in enumerate(dimensions):
46+
if value < 500:
47+
font_sizes[idx] = 8
48+
elif value < 1000:
49+
font_sizes[idx] = 12
50+
51+
if result_data['relative']:
52+
font_sizes[0] -= 2
53+
54+
for item in ax.get_yticklabels():
55+
item.set_fontsize(font_sizes[0])
56+
57+
for item in ax.get_xticklabels():
58+
item.set_fontsize(font_sizes[1])
59+
60+
ax.title.set_fontsize(font_sizes[1] + 4)
61+
62+
ax.scatter(xax, yax)
63+
ax.plot(xax, yax)
64+
65+
canvas = FigureCanvasAgg(fig)
66+
buf = BytesIO()
67+
canvas.print_png(buf)
68+
buf_data = buf.getvalue()
69+
70+
return buf_data

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/validators.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from django.core.exceptions import ValidationError
2+
3+
4+
def validate_results_request(data):
5+
"""
6+
Validates that a result request dictionary has all needed parameters
7+
and their type is correct.
8+
9+
Throws ValidationError on error.
10+
"""
11+
mandatory_data = [
12+
'env',
13+
'proj',
14+
'branch',
15+
'exe',
16+
'ben',
17+
]
18+
19+
for key in mandatory_data:
20+
if key not in data:
21+
raise ValidationError('Key "' + key +
22+
'" missing from GET request!')
23+
elif data[key] == '':
24+
raise ValidationError('Value for key "' + key +
25+
'" empty in GET request!')
26+
27+
integer_data = [
28+
'revs',
29+
'width',
30+
'height'
31+
]
32+
33+
"""
34+
Check that the items in integer_data are the correct format,
35+
if they exist
36+
"""
37+
for key in integer_data:
38+
if key in data:
39+
try:
40+
rev_value = int(data[key])
41+
except ValueError:
42+
raise ValidationError('Value for "' + key +
43+
'" is not an integer!')
44+
if rev_value <= 0:
45+
raise ValidationError('Value for "' + key + '" should be a'
46+
' strictly positive integer!')

codespeed/views.py

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
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.core.exceptions import ValidationError, ObjectDoesNotExist
10+
from django.http import HttpResponse, Http404, HttpResponseBadRequest,\
11+
HttpResponseNotFound
912
from django.shortcuts import get_object_or_404, render_to_response
1013
from django.views.decorators.http import require_GET, require_POST
1114
from django.views.decorators.csrf import csrf_exempt
@@ -15,9 +18,12 @@
1518
from .models import (Environment, Report, Project, Revision, Result,
1619
Executable, Benchmark, Branch)
1720
from .views_data import (get_default_environment, getbaselineexecutables,
18-
getdefaultexecutable, getcomparisonexes)
21+
getdefaultexecutable, getcomparisonexes,
22+
get_benchmark_results)
1923
from .results import save_result, create_report_if_enough_data
2024
from . import commits
25+
from .validators import validate_results_request
26+
from .images import gen_image_from_results
2127

2228
logger = logging.getLogger(__name__)
2329

@@ -731,3 +737,38 @@ def add_json_results(request):
731737
logger.debug("add_json_results: completed")
732738

733739
return HttpResponse("All result data saved successfully", status=202)
740+
741+
742+
def django_has_content_type():
743+
return (django.VERSION[0] > 1 or
744+
(django.VERSION[0] == 1 and django.VERSION[1] >= 6))
745+
746+
747+
@require_GET
748+
def makeimage(request):
749+
data = request.GET
750+
751+
try:
752+
validate_results_request(data)
753+
except ValidationError as err:
754+
return HttpResponseBadRequest(str(err))
755+
756+
try:
757+
result_data = get_benchmark_results(data)
758+
except ObjectDoesNotExist as err:
759+
return HttpResponseNotFound(str(err))
760+
761+
image_data = gen_image_from_results(
762+
result_data,
763+
int(data['width']) if 'width' in data else None,
764+
int(data['height']) if 'height' in data else None)
765+
766+
if django_has_content_type():
767+
response = HttpResponse(content=image_data, content_type='image/png')
768+
else:
769+
response = HttpResponse(content=image_data, mimetype='image/png')
770+
771+
response['Content-Length'] = len(image_data)
772+
response['Content-Disposition'] = 'attachment; filename=image.png'
773+
774+
return response

codespeed/views_data.py

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
from __future__ import absolute_import
33

44
from django.conf import settings
5-
6-
from codespeed.models import Executable, Revision, Project, Branch
5+
from django.core.exceptions import ObjectDoesNotExist
6+
from codespeed.models import Executable, Revision, Project, Branch,\
7+
Environment, Benchmark, Result
78

89

910
def get_default_environment(enviros, data, multi=False):
@@ -147,3 +148,83 @@ def getcomparisonexes():
147148
all_executables[proj] = executables
148149
exekeys += executablekeys
149150
return all_executables, exekeys
151+
152+
153+
def get_benchmark_results(data):
154+
environment = Environment.objects.get(name=data['env'])
155+
project = Project.objects.get(name=data['proj'])
156+
executable = Executable.objects.get(name=data['exe'], project=project)
157+
branch = Branch.objects.get(name=data['branch'], project=project)
158+
benchmark = Benchmark.objects.get(name=data['ben'])
159+
160+
number_of_revs = int(data.get('revs', 10))
161+
162+
baseline_commit_name = (data['base_commit'] if 'base_commit' in data
163+
else None)
164+
relative_results = (
165+
('relative' in data and data['relative'] in ['1', 'yes']) or
166+
baseline_commit_name is not None)
167+
168+
result_query = Result.objects.filter(
169+
benchmark=benchmark
170+
).filter(
171+
environment=environment
172+
).filter(
173+
executable=executable
174+
).filter(
175+
revision__project=project
176+
).filter(
177+
revision__branch=branch
178+
).select_related(
179+
"revision"
180+
).order_by('-date')[:number_of_revs]
181+
182+
if len(result_query) == 0:
183+
raise ObjectDoesNotExist("No results were found!")
184+
185+
result_list = [item for item in result_query]
186+
result_list.reverse()
187+
188+
if relative_results:
189+
ref_value = result_list[0].value
190+
191+
if baseline_commit_name is not None:
192+
baseline_env = environment
193+
baseline_proj = project
194+
baseline_exe = executable
195+
baseline_branch = branch
196+
197+
if 'base_env' in data:
198+
baseline_env = Environment.objects.get(name=data['base_env'])
199+
if 'base_proj' in data:
200+
baseline_proj = Project.objects.get(name=data['base_proj'])
201+
if 'base_exe' in data:
202+
baseline_exe = Executable.objects.get(name=data['base_exe'],
203+
project=baseline_proj)
204+
if 'base_branch' in data:
205+
baseline_branch = Branch.objects.get(name=data['base_branch'],
206+
project=baseline_proj)
207+
208+
base_data = Result.objects.get(
209+
benchmark=benchmark,
210+
environment=baseline_env,
211+
executable=baseline_exe,
212+
revision__project=baseline_proj,
213+
revision__branch=baseline_branch,
214+
revision__commitid=baseline_commit_name)
215+
216+
ref_value = base_data.value
217+
218+
if relative_results:
219+
for element in result_list:
220+
element.value = (100 * (element.value - ref_value)) / ref_value
221+
222+
return {
223+
'environment': environment,
224+
'project': project,
225+
'executable': executable,
226+
'branch': branch,
227+
'benchmark': benchmark,
228+
'results': result_list,
229+
'relative': relative_results,
230+
}

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

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
download_url="https://github.com/tobami/codespeed/tags",
1010
license='GNU Lesser General Public License version 2.1',
1111
keywords=["benchmarking", "visualization"],
12-
install_requires=['django>=1.6,<1.9', 'isodate>=0.4.7,<0.6'],
12+
install_requires=['django>=1.6,<1.9', 'isodate>=0.4.7,<0.6', 'matplotlib>=1.4.3'],
1313
packages=find_packages(exclude=['ez_setup', 'sample_project']),
1414
description='A web application to monitor and analyze the performance of your code',
1515
include_package_data=True,

0 commit comments

Comments
 (0)