Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,4 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
- run: pip install -r requirements.txt "django==${{ matrix.django }}.*"
- run: python manage.py test codespeed
- run: python manage.py test codespeed speed_python
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ build
dist
*.egg-info
.venv*/
speed_python/local_settings.py
8 changes: 8 additions & 0 deletions speed_python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,11 @@ Please, also refer to the [Django URL dispatcher docu]
### Codespeed settings
The main config file is `settings.py`. There you configure everything related
to your set up.

### Home page trend
`HOME_GEOMEAN_SERIES` in `settings.py` lists the executable/environment pairs
plotted on the home page as a trend of the geometric mean of all benchmarks
over the last 30 days. Each line is normalised to its first run in the window,
so 0.95 reads as "5% faster than at the start of the window". The view lives
in `speed_python/views.py`, the chart in `static/js/home_geomean.js`, and the
tests run with `python manage.py test speed_python`.
12 changes: 12 additions & 0 deletions speed_python/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,16 @@ def process_exception(self, request, exception):
# Codespeed settings that can be overwritten here.
from codespeed.settings import *

HOME_GEOMEAN_SERIES = [
{'executable': 'lto-pgo',
'project': 'CPython',
'branch': 'main',
'environment':
'speed-python'},
{'executable': 'lto-pgo',
'project': 'CPython',
'branch': 'main',
'environment': 'ampere-one'},
]

from .local_settings import *
19 changes: 19 additions & 0 deletions speed_python/static/css/home.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/* speed.python.org home page: geometric mean trend */

div#presentation div#geomean, div#presentation div#geomean > div { display: block; }
div#geomean {
max-width: 900px;
margin: 1.5em auto 0;
text-align: center;
}
div#geomean-plot { width: 100%; height: 320px; }
div#geomean-legend { min-height: 3.2em; margin: 0.4em 0 0 4em; font-size: smaller; text-align: left; }
div#geomean p.plot-caption { text-align: center; }
/* The plot area starts ~56px in (y-axis label and ticks) and ends ~5px before
the right edge, so centre the heading, legend and caption on the plot area
rather than on the box */
div#geomean h3, div#geomean p.plot-caption { padding-left: 51px; }
/* codespeed's generic div rule gives the legend's colour markers
vertical-align: middle, which lifts them off the label line */
div#geomean-legend .dygraph-legend-line { vertical-align: baseline; margin-right: 0.25em; }
div#geomean p.plot-caption { font-size: 0.9em; color: var(--text-muted); }
100 changes: 100 additions & 0 deletions speed_python/static/js/home_geomean.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/* Home page trend of the geometric mean of all benchmarks, one line per
* configured executable/environment pair (see HOME_GEOMEAN_SERIES).
*
* Reads its configuration from data attributes on #geomean-plot:
* data-url JSON endpoint (gethomegeomeandata)
* data-changes-url changes page, used when a point is clicked
*/
(function() {
var COLORS = ['#4e79a7', '#f28e2b', '#59a14f', '#e15759', '#76b7b2',
'#edc948', '#b07aa1', '#ff9da7', '#9c755f', '#bab0ac'];

function parseDate(s) {
// "YYYY/MM/DD HH:MM:SS +0000" -> Date (UTC)
var m = /^(\d+)\/(\d+)\/(\d+) (\d+):(\d+):(\d+)/.exec(s);
return new Date(Date.UTC(+m[1], m[2] - 1, +m[3], +m[4], +m[5], +m[6]));
}

function render(container, data) {
var series = (data.series || []).filter(function(s) { return s.points.length; });
if (!series.length) {
container.innerHTML = '<p class="warning">No results in the last ' +
data.days + ' days.</p>';
return;
}

// Merge all series on a shared time axis; dygraphs wants one row per x.
var rowsByTime = {};
var commits = {}; // label -> time -> {commitid, benchmarks}
series.forEach(function(s, i) {
commits[s.label] = {};
s.points.forEach(function(p) {
var t = parseDate(p[0]).getTime();
if (!rowsByTime[t]) {
rowsByTime[t] = new Array(series.length + 1);
rowsByTime[t][0] = new Date(t);
}
rowsByTime[t][i + 1] = p[1];
commits[s.label][t] = {commitid: p[2], benchmarks: p[3]};
});
});
var rows = Object.keys(rowsByTime).sort(function(a, b) { return a - b; })
.map(function(t) { return rowsByTime[t]; });
for (var r = 0; r < rows.length; r++) {
for (var c = 1; c < rows[r].length; c++) {
if (rows[r][c] === undefined) { rows[r][c] = null; }
}
}

var labels = ['Date'].concat(series.map(function(s) { return s.label; }));

new Dygraph(container, rows, {
labels: labels,
colors: COLORS.slice(0, series.length),
connectSeparatedPoints: true,
drawPoints: true,
pointSize: 2,
strokeWidth: 1.5,
highlightCircleSize: 4,
legend: 'always',
labelsDiv: document.getElementById('geomean-legend'),
labelsSeparateLines: true,
ylabel: 'Relative to ' + data.days + ' days ago',
axes: {
y: { axisLabelFormatter: function(v) { return v.toFixed(2); } }
},
valueFormatter: function(v, opts, name, g, row) {
if (name === 'Date') { return new Date(v).toISOString().slice(0, 16).replace('T', ' '); }
if (v === null || v === undefined) { return ''; }
var t = g.getValue(row, 0);
var c = commits[name] && commits[name][t];
return v.toFixed(3) + 'x' + (c ? ' (' + c.commitid.slice(0, 10) + ', ' +
c.benchmarks + ' benchmarks)' : '');
},
pointClickCallback: function(e, point) {
var c = commits[point.name] && commits[point.name][point.xval];
var s = series.filter(function(x) { return x.label === point.name; })[0];
if (!c || !s) { return; }
window.location = container.getAttribute('data-changes-url') +
'?rev=' + encodeURIComponent(c.commitid) +
'&exe=' + s.executable + '&env=' + s.environment;
}
});
}

document.addEventListener('DOMContentLoaded', function() {
var container = document.getElementById('geomean-plot');
if (!container) { return; }
var xhr = new XMLHttpRequest();
xhr.open('GET', container.getAttribute('data-url'));
xhr.onload = function() {
if (xhr.status !== 200) {
container.innerHTML = '<p class="warning">Error retrieving data</p>';
return;
}
render(container, JSON.parse(xhr.responseText));
};
xhr.onerror = xhr.onload;
xhr.send();
});
})();
20 changes: 20 additions & 0 deletions speed_python/templates/home.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
{% extends "codespeed/base_site.html" %}
{% load static %}

{% block extra_head %}
<link rel="stylesheet" type="text/css" href="{% static 'css/dygraph.css' %}" />
<link rel="stylesheet" type="text/css" href="{% static 'css/home.css' %}" />
{% endblock %}

{% block navigation %}
{% endblock navigation %}
Expand All @@ -24,12 +30,26 @@ <h2>Comparison</h2>
</div>
</a>
<br />
<div id="geomean">
<h3>How has performance changed over the last 30 days?</h3>
<div id="geomean-plot"
data-url="{% url 'gethomegeomeandata' %}"
data-changes-url="{% url 'changes' %}"></div>
<div id="geomean-legend"></div>
<p class="plot-caption">
Geometric mean of all benchmark times, relative to the first run in the
window. Lower is faster. Each line is one executable on one machine;
click a point to open that commit's results.
</p>
</div>
<div id="reports"></div>
</div>
{% endblock body %}

{% block extra_script %}
{{ block.super }}
<script type="text/javascript" src="{% static 'js/dygraph.min.js' %}"></script>
<script type="text/javascript" src="{% static 'js/home_geomean.js' %}"></script>
<script type="text/javascript">
function permalinkToChanges(permalink) {
window.location=permalink;
Expand Down
106 changes: 106 additions & 0 deletions speed_python/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""Tests for the speed.python.org specific views.

Run with: python manage.py test codespeed speed_python
"""
import json
import os
from datetime import timedelta

from django.test import TestCase, override_settings
from django.urls import reverse
from django.utils import timezone

from codespeed.models import (Project, Benchmark, Revision, Branch, Executable,
Environment, Result)


DUMMY_CACHE = {'default': {'BACKEND': 'django.core.cache.backends.dummy.DummyCache'}}
SITE_DIR = os.path.dirname(os.path.abspath(__file__))
# The test settings belong to sample_project; render this site's templates instead.
SITE_TEMPLATES = [{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(SITE_DIR, 'templates')],
'APP_DIRS': True,
'OPTIONS': {'context_processors': [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
]},
}]


@override_settings(
ROOT_URLCONF='speed_python.urls',
TEMPLATES=SITE_TEMPLATES,
CACHES=DUMMY_CACHE,
HOME_GEOMEAN_SERIES=[{'executable': 'myexe', 'environment': 'env1'}])
class TestHomeGeomean(TestCase):

def setUp(self):
self.env = Environment.objects.create(name='env1')
self.project = Project.objects.create(name='proj', default_branch='main')
self.branch = Branch.objects.create(name='main', project=self.project)
self.exe = Executable.objects.create(name='myexe', project=self.project)
self.b1 = Benchmark.objects.create(name='b1')
self.b2 = Benchmark.objects.create(name='b2')
now = timezone.now()
runs = [
# days ago, {benchmark: value}
(45, {self.b1: 9.0, self.b2: 9.0}), # outside the window
(20, {self.b1: 1.0, self.b2: 4.0}), # first in window: baseline
(10, {self.b1: 2.0, self.b2: 2.0}), # geomean of 2.0 and 0.5 -> 1.0
(5, {self.b1: 0.5}), # b2 missing: only b1 counts
]
for i, (days_ago, values) in enumerate(runs):
rev = Revision.objects.create(
commitid='c%d' % i, branch=self.branch, project=self.project,
date=now - timedelta(days=days_ago))
for bench, value in values.items():
Result.objects.create(value=value, revision=rev, executable=self.exe,
benchmark=bench, environment=self.env)

def get(self):
response = self.client.get(reverse('gethomegeomeandata'))
self.assertEqual(response.status_code, 200)
return json.loads(response.content.decode())

def test_series_normalised_to_first_revision_in_window(self):
data = self.get()
self.assertEqual(data['days'], 30)
self.assertEqual(len(data['series']), 1)
series = data['series'][0]
self.assertEqual(series['label'], 'myexe @ env1')
self.assertEqual(series['executable'], self.exe.id)
self.assertEqual(series['environment'], self.env.id)
self.assertEqual(series['branch'], 'main')
self.assertEqual(series['baseline_commitid'], 'c1')
self.assertEqual([p[2] for p in series['points']], ['c1', 'c2', 'c3'])
self.assertEqual([p[1] for p in series['points']], [1.0, 1.0, 0.5])
self.assertEqual([p[3] for p in series['points']], [2, 2, 1])

def test_branch_filter(self):
other = Branch.objects.create(name='other', project=self.project)
rev = Revision.objects.create(commitid='x', branch=other, project=self.project,
date=timezone.now())
Result.objects.create(value=100.0, revision=rev, executable=self.exe,
benchmark=self.b1, environment=self.env)
commits = [p[2] for p in self.get()['series'][0]['points']]
self.assertNotIn('x', commits)

@override_settings(HOME_GEOMEAN_SERIES=[
{'executable': 'myexe', 'environment': 'env1', 'branch': 'main'},
{'executable': 'nope', 'environment': 'env1'},
{'executable': 'myexe', 'environment': 'nope'}])
def test_unknown_entries_are_skipped(self):
data = self.get()
self.assertEqual([s['label'] for s in data['series']], ['myexe @ env1'])

@override_settings(HOME_GEOMEAN_SERIES=[])
def test_unconfigured(self):
self.assertEqual(self.get()['series'], [])

def test_home_page_embeds_the_chart(self):
response = self.client.get(reverse('home'))
self.assertEqual(response.status_code, 200)
self.assertIn('id="geomean-plot"', response.content.decode())
self.assertIn(reverse('gethomegeomeandata'), response.content.decode())
4 changes: 4 additions & 0 deletions speed_python/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@

admin.autodiscover()

from . import views

urlpatterns = [
re_path(r'^admin/', admin.site.urls),
re_path(r'^home/geomean/json/$', views.gethomegeomeandata,
name='gethomegeomeandata'),
re_path(r'^', include('codespeed.urls')),
]

Expand Down
Loading
Loading