├── 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 |
4 |

{{ message|safe }}

5 |
6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE 3 | recursive-include codespeed/fixtures timeline_tests.json 4 | recursive-include codespeed/templates/codespeed * 5 | recursive-include codespeed/static * 6 | -------------------------------------------------------------------------------- /codespeed/commits/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import, unicode_literals 3 | 4 | 5 | class CommitLogError(Exception): 6 | """An error when trying to display commit log messages""" 7 | -------------------------------------------------------------------------------- /sample_project/deploy/supervisor-speedcenter.conf: -------------------------------------------------------------------------------- 1 | [program:speedcenter] 2 | command=/path/to/your/virtualenv/bin/python /path/to/speedcenter/manage.py run_gunicorn 3 | directory=/path/to/speedcenter 4 | user=www-data 5 | autostart=true 6 | autorestart=true 7 | redirect_stderr=True 8 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os, sys 3 | 4 | if __name__ == "__main__": 5 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sample_project.settings") 6 | 7 | from django.core.management import execute_from_command_line 8 | 9 | execute_from_command_line(sys.argv) 10 | -------------------------------------------------------------------------------- /sample_project/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "codespeed/base_site.html" %} 2 | 3 | {% block title %}Page not found{% endblock %} 4 | 5 | {% block body %} 6 |
7 |

Page not found

8 | 9 |

Sorry, but the requested page could not be found.

10 |
11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /sample_project/templates/500.html: -------------------------------------------------------------------------------- 1 | {% extends "codespeed/base_site.html" %} 2 | 3 | {% block title %}Page unavailable{% endblock %} 4 | 5 | {% block body %} 6 |
7 |

Page unavailable

8 | 9 |

Sorry, but the requested page is unavailable due to a 10 | server hiccup.

11 |
12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /sample_project/templates/feeds/latest_description.html: -------------------------------------------------------------------------------- 1 | {% ifequal obj.colorcode "red" %}Performance regressed: {% else %} 2 | {% ifequal obj.colorcode "green" %}Performance improved: {% else %} 3 | {% ifequal obj.colorcode "yellow" %}Trend regressed: {% else %} 4 | No significant changes. 5 | {% endifequal %} 6 | {% endifequal %} 7 | {% endifequal %} 8 | {{ obj.summary }} 9 | -------------------------------------------------------------------------------- /sample_project/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.conf import settings 4 | from django.conf.urls import include, url 5 | from django.contrib import admin 6 | 7 | urlpatterns = [ 8 | url(r'^admin/', admin.site.urls), 9 | url(r'^', include('codespeed.urls')) 10 | ] 11 | 12 | if settings.DEBUG: 13 | # needed for development server 14 | from django.contrib.staticfiles.urls import staticfiles_urlpatterns 15 | urlpatterns += staticfiles_urlpatterns() 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | branches: 3 | only: 4 | - master 5 | env: 6 | global: 7 | - DJANGO_SETTINGS_MODULE=sample_project.settings 8 | matrix: 9 | include: 10 | - python: "2.7" 11 | env: DJANGO_VERSION=1.11 12 | - python: "3.5" 13 | env: DJANGO_VERSION=1.11 14 | - python: "3.5" 15 | env: DJANGO_VERSION=2.1 16 | install: 17 | - pip install flake8 mock 18 | - pip install -q Django==$DJANGO_VERSION 19 | - python setup.py install 20 | before_script: 21 | flake8 codespeed 22 | script: 23 | - python manage.py test codespeed 24 | -------------------------------------------------------------------------------- /codespeed/migrations/0004_branch_display_on_comparison_page.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.15 on 2020-02-24 11:26 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('codespeed', '0003_project_default_branch'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='branch', 15 | name='display_on_comparison_page', 16 | field=models.BooleanField(default=True, verbose_name='True to display this branch on the comparison page'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /sample_project/deploy/django.wsgi: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | WSGI config 5 | """ 6 | 7 | import os 8 | import sys 9 | 10 | current_dir = os.path.abspath(os.path.dirname(__file__).replace('\\','/')) 11 | dirs = current_dir.split(os.path.sep) 12 | del(dirs[-1]) 13 | codespeed_dir = os.path.sep.join(dirs) 14 | del(dirs[-1]) 15 | project_dir = os.path.sep.join(dirs) 16 | 17 | sys.path.append(project_dir) 18 | sys.path.append(codespeed_dir) 19 | os.environ['DJANGO_SETTINGS_MODULE'] = codespeed_dir.split(os.path.sep)[-1] + '.settings' 20 | 21 | from django.core.wsgi import get_wsgi_application 22 | application = get_wsgi_application() 23 | 24 | -------------------------------------------------------------------------------- /codespeed/templatetags/percentages.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django import template 4 | 5 | register = template.Library() 6 | 7 | 8 | @register.filter 9 | def percentage(value): 10 | if value == "-": 11 | return "-" 12 | elif value == float("inf"): 13 | return "+∞%" 14 | else: 15 | return "%.2f" % value 16 | 17 | 18 | @register.filter 19 | def fix_infinity(value): 20 | """Python’s ∞ prints 'inf', but JavaScript wants 'Infinity'""" 21 | if value == float("inf"): 22 | return "Infinity" 23 | elif value == float("-inf"): 24 | return "-Infinity" 25 | else: 26 | return value 27 | -------------------------------------------------------------------------------- /codespeed/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.conf import settings 3 | 4 | 5 | class CodespeedConfig(AppConfig): 6 | name = 'codespeed' 7 | 8 | def ready(self): 9 | import warnings 10 | if settings.ALLOW_ANONYMOUS_POST: 11 | warnings.warn("Results can be posted by unregistered users") 12 | warnings.warn( 13 | "In the future anonymous posting will be disabled by default", 14 | category=FutureWarning) 15 | elif not settings.REQUIRE_SECURE_AUTH: 16 | warnings.warn( 17 | "REQUIRE_SECURE_AUTH is not True. This server may prompt for" 18 | " user credentials to be submitted in plaintext") 19 | -------------------------------------------------------------------------------- /sample_project/templates/admin/base_site.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block title %}{{ title }} | {% trans 'Codespeed admin' %}{% endblock %} 5 | 6 | {% block extrastyle %} 7 | 14 | {% endblock %} 15 | 16 | 17 | {% block branding %} 18 |

{% trans 'Codespeed administration' %}

19 | {% endblock %} 20 | 21 | {% block nav-global %}{% endblock %} -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Codespeed is written and maintained by Miquel Torres Barceló and various contributors: 2 | 3 | Frank Becker 4 | Chris Adams 5 | Stefan Marr 6 | Reiner Gerecke 7 | daniloaf 8 | Joachim Breitner 9 | Rayan Chikhi 10 | str4d 11 | Alexey Palazhchenko 12 | Javier Honduvilla Coto 13 | Octavian Moraru 14 | Phil Opaola 15 | Laurent Raufaste 16 | Nicolas Cornu 17 | Elliot Saba 18 | Jean-Paul Calderone 19 | Malcolm Parsons 20 | Rayan 21 | Catalin Gabriel Manciu 22 | Clément MATHIEU 23 | Dave Collins 24 | David Fraser 25 | Kevin Modzelewski 26 | Matt Haggard 27 | Michael Thompson 28 | Reiner 29 | Zachary Ware 30 | Mark Watts 31 | Catalin G. Manciu 32 | Octavian Moraru 33 | Iskander (Alex) Sharipov 34 | Tim (Timmmm) 35 | -------------------------------------------------------------------------------- /sample_project/deploy/apache-speedcenter.conf: -------------------------------------------------------------------------------- 1 | 2 | ServerName speedcenter.foo.bar 3 | ServerAlias speed speed.foo.bar 4 | DocumentRoot /var/www/speedcenter/ 5 | ServerAdmin admin@foo.bar 6 | ErrorLog /var/log/apache2/speedcenter-errors.log 7 | CustomLog /var/log/apache2/speedcenter-access.log combined 8 | DirectoryIndex index.html index.shtml 9 | 10 | Alias /static/ /path/to/speedcenter/sitestatic/ 11 | 12 | WSGIDaemonProcess speedcenter user=www-data group=www-data python-path=/path/to/virtualenv/codespeed/lib/python2.6/site-packages/ 13 | WSGIProcessGroup speedcenter 14 | WSGIScriptAlias / /path/to/speedcenter/django.wsgi 15 | 16 | -------------------------------------------------------------------------------- /codespeed/migrations/0002_median.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('codespeed', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='benchmark', 16 | name='data_type', 17 | field=models.CharField(default='U', max_length=1, choices=[('U', 'Mean'), ('M', 'Median')]), 18 | ), 19 | migrations.AddField( 20 | model_name='result', 21 | name='q1', 22 | field=models.FloatField(null=True, blank=True), 23 | ), 24 | migrations.AddField( 25 | model_name='result', 26 | name='q3', 27 | field=models.FloatField(null=True, blank=True), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /sample_project/deploy/apache-reverseproxy.conf: -------------------------------------------------------------------------------- 1 | 2 | ServerName speed.pypy.org 3 | DocumentRoot /var/www/speed.pypy.org 4 | 5 | ErrorLog /var/log/apache2/speed-pypy.org-error.log 6 | 7 | # Possible values include: debug, info, notice, warn, error, crit, 8 | # alert, emerg. 9 | LogLevel warn 10 | 11 | CustomLog /var/log/apache2/speed-pypy.org-access.log combined 12 | ServerSignature On 13 | 14 | Alias /static/ /path/to/speedcenter/sitestatic/ 15 | 16 | 17 | Order allow,deny 18 | Allow from all 19 | 20 | 21 | ProxyRequests Off 22 | 23 | Order deny,allow 24 | Allow from all 25 | 26 | ProxyPass /static ! 27 | ProxyPass / http://localhost:8000/ 28 | ProxyPassReverse / http://localhost:8000/ 29 | 30 | 31 | -------------------------------------------------------------------------------- /sample_project/deploy/lighttpd-speedcenter.conf: -------------------------------------------------------------------------------- 1 | server.modules = ( 2 | "mod_fastcgi", 3 | "mod_rewrite", 4 | "mod_access", 5 | ) 6 | 7 | server.document-root = "/path/to/speedcenter/" 8 | 9 | fastcgi.server = ( 10 | "/django" => ( 11 | "main" => ( 12 | "socket" => "/path/to/a/sock/file", 13 | "check-local" => "disable", 14 | ) 15 | ) 16 | ) 17 | 18 | alias.url = ( 19 | # /static/admin depends of distribution. 20 | "/static/admin" => "/usr/lib/python2.7/dist-packages/django/contrib/admin/static/admin", 21 | "/static" => "/path/to/static", 22 | ) 23 | 24 | url.rewrite-once = ( 25 | "^(/static.*)$" => "$1", 26 | "^(/django/.*)$" => "$1", 27 | "^(/.*)$" => "/django$1", 28 | ) 29 | 30 | mimetype.assign = ( ".png" => "image/png", 31 | ".css" => "text/css", 32 | ".js" => "text/javascript" 33 | ) 34 | -------------------------------------------------------------------------------- /codespeed/migrations/0003_project_default_branch.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.13 on 2017-08-04 03:45 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | def get_default_branch_name(): 9 | from django.conf import settings 10 | try: 11 | return settings.DEF_BRANCH 12 | except AttributeError: 13 | return "master" 14 | 15 | 16 | class Migration(migrations.Migration): 17 | 18 | dependencies = [ 19 | ('codespeed', '0002_median'), 20 | ] 21 | 22 | operations = [ 23 | migrations.AddField( 24 | model_name='project', 25 | name='default_branch', 26 | field=models.CharField(default=get_default_branch_name, max_length=32), 27 | preserve_default=False, 28 | ), 29 | migrations.AlterField( 30 | model_name='branch', 31 | name='name', 32 | field=models.CharField(max_length=32), 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /sample_project/templates/about.html: -------------------------------------------------------------------------------- 1 | {% extends "codespeed/base_site.html" %} 2 | {% block title %}{{ block.super }}: About this site{% endblock %} 3 | {% block body %} 4 | 6 |
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 | 2 | ServerAdmin name@mail.server 3 | ServerName myspeedcenter 4 | 5 | TransferLog /var/log/apache2/myspeedcenter.pypy.org.log 6 | ErrorLog /var/log/apache2/myspeedcenter.pypy.org.err 7 | 8 | # To be able to serve images for admin site: 9 | Alias /admin_media/ /usr/local/lib/python2.7/dist-packages/django/contrib/admin/media/ 10 | 11 | Order deny,allow 12 | Allow from all 13 | 14 | 15 | # Serve media files 16 | Alias /static/ /path/to/codespeed/app/static/ 17 | 18 | Order deny,allow 19 | Allow from all 20 | 21 | 22 | #let Django handle requests 23 | WSGIScriptAlias / /path/to/example/project/example/deploy/django.wsgi 24 | 25 | Order deny,allow 26 | Allow from all 27 | 28 | 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | All files in this work, are now covered by the following copyright notice. 2 | Please note that included libraries in the media/ directory may have their own license. 3 | 4 | Copyright (c) 2009-2016 Miquel Torres 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 | 6 | 7 | {% for report in reports %} 8 | 9 | 10 | {% endfor %} 11 | 12 |
Latest Results 4 | (rss) 5 |
{{ report.revision }}{{ report.executable }}@{{ report.environment}}{{ report.item_description }}
13 | {% endif %} 14 | 15 | {% if significant_reports|length %} 16 | 17 | 20 | 21 | {% for report in significant_reports %} 22 | 23 | 24 | {% endfor %} 25 | 26 |
Latest Significant Results 18 | (rss) 19 |
{{ report.revision }}{{ report.executable }}@{{ report.environment}}{{ report.item_description }}
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 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {% ifnotequal rev.branch.project.repo_type "N" %} 21 | 22 | {% endifnotequal %} 23 | 24 |
Revision
Commit{% if rev.get_browsing_url %}{{ rev.commitid }}{% else %}{{ rev.commitid }}{% endif %}
Date{{ rev.date }}
Repo{{ rev.branch.project.repo_path }}
25 | 26 | {% ifnotequal exe.project.repo_type 'N' %} 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 42 |
Commit logs
Loading...
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
#} 15 |
16 | {% block page_header %} 17 | 18 | {% block logo %} 19 | logo 20 | {% endblock logo %} 21 | 22 |

{% block page_title %}SPEED CENTER{% endblock page_title %}

23 | {% block top_nav %} 24 | 27 | {% endblock top_nav %} 28 | {% endblock page_header %} 29 |
30 | 31 |
32 | 41 |
42 | {% block body %} 43 |
Base template
44 | {% endblock %} 45 |
46 |
47 | {% block footer %} 48 | 49 | {% endblock %} 50 |
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 |
4 | {% if prev %}{% endif %} 5 | {% if next %}{% endif %} 6 |
7 | 8 | {% for units in tablelist %} 9 |
10 | 11 | 12 | 13 | 14 | {% if units.has_stddev %}{% endif%} 15 | {% if units.hasmin %}{% endif%} 16 | {% if units.hasmax %}{% endif%} 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {% if units.hasmin %} 26 | {% endif%}{% if units.has_stddev %} 27 | {% endif%}{% if units.hasmax %} 28 | {% endif%} 29 | 30 | 31 | 32 | 33 | {% for row in units.rows|dictsort:"bench_name" %} 34 | 35 | 36 | 37 | {% if units.has_stddev %} 38 | {% endif%}{% if units.hasmin %} 39 | {% endif%}{% if units.hasmax %} 40 | {% endif%} 41 | 42 | {% endfor %} 43 | 44 |
Benchmark{{ units.units_title }} in {{ units.units }}Std devMinMaxChangeTrend
Average{{ units.totals.min }}{{ units.totals.max }}{{ units.totals.change|percentage }}{% ifnotequal units.totals.trend "-" %}{{ units.totals.trend|floatformat:2 }}%{% else %}{{ units.totals.trend }}{% endifnotequal %}
{{ row.bench_name }}{{ row.result|floatformat:units.precission }}{{ row.std_dev|floatformat:units.precission }}{{ row.val_min|floatformat:units.precission }}{{ row.val_max|floatformat:units.precission }}{{ row.change|percentage }}{% ifequal row.trend "-" %}-{% else %}{{ row.trend|floatformat:2 }}%{% endifequal %}
{% endfor %} 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 |
Executable
Name{{ exe }}
Description{{ exe.description }}
57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 |
Environment
Name{{ env.name }}
CPU{{ env.cpu }}
Memory{{ env.memory }}
OS{{ env.os }}
Kernel{{ env.kernel }}
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 | 62 |
63 |
64 |
65 |
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 | 63 | 64 | Normalization: 65 | 71 | 72 | 73 | 74 | 75 | 76 | 77 |
78 |
79 |
80 |
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($("")); 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 += ""; 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 |
80 | 81 | Show the last 82 | results 85 | 86 | 87 | 88 | 89 | 90 | {% if use_median_bands %} 91 | 95 | 99 | {% endif %} 100 | 101 |
102 |
103 |
104 |
105 |
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 | [![Build Status](https://travis-ci.org/tobami/codespeed.png?branch=master)](https://travis-ci.org/tobami/codespeed) 3 | [![PyPI version](https://img.shields.io/pypi/v/codespeed.svg)](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 | My Project 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 |