├── codespeed
├── tests
│ ├── __init__.py
│ ├── test_settings.py
│ └── test_auth.py
├── migrations
│ ├── __init__.py
│ ├── 0004_branch_display_on_comparison_page.py
│ ├── 0002_median.py
│ ├── 0003_project_default_branch.py
│ └── 0001_initial.py
├── commits
│ ├── tests
│ │ ├── __init__.py
│ │ └── test_git.py
│ ├── __init__.py
│ ├── exceptions.py
│ ├── logs.py
│ ├── subversion.py
│ ├── git.py
│ ├── mercurial.py
│ └── github.py
├── templatetags
│ ├── __init__.py
│ └── percentages.py
├── __init__.py
├── static
│ ├── css
│ │ └── timeline.css
│ ├── images
│ │ ├── asc.gif
│ │ ├── bg.gif
│ │ ├── desc.gif
│ │ ├── logo.png
│ │ ├── note.png
│ │ ├── changes.png
│ │ ├── timeline.png
│ │ └── comparison.png
│ └── js
│ │ ├── codespeed.js
│ │ ├── jqplot
│ │ ├── jqplot.canvasAxisLabelRenderer.min.js
│ │ ├── jqplot.canvasAxisTickRenderer.min.js
│ │ ├── jquery.jqplot.min.css
│ │ ├── jqplot.pointLabels.min.js
│ │ ├── jqplot.highlighter.min.js
│ │ ├── jqplot.categoryAxisRenderer.min.js
│ │ └── jqplot.dateAxisRenderer.min.js
│ │ ├── changes.js
│ │ └── jquery.address-1.6.min.js
├── templates
│ └── codespeed
│ │ ├── base_site.html
│ │ ├── nodata.html
│ │ ├── reports.html
│ │ ├── changes_logs.html
│ │ ├── changes_data.html
│ │ ├── base.html
│ │ ├── changes_table.html
│ │ ├── changes.html
│ │ ├── comparison.html
│ │ └── timeline.html
├── apps.py
├── feeds.py
├── validators.py
├── urls.py
├── images.py
├── admin.py
├── auth.py
├── results.py
└── settings.py
├── sample_project
├── __init__.py
├── repos
│ └── clonerepos
├── templates
│ ├── feeds
│ │ ├── latest_title.html
│ │ └── latest_description.html
│ ├── codespeed
│ │ └── base_site.html
│ ├── 404.html
│ ├── 500.html
│ ├── admin
│ │ └── base_site.html
│ ├── about.html
│ └── home.html
├── deploy
│ ├── supervisor-speedcenter.conf
│ ├── django.wsgi
│ ├── apache-speedcenter.conf
│ ├── apache-reverseproxy.conf
│ ├── lighttpd-speedcenter.conf
│ ├── apache-speedcenter2.conf
│ └── nginx.default-site.conf
├── urls.py
├── settings.py
├── client.py
└── README.md
├── requirements.txt
├── documentation
├── backend.mwb
├── backend.png
└── intro.wiki
├── setup.cfg
├── .gitignore
├── MANIFEST.in
├── manage.py
├── .travis.yml
├── AUTHORS
├── LICENSE
├── tools
├── save_multiple_results.py
├── save_single_result.py
├── migrate_script.py
├── create_trunks.py
├── test_performance.py
└── pypy
│ ├── savecpython.py
│ ├── saveresults.py
│ ├── import_from_json.py
│ └── test_saveresults.py
├── setup.py
└── README.md
/codespeed/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sample_project/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/codespeed/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sample_project/repos/clonerepos:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/codespeed/commits/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/codespeed/templatetags/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/codespeed/commits/__init__.py:
--------------------------------------------------------------------------------
1 | from .logs import get_logs # noqa
2 |
--------------------------------------------------------------------------------
/codespeed/__init__.py:
--------------------------------------------------------------------------------
1 | default_app_config = 'codespeed.apps.CodespeedConfig'
2 |
--------------------------------------------------------------------------------
/codespeed/static/css/timeline.css:
--------------------------------------------------------------------------------
1 | select#baseline {
2 | width: 100%;
3 | }
4 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | Django>=1.11,<2.2
2 | isodate>=0.4.7,<0.6
3 | matplotlib>=1.4.3,<2.0
4 |
--------------------------------------------------------------------------------
/documentation/backend.mwb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/python/codespeed/master/documentation/backend.mwb
--------------------------------------------------------------------------------
/documentation/backend.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/python/codespeed/master/documentation/backend.png
--------------------------------------------------------------------------------
/sample_project/templates/feeds/latest_title.html:
--------------------------------------------------------------------------------
1 | {{ obj.revision }} {{ obj.executable }}@{{ obj.environment}}
2 |
--------------------------------------------------------------------------------
/codespeed/static/images/asc.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/python/codespeed/master/codespeed/static/images/asc.gif
--------------------------------------------------------------------------------
/codespeed/static/images/bg.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/python/codespeed/master/codespeed/static/images/bg.gif
--------------------------------------------------------------------------------
/codespeed/static/images/desc.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/python/codespeed/master/codespeed/static/images/desc.gif
--------------------------------------------------------------------------------
/codespeed/static/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/python/codespeed/master/codespeed/static/images/logo.png
--------------------------------------------------------------------------------
/codespeed/static/images/note.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/python/codespeed/master/codespeed/static/images/note.png
--------------------------------------------------------------------------------
/codespeed/static/images/changes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/python/codespeed/master/codespeed/static/images/changes.png
--------------------------------------------------------------------------------
/codespeed/static/images/timeline.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/python/codespeed/master/codespeed/static/images/timeline.png
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [flake8]
2 | exclude = .git,__pycache__,migrations,static,settings.py,urls.py,views.py
3 | max-line-length = 89
4 |
--------------------------------------------------------------------------------
/codespeed/static/images/comparison.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/python/codespeed/master/codespeed/static/images/comparison.png
--------------------------------------------------------------------------------
/codespeed/templates/codespeed/base_site.html:
--------------------------------------------------------------------------------
1 | {% extends "codespeed/base.html" %}
2 |
3 | {# This exists only to be overriden #}
4 |
--------------------------------------------------------------------------------
/sample_project/templates/codespeed/base_site.html:
--------------------------------------------------------------------------------
1 | {% extends "codespeed/base.html" %}
2 |
3 | {% block title %}
4 | My Own Title
5 | {% endblock %}
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | *.swp
3 | *.swo
4 | .DS_Store
5 | *.db
6 | sample_project/repos/*
7 | override
8 | build
9 | dist
10 | codespeed.egg-info/
11 |
12 |
--------------------------------------------------------------------------------
/codespeed/templates/codespeed/nodata.html:
--------------------------------------------------------------------------------
1 | {% extends "codespeed/base_site.html" %}
2 | {% block body %}
3 |
7 |
About this site
8 |
<description of site and what benchmarks that are run>
9 |
This site runs on top of Django and Codespeed
10 |
About the benchmarks
11 |
The code can be found here .
12 |
About MyProject
13 |
<Description of the main project>
14 |
Main website: MySite
15 |
About Codespeed
16 | Codespeed is a web application to monitor and analyze the performance of your code.
17 |
Code: github.com/tobami/codespeed
18 |
Wiki: wiki.github.com/tobami/codespeed/
19 |
Contact
20 |
For problems or suggestions about this website write to...
21 |
22 | {% endblock %}
23 |
--------------------------------------------------------------------------------
/sample_project/deploy/apache-speedcenter2.conf:
--------------------------------------------------------------------------------
1 | and other contributors, see AUTHORS file.
5 |
6 | This file is part of Codespeed.
7 |
8 | Codespeed is free software; you can redistribute it and/or
9 | modify it under the terms of the GNU Lesser General Public
10 | License as published by the Free Software Foundation; either
11 | version 2.1 of the License, or (at your option) any later version.
12 |
13 | Codespeed is distributed in the hope that it will be useful,
14 | but WITHOUT ANY WARRANTY; without even the implied warranty of
15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 | Lesser General Public License for more details.
17 |
18 | You should have received a copy of the GNU Lesser General Public
19 | License along with Codespeed; if not, write to the Free Software
20 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21 |
--------------------------------------------------------------------------------
/codespeed/templates/codespeed/reports.html:
--------------------------------------------------------------------------------
1 | {% if reports|length %}
2 |
3 | Latest Results
4 | (rss )
5 |
6 |
7 | {% for report in reports %}
8 | {{ report.get_absolute_url }} {{ report.revision }}{{ report.executable }}@{{ report.environment}}
9 | {{ report.item_description }}
10 | {% endfor %}
11 |
12 |
13 | {% endif %}
14 |
15 | {% if significant_reports|length %}
16 |
17 | Latest Significant Results
18 | (rss )
19 |
20 |
21 | {% for report in significant_reports %}
22 | {{ report.get_absolute_url }} {{ report.revision }}{{ report.executable }}@{{ report.environment}}
23 | {{ report.item_description }}
24 | {% endfor %}
25 |
26 |
27 | {% endif %}
28 |
--------------------------------------------------------------------------------
/codespeed/commits/logs.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import, unicode_literals
3 |
4 | import logging
5 |
6 | logger = logging.getLogger(__name__)
7 |
8 |
9 | def get_logs(rev, startrev, update=False):
10 | logs = []
11 | project = rev.branch.project
12 | if project.repo_type == project.SUBVERSION:
13 | from .subversion import getlogs, updaterepo
14 | elif project.repo_type == project.MERCURIAL:
15 | from .mercurial import getlogs, updaterepo
16 | elif project.repo_type == project.GIT:
17 | from .git import getlogs, updaterepo
18 | elif project.repo_type == project.GITHUB:
19 | from .github import getlogs, updaterepo
20 | else:
21 | if project.repo_type not in (project.NO_LOGS, ""):
22 | logger.warning("Don't know how to retrieve logs from %s project",
23 | project.get_repo_type_display())
24 | return logs
25 |
26 | if update:
27 | updaterepo(rev.branch.project)
28 |
29 | logs = getlogs(rev, startrev)
30 |
31 | # Remove last log because the startrev log shouldn't be shown
32 | if len(logs) > 1 and logs[-1].get('commitid') == startrev.commitid:
33 | logs.pop()
34 |
35 | return logs
36 |
--------------------------------------------------------------------------------
/codespeed/templates/codespeed/changes_logs.html:
--------------------------------------------------------------------------------
1 | {% if error %}
2 | Error while retrieving logs: {{ error }}
3 | {% else %}
4 | {% for log in logs %}
5 |
6 | {{ log.date }}
7 |
8 | {% if log.author_email and show_email_address %}
9 | {{ log.author }}
10 | {% else %}
11 | {{ log.author }}
12 | {% endif %}
13 | commited
14 | {% if log.commit_browse_url %}
15 | {{ log.short_commit_id|default:log.commitid }} :
16 | {% else %}
17 | {{ log.short_commit_id|default:log.commitid }} :
18 | {% endif %}
19 |
20 | {{ log.message|linebreaksbr|urlize }}
21 | {% if log.body %}
22 | {{ log.body|linebreaksbr|urlize }}
23 | {% endif %}
24 |
25 |
26 | {% endfor %}
27 | {% endif %}
28 |
--------------------------------------------------------------------------------
/codespeed/tests/test_settings.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from django.test import TestCase
3 |
4 | from codespeed import settings as default_settings
5 |
6 |
7 | class TestCodespeedSettings(TestCase):
8 | """Test codespeed.settings
9 | """
10 |
11 | def setUp(self):
12 | self.cs_setting_keys = [key for key in dir(default_settings) if key.isupper()]
13 |
14 | def test_website_name(self):
15 | """See if WEBSITENAME is set
16 | """
17 | self.assertTrue(default_settings.WEBSITE_NAME)
18 | self.assertEqual(default_settings.WEBSITE_NAME, 'MySpeedSite',
19 | "Change codespeed settings in project.settings")
20 |
21 | def test_keys_in_settings(self):
22 | """Check that all settings attributes from codespeed.settings exist
23 | in django.conf.settings
24 | """
25 | for k in self.cs_setting_keys:
26 | self.assertTrue(hasattr(settings, k),
27 | "Key {0} is missing in settings.py.".format(k))
28 |
29 | def test_settings_attributes(self):
30 | """Check if all settings from codespeed.settings equals
31 | django.conf.settings
32 | """
33 | for k in self.cs_setting_keys:
34 | self.assertEqual(getattr(settings, k), getattr(default_settings, k))
35 |
--------------------------------------------------------------------------------
/codespeed/templates/codespeed/changes_data.html:
--------------------------------------------------------------------------------
1 |
2 | {% include 'codespeed/changes_table.html' %}
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | Revision
12 |
13 |
14 |
15 |
16 | Commit
17 | {% if rev.get_browsing_url %}{{ rev.commitid }} {% else %}{{ rev.commitid }}{% endif %}
18 |
19 | Date {{ rev.date }}
20 | {% ifnotequal rev.branch.project.repo_type "N" %}
21 | Repo {{ rev.branch.project.repo_path }}
22 | {% endifnotequal %}
23 |
24 |
25 |
26 | {% ifnotequal exe.project.repo_type 'N' %}
27 |
28 |
29 |
30 |
31 |
32 | Commit logs
33 |
34 |
35 |
36 | Loading...
37 |
38 |
42 |
43 | {% endifnotequal %}
44 |
45 |
--------------------------------------------------------------------------------
/codespeed/feeds.py:
--------------------------------------------------------------------------------
1 | from django.contrib.syndication.views import Feed
2 | from codespeed.models import Report
3 | from django.conf import settings
4 | from django.db.models import Q
5 |
6 |
7 | class ResultFeed(Feed):
8 | title = settings.WEBSITE_NAME
9 | link = "/changes/"
10 |
11 | def items(self):
12 | return Report.objects\
13 | .filter(self.result_filter())\
14 | .order_by('-revision__date')[:10]
15 |
16 | def item_title(self, item):
17 | return "%s: %s" % (item.revision.get_short_commitid(),
18 | item.item_description())
19 |
20 | description_template = "codespeed/changes_table.html"
21 |
22 | def get_context_data(self, **kwargs):
23 | report = kwargs['item']
24 | trendconfig = settings.TREND
25 |
26 | tablelist = report.get_changes_table(trendconfig)
27 |
28 | return {
29 | 'tablelist': tablelist,
30 | 'trendconfig': trendconfig,
31 | 'rev': report.revision,
32 | 'exe': report.executable,
33 | 'env': report.environment,
34 | }
35 |
36 |
37 | class LatestEntries(ResultFeed):
38 | description = "Last Results"
39 |
40 | def result_filter(self):
41 | return Q()
42 |
43 |
44 | class LatestSignificantEntries(ResultFeed):
45 | description = "Last results with significant changes"
46 |
47 | def result_filter(self):
48 | return Q(colorcode__in=('red', 'green'))
49 |
--------------------------------------------------------------------------------
/codespeed/validators.py:
--------------------------------------------------------------------------------
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!')
47 |
--------------------------------------------------------------------------------
/tools/save_multiple_results.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | ####################################################
3 | # Sample script that shows how to save result data using json #
4 | ####################################################
5 | import urllib
6 | import urllib2
7 | import json
8 |
9 |
10 | # You need to enter the real URL and have the server running
11 | CODESPEED_URL = 'http://localhost:8000/'
12 |
13 | sample_data = [
14 | {
15 | "commitid": "8",
16 | "project": "MyProject",
17 | "branch": "default",
18 | "executable": "myexe O3 64bits",
19 | "benchmark": "float",
20 | "environment": "Dual Core",
21 | "result_value": 2500.0
22 | },
23 | {
24 | "commitid": "8",
25 | "project": "MyProject",
26 | "branch": "default",
27 | "executable": "myexe O3 64bits",
28 | "benchmark": "int",
29 | "environment": "Dual Core",
30 | "result_value": 1100
31 | }
32 | ]
33 |
34 |
35 | def add(data):
36 | #params = urllib.urlencode(data)
37 | response = "None"
38 | try:
39 | f = urllib2.urlopen(
40 | CODESPEED_URL + 'result/add/json/', urllib.urlencode(data))
41 | except urllib2.HTTPError as e:
42 | print str(e)
43 | print e.read()
44 | return
45 | response = f.read()
46 | f.close()
47 | print "Server (%s) response: %s\n" % (CODESPEED_URL, response)
48 |
49 |
50 | if __name__ == "__main__":
51 | data = {'json': json.dumps(sample_data)}
52 | add(data)
53 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup, find_packages
2 |
3 | setup(
4 | name='codespeed',
5 | version='0.13.0',
6 | author='Miquel Torres',
7 | author_email='tobami@gmail.com',
8 | url='https://github.com/tobami/codespeed',
9 | download_url="https://github.com/tobami/codespeed/tags",
10 | license='GNU Lesser General Public License version 2.1',
11 | keywords=['benchmarking', 'visualization'],
12 | install_requires=['django>=1.11<2.2', 'isodate>=0.4.7,<0.6', 'matplotlib>=1.4.3,<2.0'],
13 | packages=find_packages(exclude=['ez_setup', 'sample_project']),
14 | setup_requires=['setuptools-markdown'],
15 | long_description_markdown_filename='README.md',
16 | description='A web application to monitor and analyze the performance of your code',
17 | include_package_data=True,
18 | zip_safe=False,
19 | classifiers=[
20 | 'Environment :: Web Environment',
21 | 'Framework :: Django',
22 | 'Intended Audience :: Developers',
23 | 'License :: OSI Approved :: GNU Lesser General Public License v2 (LGPLv2)',
24 | 'Operating System :: OS Independent',
25 | 'Programming Language :: Python',
26 | 'Programming Language :: Python :: 2',
27 | 'Programming Language :: Python :: 2.7',
28 | 'Programming Language :: Python :: 3',
29 | 'Programming Language :: Python :: 3.4',
30 | 'Programming Language :: Python :: 3.5',
31 | 'Programming Language :: Python :: 3.6',
32 | 'Topic :: Internet :: WWW/HTTP :: WSGI :: Application',
33 | ]
34 | )
35 |
--------------------------------------------------------------------------------
/codespeed/static/js/codespeed.js:
--------------------------------------------------------------------------------
1 | function readCheckbox(el) {
2 | /* Builds a string that holds all checked values in an input form */
3 | var config = "";
4 | $(el).each(function() {
5 | config += $(this).val() + ",";
6 | });
7 | // Remove last comma
8 | config = config.slice(0, -1);
9 | return config;
10 | }
11 |
12 | function getLoadText(text, h) {
13 | var loadtext = '';
14 | var pstyle = "";
15 | if (h > 0) {
16 | h = h - 32;
17 | if(h < 80) { h = 180; }
18 | else if (h > 400) { h = 400; }
19 | pstyle = ' style="line-height:' + h + 'px;"';
20 | }
21 | loadtext += '
'+ text;
22 | loadtext += '
';
23 | return loadtext;
24 | }
25 |
26 | $(function() {
27 | // Check all and none links
28 | $('.checkall').each(function() {
29 | var inputs = $(this).parent().children("li").children("input");
30 | $(this).click(function() {
31 | inputs.prop("checked", true);
32 | return false;
33 | });
34 | });
35 |
36 | $('.uncheckall').each(function() {
37 | var inputs = $(this).parent().children("li").children("input");
38 | $(this).click(function() {
39 | inputs.prop("checked", false);
40 | return false;
41 | });
42 | });
43 |
44 | $('.togglefold').each(function() {
45 | var lis = $(this).parent().children("li");
46 | $(this).click(function() {
47 | lis.slideToggle();
48 | return false;
49 | });
50 | });
51 | });
52 |
--------------------------------------------------------------------------------
/codespeed/urls.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from django.conf.urls import url
3 | from django.views.generic import TemplateView
4 |
5 | from codespeed import views
6 | from codespeed.feeds import LatestEntries, LatestSignificantEntries
7 |
8 | urlpatterns = [
9 | url(r'^$', views.HomeView.as_view(), name='home'),
10 | url(r'^about/$',
11 | TemplateView.as_view(template_name='about.html'), name='about'),
12 | # RSS for reports
13 | url(r'^feeds/latest/$', LatestEntries(), name='latest-results'),
14 | url(r'^feeds/latest_significant/$', LatestSignificantEntries(),
15 | name='latest-significant-results'),
16 | ]
17 |
18 | urlpatterns += [
19 | url(r'^historical/json/$', views.gethistoricaldata, name='gethistoricaldata'),
20 | url(r'^reports/$', views.reports, name='reports'),
21 | url(r'^changes/$', views.changes, name='changes'),
22 | url(r'^changes/table/$', views.getchangestable, name='getchangestable'),
23 | url(r'^changes/logs/$', views.displaylogs, name='displaylogs'),
24 | url(r'^timeline/$', views.timeline, name='timeline'),
25 | url(r'^timeline/json/$', views.gettimelinedata, name='gettimelinedata'),
26 | url(r'^comparison/$', views.comparison, name='comparison'),
27 | url(r'^comparison/json/$', views.getcomparisondata, name='getcomparisondata'),
28 | url(r'^makeimage/$', views.makeimage, name='makeimage'),
29 | ]
30 |
31 | urlpatterns += [
32 | # URLs for adding results
33 | url(r'^result/add/json/$', views.add_json_results, name='add-json-results'),
34 | url(r'^result/add/$', views.add_result, name='add-result'),
35 | ]
36 |
--------------------------------------------------------------------------------
/tools/save_single_result.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | ####################################################
3 | # Sample script that shows how to save result data #
4 | ####################################################
5 | from datetime import datetime
6 | import urllib
7 | import urllib2
8 |
9 | # You need to enter the real URL and have the server running
10 | CODESPEED_URL = 'http://localhost:8000/'
11 |
12 | current_date = datetime.today()
13 |
14 | # Mandatory fields
15 | data = {
16 | 'commitid': '14',
17 | 'branch': 'default', # Always use default for trunk/master/tip
18 | 'project': 'MyProject',
19 | 'executable': 'myexe O3 64bits',
20 | 'benchmark': 'float',
21 | 'environment': "Dual Core",
22 | 'result_value': 4000,
23 | }
24 |
25 | # Optional fields
26 | data.update({
27 | 'revision_date': current_date, # Optional. Default is taken either
28 | # from VCS integration or from current date
29 | 'result_date': current_date, # Optional, default is current date
30 | 'std_dev': 1.11111, # Optional. Default is blank
31 | 'max': 4001.6, # Optional. Default is blank
32 | 'min': 3995.1, # Optional. Default is blank
33 | })
34 |
35 |
36 | def add(data):
37 | params = urllib.urlencode(data)
38 | response = "None"
39 | print "Saving result for executable %s, revision %s, benchmark %s" % (
40 | data['executable'], data['commitid'], data['benchmark'])
41 | try:
42 | f = urllib2.urlopen(CODESPEED_URL + 'result/add/', params)
43 | except urllib2.HTTPError as e:
44 | print str(e)
45 | print e.read()
46 | return
47 | response = f.read()
48 | f.close()
49 | print "Server (%s) response: %s\n" % (CODESPEED_URL, response)
50 |
51 | if __name__ == "__main__":
52 | add(data)
53 |
--------------------------------------------------------------------------------
/tools/migrate_script.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """Adds the default branch to all existing revisions
3 |
4 | Note: This file is assumed to be in the same directory
5 | as the project settings.py. Otherwise you have to set the
6 | shell environment DJANGO_SETTINGS_MODULE
7 | """
8 | import sys
9 | import os
10 |
11 |
12 | ## Setup to import models from Django app ##
13 | def import_from_string(name):
14 | """helper to import module from a given string"""
15 |
16 | components = name.split('.')[1:]
17 | return reduce(lambda mod, y: getattr(mod, y), components, __import__(name))
18 |
19 | sys.path.append(os.path.abspath('..'))
20 |
21 | if 'DJANGO_SETTINGS_MODULE' in os.environ:
22 | settings = import_from_string(os.environ['DJANGO_SETTINGS_MODULE'])
23 | else:
24 | try:
25 | import settings # Assumed to be in the same directory.
26 | except ImportError:
27 | import sys
28 | sys.stderr.write(
29 | "Error: Can't find the file 'settings.py' in the directory "
30 | "containing %r. It appears you've customized things.\nYou'll have "
31 | "to run django-admin.py, passing it your settings module.\n(If the"
32 | " file settings.py does indeed exist, it's causing an ImportError "
33 | "somehow.)\n" % __file__)
34 | sys.exit(1)
35 |
36 | from django.core.management import setup_environ
37 | setup_environ(settings)
38 |
39 | from codespeed.models import Revision, Branch
40 |
41 |
42 | def main():
43 | """add default branch to revisions"""
44 | branches = Branch.objects.filter(name='default')
45 |
46 | for branch in branches:
47 | for rev in Revision.objects.filter(project=branch.project):
48 | rev.branch = branch
49 | rev.save()
50 |
51 | if __name__ == '__main__':
52 | main()
53 |
--------------------------------------------------------------------------------
/tools/create_trunks.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Create the default branch for all existing projects
4 | Starting v 0.8.0 that is mandatory.
5 |
6 | Note: This file is assumed to be in the same directory
7 | as the project settings.py. Otherwise you have to set the
8 | shell environment DJANGO_SETTINGS_MODULE
9 | """
10 | import sys
11 | import os
12 |
13 |
14 | ## Setup to import models from Django app ##
15 | def import_from_string(name):
16 | """helper to import module from a given string"""
17 | components = name.split('.')[1:]
18 | return reduce(lambda mod, y: getattr(mod, y), components, __import__(name))
19 |
20 |
21 | sys.path.append(os.path.abspath('..'))
22 |
23 |
24 | if 'DJANGO_SETTINGS_MODULE' in os.environ:
25 | settings = import_from_string(os.environ['DJANGO_SETTINGS_MODULE'])
26 | else:
27 | try:
28 | import settings # Assumed to be in the same directory.
29 | except ImportError:
30 | import sys
31 | sys.stderr.write(
32 | "Error: Can't find the file 'settings.py' in the directory "
33 | "containing %r. It appears you've customized things.\nYou'll have "
34 | "to run django-admin.py, passing it your settings module.\n(If the"
35 | " file settings.py does indeed exist, it's causing an ImportError "
36 | "somehow.)\n" % __file__)
37 | sys.exit(1)
38 |
39 | from django.core.management import setup_environ
40 | setup_environ(settings)
41 | from codespeed.models import Branch, Project
42 |
43 |
44 | def main():
45 | """Add Branch default to projects if not there"""
46 | projects = Project.objects.all()
47 |
48 | for proj in projects:
49 | if not proj.branches.filter(name='default'):
50 | trunk = Branch(name='default', project=proj)
51 | trunk.save()
52 | print "Created branch 'default' for project {0}".format(proj)
53 |
54 |
55 | if __name__ == '__main__':
56 | main()
57 |
--------------------------------------------------------------------------------
/tools/test_performance.py:
--------------------------------------------------------------------------------
1 | import timeit
2 | import urllib
3 | import sys
4 |
5 |
6 | SPEEDURL = 'http://localhost:8000/'
7 |
8 | benchmarks = ['ai', 'django', 'spambayes', 'grid']
9 |
10 |
11 | def test_overview():
12 | data = {
13 | "trend": 10,
14 | "baseline": 1,
15 | "revision": 73893,
16 | "executable": "1",
17 | "host": "bigdog",
18 | }
19 | params = urllib.urlencode(data)
20 | page = urllib.urlopen(SPEEDURL + 'overview/table/?' + params)
21 | jsonstring = page.read()
22 | page.close()
23 | if not '' in jsonstring:
24 | print "bad overview response"
25 | sys.exit(1)
26 |
27 |
28 | def test_timeline(bench):
29 | data = {
30 | "executables": "1,2,6",
31 | "baseline": "true",
32 | "benchmark": bench,
33 | "host": "bigdog",
34 | "revisions": 200
35 | }
36 | params = urllib.urlencode(data)
37 | page = urllib.urlopen(SPEEDURL + 'timeline/json/?' + params)
38 | jsonstring = page.read()
39 | #print jsonstring
40 | page.close()
41 | if not '"lessisbetter": " (less is better)", "baseline":' in jsonstring \
42 | or not', "error": "None"}' in jsonstring:
43 | print "bad timeline response"
44 | sys.exit(1)
45 |
46 | if __name__ == "__main__":
47 | t = timeit.Timer('test_overview()', 'from __main__ import test_overview')
48 | results = t.repeat(20, 1)
49 | print
50 | print "OVERVIEW RESULTS"
51 | print "min:", min(results)
52 | print "avg:", sum(results) / len(results)
53 | print
54 | print "TIMELINE RESULTS"
55 | for bench in benchmarks:
56 | t = timeit.Timer('test_timeline("' + bench + '")',
57 | 'from __main__ import test_timeline')
58 | results = t.repeat(20, 1)
59 | print "benchmark =", bench
60 | print "min:", min(results)
61 | print "avg:", sum(results) / len(results)
62 | print
63 |
--------------------------------------------------------------------------------
/codespeed/images.py:
--------------------------------------------------------------------------------
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
71 |
--------------------------------------------------------------------------------
/codespeed/commits/tests/test_git.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | from django.test import TestCase, override_settings
3 | from mock import Mock, patch
4 |
5 | from codespeed.commits.git import getlogs
6 | from codespeed.models import Project, Revision, Branch, Environment
7 |
8 |
9 | @override_settings(ALLOW_ANONYMOUS_POST=True)
10 | class GitTest(TestCase):
11 | def setUp(self):
12 | self.env = Environment.objects.create(name='env')
13 | self.project = Project.objects.create(name='project',
14 | repo_path='path',
15 | repo_type=Project.GIT)
16 | self.branch = Branch.objects.create(name='default',
17 | project_id=self.project.id)
18 | self.revision = Revision.objects.create(
19 | **{
20 | 'commitid': 'id1',
21 | 'date': datetime.now(),
22 | 'project_id': self.project.id,
23 | 'branch_id': self.branch.id,
24 | }
25 | )
26 |
27 | @patch("codespeed.commits.git.Popen")
28 | def test_git_output_parsing(self, popen):
29 | # given
30 | outputs = {
31 | "log": b"id\x00long_id\x001583489681\x00author\x00email\x00msg\x00\x1e",
32 | "tag": b'tag',
33 | }
34 |
35 | def side_effect(cmd, *args, **kwargs):
36 | ret = Mock()
37 | ret.returncode = 0
38 | git_command = cmd[1] if len(cmd) > 0 else None
39 | output = outputs.get(git_command, b'')
40 | ret.communicate.return_value = (output, b'')
41 | return ret
42 |
43 | popen.side_effect = side_effect
44 |
45 | # when
46 | # revision doesn't matter here, git commands are mocked
47 | logs = getlogs(self.revision, self.revision)
48 |
49 | # then
50 | expected = {
51 | 'date': '2020-03-06 04:14:41',
52 | 'message': 'msg',
53 | 'commitid': 'long_id',
54 | 'author': 'author',
55 | 'author_email': 'email',
56 | 'body': '',
57 | 'short_commit_id': 'id',
58 | 'tag': 'tag',
59 | }
60 | self.assertEquals([expected], logs)
61 |
--------------------------------------------------------------------------------
/tools/pypy/savecpython.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import urllib, urllib2
3 | from datetime import datetime
4 |
5 | SPEEDURL = 'http://127.0.0.1:8000/'
6 | #SPEEDURL = 'http://speed.pypy.org/'
7 |
8 | def save(project, revision, results, options, executable, host, testing=False):
9 | testparams = []
10 | #Parse data
11 | data = {}
12 | current_date = datetime.today()
13 |
14 | for b in results:
15 | bench_name = b[0]
16 | res_type = b[1]
17 | results = b[2]
18 | value = 0
19 | if res_type == "SimpleComparisonResult":
20 | value = results['base_time']
21 | elif res_type == "ComparisonResult":
22 | value = results['avg_base']
23 | else:
24 | print("ERROR: result type unknown " + b[1])
25 | return 1
26 | data = {
27 | 'commitid': revision,
28 | 'project': project,
29 | 'executable': executable,
30 | 'benchmark': bench_name,
31 | 'environment': host,
32 | 'result_value': value,
33 | 'result_date': current_date,
34 | }
35 | if res_type == "ComparisonResult":
36 | data['std_dev'] = results['std_changed']
37 | if testing: testparams.append(data)
38 | else: send(data)
39 | if testing: return testparams
40 | else: return 0
41 |
42 | def send(data):
43 | #save results
44 | params = urllib.urlencode(data)
45 | f = None
46 | response = "None"
47 | info = str(datetime.today()) + ": Saving result for " + data['executable'] + " revision "
48 | info += str(data['commitid']) + ", benchmark " + data['benchmark']
49 | print(info)
50 | try:
51 | f = urllib2.urlopen(SPEEDURL + 'result/add/', params)
52 | response = f.read()
53 | f.close()
54 | except urllib2.URLError, e:
55 | if hasattr(e, 'reason'):
56 | response = '\n We failed to reach a server\n'
57 | response += ' Reason: ' + str(e.reason)
58 | elif hasattr(e, 'code'):
59 | response = '\n The server couldn\'t fulfill the request\n'
60 | response += ' Error code: ' + str(e)
61 | print("Server (%s) response: %s\n" % (SPEEDURL, response))
62 | return 1
63 | return 0
64 |
--------------------------------------------------------------------------------
/codespeed/static/js/jqplot/jqplot.canvasAxisLabelRenderer.min.js:
--------------------------------------------------------------------------------
1 | !function(t){t.jqplot.CanvasAxisLabelRenderer=function(e){this.angle=0,this.axis,this.show=!0,this.showLabel=!0,this.label="",this.fontFamily='"Trebuchet MS", Arial, Helvetica, sans-serif',this.fontSize="11pt",this.fontWeight="normal",this.fontStretch=1,this.textColor="#666666",this.enableFontSupport=!0,this.pt2px=null,this._elem,this._ctx,this._plotWidth,this._plotHeight,this._plotDimensions={height:null,width:null},t.extend(!0,this,e),null==e.angle&&"xaxis"!=this.axis&&"x2axis"!=this.axis&&(this.angle=-90);var i={fontSize:this.fontSize,fontWeight:this.fontWeight,fontStretch:this.fontStretch,fillStyle:this.textColor,angle:this.getAngleRad(),fontFamily:this.fontFamily};this.pt2px&&(i.pt2px=this.pt2px),this.enableFontSupport&&t.jqplot.support_canvas_text()?this._textRenderer=new t.jqplot.CanvasFontRenderer(i):this._textRenderer=new t.jqplot.CanvasTextRenderer(i)},t.jqplot.CanvasAxisLabelRenderer.prototype.init=function(e){t.extend(!0,this,e),this._textRenderer.init({fontSize:this.fontSize,fontWeight:this.fontWeight,fontStretch:this.fontStretch,fillStyle:this.textColor,angle:this.getAngleRad(),fontFamily:this.fontFamily})},t.jqplot.CanvasAxisLabelRenderer.prototype.getWidth=function(t){if(this._elem)return this._elem.outerWidth(!0);var e=this._textRenderer,i=e.getWidth(t),n=e.getHeight(t),s=Math.abs(Math.sin(e.angle)*n)+Math.abs(Math.cos(e.angle)*i);return s},t.jqplot.CanvasAxisLabelRenderer.prototype.getHeight=function(t){if(this._elem)return this._elem.outerHeight(!0);var e=this._textRenderer,i=e.getWidth(t),n=e.getHeight(t),s=Math.abs(Math.cos(e.angle)*n)+Math.abs(Math.sin(e.angle)*i);return s},t.jqplot.CanvasAxisLabelRenderer.prototype.getAngleRad=function(){var t=this.angle*Math.PI/180;return t},t.jqplot.CanvasAxisLabelRenderer.prototype.draw=function(e,i){this._elem&&(t.jqplot.use_excanvas&&void 0!==window.G_vmlCanvasManager.uninitElement&&window.G_vmlCanvasManager.uninitElement(this._elem.get(0)),this._elem.emptyForce(),this._elem=null);var n=i.canvasManager.getCanvas();this._textRenderer.setText(this.label,e);var s=this.getWidth(e),a=this.getHeight(e);return n.width=s,n.height=a,n.style.width=s,n.style.height=a,n=i.canvasManager.initCanvas(n),this._elem=t(n),this._elem.css({position:"absolute"}),this._elem.addClass("jqplot-"+this.axis+"-label"),n=null,this._elem},t.jqplot.CanvasAxisLabelRenderer.prototype.pack=function(){this._textRenderer.draw(this._elem.get(0).getContext("2d"),this.label)}}(jQuery);
--------------------------------------------------------------------------------
/sample_project/settings.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Django settings for a Codespeed project.
3 | import os
4 |
5 | DEBUG = True
6 |
7 | BASEDIR = os.path.abspath(os.path.dirname(__file__))
8 | TOPDIR = os.path.split(BASEDIR)[1]
9 |
10 | #: The directory which should contain checked out source repositories:
11 | REPOSITORY_BASE_PATH = os.path.join(BASEDIR, "repos")
12 |
13 | ADMINS = (
14 | # ('Your Name', 'your_email@domain.com'),
15 | )
16 |
17 | MANAGERS = ADMINS
18 | DATABASES = {
19 | 'default': {
20 | 'ENGINE': 'django.db.backends.sqlite3',
21 | 'NAME': os.path.join(BASEDIR, 'data.db'),
22 | }
23 | }
24 |
25 | TIME_ZONE = 'America/Chicago'
26 |
27 | LANGUAGE_CODE = 'en-us'
28 |
29 | SITE_ID = 1
30 |
31 | USE_I18N = False
32 |
33 | MEDIA_ROOT = os.path.join(BASEDIR, "media")
34 |
35 | MEDIA_URL = '/media/'
36 |
37 | ADMIN_MEDIA_PREFIX = '/static/admin/'
38 |
39 | SECRET_KEY = 'as%n_m#)^vee2pe91^^@c))sl7^c6t-9r8n)_69%)2yt+(la2&'
40 |
41 |
42 | MIDDLEWARE = (
43 | 'django.middleware.common.CommonMiddleware',
44 | 'django.contrib.sessions.middleware.SessionMiddleware',
45 | 'django.middleware.csrf.CsrfViewMiddleware',
46 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
47 | 'django.contrib.messages.middleware.MessageMiddleware',
48 | )
49 |
50 | ROOT_URLCONF = '{0}.urls'.format(TOPDIR)
51 |
52 |
53 | TEMPLATES = [
54 | {
55 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
56 | 'DIRS': [os.path.join(BASEDIR, 'templates')],
57 | 'APP_DIRS': True,
58 | 'OPTIONS': {
59 | 'context_processors': [
60 | 'django.template.context_processors.debug',
61 | 'django.template.context_processors.request',
62 | 'django.contrib.auth.context_processors.auth',
63 | 'django.contrib.messages.context_processors.messages',
64 | ],
65 | },
66 | },
67 | ]
68 |
69 | INSTALLED_APPS = (
70 | 'django.contrib.auth',
71 | 'django.contrib.contenttypes',
72 | 'django.contrib.sessions',
73 | 'django.contrib.messages',
74 | 'django.contrib.admin',
75 | 'django.contrib.staticfiles',
76 | 'codespeed',
77 | )
78 |
79 |
80 | STATIC_URL = '/static/'
81 | STATIC_ROOT = os.path.join(BASEDIR, "sitestatic")
82 | STATICFILES_DIRS = (
83 | os.path.join(BASEDIR, 'static'),
84 | )
85 |
86 |
87 | # Codespeed settings that can be overwritten here.
88 | from codespeed.settings import *
89 |
--------------------------------------------------------------------------------
/codespeed/templates/codespeed/base.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 |
3 |
4 |
5 | {% block title %}MyProject's Speed Center{% endblock %}
6 |
7 |
8 |
9 |
10 | {% block extra_head %}{% endblock %}
11 |
12 |
13 |
14 | {# TODO: Rename id=title to id=header and/or switch to
51 |
52 |
53 |
54 | {% block extra_script %}{% endblock %}
55 |
56 |
57 |
--------------------------------------------------------------------------------
/tools/pypy/saveresults.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #######################################################
3 | # This script saves result data #
4 | # It expects the format of unladen swallow's perf.py #
5 | #######################################################
6 | import urllib, urllib2
7 | from datetime import datetime
8 |
9 | #SPEEDURL = 'http://127.0.0.1:8000/'
10 | SPEEDURL = 'http://speed.pypy.org/'
11 |
12 | def save(project, revision, results, options, executable, environment, testing=False):
13 | testparams = []
14 | #Parse data
15 | data = {}
16 | current_date = datetime.today()
17 | for b in results:
18 | bench_name = b[0]
19 | res_type = b[1]
20 | results = b[2]
21 | value = 0
22 | if res_type == "SimpleComparisonResult":
23 | value = results['changed_time']
24 | elif res_type == "ComparisonResult":
25 | value = results['avg_changed']
26 | else:
27 | print("ERROR: result type unknown " + b[1])
28 | return 1
29 | data = {
30 | 'commitid': revision,
31 | 'project': project,
32 | 'executable': executable,
33 | 'benchmark': bench_name,
34 | 'environment': environment,
35 | 'result_value': value,
36 | }
37 | if res_type == "ComparisonResult":
38 | data['std_dev'] = results['std_changed']
39 | if testing: testparams.append(data)
40 | else: send(data)
41 | if testing: return testparams
42 | else: return 0
43 |
44 | def send(data):
45 | #save results
46 | params = urllib.urlencode(data)
47 | f = None
48 | response = "None"
49 | info = str(datetime.today()) + ": Saving result for " + data['executable']
50 | info += " revision " + " " + str(data['commitid']) + ", benchmark "
51 | info += data['benchmark']
52 | print(info)
53 | try:
54 | f = urllib2.urlopen(SPEEDURL + 'result/add/', params)
55 | response = f.read()
56 | f.close()
57 | except urllib2.URLError as e:
58 | if hasattr(e, 'reason'):
59 | response = '\n We failed to reach a server\n'
60 | response += ' Reason: ' + str(e.reason)
61 | elif hasattr(e, 'code'):
62 | response = '\n The server couldn\'t fulfill the request\n'
63 | response += ' Error code: ' + str(e)
64 | print("Server (%s) response: %s\n" % (SPEEDURL, response))
65 | return 1
66 | return 0
67 |
--------------------------------------------------------------------------------
/codespeed/commits/subversion.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """Subversion commit logs support"""
3 | from __future__ import absolute_import
4 |
5 | from datetime import datetime
6 |
7 | from .exceptions import CommitLogError
8 |
9 |
10 | def updaterepo(project):
11 | """Not needed for a remote subversion repo"""
12 | return [{'error': False}]
13 |
14 |
15 | def get_tag(rev_num, repo_path, client):
16 | tags_url = repo_path + '/tags'
17 | tags = client.ls(tags_url)
18 |
19 | for tag in tags:
20 | if 'created_rev' in tag and tag['created_rev'].number == rev_num:
21 | if 'name' in tag:
22 | return tag['name'].split('/')[-1]
23 | return ''
24 |
25 |
26 | def getlogs(newrev, startrev):
27 | import pysvn
28 |
29 | logs = []
30 | log_messages = []
31 | loglimit = 200
32 |
33 | def get_login(realm, username, may_save):
34 | repo_user = newrev.branch.project.repo_user
35 | repo_pass = newrev.branch.project.repo_pass
36 | return True, repo_user, repo_pass, False
37 |
38 | client = pysvn.Client()
39 | if newrev.branch.project.repo_user != "":
40 | client.callback_get_login = get_login
41 |
42 | try:
43 | log_messages = \
44 | client.log(
45 | newrev.branch.project.repo_path,
46 | revision_start=pysvn.Revision(
47 | pysvn.opt_revision_kind.number, startrev.commitid
48 | ),
49 | revision_end=pysvn.Revision(
50 | pysvn.opt_revision_kind.number, newrev.commitid
51 | )
52 | )
53 | except pysvn.ClientError as e:
54 | raise CommitLogError(e.args)
55 | except ValueError:
56 | raise CommitLogError(
57 | "'%s' is an invalid subversion revision number" % newrev.commitid)
58 | log_messages.reverse()
59 | s = len(log_messages)
60 | while s > loglimit:
61 | log_messages = log_messages[:s]
62 | s = len(log_messages) - 1
63 |
64 | for log in log_messages:
65 | try:
66 | author = log.author
67 | except AttributeError:
68 | author = ""
69 | date = datetime.fromtimestamp(log.date).strftime("%Y-%m-%d %H:%M:%S")
70 | message = log.message
71 | tag = get_tag(log.revision.number,
72 | newrev.branch.project.repo_path, client)
73 | # Add log unless it is the last commit log, which has already been tested
74 | logs.append({
75 | 'date': date, 'author': author, 'message': message,
76 | 'commitid': log.revision.number, 'tag': tag})
77 | return logs
78 |
--------------------------------------------------------------------------------
/tools/pypy/import_from_json.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | ################################################################################
3 | # This script imports PyPy's result data from json files located on the server #
4 | ################################################################################
5 | import simplejson, urllib2
6 | import sys
7 | from xml.dom.minidom import parse
8 | from datetime import datetime
9 | import saveresults, savecpython
10 |
11 | RESULTS_URLS = {
12 | 'pypy-c-jit': 'http://buildbot.pypy.org/bench_results/',
13 | 'pypy-c': 'http://buildbot.pypy.org/bench_results_nojit/',
14 | }
15 | START_REV = 79485
16 | END_REV = 79485
17 | PROJECT = "PyPy"
18 |
19 | for INTERP in RESULTS_URLS:
20 | RESULTS_URL = RESULTS_URLS[INTERP]
21 | # get json filenames
22 | filelist = []
23 | try:
24 | datasource = urllib2.urlopen(RESULTS_URL)
25 | dom = parse(datasource)
26 | for elem in dom.getElementsByTagName('td'):
27 | for e in elem.childNodes:
28 | if len(e.childNodes):
29 | filename = e.firstChild.toxml()
30 | if e.tagName == "a" and ".json" in filename:
31 | if int(filename.replace(".json", "")) >= START_REV and\
32 | int(filename.replace(".json", "")) <= END_REV:
33 | filelist.append(filename)
34 | except urllib2.URLError, e:
35 | response = "None"
36 | if hasattr(e, 'reason'):
37 | response = '\n We failed to reach ' + RESULTS_URL + '\n'
38 | response += ' Reason: ' + str(e.reason)
39 | elif hasattr(e, 'code'):
40 | response = '\n The server couldn\'t fulfill the request\n'
41 | response += ' Error code: ' + str(e)
42 | print "Results Server (%s) response: %s\n" % (RESULTS_URL, response)
43 | sys.exit(1)
44 | finally:
45 | datasource.close()
46 |
47 | # read json result and save to speed.pypy.org
48 | for filename in filelist:
49 | print "Reading %s..." % filename
50 | f = urllib2.urlopen(RESULTS_URL + filename)
51 | result = simplejson.load(f)
52 | f.close()
53 | proj = PROJECT
54 | revision = result['revision']
55 | interpreter = INTERP
56 | int_options = ""
57 | options = ""
58 | if 'options' in result:
59 | options = result['options']
60 |
61 | host = 'tannit'
62 | #saveresults.save(proj, revision, result['results'], options, interpreter, host)
63 | if filename == filelist[len(filelist)-1]:
64 | savecpython.save('cpython', '100', result['results'], options, 'cpython', host)
65 | print "\nOK"
66 |
--------------------------------------------------------------------------------
/sample_project/deploy/nginx.default-site.conf:
--------------------------------------------------------------------------------
1 | # You may add here your
2 | # server {
3 | # ...
4 | # }
5 | # statements for each of your virtual hosts
6 |
7 | upstream app_server_speedcenter {
8 | server localhost:8000 fail_timeout=0;
9 | }
10 |
11 | server {
12 |
13 | listen 80; ## listen for ipv4
14 | listen [::]:88 default ipv6only=on; ## listen for ipv6
15 |
16 | server_name localhost;
17 |
18 | access_log /var/log/nginx/localhost.access.log;
19 |
20 | # path for static files
21 | location ~ ^/static/(.*)$ {
22 | alias /path/to/speedcenter/sitestatic/$1;
23 | }
24 |
25 | location / {
26 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
27 | proxy_set_header Host $http_host;
28 | proxy_redirect off;
29 |
30 | if (!-f $request_filename) {
31 | proxy_pass http://app_server_speedcenter;
32 | break;
33 | }
34 | }
35 |
36 | #error_page 404 /404.html;
37 |
38 | # redirect server error pages to the static page /50x.html
39 | #
40 | #error_page 500 502 503 504 /50x.html;
41 | #location = /50x.html {
42 | # root /var/www/nginx-default;
43 | #}
44 |
45 | # proxy the PHP scripts to Apache listening on 127.0.0.1:80
46 | #
47 | #location ~ \.php$ {
48 | #proxy_pass http://127.0.0.1;
49 | #}
50 |
51 | # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
52 | #
53 | #location ~ \.php$ {
54 | #fastcgi_pass 127.0.0.1:9000;
55 | #fastcgi_index index.php;
56 | #fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
57 | #includefastcgi_params;
58 | #}
59 |
60 | # deny access to .htaccess files, if Apache's document root
61 | # concurs with nginx's one
62 | #
63 | #location ~ /\.ht {
64 | #deny all;
65 | #}
66 | }
67 |
68 |
69 | # another virtual host using mix of IP-, name-, and port-based configuration
70 | #
71 | #server {
72 | #listen 8000;
73 | #listen somename:8080;
74 | #server_name somename alias another.alias;
75 |
76 | #location / {
77 | #root html;
78 | #index index.html index.htm;
79 | #}
80 | #}
81 |
82 |
83 | # HTTPS server
84 | #
85 | #server {
86 | #listen 443;
87 | #server_name localhost;
88 |
89 | #ssl on;
90 | #ssl_certificate cert.pem;
91 | #ssl_certificate_key cert.key;
92 |
93 | #ssl_session_timeout 5m;
94 |
95 | #ssl_protocols SSLv3 TLSv1;
96 | #ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv3:+EXP;
97 | #ssl_prefer_server_ciphers on;
98 |
99 | #location / {
100 | #root html;
101 | #index index.html index.htm;
102 | #}
103 | #}
104 |
--------------------------------------------------------------------------------
/codespeed/static/js/jqplot/jqplot.canvasAxisTickRenderer.min.js:
--------------------------------------------------------------------------------
1 | !function(t){t.jqplot.CanvasAxisTickRenderer=function(e){this.mark="outside",this.showMark=!0,this.showGridline=!0,this.isMinorTick=!1,this.angle=0,this.markSize=4,this.show=!0,this.showLabel=!0,this.labelPosition="auto",this.label="",this.value=null,this._styles={},this.formatter=t.jqplot.DefaultTickFormatter,this.formatString="",this.prefix="",this.fontFamily='"Trebuchet MS", Arial, Helvetica, sans-serif',this.fontSize="10pt",this.fontWeight="normal",this.fontStretch=1,this.textColor="#666666",this.enableFontSupport=!0,this.pt2px=null,this._elem,this._ctx,this._plotWidth,this._plotHeight,this._plotDimensions={height:null,width:null},t.extend(!0,this,e);var i={fontSize:this.fontSize,fontWeight:this.fontWeight,fontStretch:this.fontStretch,fillStyle:this.textColor,angle:this.getAngleRad(),fontFamily:this.fontFamily};this.pt2px&&(i.pt2px=this.pt2px),this.enableFontSupport&&t.jqplot.support_canvas_text()?this._textRenderer=new t.jqplot.CanvasFontRenderer(i):this._textRenderer=new t.jqplot.CanvasTextRenderer(i)},t.jqplot.CanvasAxisTickRenderer.prototype.init=function(e){t.extend(!0,this,e),this._textRenderer.init({fontSize:this.fontSize,fontWeight:this.fontWeight,fontStretch:this.fontStretch,fillStyle:this.textColor,angle:this.getAngleRad(),fontFamily:this.fontFamily})},t.jqplot.CanvasAxisTickRenderer.prototype.getWidth=function(t){if(this._elem)return this._elem.outerWidth(!0);var e=this._textRenderer,i=e.getWidth(t),s=e.getHeight(t),n=Math.abs(Math.sin(e.angle)*s)+Math.abs(Math.cos(e.angle)*i);return n},t.jqplot.CanvasAxisTickRenderer.prototype.getHeight=function(t){if(this._elem)return this._elem.outerHeight(!0);var e=this._textRenderer,i=e.getWidth(t),s=e.getHeight(t),n=Math.abs(Math.cos(e.angle)*s)+Math.abs(Math.sin(e.angle)*i);return n},t.jqplot.CanvasAxisTickRenderer.prototype.getTop=function(){return this._elem?this._elem.position().top:null},t.jqplot.CanvasAxisTickRenderer.prototype.getAngleRad=function(){var t=this.angle*Math.PI/180;return t},t.jqplot.CanvasAxisTickRenderer.prototype.setTick=function(t,e,i){return this.value=t,i&&(this.isMinorTick=!0),this},t.jqplot.CanvasAxisTickRenderer.prototype.draw=function(e,i){this.label||(this.label=this.prefix+this.formatter(this.formatString,this.value)),this._elem&&(t.jqplot.use_excanvas&&void 0!==window.G_vmlCanvasManager.uninitElement&&window.G_vmlCanvasManager.uninitElement(this._elem.get(0)),this._elem.emptyForce(),this._elem=null);var s=i.canvasManager.getCanvas();this._textRenderer.setText(this.label,e);var n=this.getWidth(e),h=this.getHeight(e);return s.width=n,s.height=h,s.style.width=n,s.style.height=h,s.style.textAlign="left",s.style.position="absolute",s=i.canvasManager.initCanvas(s),this._elem=t(s),this._elem.css(this._styles),this._elem.addClass("jqplot-"+this.axis+"-tick"),s=null,this._elem},t.jqplot.CanvasAxisTickRenderer.prototype.pack=function(){this._textRenderer.draw(this._elem.get(0).getContext("2d"),this.label)}}(jQuery);
--------------------------------------------------------------------------------
/codespeed/static/js/jqplot/jquery.jqplot.min.css:
--------------------------------------------------------------------------------
1 | .jqplot-xaxis,.jqplot-xaxis-label{margin-top:10px}.jqplot-x2axis,.jqplot-x2axis-label{margin-bottom:10px}.jqplot-target{position:relative;color:#666;font-family:"Trebuchet MS",Arial,Helvetica,sans-serif;font-size:1em}.jqplot-axis{font-size:.75em}.jqplot-yaxis{margin-right:10px}.jqplot-y2axis,.jqplot-y3axis,.jqplot-y4axis,.jqplot-y5axis,.jqplot-y6axis,.jqplot-y7axis,.jqplot-y8axis,.jqplot-y9axis,.jqplot-yMidAxis{margin-left:10px;margin-right:10px}.jqplot-axis-tick,.jqplot-x2axis-tick,.jqplot-xaxis-tick,.jqplot-y2axis-tick,.jqplot-y3axis-tick,.jqplot-y4axis-tick,.jqplot-y5axis-tick,.jqplot-y6axis-tick,.jqplot-y7axis-tick,.jqplot-y8axis-tick,.jqplot-y9axis-tick,.jqplot-yMidAxis-tick,.jqplot-yaxis-tick{position:absolute;white-space:pre}.jqplot-xaxis-tick{top:0;left:15px;vertical-align:top}.jqplot-x2axis-tick{bottom:0;left:15px;vertical-align:bottom}.jqplot-yaxis-tick{right:0;top:15px;text-align:right}.jqplot-yaxis-tick.jqplot-breakTick{right:-20px;margin-right:0;padding:1px 5px;z-index:2;font-size:1.5em}.jqplot-x2axis-label,.jqplot-xaxis-label,.jqplot-yMidAxis-label,.jqplot-yaxis-label{font-size:11pt;position:absolute}.jqplot-y2axis-tick,.jqplot-y3axis-tick,.jqplot-y4axis-tick,.jqplot-y5axis-tick,.jqplot-y6axis-tick,.jqplot-y7axis-tick,.jqplot-y8axis-tick,.jqplot-y9axis-tick{left:0;top:15px;text-align:left}.jqplot-yMidAxis-tick{text-align:center;white-space:nowrap}.jqplot-yaxis-label{margin-right:10px}.jqplot-y2axis-label,.jqplot-y3axis-label,.jqplot-y4axis-label,.jqplot-y5axis-label,.jqplot-y6axis-label,.jqplot-y7axis-label,.jqplot-y8axis-label,.jqplot-y9axis-label{font-size:11pt;margin-left:10px;position:absolute}.jqplot-meterGauge-tick{font-size:.75em;color:#999}.jqplot-meterGauge-label{font-size:1em;color:#999}table.jqplot-table-legend{margin:12px}table.jqplot-cursor-legend,table.jqplot-table-legend{background-color:rgba(255,255,255,.6);border:1px solid #ccc;position:absolute;font-size:.75em}td.jqplot-table-legend{vertical-align:middle}td.jqplot-seriesToggle:active,td.jqplot-seriesToggle:hover{cursor:pointer}.jqplot-table-legend .jqplot-series-hidden{text-decoration:line-through}div.jqplot-table-legend-swatch-outline{border:1px solid #ccc;padding:1px}div.jqplot-table-legend-swatch{width:0;height:0;border-width:5px 6px;border-style:solid}.jqplot-title{top:0;left:0;padding-bottom:.5em;font-size:1.2em}table.jqplot-cursor-tooltip{border:1px solid #ccc;font-size:.75em}.jqplot-canvasOverlay-tooltip,.jqplot-cursor-tooltip,.jqplot-highlighter-tooltip{border:1px solid #ccc;font-size:.75em;white-space:nowrap;background:rgba(208,208,208,.5);padding:1px}.jqplot-point-label{font-size:.75em;z-index:2}td.jqplot-cursor-legend-swatch{vertical-align:middle;text-align:center}div.jqplot-cursor-legend-swatch{width:1.2em;height:.7em}.jqplot-error{text-align:center}.jqplot-error-message{position:relative;top:46%;display:inline-block}div.jqplot-bubble-label{font-size:.8em;padding-left:2px;padding-right:2px;color:rgb(20%,20%,20%)}div.jqplot-bubble-label.jqplot-bubble-label-highlight{background:rgba(90%,90%,90%,.7)}div.jqplot-noData-container{text-align:center;background-color:rgba(96%,96%,96%,.3)}
--------------------------------------------------------------------------------
/codespeed/templates/codespeed/changes_table.html:
--------------------------------------------------------------------------------
1 | {% load percentages %}
2 |
3 |
7 |
8 | {% for units in tablelist %}
9 |
10 |
11 |
12 | Benchmark
13 | {{ units.units_title }} in {{ units.units }}
14 | {% if units.has_stddev %}Std dev {% endif%}
15 | {% if units.hasmin %}Min {% endif%}
16 | {% if units.hasmax %}Max {% endif%}
17 | Change
18 | Trend
19 |
20 |
21 |
22 |
23 | Average
24 |
25 | {% if units.hasmin %}
26 | {% endif%}{% if units.has_stddev %}{{ units.totals.min }}
27 | {% endif%}{% if units.hasmax %}{{ units.totals.max }}
28 | {% endif%}{{ units.totals.change|percentage }}
29 | {% ifnotequal units.totals.trend "-" %}{{ units.totals.trend|floatformat:2 }}%{% else %}{{ units.totals.trend }}{% endifnotequal %}
30 |
31 |
32 |
33 | {% for row in units.rows|dictsort:"bench_name" %}
34 |
35 | {{ row.bench_name }}
36 | {{ row.result|floatformat:units.precission }}
37 | {% if units.has_stddev %}{{ row.std_dev|floatformat:units.precission }}
38 | {% endif%}{% if units.hasmin %}{{ row.val_min|floatformat:units.precission }}
39 | {% endif%}{% if units.hasmax %}{{ row.val_max|floatformat:units.precission }}
40 | {% endif%}{{ row.change|percentage }}
41 | {% ifequal row.trend "-" %}-{% else %}{{ row.trend|floatformat:2 }}%{% endifequal %}
42 | {% endfor %}
43 |
44 |
{% endfor %}
45 |
46 |
47 |
48 |
49 | Executable
50 |
51 |
52 |
53 | Name {{ exe }}
54 | Description {{ exe.description }}
55 |
56 |
57 |
58 |
59 |
60 | Environment
61 |
62 |
63 |
64 | Name {{ env.name }}
65 | CPU {{ env.cpu }}
66 | Memory {{ env.memory }}
67 | OS {{ env.os }}
68 | Kernel {{ env.kernel }}
69 |
70 |
71 |
--------------------------------------------------------------------------------
/codespeed/admin.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from django import forms
4 | from django.contrib import admin
5 |
6 | from codespeed.models import (Project, Revision, Executable, Benchmark, Branch,
7 | Result, Environment, Report)
8 |
9 |
10 | class ProjectForm(forms.ModelForm):
11 |
12 | default_branch = forms.CharField(max_length=32, required=False)
13 |
14 | def clean(self):
15 | if not self.cleaned_data.get('default_branch'):
16 | repo_type = self.cleaned_data['repo_type']
17 | if repo_type in [Project.GIT, Project.GITHUB]:
18 | self.cleaned_data['default_branch'] = "master"
19 | elif repo_type == Project.MERCURIAL:
20 | self.cleaned_data['default_branch'] = "default"
21 | elif repo_type == Project.SUBVERSION:
22 | self.cleaned_data['default_branch'] = "trunk"
23 | else:
24 | self.add_error('default_branch', 'This field is required.')
25 |
26 | class Meta:
27 | model = Project
28 | fields = '__all__'
29 |
30 |
31 | @admin.register(Project)
32 | class ProjectAdmin(admin.ModelAdmin):
33 | list_display = ('name', 'repo_type', 'repo_path', 'track')
34 |
35 | form = ProjectForm
36 |
37 |
38 | @admin.register(Branch)
39 | class BranchAdmin(admin.ModelAdmin):
40 | list_display = ('name', 'project', 'display_on_comparison_page')
41 | list_filter = ('project',)
42 |
43 |
44 | @admin.register(Revision)
45 | class RevisionAdmin(admin.ModelAdmin):
46 | list_display = ('commitid', 'branch', 'tag', 'date')
47 | list_filter = ('branch__project', 'branch', 'tag', 'date')
48 | search_fields = ('commitid', 'tag')
49 |
50 |
51 | @admin.register(Executable)
52 | class ExecutableAdmin(admin.ModelAdmin):
53 | list_display = ('name', 'description', 'id', 'project')
54 | list_filter = ('project',)
55 | ordering = ['name']
56 | search_fields = ('name', 'description', 'project__name')
57 |
58 |
59 | @admin.register(Benchmark)
60 | class BenchmarkAdmin(admin.ModelAdmin):
61 | list_display = ('name', 'benchmark_type', 'data_type', 'description',
62 | 'units_title', 'units', 'lessisbetter',
63 | 'default_on_comparison')
64 | list_filter = ('data_type', 'lessisbetter')
65 | ordering = ['name']
66 | search_fields = ('name', 'description')
67 |
68 |
69 | @admin.register(Environment)
70 | class EnvironmentAdmin(admin.ModelAdmin):
71 | list_display = ('name', 'cpu', 'memory', 'os', 'kernel')
72 | ordering = ['name']
73 | search_fields = ('name', 'cpu', 'memory', 'os', 'kernel')
74 |
75 |
76 | @admin.register(Result)
77 | class ResultAdmin(admin.ModelAdmin):
78 | list_display = ('revision', 'benchmark', 'executable', 'environment',
79 | 'value', 'date')
80 | list_filter = ('environment', 'executable', 'date', 'benchmark')
81 |
82 |
83 | def recalculate_report(modeladmin, request, queryset):
84 | for report in queryset:
85 | report.save()
86 |
87 |
88 | recalculate_report.short_description = "Recalculate reports"
89 |
90 |
91 | @admin.register(Report)
92 | class ReportAdmin(admin.ModelAdmin):
93 | list_display = ('revision', 'summary', 'colorcode')
94 | list_filter = ('environment', 'executable')
95 | ordering = ['-revision']
96 | actions = [recalculate_report]
97 |
--------------------------------------------------------------------------------
/codespeed/templates/codespeed/changes.html:
--------------------------------------------------------------------------------
1 | {% extends "codespeed/base_site.html" %}
2 |
3 | {% load static %}
4 |
5 | {% block title %}{{ block.super }}: Changes{% endblock %}
6 | {% block description %}{% if pagedesc %}{{ pagedesc }}{% else %}{{ block.super }}{% endif %}{% endblock %}
7 |
8 | {% block navigation %}
9 | {{ block.super }}
10 | {% endblock %}
11 | {% block nav-changes %}class="current">Changes {% endblock %}
12 |
13 | {% block body %}
14 |
58 |
59 |
60 |
Results for revision
61 |
Permalink
62 |
63 |
66 | {% endblock %}
67 |
68 | {% block extra_script %}
69 | {{ block.super }}
70 |
71 |
72 |
92 | {% endblock %}
93 |
--------------------------------------------------------------------------------
/codespeed/auth.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import types
3 | from functools import wraps
4 | from django.contrib.auth import authenticate, login
5 | from django.http import HttpResponse, HttpResponseForbidden
6 | from django.conf import settings
7 | from base64 import b64decode
8 |
9 | __ALL__ = ['basic_auth_required']
10 | logger = logging.getLogger(__name__)
11 |
12 |
13 | def is_authenticated(request):
14 | # NOTE: We do type check so we also support newer versions of Django when
15 | # is_authenticated and some other methods have been properties
16 | if isinstance(request.user.is_authenticated, (types.FunctionType,
17 | types.MethodType)):
18 | return request.user.is_authenticated()
19 | elif isinstance(request.user.is_authenticated, bool):
20 | return request.user.is_authenticated
21 | else:
22 | logger.info('Got unexpected type for request.user.is_authenticated '
23 | 'variable')
24 | return False
25 |
26 |
27 | def basic_auth_required(realm='default'):
28 | def _helper(func):
29 | @wraps(func)
30 | def _decorator(request, *args, **kwargs):
31 | allowed = False
32 | logger.info('request is secure? {}'.format(request.is_secure()))
33 | if settings.ALLOW_ANONYMOUS_POST:
34 | logger.debug('allowing anonymous post')
35 | allowed = True
36 | elif hasattr(request, 'user') and is_authenticated(request=request):
37 | allowed = True
38 | elif 'HTTP_AUTHORIZATION' in request.META:
39 | logger.debug('checking for http authorization header')
40 | if settings.REQUIRE_SECURE_AUTH and not request.is_secure():
41 | return insecure_connection_response()
42 | http_auth = request.META['HTTP_AUTHORIZATION']
43 | authmeth, auth = http_auth.split(' ', 1)
44 | if authmeth.lower() == 'basic':
45 | username, password = decode_basic_auth(auth)
46 | user = authenticate(username=username, password=password)
47 | if user is not None and user.is_active:
48 | logger.info(
49 | 'Authentication succeeded for {}'.format(username))
50 | login(request, user)
51 | allowed = True
52 | else:
53 | logger.info(
54 | 'Failed auth for {}'.format(username))
55 | return HttpResponseForbidden()
56 | if allowed:
57 | return func(request, *args, **kwargs)
58 |
59 | if settings.REQUIRE_SECURE_AUTH and not request.is_secure():
60 | logger.debug('not requesting auth over an insecure channel')
61 | return insecure_connection_response()
62 | else:
63 | res = HttpResponse()
64 | res.status_code = 401
65 | res.reason_phrase = 'Unauthorized'
66 | res['WWW-Authenticate'] = 'Basic realm="{}"'.format(realm)
67 | return res
68 | return _decorator
69 |
70 | return _helper
71 |
72 |
73 | def insecure_connection_response():
74 | return HttpResponseForbidden('Secure connection required')
75 |
76 |
77 | def decode_basic_auth(auth):
78 | authb = b64decode(auth.strip())
79 | auth = authb.decode()
80 | return auth.split(':', 1)
81 |
--------------------------------------------------------------------------------
/codespeed/commits/git.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import logging
3 | import os
4 |
5 | from subprocess import Popen, PIPE
6 | from django.conf import settings
7 | from .exceptions import CommitLogError
8 |
9 | logger = logging.getLogger(__name__)
10 |
11 |
12 | def execute_command(cmd, cwd):
13 | p = Popen(cmd, stdout=PIPE, stderr=PIPE, cwd=cwd)
14 | stdout, stderr = p.communicate()
15 | stdout = stdout.decode('utf8') if stdout is not None else stdout
16 | stderr = stderr.decode('utf8') if stderr is not None else stderr
17 | return (p, stdout, stderr)
18 |
19 |
20 | def updaterepo(project, update=True):
21 | if os.path.exists(project.working_copy):
22 | if not update:
23 | return
24 |
25 | p, _, stderr = execute_command(['git', 'pull'], cwd=project.working_copy)
26 |
27 | if p.returncode != 0:
28 | raise CommitLogError("git pull returned %s: %s" % (p.returncode,
29 | stderr))
30 | else:
31 | return [{'error': False}]
32 | else:
33 | cmd = ['git', 'clone', project.repo_path, project.repo_name]
34 | p, stdout, stderr = execute_command(cmd, settings.REPOSITORY_BASE_PATH)
35 | logger.debug('Cloning Git repo {0} for project {1}'.format(
36 | project.repo_path, project))
37 |
38 | if p.returncode != 0:
39 | raise CommitLogError("%s returned %s: %s" % (
40 | " ".join(cmd), p.returncode, stderr))
41 | else:
42 | return [{'error': False}]
43 |
44 |
45 | def getlogs(endrev, startrev):
46 | updaterepo(endrev.branch.project, update=False)
47 |
48 | # NULL separated values delimited by 0x1e record separators
49 | # See PRETTY FORMATS in git-log(1):
50 | if hasattr(settings, 'GIT_USE_COMMIT_DATE') and settings.GIT_USE_COMMIT_DATE:
51 | logfmt = '--format=format:%h%x00%H%x00%ct%x00%an%x00%ae%x00%s%x00%b%x1e'
52 | else:
53 | logfmt = '--format=format:%h%x00%H%x00%at%x00%an%x00%ae%x00%s%x00%b%x1e'
54 |
55 | cmd = ["git", "log", logfmt]
56 |
57 | if endrev.commitid != startrev.commitid:
58 | cmd.append("%s...%s" % (startrev.commitid, endrev.commitid))
59 | else:
60 | cmd.append("-1") # Only return one commit
61 | cmd.append(endrev.commitid)
62 |
63 | working_copy = endrev.branch.project.working_copy
64 | p, stdout, stderr = execute_command(cmd, working_copy)
65 |
66 | if p.returncode != 0:
67 | raise CommitLogError("%s returned %s: %s" % (
68 | " ".join(cmd), p.returncode, stderr))
69 | logs = []
70 | for log in filter(None, stdout.split('\x1e')):
71 | (short_commit_id, commit_id, date_t, author_name, author_email,
72 | subject, body) = map(lambda s: s.strip(), log.split('\x00', 7))
73 |
74 | cmd = ["git", "tag", "--points-at", commit_id]
75 |
76 | try:
77 | p, stdout, stderr = execute_command(cmd, working_copy)
78 | except Exception:
79 | logger.debug('Failed to get tag', exc_info=True)
80 |
81 | tag = stdout.strip() if p.returncode == 0 else ""
82 | date = datetime.datetime.fromtimestamp(
83 | int(date_t)).strftime("%Y-%m-%d %H:%M:%S")
84 |
85 | logs.append({
86 | 'date': date,
87 | 'message': subject,
88 | 'commitid': commit_id,
89 | 'author': author_name,
90 | 'author_email': author_email,
91 | 'body': body,
92 | 'short_commit_id': short_commit_id,
93 | 'tag': tag
94 | })
95 |
96 | return logs
97 |
--------------------------------------------------------------------------------
/documentation/intro.wiki:
--------------------------------------------------------------------------------
1 | = Introduction to Codespeed =
2 |
3 | == Concepts in Codespeed ==
4 |
5 | === Projects, revisions, branches and tags ===
6 |
7 | A **project** is a software project that has a version control repository. Within that repository will be multiple **branch**es, each of which contains a sequence of **revision**s.
8 |
9 | An **executable** is a compilation of a **project**, which benchmarks are run against. The concept of executables historically comes from testing multiple implementations of Python - CPython, PyPy, etc. In that context, there were multiple executables that had been compiled from different projects (cpython/pypy), with different versions (cpython 2.6.2, pypy 1.3, 1.4, 1.5, latest) and different compilation options (cpython can optionally use psyco-profile, pypy can optionally use a jit). Each of these executables were meant to be interchangeable...
10 |
11 | Certain **revision**s can optionally be given a **tag**. They then make the results from the executables of their project available as baselines to compare other results against on the timeline and comparison screens.
12 |
13 | === Benchmarks ===
14 |
15 | A **benchmark** is a particular test that returns a metric on performance. The benchmark specifies the units of that metric, and whether //less is better// (e.g. for time, a lower number of seconds is usually optimal), or not (e.g. for throughput). Benchmarks come in two kinds: //cross-project// and //own-project//. Only cross-project benchmarks are shown on the comparison screen.
16 |
17 | === Environment ===
18 |
19 | An **environment** is a particular context in which tests are executed - a machine with a given CPU, operating system, and speed. Recording this is significant to ensure that comparison between results is meaningful.
20 |
21 | === Results ===
22 |
23 | The **result**s of running a particular **benchmark** in a particular **environment**, on an **executable** compiled from a particular **revision** of a **project**, are uploaded to the server. Each **result** must have a //value//, and can optionally also have a //minimum//, //maximum//, and //standard deviation//.
24 |
25 | **Result**s are grouped into **report**s that cover all the benchmarks on a particular revision and executable.
26 |
27 | == Screens ==
28 |
29 | === Changes ===
30 |
31 | This screen can be used to monitor the detailed impact a set of revisions has on the benchmark results, as well as browsing those results.
32 |
33 | Only projects that are configured to //track changes// can be selected. Any executable from those projects can be chosen, and a list of recent revisions with benchmark results is presented for selection.
34 |
35 | The benchmark results are presented along with the change from the previous set of results, revision info and the commit logs since the previous revision
36 |
37 | === Timeline ===
38 |
39 | This shows how benchmark results have varied over time. Only results from the default project are shown, but they can be normalized against any of the valid tagged baseline revisions from any project.
40 |
41 | === Comparison ===
42 |
43 | This compares results across executables (and therefore projects). Possible executables are the latest version on each branch of each project (including the default), plus any tagged baseline revisions.
44 |
45 | Benchmarks are grouped by units. Only benchmarks marked as //cross-project// are shown.
46 |
47 | Users can select a subset of executables, benchmarks and environments from those available. There are also some useful statistical options to normalize against a particular executable (including the latest) as baseline, and display the charts as stacked bars or relative bars.
48 |
49 |
--------------------------------------------------------------------------------
/codespeed/commits/mercurial.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import
3 |
4 | import os
5 | import datetime
6 | from subprocess import Popen, PIPE
7 | import logging
8 |
9 | from django.conf import settings
10 |
11 | from .exceptions import CommitLogError
12 |
13 | logger = logging.getLogger(__name__)
14 |
15 |
16 | def updaterepo(project, update=True):
17 | if os.path.exists(project.working_copy):
18 | if not update:
19 | return
20 |
21 | p = Popen(['hg', 'pull', '-u'], stdout=PIPE, stderr=PIPE,
22 | cwd=project.working_copy)
23 | stdout, stderr = p.communicate()
24 |
25 | if p.returncode != 0 or stderr:
26 | raise CommitLogError("hg pull returned %s: %s" % (p.returncode,
27 | stderr))
28 | else:
29 | return [{'error': False}]
30 | else:
31 | # Clone repo
32 | cmd = ['hg', 'clone', project.repo_path, project.repo_name]
33 |
34 | p = Popen(cmd, stdout=PIPE, stderr=PIPE,
35 | cwd=settings.REPOSITORY_BASE_PATH)
36 | logger.debug('Cloning Mercurial repo {0} for project {1}'.format(
37 | project.repo_path, project))
38 | stdout, stderr = p.communicate()
39 |
40 | if p.returncode != 0:
41 | raise CommitLogError("%s returned %s: %s" % (" ".join(cmd),
42 | p.returncode,
43 | stderr))
44 | else:
45 | return [{'error': False}]
46 |
47 |
48 | def getlogs(endrev, startrev):
49 | updaterepo(endrev.branch.project, update=False)
50 |
51 | cmd = [
52 | "hg", "log",
53 | "-r", "%s::%s" % (startrev.commitid, endrev.commitid),
54 | "--template",
55 | ("{rev}:{node|short}\n{node}\n{author|user}\n{author|email}"
56 | "\n{date}\n{tags}\n{desc}\n=newlog=\n")
57 | ]
58 |
59 | working_copy = endrev.branch.project.working_copy
60 | p = Popen(cmd, stdout=PIPE, stderr=PIPE, cwd=working_copy)
61 | stdout, stderr = p.communicate()
62 |
63 | if p.returncode != 0:
64 | raise CommitLogError(str(stderr))
65 | else:
66 | stdout = stdout.rstrip('\n') # Remove last newline
67 | logs = []
68 | for log in stdout.split("=newlog=\n"):
69 | elements = []
70 | elements = log.split('\n')[:-1]
71 | if len(elements) < 7:
72 | # "Malformed" log
73 | logs.append({
74 | 'date': '-', 'message': 'error parsing log', 'commitid': '-'})
75 | else:
76 | short_commit_id = elements.pop(0)
77 | commit_id = elements.pop(0)
78 | author_name = elements.pop(0)
79 | author_email = elements.pop(0)
80 | date = elements.pop(0)
81 | tag = elements.pop(0)
82 | tag = "" if tag == "tip" else tag
83 | # All other newlines should belong to the description text. Join.
84 | message = '\n'.join(elements)
85 |
86 | # Parse date
87 | date = date.split('-')[0]
88 | date = datetime.datetime.fromtimestamp(
89 | float(date)).strftime("%Y-%m-%d %H:%M:%S")
90 |
91 | # Add changeset info
92 | logs.append({
93 | 'date': date,
94 | 'author': author_name,
95 | 'author_email': author_email,
96 | 'message': message,
97 | 'short_commit_id': short_commit_id,
98 | 'commitid': commit_id,
99 | 'tag': tag
100 | })
101 | # Remove last log here because mercurial saves the short hast as commitid now
102 | if len(logs) > 1 and logs[-1].get('short_commit_id') == startrev.commitid:
103 | logs.pop()
104 | return logs
105 |
--------------------------------------------------------------------------------
/sample_project/client.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | from urlparse import urljoin
4 | import logging
5 | import platform
6 | import urllib
7 | import sys
8 |
9 |
10 | def save_to_speedcenter(url=None, project=None, commitid=None, executable=None,
11 | benchmark=None, result_value=None, **kwargs):
12 | """Save a benchmark result to your speedcenter server
13 |
14 | Mandatory:
15 |
16 | :param url:
17 | Codespeed server endpoint
18 | (e.g. `http://codespeed.example.org/result/add/`)
19 | :param project:
20 | Project name
21 | :param commitid:
22 | VCS identifier
23 | :param executable:
24 | The executable name
25 | :param benchmark:
26 | The name of this particular benchmark
27 | :param float result_value:
28 | The benchmark result
29 |
30 | Optional:
31 |
32 | :param environment:
33 | System description
34 | :param date revision_date:
35 | Optional, default will be either VCS commit, if available, or the
36 | current date
37 | :param date result_date:
38 | Optional
39 | :param float std_dev:
40 | Optional
41 | :param float max:
42 | Optional
43 | :param float min:
44 | Optional
45 | """
46 |
47 | data = {
48 | 'project': project,
49 | 'commitid': commitid,
50 | 'executable': executable,
51 | 'benchmark': benchmark,
52 | 'result_value': result_value,
53 | }
54 |
55 | data.update(kwargs)
56 |
57 | if not data.get('environment', None):
58 | data['environment'] = platform.platform(aliased=True)
59 |
60 | f = urllib.urlopen(url, urllib.urlencode(data))
61 |
62 | response = f.read()
63 | status = f.getcode()
64 |
65 | f.close()
66 |
67 | if status == 202:
68 | logging.debug("Server %s: HTTP %s: %s", url, status, response)
69 | else:
70 | raise IOError("Server %s returned HTTP %s" % (url, status))
71 |
72 |
73 | if __name__ == "__main__":
74 | from optparse import OptionParser
75 |
76 | parser = OptionParser()
77 | parser.add_option("--benchmark")
78 | parser.add_option("--commitid")
79 | parser.add_option("--environment",
80 | help="Use a custom Codespeed environment")
81 | parser.add_option("--executable")
82 | parser.add_option("--max", type="float")
83 | parser.add_option("--min", type="float")
84 | parser.add_option("--project")
85 | parser.add_option("--branch")
86 | parser.add_option("--result-date")
87 | parser.add_option("--result-value", type="float")
88 | parser.add_option("--revision_date")
89 | parser.add_option("--std-dev", type="float")
90 | parser.add_option("--url",
91 | help="URL of your Codespeed server (e.g. http://codespeed.example.org)")
92 |
93 | (options, args) = parser.parse_args()
94 |
95 | if args:
96 | parser.error("All arguments must be provided as command-line options")
97 |
98 | # Yes, the optparse manpage has a snide comment about "required options"
99 | # being gramatically dubious. Yes, it's still wrong about not needing to
100 | # do this.
101 | required = ('url', 'environment', 'project', 'commitid', 'executable',
102 | 'benchmark', 'result_value')
103 |
104 | if not all(getattr(options, i) for i in required):
105 | parser.error("The following parameters must be provided:\n\t%s" % "\n\t".join(
106 | "--%s".replace("_", "-") % i for i in required))
107 |
108 | kwargs = {}
109 | for k, v in options.__dict__.items():
110 | if v is not None:
111 | kwargs[k] = v
112 | kwargs.setdefault('branch', 'default')
113 |
114 | if not kwargs['url'].endswith("/result/add/"):
115 | kwargs['url'] = urljoin(kwargs['url'], '/result/add/')
116 |
117 | try:
118 | save_to_speedcenter(**kwargs)
119 | sys.exit(0)
120 | except StandardError as e:
121 | logging.error("Error saving results: %s", e)
122 | sys.exit(1)
123 |
--------------------------------------------------------------------------------
/codespeed/static/js/jqplot/jqplot.pointLabels.min.js:
--------------------------------------------------------------------------------
1 | !function(t){t.jqplot.PointLabels=function(e){this.show=t.jqplot.config.enablePlugins,this.location="n",this.labelsFromSeries=!1,this.seriesLabelIndex=null,this.labels=[],this._labels=[],this.stackedValue=!1,this.ypadding=6,this.xpadding=6,this.escapeHTML=!0,this.edgeTolerance=-5,this.formatter=t.jqplot.DefaultTickFormatter,this.formatString="",this.hideZeros=!1,this._elems=[],t.extend(!0,this,e)};var e={nw:0,n:1,ne:2,e:3,se:4,s:5,sw:6,w:7},s=["se","s","sw","w","nw","n","ne","e"];t.jqplot.PointLabels.init=function(e,s,i,a,l){var r=t.extend(!0,{},i,a);r.pointLabels=r.pointLabels||{},this.renderer.constructor!==t.jqplot.BarRenderer||"horizontal"!==this.barDirection||r.pointLabels.location||(r.pointLabels.location="e"),this.plugins.pointLabels=new t.jqplot.PointLabels(r.pointLabels),this.plugins.pointLabels.setLabels.call(this)},t.jqplot.PointLabels.prototype.setLabels=function(){var e,s=this.plugins.pointLabels;if(e=null!=s.seriesLabelIndex?s.seriesLabelIndex:this.renderer.constructor===t.jqplot.BarRenderer&&"horizontal"===this.barDirection?this._plotData[0].length<3?0:this._plotData[0].length-1:0===this._plotData.length?0:this._plotData[0].length-1,s._labels=[],0===s.labels.length||s.labelsFromSeries)if(s.stackedValue){if(this._plotData.length&&this._plotData[0].length)for(var i=0;ij||L+m>q)&&h.remove(),h=null,p=null}}}},t.jqplot.postSeriesInitHooks.push(t.jqplot.PointLabels.init),t.jqplot.postDrawSeriesHooks.push(t.jqplot.PointLabels.draw)}(jQuery);
--------------------------------------------------------------------------------
/codespeed/templates/codespeed/comparison.html:
--------------------------------------------------------------------------------
1 | {% extends "codespeed/base_site.html" %}
2 |
3 | {% load static %}
4 |
5 | {% block title %}{{ block.super }}: Comparison{% endblock %}
6 |
7 | {% block extra_head %}
8 | {{ block.super }}
9 |
10 | {% endblock %}
11 |
12 | {% block navigation %}
13 | {{ block.super }}
14 | {% endblock %}
15 | {% block nav-comparison %}class="current">Comparison {% endblock %}
16 |
17 | {% block body %}
18 |
57 |
58 |
59 |
Chart type:
60 | {% for chart in charts %}
61 | {{ chart }} {% endfor %}
62 |
63 |
64 |
Normalization:
65 |
66 | None {% for proj, exes in executables.items %}
67 | {% for exe in exes %}
68 | {{ exe.name }} {% endfor %}
69 | {% endfor %}
70 |
71 |
72 |
73 | horizontal
74 |
75 |
76 |
Permalink
77 |
78 |
81 | {% endblock %}
82 |
83 | {% block extra_script %}
84 | {{ block.super }}
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
107 | {% endblock %}
108 |
--------------------------------------------------------------------------------
/codespeed/tests/test_auth.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import mock
4 |
5 | from django.test import TestCase, override_settings
6 | from django.http import HttpResponse
7 | from django.contrib.auth.models import AnonymousUser
8 | from django.test import RequestFactory
9 |
10 | from codespeed.auth import basic_auth_required
11 | from codespeed.views import add_result
12 |
13 |
14 | @override_settings(ALLOW_ANONYMOUS_POST=False)
15 | class AuthModuleTestCase(TestCase):
16 | @override_settings(ALLOW_ANONYMOUS_POST=True)
17 | def test_allow_anonymous_post_is_true(self):
18 | wrapped_function = mock.Mock()
19 | wrapped_function.__name__ = 'mock'
20 | wrapped_function.return_value = 'success'
21 |
22 | request = mock.Mock()
23 | request.user = AnonymousUser()
24 | request.META = {}
25 |
26 | res = basic_auth_required()(wrapped_function)(request=request)
27 | self.assertEqual(wrapped_function.call_count, 1)
28 | self.assertEqual(res, 'success')
29 |
30 | def test_basic_auth_required_django_pre_2_0_succesful_auth(self):
31 | # request.user.is_authenticated is a method (pre Django 2.0)
32 | user = mock.Mock()
33 | user.is_authenticated = lambda: True
34 |
35 | request = mock.Mock()
36 | request.user = user
37 |
38 | wrapped_function = mock.Mock()
39 | wrapped_function.__name__ = 'mock'
40 | wrapped_function.return_value = 'success'
41 |
42 | res = basic_auth_required()(wrapped_function)(request=request)
43 | self.assertEqual(wrapped_function.call_count, 1)
44 | self.assertEqual(res, 'success')
45 |
46 | def test_basic_auth_required_django_pre_2_0_failed_auth(self):
47 | # request.user.is_authenticated is a method (pre Django 2.0)
48 | user = mock.Mock()
49 | user.is_authenticated = lambda: False
50 |
51 | request = mock.Mock()
52 | request.user = user
53 | request.META = {}
54 |
55 | wrapped_function = mock.Mock()
56 | wrapped_function.__name__ = 'mock'
57 |
58 | res = basic_auth_required()(wrapped_function)(request=request)
59 | self.assertTrue(isinstance(res, HttpResponse))
60 | self.assertEqual(res.status_code, 401)
61 | self.assertEqual(wrapped_function.call_count, 0)
62 |
63 | # Also test with actual AnonymousUser class which will have different
64 | # implementation under different Django versions
65 | request.user = AnonymousUser()
66 |
67 | res = basic_auth_required()(wrapped_function)(request=request)
68 | self.assertTrue(isinstance(res, HttpResponse))
69 | self.assertEqual(res.status_code, 401)
70 | self.assertEqual(wrapped_function.call_count, 0)
71 |
72 | def test_basic_auth_required_django_post_2_0_successful_auth(self):
73 | # request.user.is_authenticated is a property (post Django 2.0)
74 | user = mock.Mock()
75 | user.is_authenticated = True
76 |
77 | request = mock.Mock()
78 | request.user = user
79 |
80 | wrapped_function = mock.Mock()
81 | wrapped_function.__name__ = 'mock'
82 | wrapped_function.return_value = 'success'
83 |
84 | res = basic_auth_required()(wrapped_function)(request=request)
85 | self.assertEqual(wrapped_function.call_count, 1)
86 | self.assertEqual(res, 'success')
87 |
88 | def test_basic_auth_required_django_post_2_0_failed_auth(self):
89 | # request.user.is_authenticated is a property (post Django 2.0)
90 | user = mock.Mock()
91 | user.is_authenticated = False
92 |
93 | request = mock.Mock()
94 | request.user = user
95 | request.META = {}
96 |
97 | wrapped_function = mock.Mock()
98 | wrapped_function.__name__ = 'mock'
99 |
100 | res = basic_auth_required()(wrapped_function)(request=request)
101 | self.assertTrue(isinstance(res, HttpResponse))
102 | self.assertEqual(res.status_code, 401)
103 | self.assertEqual(wrapped_function.call_count, 0)
104 |
105 | # Also test with actual AnonymousUser class which will have different
106 | # implementation under different Django versions
107 | request.user = AnonymousUser()
108 |
109 | res = basic_auth_required()(wrapped_function)(request=request)
110 | self.assertTrue(isinstance(res, HttpResponse))
111 | self.assertEqual(res.status_code, 401)
112 | self.assertEqual(wrapped_function.call_count, 0)
113 |
114 | @mock.patch('codespeed.views.save_result', mock.Mock())
115 | def test_basic_auth_with_failed_auth_request_factory(self):
116 | request_factory = RequestFactory()
117 |
118 | request = request_factory.get('/timeline')
119 | request.user = AnonymousUser()
120 | request.method = 'POST'
121 |
122 | response = add_result(request)
123 | self.assertEqual(response.status_code, 403)
124 |
125 | @mock.patch('codespeed.views.create_report_if_enough_data', mock.Mock())
126 | @mock.patch('codespeed.views.save_result', mock.Mock(return_value=([1, 2, 3], None)))
127 | def test_basic_auth_successefull_auth_request_factory(self):
128 | request_factory = RequestFactory()
129 |
130 | user = mock.Mock()
131 | user.is_authenticated = True
132 |
133 | request = request_factory.get('/result/add')
134 | request.user = user
135 | request.method = 'POST'
136 |
137 | response = add_result(request)
138 | self.assertEqual(response.status_code, 202)
139 |
--------------------------------------------------------------------------------
/codespeed/results.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import
3 |
4 | import logging
5 | from datetime import datetime
6 |
7 | from django.core.exceptions import ValidationError
8 |
9 | from .models import (Environment, Project, Branch, Benchmark, Executable,
10 | Revision, Result, Report)
11 | from . import commits
12 |
13 | logger = logging.getLogger(__name__)
14 |
15 |
16 | def validate_result(item):
17 | """
18 | Validates that a result dictionary has all needed parameters
19 |
20 | It returns a tuple
21 | Environment, False when no errors where found
22 | Errormessage, True when there is an error
23 | """
24 | mandatory_data = [
25 | 'commitid',
26 | 'branch',
27 | 'project',
28 | 'executable',
29 | 'benchmark',
30 | 'environment',
31 | 'result_value',
32 | ]
33 |
34 | error = True
35 | for key in mandatory_data:
36 | if key not in item:
37 | return 'Key "' + key + '" missing from request', error
38 | elif key in item and item[key] == "":
39 | return 'Value for key "' + key + '" empty in request', error
40 |
41 | # Check that the Environment exists
42 | try:
43 | e = Environment.objects.get(name=item['environment'])
44 | error = False
45 | return e, error
46 | except Environment.DoesNotExist:
47 | return "Environment %(environment)s not found" % item, error
48 |
49 |
50 | def save_result(data, update_repo=True):
51 | res, error = validate_result(data)
52 | if error:
53 | return res, True
54 | else:
55 | assert(isinstance(res, Environment))
56 | env = res
57 |
58 | p, created = Project.objects.get_or_create(name=data["project"])
59 | branch, created = Branch.objects.get_or_create(name=data["branch"],
60 | project=p)
61 | b, created = Benchmark.objects.get_or_create(name=data["benchmark"])
62 |
63 | if created:
64 | if "description" in data:
65 | b.description = data["description"]
66 | if "units" in data:
67 | b.units = data["units"]
68 | if "units_title" in data:
69 | b.units_title = data["units_title"]
70 | if "lessisbetter" in data:
71 | b.lessisbetter = data["lessisbetter"]
72 | b.full_clean()
73 | b.save()
74 |
75 | try:
76 | rev = branch.revisions.get(commitid=data['commitid'])
77 | except Revision.DoesNotExist:
78 | rev_date = data.get("revision_date")
79 | # "None" (as string) can happen when we urlencode the POST data
80 | if not rev_date or rev_date in ["", "None"]:
81 | rev_date = datetime.today()
82 | rev = Revision(branch=branch, project=p, commitid=data['commitid'],
83 | date=rev_date)
84 | try:
85 | rev.full_clean()
86 | except ValidationError as e:
87 | return str(e), True
88 | if p.repo_type not in ("N", ""):
89 | try:
90 | commit_logs = commits.get_logs(rev, rev, update=update_repo)
91 | except commits.exceptions.CommitLogError as e:
92 | logger.warning("unable to save revision %s info: %s", rev, e,
93 | exc_info=True)
94 | else:
95 | if commit_logs:
96 | log = commit_logs[0]
97 | rev.author = log['author']
98 | rev.date = log['date']
99 | rev.message = log['message']
100 | rev.tag = log['tag']
101 |
102 | rev.save()
103 |
104 | exe, created = Executable.objects.get_or_create(
105 | name=data['executable'],
106 | project=p
107 | )
108 |
109 | try:
110 | r = Result.objects.get(
111 | revision=rev, executable=exe, benchmark=b, environment=env)
112 | except Result.DoesNotExist:
113 | r = Result(revision=rev, executable=exe, benchmark=b, environment=env)
114 |
115 | r.value = data["result_value"]
116 | if 'result_date' in data:
117 | r.date = data["result_date"]
118 | elif rev.date:
119 | r.date = rev.date
120 | else:
121 | r.date = datetime.now()
122 |
123 | r.std_dev = data.get('std_dev')
124 | r.val_min = data.get('min')
125 | r.val_max = data.get('max')
126 | r.q1 = data.get('q1')
127 | r.q3 = data.get('q3')
128 |
129 | r.full_clean()
130 | r.save()
131 |
132 | return (rev, exe, env), False
133 |
134 |
135 | def create_report_if_enough_data(rev, exe, e):
136 | """Triggers Report creation when there are enough results"""
137 | if exe.project.track is not True:
138 | return False
139 |
140 | last_revs = Revision.objects.filter(
141 | branch=rev.branch
142 | ).order_by('-date')[:2]
143 | if len(last_revs) > 1:
144 | current_results = rev.results.filter(executable=exe, environment=e)
145 | last_results = last_revs[1].results.filter(
146 | executable=exe, environment=e)
147 | # If there is are at least as many results as in the last revision,
148 | # create new report
149 | if len(current_results) >= len(last_results):
150 | logger.debug("create_report_if_enough_data: About to create new report")
151 | report, created = Report.objects.get_or_create(
152 | executable=exe, environment=e, revision=rev
153 | )
154 | report.full_clean()
155 | report.save()
156 | logger.debug("Created new report for branch %s and revision %s",
157 | rev.branch, rev.commitid)
158 |
--------------------------------------------------------------------------------
/codespeed/settings.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """Default settings for Codespeed"""
3 |
4 | ## General default options ##
5 | WEBSITE_NAME = "MySpeedSite" # This name will be used in the reports RSS feed
6 |
7 | DEF_ENVIRONMENT = None # Name of the environment which should be selected as default
8 |
9 | DEF_BASELINE = None # Which executable + revision should be default as a baseline
10 | # Given as the name of the executable and commitid of the revision
11 | # Example: DEF_BASELINE = {'executable': 'baseExe', 'revision': '444'}
12 |
13 | TREND = 10 # Default value for the depth of the trend
14 | # Used by reports for the latest runs and changes view
15 |
16 | # Threshold that determines when a performance change over the last result is significant
17 | CHANGE_THRESHOLD = 3.0
18 |
19 | # Threshold that determines when a performance change
20 | # over a number of revisions is significant
21 | TREND_THRESHOLD = 5.0
22 |
23 | ## Home view options ##
24 | SHOW_REPORTS = True # Show report tables
25 | SHOW_HISTORICAL = False # Show historical graphs
26 |
27 | ## Changes view options ##
28 | DEF_EXECUTABLE = None # Executable that should be chosen as default in the changes view
29 | # Given as the name of the executable.
30 | # Example: DEF_EXECUTABLE = "myexe O3 64bits"
31 |
32 | SHOW_AUTHOR_EMAIL_ADDRESS = True # Whether to show the authors email address in the
33 | # changes log
34 |
35 | ## Timeline view options ##
36 | DEF_BENCHMARK = None # Default selected benchmark. Possible values:
37 | # None: will show a grid of plot thumbnails, or a
38 | # text message when the number of plots exceeds 30
39 | # "grid": will always show as default the grid of plots
40 | # "show_none": will show a text message (better
41 | # default when there are lots of benchmarks)
42 | # "mybench": will select benchmark named "mybench"
43 |
44 | DEF_TIMELINE_LIMIT = 50 # Default number of revisions to be plotted
45 | # Possible values 10,50,200,1000
46 |
47 | TIMELINE_GRID_LIMIT = 30 # Number of benchmarks beyond which the timeline view
48 | # is disabled as default setting. Too many benchmarks make
49 | # the view slow, and put load on the database, which may be
50 | # undeseriable.
51 |
52 | TIMELINE_GRID_PAGING = 4 # Number of benchmarks to be send in one grid request
53 | # May be adjusted to improve the performance of the timeline grid view.
54 | # If a large number of benchmarks is in the system,
55 | # and the database is not fast, it can take a long time
56 | # to send all results.
57 |
58 | #TIMELINE_BRANCHES = True # NOTE: Only the default branch is currently shown
59 | # Get timeline results for specific branches
60 | # Set to False if you want timeline plots and results only for trunk.
61 |
62 | ## Comparison view options ##
63 | CHART_TYPE = 'normal bars' # The options are 'normal bars', 'stacked bars' and 'relative bars'
64 |
65 | NORMALIZATION = False # True will enable normalization as the default selection
66 | # in the Comparison view. The default normalization can be
67 | # chosen in the defaultbaseline setting
68 |
69 | CHART_ORIENTATION = 'vertical' # 'vertical' or 'horizontal can be chosen as
70 | # default chart orientation
71 |
72 | COMP_EXECUTABLES = None # Which executable + revision should be checked as default
73 | # Given as a list of tuples containing the
74 | # name of an executable + commitid of a revision
75 | # An 'L' denotes the last revision
76 | # Example:
77 | # COMP_EXECUTABLES = [
78 | # ('myexe', '21df2423ra'),
79 | # ('myexe', 'L'),]
80 |
81 | COMPARISON_COMMIT_TAGS = None # List of tag names which should be included in the executables list
82 | # on the comparision page.
83 | # This comes handy where project contains a lot of tags, but you only want
84 | # to list subset of them on the comparison page.
85 | # If this value is set to None (default value), all the available tags will
86 | # be included.
87 |
88 | TIMELINE_EXECUTABLE_NAME_MAX_LEN = 22 # Maximum length of the executable name used in the
89 | # Changes and Timeline view. If the name is longer, the name
90 | # will be truncated and "..." will be added at the end.
91 |
92 | COMPARISON_EXECUTABLE_NAME_MAX_LEN = 20 # Maximum length of the executable name used in the
93 | # Coomparison view. If the name is longer, the name
94 |
95 | USE_MEDIAN_BANDS = True # True to enable median bands on Timeline view
96 |
97 |
98 | ALLOW_ANONYMOUS_POST = True # Whether anonymous users can post results
99 | REQUIRE_SECURE_AUTH = True # Whether auth needs to be over a secure channel
100 |
101 | US_TZ_AWARE_DATES = False # True to use timezone aware datetime objects with Github provider.
102 | # NOTE: Some database backends may not support tz aware dates.
103 |
104 | GITHUB_OAUTH_TOKEN = None # Github oAuth token to use when using Github repo type. If not
105 | # specified, it will utilize unauthenticated requests which have
106 | # low rate limits.
107 |
--------------------------------------------------------------------------------
/sample_project/README.md:
--------------------------------------------------------------------------------
1 | # Codespeed Example instance
2 |
3 | Codespeed uses the Web framework [Django](http://djangoproject.com/). To get a
4 | Codespeed instance running you need to set up a Django Project. This directory
5 | is just such a project for your reference and a jump start to create your own.
6 |
7 | ## For the impatient
8 |
9 | Warning: It is recommended to use [virtualenv](http://pypi.python.org/pypi/virtualenv) to avoid installing
10 | stuff on the root path of your operating system.
11 | However, it works also this way and might be desired in production
12 | environments.
13 |
14 | ### Testing with the built-in Development Server
15 | That will give you *just* the Django development server version. Please
16 | refer to *Installing for Production* for serious installations.
17 |
18 | It is assumed you are in the root directory of the Codespeed software.
19 |
20 | 1. Install the Python pip module
21 | `which pip >/dev/null || easy_install pip`
22 | (You might be required to use sudo)
23 | 2. You *must* copy the `sample_project` directory to your project. (Prevents updates on
24 | git tracked files in the future.) Let's call it speedcenter
25 | `cp -r sample_project speedcenter`
26 | 3a. (When configuring your own project) `pip install codespeeed`
27 | 3b. (For Codespeed development) Install Django and other dependencies using pip
28 | `pip install -r requirements.txt`. This will not install codespeed itself, as we want runserver to only "see" the local codespeed copy
29 | 4. Add codespeed to your Python path
30 | Either
31 | `export PYTHONPATH=../:$PYTHONPATH`
32 | or
33 | `ln -s ./codespeed ./sample_project`
34 | 5. Apply the migrations:
35 | `python manage.py migrate`
36 | Optionally, you may want to load the fixture data for a try
37 | `python manage.py loaddata ./codespeed/fixtures/testdata.json`
38 | 6. Finally, start the Django development server.
39 | `python manage.py runserver`
40 | 7. Enjoy.
41 | `python -m webbrowser -n http://localhost:8000`
42 |
43 | ## Installing for production
44 | There are many choices to get Django Web apps served. It all depends on
45 | your preferences and existing set up. Two options are shown. Please do
46 | not hesitate to consult a search engine to tune your set-up.
47 |
48 | ### NGINX + GUNICORN: Easy as manage.py runserver
49 | Assumed you have a [Debian](http://www.debian.org) like system.
50 |
51 | 1. Follow the steps from the development server set-up up to the the 6th step (database init).
52 | 2. Install [nginx](http://nginx.net/) and [gunicorn](http://gunicorn.org/)
53 | `sudo apt-get install nginx gunicorn`
54 | 3. Tune /etc/nginx/sites-enabled/default to match
55 | deploy/nginx.default-site.conf
56 | (Hint: See diff /etc/nginx/sites-enabled/default deploy/nginx.default-site.conf
57 | for changes)
58 | Note, the sitestatic dir needs to point to your speedcenter/sitestatic dir!
59 | 4. Restart nginx
60 | /etc/init.d/nginx restart`
61 | 5. Prepare static files
62 | `cd /path/to/speedcenter/`
63 | `python ./manage.py collectstatic`
64 | 6. Add 'gunicorn' to your INSTALLED_APPS in settings.py
65 | INSTALLED_APPS = (
66 | 'django.contrib.auth',
67 | [...]
68 | 'south',
69 | 'gunicorn'
70 | )
71 | 6. Run speedcenter by
72 | `python ./manage.py run_gunicorn`
73 | 7. Check your new speedcenter site! Great! But wait, who runs gunicorn after the
74 | terminal exits?
75 | There are several options like upstart, runit, or supervisor.
76 | Let's go with supervisor:
77 | 1. + to exit gunicorn
78 | 2. `apt-get install supervisor`
79 | 3. `cp deploy/supervisor-speedcenter.conf /etc/supervisor/conf.d/speedcenter.conf`
80 | 4. `$EDITOR /etc/supervisor/conf.d/speedcenter.conf #adjust the path`
81 | 5. `supervisorctl update`
82 | 6. `supervisorctl status`
83 | speedcenter RUNNING pid 2036, uptime 0:00:05
84 | 8. Warning: You may find another way to run gunicorn using `gunicorn_django`. That might
85 | have a shebang of `#!/usr/bin/python` bypassing your virtualenv. Run it out of your
86 | virtualenv by `python $(which gunicorn_django)`
87 |
88 | ### Good old Apache + mod_wsgi
89 | If you don't like surprises and are not into experimenting go with the old work horse.
90 | Assumed you have a [Debian](http://www.debian.org) like system.
91 |
92 | 1. Follow the steps from the development server set-up
93 | 2. Prepare static files
94 | `cd /path/to/speedcenter/`
95 | `python ./manage.py collectstatic`
96 | 3. Install apache and mod_wsgi
97 | `apt-get install apache2 libapache2-mod-wsgi`
98 | 4. Copy deploy/apache-speedcenter.conf
99 | `cp deploy/apache-speedcenter.conf /etc/apache2/sites-available/speedcenter.conf`
100 | 5. Edit /etc/apache2/sites-available/speedcenter.conf to match your needs
101 | 6. Enable the new vhost
102 | `a2ensite speedcenter.conf`
103 | 7. Restart apache
104 | `/etc/init.d/apache2 restart`
105 | 8. Check your new vhost.
106 |
107 | ## Customisations
108 |
109 | ### Using your own Templates
110 | Just edit your very own Django templates in `speedcenter/templates`. A good
111 | start is `codespeed/base.html` the root of all templates.
112 |
113 | If you need to change the codespeed templates:
114 | 1. Copy the templates from the codespeed module into your Django project folder.
115 | `cp -r codespeed/templates/codespeed speedcenter/templates/`
116 | 2. Edit the templates in speedcenter/templates/codespeed/*html
117 | Please, also refer to the [Django template docu]
118 | (http://docs.djangoproject.com/en/1.4/ref/templates/)
119 |
120 | ### Changing the URL Scheme
121 | If you don't want to have your speedcenter in the root url you can change urls.py.
122 | Comment (add a '#' at the beginning) line number 25 `(r'^', include('cod...`
123 | and uncomment the next line `(r'^speed/', include('cod...` (Note, Python is
124 | picky about indentation).
125 | Please, also refer to the [Django URL dispatcher docu]
126 | (http://docs.djangoproject.com/en/1.4/topics/http/urls/).
127 |
128 | ### Codespeed settings
129 | The main config file is `settings.py`. There you configure everything related
130 | to your set up.
131 |
--------------------------------------------------------------------------------
/codespeed/static/js/changes.js:
--------------------------------------------------------------------------------
1 | var Changes = (function(window){
2 |
3 | // Localize globals
4 | var TIMELINE_URL = window.TIMELINE_URL, getLoadText = window.getLoadText;
5 |
6 | var currentproject, changethres, trendthres, projectmatrix, revisionboxes = {};
7 |
8 | function getConfiguration(revision) {
9 | return {
10 | tre: $("#trend option:selected").val(),
11 | rev: revision || $("#revision option:selected").val(),
12 | exe: $("input[name='executable']:checked").val(),
13 | env: $("input[name='environment']:checked").val()
14 | };
15 | }
16 |
17 | function permalinkToTimeline(benchmark, environment) {
18 | window.location=window.TIMELINE_URL + "?ben=" + benchmark + "&env=" + environment;
19 | }
20 |
21 | //colors number based on a threshold
22 | function getColorcode(change, theigh, tlow) {
23 | if (change < tlow) { return "status-red"; }
24 | else if (change > theigh) { return "status-green"; }
25 | else { return "status-node"; }
26 | }
27 |
28 | function colorTable() {
29 | //color two colums to the right starting with index = last-1
30 | // Each because there is one table per units type
31 | $(".tablesorter").each(function() {
32 | // Find column index of the current change column (one before last)
33 | var index = $(this).find("thead tr th").length - 2;
34 | var lessisbetter = $(this).data("lessisbetter");
35 |
36 | $(this).find(":not(thead) > tr").each(function() {
37 | var change = $(this).data("change"),
38 | trend = $(this).data("trend");
39 |
40 | // Check whether less is better
41 | if (lessisbetter === "False") {
42 | change = -change;
43 | trend = -trend;
44 | }
45 | //Color change column
46 | $(this).children("td:eq("+index+")").addClass(getColorcode(-change, changethres, -changethres));
47 | //Color trend column
48 | $(this).children("td:eq("+(index+1)+")").addClass(getColorcode(-trend, trendthres, -trendthres));
49 | });
50 | });
51 | }
52 |
53 | function updateTable() {
54 | colorTable();
55 |
56 | $(".tablesorter > tbody")
57 | //Add permalink events to table rows
58 | .on("click", "tr", function() {
59 | var environment = $("input[name='environment']:checked").val();
60 | permalinkToTimeline($(this).children("td:eq(0)").text(), environment);
61 | })
62 | //Add hover effect to rows
63 | .on("mouseenter mouseleave", "tr", function() {
64 | $(this).toggleClass("highlight");
65 | });
66 |
67 | //Configure table as tablesorter
68 | $(".tablesorter").tablesorter({widgets: ['zebra']});
69 |
70 | // Set prev and next links
71 | $("#previous").click(function() {
72 | refreshContentRev(
73 | $("#previous").data("revision"),
74 | $("#previous").data("desc")
75 | );
76 | });
77 | $("#next").click(function() {
78 | refreshContentRev(
79 | $("#next").data("revision"),
80 | $("#next").data("desc")
81 | );
82 | });
83 | }
84 |
85 | function refreshContent() {
86 | refreshContentTable($("#revision option:selected").val());
87 | }
88 |
89 | function refreshContentRev(revision, desc) {
90 | if ($('#revision option[value='+revision+']').length == 0) {
91 | $("#revision").append($("" + desc + " "));
92 | }
93 | $("#revision").val(revision);
94 | refreshContentTable(revision);
95 | }
96 |
97 | function refreshContentTable(revision) {
98 | var h = $("#content").height();//get height for loading text
99 | $("#contentwrap").fadeOut("fast", function() {
100 | $(this).show();
101 | $(this).html(getLoadText("Loading...", h));
102 | $(this).load("table/", $.param(getConfiguration(revision)), function() { updateTable(); });
103 | });
104 | }
105 |
106 | function changeRevisions() {
107 | // This function repopulates the revision selectbox everytime a new
108 | // executable is selected that corresponds to a different project.
109 | var executable = $("input[name='executable']:checked").val(),
110 | selected_project = projectmatrix[executable];
111 |
112 | if (selected_project !== currentproject) {
113 | $("#revision").html(revisionboxes[selected_project]);
114 | currentproject = selected_project;
115 |
116 | //Give visual cue that the select box has changed
117 | var bgc = $("#revision").parent().parent().css("backgroundColor");
118 | $("#revision").parent()
119 | .animate({ backgroundColor: "#9DADC6" }, 200, function() {
120 | // Animation complete.
121 | $(this).animate({ backgroundColor: bgc }, 1500);
122 | });
123 | }
124 | refreshContent();
125 | }
126 |
127 | function config(c) {
128 | changethres = c.changethres;
129 | trendthres = c.trendthres;
130 | }
131 |
132 | function init(defaults) {
133 | currentproject = defaults.project;
134 | projectmatrix = defaults.projectmatrix;
135 |
136 | $.each(defaults.revisionlists, function(project, revs) {
137 | var options = "";
138 | $.each(revs, function(index, r) {
139 | options += "" + r[0] + " ";
140 | });
141 | revisionboxes[project] = options;
142 | });
143 |
144 | $("#trend").val(defaults.trend);
145 | $("#trend").change(refreshContent);
146 |
147 | $("#executable" + defaults.executable).prop('checked', true);
148 | $("input[name='executable']").change(changeRevisions);
149 |
150 | $("#env" + defaults.environment).prop('checked', true);
151 | $("input[name='environment']").change(refreshContent);
152 |
153 | $("#revision").html(revisionboxes[defaults.project]);
154 | $("#revision").val(defaults.revision);
155 | $("#revision").change(refreshContent);
156 |
157 | $("#permalink").click(function() {
158 | window.location = "?" + $.param(getConfiguration());
159 | });
160 |
161 | refreshContent();
162 | }
163 |
164 | return {
165 | init: init,
166 | config: config
167 | };
168 |
169 | })(window);
170 |
--------------------------------------------------------------------------------
/codespeed/templates/codespeed/timeline.html:
--------------------------------------------------------------------------------
1 | {% extends "codespeed/base_site.html" %}
2 | {% load static %}
3 |
4 | {% block title %}{{ block.super }}: Timeline{% endblock %}
5 | {% block description %}{% if pagedesc %}{{ pagedesc }}{% else %}{{ block.super }}{% endif %}{% endblock %}
6 |
7 | {% block extra_head %}
8 | {{ block.super }}
9 |
10 |
11 | {% endblock %}
12 |
13 | {% block navigation %}
14 | {{ block.super }}
15 | {% endblock %}
16 | {% block nav-timeline %}class="current">Timeline {% endblock %}
17 |
18 | {% block body %}
19 |
78 |
79 |
102 |
106 | {% endblock %}
107 |
108 | {% block extra_script %}
109 | {{ block.super }}
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
122 |
123 |
138 | {% endblock %}
139 |
--------------------------------------------------------------------------------
/tools/pypy/test_saveresults.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import saveresults
3 | import unittest
4 |
5 | class testSaveresults(unittest.TestCase):
6 | '''Tests Saveresults script for saving data to speed.pypy.org'''
7 | fixture = [
8 | ['ai', 'ComparisonResult', {'avg_base': 0.42950453758219992, 'timeline_link': None, 'avg_changed': 0.43322672843939997, 'min_base': 0.42631793022199999, 'delta_min': '1.0065x faster', 'delta_avg': '1.0087x slower', 'std_changed': 0.0094009621054567376, 'min_changed': 0.423564910889, 'delta_std': '2.7513x larger', 'std_base': 0.0034169249420902843, 't_msg': 'Not significant\n'}],
9 | ['chaos', 'ComparisonResult', {'avg_base': 0.41804099082939999, 'timeline_link': None, 'avg_changed': 0.11744904518135998, 'min_base': 0.41700506210299998, 'delta_min': '9.0148x faster', 'delta_avg': '3.5593x faster', 'std_changed': 0.14350186143481433, 'min_changed': 0.046257972717299999, 'delta_std': '108.8162x larger', 'std_base': 0.0013187546718754512, 't_msg': 'Significant (t=4.683672, a=0.95)\n'}],
10 | ['django', 'ComparisonResult', {'avg_base': 0.83651852607739996, 'timeline_link': None, 'avg_changed': 0.48571481704719999, 'min_base': 0.82990884780899998, 'delta_min': '1.7315x faster', 'delta_avg': '1.7222x faster', 'std_changed': 0.006386606999421761, 'min_changed': 0.47929787635799997, 'delta_std': '1.7229x smaller', 'std_base': 0.011003382690633789, 't_msg': 'Significant (t=61.655971, a=0.95)\n'}],
11 | ['fannkuch', 'ComparisonResult', {'avg_base': 1.8561528205879998, 'timeline_link': None, 'avg_changed': 0.38401727676399999, 'min_base': 1.84801197052, 'delta_min': '5.0064x faster', 'delta_avg': '4.8335x faster', 'std_changed': 0.029594360755246251, 'min_changed': 0.36913013458299998, 'delta_std': '3.2353x larger', 'std_base': 0.0091472519207758066, 't_msg': 'Significant (t=106.269998, a=0.95)\n'}],
12 | ['float', 'ComparisonResult', {'avg_base': 0.50523018836940004, 'timeline_link': None, 'avg_changed': 0.15490598678593998, 'min_base': 0.49911379814099999, 'delta_min': '6.2651x faster', 'delta_avg': '3.2615x faster', 'std_changed': 0.057739598339608837, 'min_changed': 0.079665899276699995, 'delta_std': '7.7119x larger', 'std_base': 0.007487037523761327, 't_msg': 'Significant (t=13.454285, a=0.95)\n'}], ['gcbench', 'SimpleComparisonResult', {'base_time': 27.236408948899999, 'changed_time': 5.3500790595999996, 'time_delta': '5.0908x faster'}],
13 | ['html5lib', 'SimpleComparisonResult', {'base_time': 11.666918992999999, 'changed_time': 12.6703209877, 'time_delta': '1.0860x slower'}],
14 | ['richards', 'ComparisonResult', {'avg_base': 0.29083266258220003, 'timeline_link': None, 'avg_changed': 0.029299402236939998, 'min_base': 0.29025602340700002, 'delta_min': '10.7327x faster', 'delta_avg': '9.9262x faster', 'std_changed': 0.0033452973342946888, 'min_changed': 0.027044057846099999, 'delta_std': '5.6668x larger', 'std_base': 0.00059033067516221327, 't_msg': 'Significant (t=172.154488, a=0.95)\n'}],
15 | ['rietveld', 'ComparisonResult', {'avg_base': 0.46909418106079998, 'timeline_link': None, 'avg_changed': 1.312631273269, 'min_base': 0.46490097045899997, 'delta_min': '2.1137x slower', 'delta_avg': '2.7982x slower', 'std_changed': 0.44401595627955542, 'min_changed': 0.98267102241500004, 'delta_std': '76.0238x larger', 'std_base': 0.0058404831974135556, 't_msg': 'Significant (t=-4.247692, a=0.95)\n'}],
16 | ['slowspitfire', 'ComparisonResult', {'avg_base': 0.66740002632140005, 'timeline_link': None, 'avg_changed': 1.6204295635219998, 'min_base': 0.65965509414699997, 'delta_min': '1.9126x slower', 'delta_avg': '2.4280x slower', 'std_changed': 0.27415559151786589, 'min_changed': 1.26167798042, 'delta_std': '20.1860x larger', 'std_base': 0.013581457669479846, 't_msg': 'Significant (t=-7.763579, a=0.95)\n'}],
17 | ['spambayes', 'ComparisonResult', {'avg_base': 0.279049730301, 'timeline_link': None, 'avg_changed': 1.0178018569945999, 'min_base': 0.27623891830399999, 'delta_min': '3.3032x slower', 'delta_avg': '3.6474x slower', 'std_changed': 0.064953583956645466, 'min_changed': 0.91246294975300002, 'delta_std': '28.9417x larger', 'std_base': 0.0022442880892229711, 't_msg': 'Significant (t=-25.416839, a=0.95)\n'}],
18 | ['spectral-norm', 'ComparisonResult', {'avg_base': 0.48315834999099999, 'timeline_link': None, 'avg_changed': 0.066225481033300004, 'min_base': 0.476922035217, 'delta_min': '8.0344x faster', 'delta_avg': '7.2957x faster', 'std_changed': 0.013425108838933627, 'min_changed': 0.059360027313200003, 'delta_std': '1.9393x larger', 'std_base': 0.0069225510731835901, 't_msg': 'Significant (t=61.721418, a=0.95)\n'}],
19 | ['spitfire', 'ComparisonResult', {'avg_base': 7.1179999999999994, 'timeline_link': None, 'avg_changed': 7.2780000000000005, 'min_base': 7.04, 'delta_min': '1.0072x faster', 'delta_avg': '1.0225x slower', 'std_changed': 0.30507376157250898, 'min_changed': 6.9900000000000002, 'delta_std': '3.4948x larger', 'std_base': 0.08729261137118062, 't_msg': 'Not significant\n'}],
20 | ['twisted_iteration', 'SimpleComparisonResult', {'base_time': 0.148289627437, 'changed_time': 0.035354803126799998, 'time_delta': '4.1943x faster'}],
21 | ['twisted_web', 'SimpleComparisonResult', {'base_time': 0.11312217194599999, 'changed_time': 0.625, 'time_delta': '5.5250x slower'}]
22 | ]
23 |
24 | def testGoodInput(self):
25 | '''Given correct result data, check that every result being saved has the right parameters'''
26 | for resultparams in saveresults.save("pypy", 71212, self.fixture, "", "pypy-c-jit", "tannit", True):
27 | self.assertEqual(resultparams['project'], "pypy")
28 | self.assertEqual(resultparams['commitid'], 71212)
29 | self.assertEqual(resultparams['executable'], "pypy-c-jit")
30 | # get dict with correct data for this benchmark
31 | fixturedata = []
32 | benchfound = False
33 | for res in self.fixture:
34 | if res[0] == resultparams['benchmark']:
35 | fixturedata = res
36 | benchfound = True
37 | break
38 | self.assertTrue(benchfound)
39 | # get correct result value depending on the type of result
40 | fixturevalue = 0
41 | if fixturedata[1] == "SimpleComparisonResult":
42 | fixturevalue = fixturedata[2]['changed_time']
43 | else:
44 | fixturevalue = fixturedata[2]['avg_changed']
45 | self.assertEqual(resultparams['result_value'], fixturevalue)
46 |
47 | if __name__ == "__main__":
48 | unittest.main()
49 |
--------------------------------------------------------------------------------
/codespeed/commits/github.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | """
3 | Specialized Git backend which uses Github.com for all of the heavy work
4 |
5 | Among other things, this means that the codespeed server doesn't need to have
6 | git installed, the ability to write files, etc.
7 | """
8 | from __future__ import absolute_import
9 |
10 | import logging
11 | try:
12 | # Python 3
13 | from urllib.request import urlopen
14 | from urllib.request import Request
15 | except ImportError:
16 | # Python 2
17 | from urllib2 import urlopen
18 | from urllib2 import Request
19 | import re
20 | import json
21 |
22 | import isodate
23 | from django.core.cache import cache
24 | from django.conf import settings
25 |
26 | from .exceptions import CommitLogError
27 |
28 | logger = logging.getLogger(__name__)
29 |
30 | GITHUB_URL_RE = re.compile(
31 | r'^(?P\w+)://github.com/(?P[^/]+)/(?P[^/]+)([.]git)?$')
32 |
33 | # We currently use a simple linear search of on a single parent to retrieve
34 | # the history. This is often good enough, but might miss the actual starting
35 | # point. Thus, we need to terminate the search after a resonable number of
36 | # revisions.
37 | GITHUB_REVISION_LIMIT = 10
38 |
39 |
40 | def updaterepo(project, update=True):
41 | return
42 |
43 |
44 | def fetch_json(url):
45 | json_obj = cache.get(url)
46 |
47 | if json_obj is None:
48 | github_oauth_token = getattr(settings, 'GITHUB_OAUTH_TOKEN', None)
49 |
50 | if github_oauth_token:
51 | headers = {'Authorization': 'token %s' % (github_oauth_token)}
52 | else:
53 | headers = {}
54 |
55 | request = Request(url=url, headers=headers)
56 |
57 | try:
58 | json_obj = json.load(urlopen(request))
59 | except IOError as e:
60 | logger.exception("Unable to load %s: %s",
61 | url, e, exc_info=True)
62 | raise e
63 |
64 | if "message" in json_obj and \
65 | json_obj["message"] in ("Not Found", "Server Error",):
66 | # We'll still cache these for a brief period of time to avoid
67 | # making too many requests:
68 | cache.set(url, json_obj, 300)
69 | else:
70 | # We'll cache successes for a very long period of time since
71 | # SCM diffs shouldn't change:
72 | cache.set(url, json_obj, 86400 * 30)
73 |
74 | if "message" in json_obj and \
75 | json_obj["message"] in ("Not Found", "Server Error",):
76 | raise CommitLogError(
77 | "Unable to load %s: %s" % (url, json_obj["message"]))
78 |
79 | return json_obj
80 |
81 |
82 | def retrieve_tag(commit_id, username, project):
83 | tags_url = 'https://api.github.com/repos/%s/%s/git/refs/tags' % (
84 | username, project)
85 |
86 | tags_json = fetch_json(tags_url)
87 | for tag in tags_json:
88 | if tag['object']['sha'] == commit_id:
89 | return tag['ref'].split("refs/tags/")[-1]
90 |
91 | return ""
92 |
93 |
94 | def retrieve_revision(commit_id, username, project, revision=None):
95 | commit_url = 'https://api.github.com/repos/%s/%s/git/commits/%s' % (
96 | username, project, commit_id)
97 |
98 | commit_json = fetch_json(commit_url)
99 |
100 | date = isodate.parse_datetime(commit_json['committer']['date'])
101 | tag = retrieve_tag(commit_id, username, project)
102 |
103 | if revision:
104 | # Overwrite any existing data we might have for this revision since
105 | # we never want our records to be out of sync with the actual VCS:
106 | if not getattr(settings, 'USE_TZ_AWARE_DATES', False):
107 | # We need to convert the timezone-aware date to a naive (i.e.
108 | # timezone-less) date in UTC to avoid killing MySQL:
109 | logger.debug('USE_TZ_AWARE_DATES setting is set to False, '
110 | 'converting datetime object to a naive one')
111 | revision.date = date.astimezone(
112 | isodate.tzinfo.Utc()).replace(tzinfo=None)
113 | revision.author = commit_json['author']['name']
114 | revision.message = commit_json['message']
115 | revision.full_clean()
116 | revision.save()
117 |
118 | return {'date': date,
119 | 'message': commit_json['message'],
120 | 'body': "", # TODO: pretty-print diffs
121 | 'author': commit_json['author']['name'],
122 | 'author_email': commit_json['author']['email'],
123 | 'commitid': commit_json['sha'],
124 | 'short_commit_id': commit_json['sha'][0:7],
125 | 'parents': commit_json['parents'],
126 | 'tag': tag}
127 |
128 |
129 | def getlogs(endrev, startrev):
130 | if endrev != startrev:
131 | revisions = endrev.branch.revisions.filter(
132 | date__lte=endrev.date, date__gte=startrev.date)
133 | else:
134 | revisions = [i for i in (startrev, endrev) if i.commitid]
135 |
136 | if endrev.branch.project.repo_path[-1] == '/':
137 | endrev.branch.project.repo_path = endrev.branch.project.repo_path[:-1]
138 |
139 | m = GITHUB_URL_RE.match(endrev.branch.project.repo_path)
140 |
141 | if not m:
142 | raise ValueError(
143 | "Unable to parse Github URL %s" % endrev.branch.project.repo_path)
144 |
145 | username = m.group("username")
146 | project = m.group("project")
147 |
148 | logs = []
149 | last_rev_data = None
150 | revision_count = 0
151 | ancestor_found = False
152 | # TODO: get all revisions between endrev and startrev,
153 | # not only those present in the Codespeed DB
154 |
155 | for revision in revisions:
156 | last_rev_data = retrieve_revision(
157 | revision.commitid, username, project, revision)
158 | logs.append(last_rev_data)
159 | revision_count += 1
160 | ancestor_found = (
161 | startrev.commitid in [
162 | rev['sha'] for rev in last_rev_data['parents']])
163 |
164 | # Simple approach to find the startrev, stop after found or after
165 | # #GITHUB_REVISION_LIMIT revisions are fetched
166 | while (revision_count < GITHUB_REVISION_LIMIT
167 | and not ancestor_found
168 | and len(last_rev_data['parents']) > 0):
169 | last_rev_data = retrieve_revision(
170 | last_rev_data['parents'][0]['sha'], username, project)
171 | logs.append(last_rev_data)
172 | revision_count += 1
173 | ancestor_found = (
174 | startrev.commitid in [
175 | rev['sha'] for rev in last_rev_data['parents']])
176 |
177 | return sorted(logs, key=lambda i: i['date'], reverse=True)
178 |
--------------------------------------------------------------------------------
/codespeed/static/js/jqplot/jqplot.highlighter.min.js:
--------------------------------------------------------------------------------
1 | !function(t){function i(i,e){var o=i.plugins.highlighter,r=i.series[e.seriesIndex],s=r.markerRenderer,l=o.markerRenderer;l.style=s.style,l.lineWidth=s.lineWidth+o.lineWidthAdjust,l.size=s.size+o.sizeAdjust;var h=t.jqplot.getColorComponents(s.color),a=[h[0],h[1],h[2]],n=h[3]>=.6?.6*h[3]:h[3]*(2-h[3]);l.color="rgba("+a[0]+","+a[1]+","+a[2]+","+n+")",l.init(),l.draw(r.gridData[e.pointIndex][0],r.gridData[e.pointIndex][1],o.highlightCanvas._ctx)}function e(i,e,o){var h=i.plugins.highlighter,a=h._tooltipElem,n=e.highlighter||{},g=t.extend(!0,{},h,n);if(g.useAxesFormatters){for(var p,d=e._xaxis._ticks[0].formatter,f=e._yaxis._ticks[0].formatter,u=e._xaxis._ticks[0].formatString,c=e._yaxis._ticks[0].formatString,v=d(u,o.data[0]),x=[],m=1;ms.max||null==s.max)&&(s.max=h[a][0])):((h[a][1]s.max||null==s.max)&&(s.max=h[a][1]))}this.groupLabels.length&&(this.groups=this.groupLabels.length)},t.jqplot.CategoryAxisRenderer.prototype.createTicks=function(){var e,s,i,r,h,a=(this._ticks,this.ticks),n=this.name;this._dataBounds;if(a.length){if(this.groups>1&&!this._grouped){for(var l=a.length,o=parseInt(l/this.groups,10),p=0,h=o;l>h;h+=o)a.splice(h+p,0," "),p++;this._grouped=!0}this.min=.5,this.max=a.length+.5;var u=this.max-this.min;for(this.numberTicks=2*a.length+1,h=0;h1&&!this._grouped){for(var l=g.length,o=parseInt(l/this.groups,10),p=0,h=o;l>h;h+=o+1)g[h]=" ";this._grouped=!0}i=x+.5,null==this.numberTicks&&(this.numberTicks=2*x+1);var u=i-s;this.min=s,this.max=i;var k=0,v=parseInt(3+e/10,10),o=parseInt(x/v,10);null==this.tickInterval&&(this.tickInterval=u/(this.numberTicks-1));for(var h=0;h0&&o>k?(_.showLabel=!1,k+=1):(_.showLabel=!0,k=0),_.label=_.formatter(_.formatString,g[(h-1)/2]),_.showMark=!1,_.showGridline=!1),_.setTick(r,this.name),this._ticks.push(_)}}},t.jqplot.CategoryAxisRenderer.prototype.draw=function(e,s){if(this.show){this.renderer.createTicks.call(this);if(this._elem&&this._elem.emptyForce(),this._elem=this._elem||t('
'),"xaxis"==this.name||"x2axis"==this.name?this._elem.width(this._plotDimensions.width):this._elem.height(this._plotDimensions.height),this.labelOptions.axis=this.name,this._label=new this.labelRenderer(this.labelOptions),this._label.show){var i=this._label.draw(e,s);i.appendTo(this._elem)}for(var r=this._ticks,h=0;h');i.html(this.groupLabels[h]),this._groupLabels.push(i),i.appendTo(this._elem)}}return this._elem},t.jqplot.CategoryAxisRenderer.prototype.set=function(){var e,s=0,i=0,r=0,h=null==this._label?!1:this._label.show;if(this.show){for(var a=this._ticks,n=0;ns&&(s=e))}for(var o=0,n=0;no&&(o=e)}h&&(i=this._label._elem.outerWidth(!0),r=this._label._elem.outerHeight(!0)),"xaxis"==this.name?(s+=o+r,this._elem.css({height:s+"px",left:"0px",bottom:"0px"})):"x2axis"==this.name?(s+=o+r,this._elem.css({height:s+"px",left:"0px",top:"0px"})):"yaxis"==this.name?(s+=o+i,this._elem.css({width:s+"px",left:"0px",top:"0px"}),h&&this._label.constructor==t.jqplot.AxisLabelRenderer&&this._label._elem.css("width",i+"px")):(s+=o+i,this._elem.css({width:s+"px",right:"0px",top:"0px"}),h&&this._label.constructor==t.jqplot.AxisLabelRenderer&&this._label._elem.css("width",i+"px"))}},t.jqplot.CategoryAxisRenderer.prototype.pack=function(e,s){var i,r=this._ticks,h=this.max,a=this.min,n=s.max,l=s.min,o=null==this._label?!1:this._label.show;for(var p in e)this._elem.css(p,e[p]);this._offsets=s;var u=n-l,_=h-a;if(this.reverse?(this.u2p=function(t){return l+(h-t)*u/_},this.p2u=function(t){return a+(t-l)*_/u},"xaxis"==this.name||"x2axis"==this.name?(this.series_u2p=function(t){return(h-t)*u/_},this.series_p2u=function(t){return t*_/u+h}):(this.series_u2p=function(t){return(a-t)*u/_},this.series_p2u=function(t){return t*_/u+a})):(this.u2p=function(t){return(t-a)*u/_+l},this.p2u=function(t){return(t-l)*_/u+a},"xaxis"==this.name||"x2axis"==this.name?(this.series_u2p=function(t){return(t-a)*u/_},this.series_p2u=function(t){return t*_/u+a}):(this.series_u2p=function(t){return(t-h)*u/_},this.series_p2u=function(t){return t*_/u+h})),this.show)if("xaxis"==this.name||"x2axis"==this.name){for(i=0;iw;w++)if(!(w>=this._ticks.length-1)&&this._ticks[w]._elem&&" "!=this._ticks[w].label){var c=this._ticks[w]._elem,p=c.position();k+=p.left+c.outerWidth(!0)/2,v++}k/=v,this._groupLabels[i].css({left:k-this._groupLabels[i].outerWidth(!0)/2}),this._groupLabels[i].css(d[0],d[1])}}else{for(i=0;i0?-c._textRenderer.height*Math.cos(-c._textRenderer.angle)/2:-c.getHeight()+c._textRenderer.height*Math.cos(c._textRenderer.angle)/2;break;case"middle":g=-c.getHeight()/2;break;default:g=-c.getHeight()/2}}else g=-c.getHeight()/2;var m=this.u2p(c.value)+g+"px";c._elem.css("top",m),c.pack()}}var d=["left",0];if(o){var R=this._label._elem.outerHeight(!0);this._label._elem.css("top",n-u/2-R/2+"px"),"yaxis"==this.name?(this._label._elem.css("left","0px"),d=["left",this._label._elem.outerWidth(!0)]):(this._label._elem.css("right","0px"),d=["right",this._label._elem.outerWidth(!0)]),this._label.pack()}var f=parseInt(this._ticks.length/this.groups,10)+1;for(i=0;iw;w++)if(!(w>=this._ticks.length-1)&&this._ticks[w]._elem&&" "!=this._ticks[w].label){var c=this._ticks[w]._elem,p=c.position();k+=p.top+c.outerHeight()/2,v++}k/=v,this._groupLabels[i].css({top:k-this._groupLabels[i].outerHeight()/2}),this._groupLabels[i].css(d[0],d[1])}}}}(jQuery);
--------------------------------------------------------------------------------
/codespeed/static/js/jqplot/jqplot.dateAxisRenderer.min.js:
--------------------------------------------------------------------------------
1 | !function(t){function i(t,i,e){for(var s,a,n,r=Number.MAX_VALUE,h=0,l=m.length;l>h;h++)s=Math.abs(e-m[h]),r>s&&(r=s,a=m[h],n=o[h]);return[a,n]}t.jqplot.DateAxisRenderer=function(){t.jqplot.LinearAxisRenderer.call(this),this.date=new t.jsDate};var e=1e3,s=60*e,a=60*s,n=24*a,r=7*n,h=30.4368499*n,l=365.242199*n,o=["%M:%S.%#N","%M:%S.%#N","%M:%S.%#N","%M:%S","%M:%S","%M:%S","%M:%S","%H:%M:%S","%H:%M:%S","%H:%M","%H:%M","%H:%M","%H:%M","%H:%M","%H:%M","%a %H:%M","%a %H:%M","%b %e %H:%M","%b %e %H:%M","%b %e %H:%M","%b %e %H:%M","%v","%v","%v","%v","%v","%v","%v"],m=[.1*e,.2*e,.5*e,e,2*e,5*e,10*e,15*e,30*e,s,2*s,5*s,10*s,15*s,30*s,a,2*a,4*a,6*a,8*a,12*a,n,2*n,3*n,4*n,5*n,r,2*r];t.jqplot.DateAxisRenderer.prototype=new t.jqplot.LinearAxisRenderer,t.jqplot.DateAxisRenderer.prototype.constructor=t.jqplot.DateAxisRenderer,t.jqplot.DateTickFormatter=function(i,e){return i||(i="%Y/%m/%d"),t.jsDate.strftime(e,i)},t.jqplot.DateAxisRenderer.prototype.init=function(i){this.tickOptions.formatter=t.jqplot.DateTickFormatter,this.tickInset=0,this.drawBaseline=!0,this.baselineWidth=null,this.baselineColor=null,this.daTickInterval=null,this._daTickInterval=null,t.extend(!0,this,i);for(var e,s,a,n,r,h,l,o=this._dataBounds,m=0;mo.max||null==o.max)&&(o.max=n[c][0]),c>0&&(l=Math.abs(n[c][0]-n[c-1][0]),e.intervals.push(l),e.frequencies.hasOwnProperty(l)?e.frequencies[l]+=1:e.frequencies[l]=1),s+=l):(n[c][1]=new t.jsDate(n[c][1]).getTime(),r[c][1]=new t.jsDate(n[c][1]).getTime(),h[c][1]=new t.jsDate(n[c][1]).getTime(),(null!=n[c][1]&&n[c][1]o.max||null==o.max)&&(o.max=n[c][1]),c>0&&(l=Math.abs(n[c][1]-n[c-1][1]),e.intervals.push(l),e.frequencies.hasOwnProperty(l)?e.frequencies[l]+=1:e.frequencies[l]=1)),s+=l;if(a.renderer.bands){if(a.renderer.bands.hiData.length)for(var u=a.renderer.bands.hiData,c=0,k=u.length;k>c;c++)"xaxis"===this.name||"x2axis"===this.name?(u[c][0]=new t.jsDate(u[c][0]).getTime(),(null!=u[c][0]&&u[c][0]>o.max||null==o.max)&&(o.max=u[c][0])):(u[c][1]=new t.jsDate(u[c][1]).getTime(),(null!=u[c][1]&&u[c][1]>o.max||null==o.max)&&(o.max=u[c][1]));if(a.renderer.bands.lowData.length)for(var u=a.renderer.bands.lowData,c=0,k=u.length;k>c;c++)"xaxis"===this.name||"x2axis"===this.name?(u[c][0]=new t.jsDate(u[c][0]).getTime(),(null!=u[c][0]&&u[c][0]=j){var F=i(s,a,j),y=F[0];this._autoFormatString=F[1],s=new t.jsDate(s),s=Math.floor((s.getTime()-s.getUtcOffset())/y)*y+s.getUtcOffset(),b=Math.ceil((a-s)/y)+1,this.min=s,this.max=s+(b-1)*y,this.maxo;o++)I.value=this.min+o*y,M=new this.tickRenderer(I),this._overrideFormatString&&""!=this._autoFormatString&&(M.formatString=this._autoFormatString),this.showTicks?this.showTickMarks||(M.showMark=!1):(M.showLabel=!1,M.showMark=!1),this._ticks.push(M);T=this.tickInterval}else if(9*h>=j){this._autoFormatString="%v";var H=Math.round(j/h);1>H?H=1:H>6&&(H=6);var R=new t.jsDate(s).setDate(1).setHours(0,0,0,0),O=new t.jsDate(a),A=new t.jsDate(a).setDate(1).setHours(0,0,0,0);O.getTime()!==A.getTime()&&(A=A.add(1,"month"));var L=A.diff(R,"month");b=Math.ceil(L/H)+1,this.min=R.getTime(),this.max=R.clone().add((b-1)*H,"month").getTime(),this.numberTicks=b;for(var o=0;b>o;o++)0===o?I.value=R.getTime():I.value=R.add(H,"month").getTime(),M=new this.tickRenderer(I),this._overrideFormatString&&""!=this._autoFormatString&&(M.formatString=this._autoFormatString),this.showTicks?this.showTickMarks||(M.showMark=!1):(M.showLabel=!1,M.showMark=!1),this._ticks.push(M);T=H*h}else{this._autoFormatString="%v";var H=Math.round(j/l);1>H&&(H=1);var R=new t.jsDate(s).setMonth(0,1).setHours(0,0,0,0),A=new t.jsDate(a).add(1,"year").setMonth(0,1).setHours(0,0,0,0),N=A.diff(R,"year");b=Math.ceil(N/H)+1,this.min=R.getTime(),this.max=R.clone().add((b-1)*H,"year").getTime(),this.numberTicks=b;for(var o=0;b>o;o++)0===o?I.value=R.getTime():I.value=R.add(H,"year").getTime(),M=new this.tickRenderer(I),this._overrideFormatString&&""!=this._autoFormatString&&(M.formatString=this._autoFormatString),this.showTicks?this.showTickMarks||(M.showMark=!1):(M.showLabel=!1,M.showMark=!1),this._ticks.push(M);T=H*l}}else{if(v="xaxis"==u||"x2axis"==u?this._plotDimensions.width:this._plotDimensions.height,null!=this.min&&null!=this.max&&null!=this.numberTicks&&(this.tickInterval=null),null!=this.tickInterval&&null!=g&&(this.daTickInterval=g),s==a){var z=432e5;s-=z,a+=z}f=a-s;var B,P;2+parseInt(Math.max(0,v-100)/100,10);if(B=null!=this.min?new t.jsDate(this.min).getTime():s-f/2*(this.padMin-1),P=null!=this.max?new t.jsDate(this.max).getTime():a+f/2*(this.padMax-1),this.min=B,this.max=P,f=this.max-this.min,null==this.numberTicks)if(null!=this.daTickInterval){var U=new t.jsDate(this.max).diff(this.min,this.daTickInterval[1],!0);this.numberTicks=Math.ceil(U/this.daTickInterval[0])+1,this.max=new t.jsDate(this.min).add((this.numberTicks-1)*this.daTickInterval[0],this.daTickInterval[1]).getTime()}else v>200?this.numberTicks=parseInt(3+(v-200)/100,10):this.numberTicks=2;T=f/(this.numberTicks-1)/1e3,null==this.daTickInterval&&(this.daTickInterval=[T,"seconds"]);for(var o=0;o"+l.title.replace(/\'/g,"\\'")+"
49 |
50 |
51 |
52 |
53 |
54 |
55 | {% endif %}
56 |
57 |
216 | {% endblock %}
217 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Codespeed
2 | [](https://travis-ci.org/tobami/codespeed)
3 | [](https://pypi.python.org/pypi/codespeed)
4 |
5 | Codespeed is a web application to monitor and analyze the performance of your code.
6 |
7 | Known to be used by [CPython](https://speed.python.org), [PyPy](http://speed.pypy.org), [Twisted](http://speed.twistedmatrix.com) and others.
8 |
9 | For an overview of some application concepts see the [wiki page](https://github.com/tobami/codespeed/wiki/Overview)
10 |
11 | ## Installation
12 |
13 | You will need Python 2.7 or 3.5+.
14 |
15 | To install dependencies and the codespeed Django app:
16 |
17 | pip install codespeed
18 |
19 | If you want version control integration, there are additional requirements:
20 |
21 | * Subversion needs pysvn: `python-svn`
22 | * Mercurial needs the package `mercurial` to clone the repo locally
23 | * git needs the `git` package to clone the repo
24 | * For Github the isodate package is required, but not git: `pip install isodate`
25 |
26 | **Note**: For git or mercurial repos, the first time the changes view is accessed,
27 | Codespeed will try to clone the repo, which depending on the size of the project
28 | can take a long time. Please be patient.
29 |
30 | * Download the last stable release from
31 | [github.com/tobami/codespeed/tags](https://github.com/tobami/codespeed/tags), unpack it and install it with `python setup.py install`.
32 | * To get started, you can use the `sample_project` directory as a starting point for your Django project, which can be normally configured by editing `sample_project/settings.py`.
33 | * For simplicity, you can use the default sqlite configuration, which will save
34 | the data to a database named `data.db`
35 | * Create the DB by typing from the root directory:
36 |
37 | python manage.py migrate
38 |
39 | * Create an admin user:
40 |
41 | python manage.py createsuperuser
42 |
43 | * For testing purposes, you can now start the development server:
44 |
45 | python manage.py runserver 8000
46 |
47 | The codespeed installation can now be accessed by navigating to `http://localhost:8000/`.
48 |
49 | **Note**: for production, you should configure a real server like Apache or nginx (refer to the [Django docs](http://docs.djangoproject.com/en/dev/howto/deployment/)). You should also
50 | modify `sample_project/settings.py` and set `DEBUG = False`.
51 | [`sample_project/README.md`](https://github.com/tobami/codespeed/tree/master/sample_project/README.md) also describes some production settings.
52 |
53 | ## Codespeed configuration
54 |
55 | ### Using the provided test data
56 |
57 | If you want to test drive Codespeed, you can use the testdata.json fixtures to have a working data set to browse.
58 |
59 | * From the root directory, type:
60 |
61 | ./manage.py loaddata codespeed/fixtures/testdata.json
62 |
63 | ### Starting from scratch
64 |
65 | Before you can start saving (and displaying) data, you need to first create an
66 | environment and define a default project.
67 |
68 | * Go to `http://localhost:8000/admin/codespeed/environment/`
69 | and create an environment.
70 | * Go to `http://localhost:8000/admin/codespeed/project/`
71 | and create a project.
72 |
73 | Check the field "Track changes" and, in case you want version control
74 | integration, configure the relevant fields.
75 |
76 | **Note**: Only executables associated to projects with a checked "track changes"
77 | field will be shown in the Changes and Timeline views.
78 |
79 | **Note**: Git and Mercurial need to locally clone the repository. That means that your `sample_project/repos` directory will need to be owned by the server. In the case of a typical Apache installation, you'll need to type `sudo chown www-data:www-data sample_project/repos`
80 |
81 | ## Saving data
82 |
83 | Data is saved POSTing to `http://localhost:8000/result/add/`.
84 |
85 | You can use the script `tools/save_single_result.py` as a guide.
86 | When saving large quantities of data, it is recommended to use the JSON API instead:
87 | `http://localhost:8000/result/add/json/`
88 |
89 | An example script is located at `tools/save_multiple_results.py`
90 |
91 | **Note**: If the given executable, benchmark, project, or
92 | revision do not yet exist, they will be automatically created, together with the
93 | actual result entry. The only model which won't be created automatically is the
94 | environment. It must always exist or the data won't be saved (that is the reason
95 | it is described as a necessary step in the previous "Codespeed configuration"
96 | section).
97 |
98 | ## Further customization
99 |
100 | ### Custom Settings
101 |
102 | You may override any of the default settings by setting them in
103 | `sample_project/settings.py`. It is strongly recommended that you only override the
104 | settings you need by importing the default settings and replacing only the
105 | values needed for your customizations:
106 |
107 | from codespeed.settings import *
108 |
109 | DEF_ENVIRONMENT = "Dual Core 64 bits"
110 |
111 | ### Site-wide Changes
112 |
113 | All pages inherit from the `base.html` template. To change every page on the site
114 | simply edit (`sample_project/templates/codespeed/base_site.html`) and override
115 | the appropriate block:
116 |
117 | * Custom title: you may replace the default "My Speed Center" for the title
118 | block with your prefered value:
119 |
120 | {% block title %}
121 | My Project's Speed Center
122 | {% endblock %}
123 |
124 | * Replacing logo.png: Place your logo in `sample_project/static/images/logo.png`
125 | * Logo with custom filename: Place your logo in `sample_project/static/images/` and add a block like
126 | this to `base_site.html`:
127 |
128 | {% block logo %}
129 |
130 | {% endblock logo %}
131 |
132 | n.b. the layout will stay exactly the same for any image with a height of
133 | 48px (any width will do)
134 |
135 | * Custom JavaScript or CSS: add your files to the `sample_project/static/js` directory
136 | and extend the `extra_head` template block:
137 |
138 | {% block extra_head %}
139 | {{ block.super }}
140 |