--- /dev/null
+#!/usr/bin/env python
+#
+# WvTest:
+# Copyright (C) 2012, 2014 Michal Sojka <sojka@os.inf.tu-dresden.de>
+# Licensed under the GNU Library General Public License, version 2.
+# See the included file named LICENSE for license information.
+#
+# This script converts a sequence of wvtest protocol outputs with
+# results of performance measurements to interactive graphs (HTML +
+# JavaScript). An example can be seen at
+# http://os.inf.tu-dresden.de/~sojka/nul/performance.html.
+
+from __future__ import print_function
+import sys
+import re
+import os
+import os.path
+import string
+import time
+import numpy as np
+import json
+
+
+re_prefix = "\([0-9]+\) (?:# )?"
+re_date = re.compile('^Date: (.*)')
+re_testing = re.compile('^('+re_prefix+')?\s*Testing "(.*)" in (.*):\s*$')
+re_commit = re.compile('.*(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}).*, commit: (.*)')
+re_commithash = re.compile('([0-9a-f]{7})(-dirty)? \(')
+re_assertion = re.compile('^('+re_prefix+')?!\s*(.*?)\s+(\S+)\s*$')
+re_perf = re.compile('^('+re_prefix+')?!\s*(.*?)\s+PERF:\s*(.*?)\s+(\S+)\s*$')
+re_perfaxis = re.compile('axis="([^"]+)"')
+
+def dateConv(date):
+ d = time.gmtime(time.mktime(date))
+ return int(time.mktime(d))*1000
+
+
+class Axis:
+ def __init__(self, name=None, units=None):
+ self.name = name
+ self.units = units
+ self.num = None
+ def getLabel(self):
+ if self.units and self.name:
+ return "%s [%s]" % (self.name, self.units)
+ elif self.units:
+ return self.units
+ else:
+ return self.name
+ def __repr__(self): return "Axis(name=%s units=%s, id=%s)" % (self.name, self.units, hex(id(self)))
+
+class Column:
+ def __init__(self, name, units, axis):
+ self.name = name
+ self.units = units
+ self.axis = axis
+ def __repr__(self): return "Column(name=%s units=%s axis=%s)" % (self.name, self.units, repr(self.axis))
+
+class Row(dict):
+ def __init__(self, graph, date):
+ self.graph = graph
+ self.date = date
+ def __getitem__(self, column):
+ try:
+ return dict.__getitem__(self, column)
+ except KeyError:
+ return None
+ def getDate(self):
+ return dateConv(self.date)
+
+class Graph:
+ def __init__(self, id, title):
+ self.columns = {}
+ self.columns_ordered = []
+ self.id = id
+ self.title = title
+ self.rows = []
+ self.date2row = {}
+ self.axes = {}
+ self.axes_ordered = []
+ self.dataname = title.translate(string.maketrans(" /", "--"), "\"':,+()").lower()+".json"
+
+ def __getitem__(self, date):
+ try:
+ rownum = self.date2row[date]
+ except KeyError:
+ rownum = len(self.rows)
+ self.date2row[date] = rownum
+ try:
+ return self.rows[rownum]
+ except IndexError:
+ self.rows[rownum:rownum] = [Row(self, date)]
+ return self.rows[rownum]
+
+ def addValue(self, date, col, val, units):
+ row = self[date]
+ row[col] = val
+ if col not in self.columns:
+ axis=self.getAxis(units) or self.addAxis(units, Axis(units=units))
+ column = Column(col, units, axis)
+ self.columns[col] = column
+ self.columns_ordered.append(column)
+ else:
+ column = self.columns[col]
+ self.columns_ordered.remove(column)
+ self.columns_ordered.append(column)
+ self.columns[col].units=units
+ self.columns[col].axis.units=units
+
+ def addAxis(self, key, axis):
+ self.axes[key] = axis
+ return axis
+
+ def setAxis(self, col, key):
+ self.columns[col].axis = self.getAxis(key) or self.addAxis(key, Axis(name=key))
+
+ def getAxis(self, key):
+ if key not in self.axes: return None
+ return self.axes[key]
+
+ def findRanges(self):
+ for axis in list(self.axes.values()):
+ cols = [col for col in list(self.columns.values()) if col.axis == axis]
+ values = []
+ for col in cols:
+ allvalues = np.array([row[col.name] for row in self.rows if row[col.name] != None], np.float64)
+ lastmonth = allvalues[-30:]
+ values.extend(lastmonth);
+ if len(values) > 0:
+ axis.minrange = np.mean(values)/10.0
+
+ def fixupAxisNumbers(self):
+ # Sort axes according to the columns and number them
+ num = 0
+ for column in self.columns_ordered:
+ axis = column.axis
+ if axis not in self.axes_ordered:
+ self.axes_ordered.insert(0, axis)
+ axis.num = num
+ num += 1
+ num = 0
+ for axis in self.axes_ordered:
+ axis.num = num
+ num += 1
+
+ def options_json(self):
+ options = {
+ 'rangeSelector': {
+ 'selected': 2 # 6m
+ },
+ 'title': {
+ 'text': self.title
+ },
+ 'legend': {
+ 'enabled': True,
+ 'floating': False,
+ 'verticalAlign': "top",
+ 'x': 100,
+ 'y': 60,
+ },
+ 'tooltip': {
+ 'formatter': "FUN(tooltip_formatter)END",
+ 'style': {
+ 'whiteSpace': 'normal',
+ 'width': '400px',
+ },
+ },
+ 'plotOptions': {
+ 'series': {
+ 'events': {
+ 'click': "FUN(series_onclick)END",
+ },
+ 'dataGrouping' : {
+ 'enabled' : False,
+ },
+ }
+ },
+ 'yAxis': [{
+ 'lineWidth': 1,
+ 'labels': { 'align': 'right',
+ 'x': -3 },
+ 'title': { 'text': axis.getLabel() },
+ 'minRange': axis.minrange,
+ } for axis in self.axes_ordered],
+ 'series': [{ 'name': '%s [%s]' % (col.name, col.units),
+ 'yAxis': col.axis.num }
+ for col in self.columns_ordered]
+ }
+ return json.dumps(options, indent=True).replace('"FUN(', '').replace(')END"', '')
+
+ def getData(self):
+ data = [[[row.getDate(), row[col.name]] for row in self.rows] for col in self.columns_ordered]
+ return json.dumps(data).replace('], [', '],\n[')
+
+
+class Graphs(dict):
+ pass
+
+graphs = Graphs()
+date2commit = {}
+commit2msg = {}
+
+date = time.localtime(time.time())
+
+for line in sys.stdin:
+ line = line.rstrip()
+
+ match = re_date.match(line)
+ if (match):
+ date = time.strptime(match.group(1), "%a, %d %b %Y %H:%M:%S +0200")
+ continue
+
+ match = re_testing.match(line)
+ if match:
+ what = match.group(2)
+ where = match.group(3)
+
+ match = re_commit.match(what)
+ if match:
+ date = time.strptime(match.group(1), "%Y-%m-%d %H:%M:%S")
+ commit = match.group(2)
+ match = re_commithash.search(commit);
+ if match:
+ commithash = match.group(1)
+ else:
+ commithash = None
+ date2commit[dateConv(date)] = commithash
+ commit2msg[commithash] = commit
+
+ (basename, ext) = os.path.splitext(os.path.basename(where))
+
+ if what != "all": title = what
+ else: title = basename
+ try:
+ graph = graphs[basename]
+ except KeyError:
+ graph = Graph(basename, title)
+ graphs[basename] = graph
+ continue
+
+ match = re_perf.match(line)
+ if match:
+ perfstr = match.group(3)
+ perf = perfstr.split()
+ col = perf[0]
+ try:
+ val = float(perf[1])
+ except ValueError:
+ val = None
+ try:
+ units = perf[2]
+ if '=' in units: units = None
+ except:
+ units = None
+ if match.group(4) != "ok":
+ val=None
+
+ graph.addValue(date, col, val, units)
+
+ match = re_perfaxis.search(perfstr)
+ if match:
+ graph.setAxis(col, match.group(1));
+
+graphs = [g for g in list(graphs.values()) if len(g.columns)]
+graphs = sorted(graphs, key=lambda g: g.title.lower())
+
+for g in graphs:
+ g.findRanges()
+ g.fixupAxisNumbers()
+
+print("""
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <title>NUL Performance Plots</title>
+
+ <script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js"></script>
+ <script type="text/javascript">
+ function tooltip_formatter() {
+ var s = '<b>'+ Highcharts.dateFormat('%a, %d %b %Y %H:%M:%S', this.x) +'</b><br/>';
+ s += commit2msg[date2commit[this.x]];
+ $.each(this.points, function(i, point) {
+ s += '<br/><span style="color:'+ point.series.color+';">'+ point.series.name +'</span>: '+point.y;
+ });
+ return s;
+ }
+ function series_onclick(event) {
+ var prevpoint = null;
+ for (var i in this.data) {
+ if (event.point == this.data[i]) {
+ if (i > 0) prevpoint = this.data[i-1];
+ break;
+ }
+ }
+ if (prevpoint && date2commit[prevpoint.x] != date2commit[event.point.x])
+ window.location = "https://github.com/TUD-OS/NUL/compare/"+date2commit[prevpoint.x]+'...'+date2commit[event.point.x];
+ else
+ window.location = "https://github.com/TUD-OS/NUL/commit/"+date2commit[event.point.x];
+ }""")
+
+def make_int_keys(json):
+ r = re.compile('"(\d+)"(.*)')
+ s = ''
+ for match in r.finditer(json):
+ s += match.expand('\\1\\2')
+ return s
+
+print("var date2commit = {%s};" % ",\n".join(["%d: '%s'" % (k, date2commit[k]) for k in sorted(date2commit.keys())]))
+print("var commit2msg = %s;" % json.dumps(commit2msg, indent=True))
+# for d in sorted(date2commit.keys()):
+# v = commits[d];
+# print('\t%d: { msg: "%s", hash: "%s" },' % (1000*time.mktime(d), v[0].replace('"', '\\"'), str(v[1]).replace('"', '\\"')))
+print("""
+ </script>
+ <script type="text/javascript" src="js/highstock.js"></script>
+ </head>
+
+ <body>
+ <h1>NUL Performance Plots</h1>
+ <p>The graphs below show performance numbers from various
+ benchmarks that run nightly on NUL repository.</p>
+ <p>Table of content:</p>
+ <ul>
+""")
+for graph in graphs:
+ print(" <li><a href='#%s'>%s</a></li>" % (graph.title, graph.title))
+print(" </ul>")
+for graph in graphs:
+ print("""
+ <h2><a name='%(title)s'>%(title)s</a></h2>
+ <div id="%(id)s" style="height: 400px"></div>
+ <script>
+$.getJSON('%(dataname)s', function(data) {
+ var options = %(options)s
+ for (var i=0; i < data.length; i++)
+ options['series'][i]['data'] = data[i];
+ $("#%(id)s").highcharts("StockChart", options);
+});
+</script>
+""" % {'id': graph.id,
+ 'title': graph.title,
+ 'dataname': graph.dataname,
+ 'options': graph.options_json()})
+ print(graph.getData(), file=open(graph.dataname, 'w'))
+print("""
+ </body>
+</html>
+""")
+
+# Local Variables:
+# compile-command: "make perf"
+# End: