]> rtime.felk.cvut.cz Git - nul-nightly.git/blob - wvperf2html.py
Clicking a point in graph goes to the commit/range on github
[nul-nightly.git] / wvperf2html.py
1 #!/usr/bin/env python
2 #
3 # WvTest:
4 #   Copyright (C) 2012, 2014 Michal Sojka <sojka@os.inf.tu-dresden.de>
5 #       Licensed under the GNU Library General Public License, version 2.
6 #       See the included file named LICENSE for license information.
7 #
8 # This script converts a sequence of wvtest protocol outputs with
9 # results of performance measurements to interactive graphs (HTML +
10 # JavaScript). An example can be seen at
11 # http://os.inf.tu-dresden.de/~sojka/nul/performance.html.
12
13 from __future__ import print_function
14 import sys
15 import re
16 import os
17 import os.path
18 import string
19 import time
20 import numpy as np
21 import json
22
23
24 re_prefix = "\([0-9]+\) (?:#   )?"
25 re_date = re.compile('^Date: (.*)')
26 re_testing = re.compile('^('+re_prefix+')?\s*Testing "(.*)" in (.*):\s*$')
27 re_commit = re.compile('.*(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}).*, commit: (.*)')
28 re_commithash = re.compile('([0-9a-f]{7})(-dirty)? \(')
29 re_assertion = re.compile('^('+re_prefix+')?!\s*(.*?)\s+(\S+)\s*$')
30 re_perf =  re.compile('^('+re_prefix+')?!\s*(.*?)\s+PERF:\s*(.*?)\s+(\S+)\s*$')
31 re_perfaxis = re.compile('axis="([^"]+)"')
32
33 def dateConv(date):
34     d = time.gmtime(time.mktime(date))
35     return int(time.mktime(d))*1000
36
37
38 class Axis:
39     def __init__(self, name=None, units=None):
40         self.name = name
41         self.units = units
42         self.num = None
43     def getLabel(self):
44         if self.units and self.name:
45             return "%s [%s]" % (self.name, self.units)
46         elif self.units:
47             return self.units
48         else:
49             return self.name
50     def __repr__(self): return "Axis(name=%s units=%s, id=%s)" % (self.name, self.units, hex(id(self)))
51
52 class Column:
53     def __init__(self, name, units, axis):
54         self.name = name
55         self.units = units
56         self.axis = axis
57     def __repr__(self): return "Column(name=%s units=%s axis=%s)" % (self.name, self.units, repr(self.axis))
58
59 class Row(dict):
60     def __init__(self, graph, date):
61         self.graph = graph
62         self.date = date
63     def __getitem__(self, column):
64         try:
65             return dict.__getitem__(self, column)
66         except KeyError:
67             return None
68     def getDate(self):
69         return dateConv(self.date)
70
71 class Graph:
72     def __init__(self, id, title):
73         self.columns = {}
74         self.columns_ordered = []
75         self.id = id
76         self.title = title
77         self.rows = []
78         self.date2row = {}
79         self.axes = {}
80         self.axes_ordered = []
81         self.dataname = title.translate(string.maketrans(" /", "--"), "\"':,+()").lower()+".json"
82
83     def __getitem__(self, date):
84         try:
85             rownum = self.date2row[date]
86         except KeyError:
87             rownum = len(self.rows)
88             self.date2row[date] = rownum
89         try:
90             return self.rows[rownum]
91         except IndexError:
92             self.rows[rownum:rownum] = [Row(self, date)]
93             return self.rows[rownum]
94
95     def addValue(self, date, col, val, units):
96         row = self[date]
97         row[col] = val
98         if col not in self.columns:
99             axis=self.getAxis(units) or self.addAxis(units, Axis(units=units))
100             column = Column(col, units, axis)
101             self.columns[col] = column
102             self.columns_ordered.append(column)
103         else:
104             column = self.columns[col]
105             self.columns_ordered.remove(column)
106             self.columns_ordered.append(column)
107         self.columns[col].units=units
108         self.columns[col].axis.units=units
109
110     def addAxis(self, key, axis):
111         self.axes[key] = axis
112         return axis
113
114     def setAxis(self, col, key):
115         self.columns[col].axis = self.getAxis(key) or self.addAxis(key, Axis(name=key))
116
117     def getAxis(self, key):
118         if key not in self.axes: return None
119         return self.axes[key]
120
121     def findRanges(self):
122         for axis in list(self.axes.values()):
123             cols = [col for col in list(self.columns.values()) if col.axis == axis]
124             low = None
125             high = None
126             all_in_range = True
127             for col in cols:
128                 values = np.array([row[col.name] for row in self.rows if row[col.name] != None], np.float64)
129                 if low == None and high == None:
130                     lastmonth = values[-30:]
131                     median = np.median(lastmonth)
132                     low  = median * 0.95
133                     high = median * 1.05
134
135                 if (values > high).any() or (values < low).any():
136                     all_in_range = False
137             if all_in_range:
138                 axis.yrange_max = high
139                 axis.yrange_min = low
140             else:
141                 axis.yrange_max = None
142                 axis.yrange_min = None
143
144     def fixupAxisNumbers(self):
145         # Sort axes according to the columns and number them
146         num = 0
147         for column in self.columns_ordered:
148             axis = column.axis
149             if axis not in self.axes_ordered:
150                 self.axes_ordered.insert(0, axis)
151                 axis.num = num
152                 num += 1
153         num = 0
154         for axis in self.axes_ordered:
155             axis.num = num
156             num += 1
157
158     def options_json(self):
159         options = {
160             'rangeSelector': {
161                 'selected': 2 # 6m
162             },
163             'title': {
164                 'text': self.title
165             },
166             'legend': {
167                 'enabled': True,
168                 'floating': False,
169                 'verticalAlign': "top",
170                 'x': 100,
171                 'y': 60,
172             },
173             'tooltip': {
174                 'formatter': "FUN(tooltip_formatter)END",
175                 'style': {
176                                     'whiteSpace': 'normal',
177                                     'width': '400px',
178                     },
179             },
180             'plotOptions': {
181                 'series': {
182                     'events': {
183                         'click': "FUN(series_onclick)END",
184                     },
185                     'dataGrouping' : {
186                         'enabled' : False,
187                     },
188                 }
189             },
190             'yAxis': [{
191                     'lineWidth': 1,
192                     'labels': { 'align': 'right',
193                         'x': -3 },
194                     'title': { 'text': axis.getLabel() },
195                     'min': axis.yrange_min,
196                     'max': axis.yrange_max,
197                     } for axis in self.axes_ordered],
198             'series': [{ 'name': '%s [%s]' % (col.name, col.units),
199                  'yAxis': col.axis.num }
200                 for col in self.columns_ordered]
201             }
202         return json.dumps(options, indent=True).replace('"FUN(', '').replace(')END"', '')
203
204     def getData(self):
205         data = [[[row.getDate(), row[col.name]] for row in self.rows] for col in self.columns_ordered]
206         return json.dumps(data).replace('], [', '],\n[')
207
208
209 class Graphs(dict):
210     pass
211
212 graphs = Graphs()
213 date2commit = {}
214 commit2msg = {}
215
216 date = time.localtime(time.time())
217
218 for line in sys.stdin:
219     line = line.rstrip()
220
221     match = re_date.match(line)
222     if (match):
223         date = time.strptime(match.group(1), "%a, %d %b %Y %H:%M:%S +0200")
224         continue
225
226     match = re_testing.match(line)
227     if match:
228         what = match.group(2)
229         where = match.group(3)
230
231         match = re_commit.match(what)
232         if match:
233             date = time.strptime(match.group(1), "%Y-%m-%d %H:%M:%S")
234             commit = match.group(2)
235             match = re_commithash.search(commit);
236             if match:
237                 commithash = match.group(1)
238             else:
239                 commithash = None
240             date2commit[dateConv(date)] = commithash
241             commit2msg[commithash] = commit
242
243         (basename, ext) = os.path.splitext(os.path.basename(where))
244
245         if what != "all": title = what
246         else: title = basename
247         try:
248             graph = graphs[basename]
249         except KeyError:
250             graph = Graph(basename, title)
251             graphs[basename] = graph
252         continue
253
254     match = re_perf.match(line)
255     if match:
256         perfstr = match.group(3)
257         perf = perfstr.split()
258         col = perf[0]
259         try:
260             val = float(perf[1])
261         except ValueError:
262             val = None
263         try:
264             units = perf[2]
265             if '=' in units: units = None
266         except:
267             units = None
268         if match.group(4) != "ok":
269             val=None
270
271         graph.addValue(date, col, val, units)
272
273         match = re_perfaxis.search(perfstr)
274         if match:
275             graph.setAxis(col, match.group(1));
276
277 graphs = [g for g in list(graphs.values()) if len(g.columns)]
278 graphs = sorted(graphs, key=lambda g: g.title.lower())
279
280 for g in graphs:
281     g.findRanges()
282     g.fixupAxisNumbers()
283
284 print("""
285 <!DOCTYPE HTML>
286 <html>
287   <head>
288   <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
289   <title>NUL Performance Plots</title>
290
291   <script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js"></script>
292   <script type="text/javascript">
293     function tooltip_formatter() {
294       var s = '<b>'+ Highcharts.dateFormat('%a, %d %b %Y %H:%M:%S', this.x) +'</b><br/>';
295       s += commit2msg[date2commit[this.x]];
296       $.each(this.points, function(i, point) {
297         s += '<br/><span style="color:'+ point.series.color+';">'+ point.series.name +'</span>: '+point.y;
298       });
299       return s;
300     }
301     function series_onclick(event) {
302       var prevpoint = null;
303       for (var i in this.data) {
304         if (event.point == this.data[i]) {
305           if (i > 0) prevpoint = this.data[i-1];
306           break;
307         }
308       }
309       if (prevpoint && date2commit[prevpoint.x] != date2commit[event.point.x])
310         window.location = "https://github.com/TUD-OS/NUL/compare/"+date2commit[prevpoint.x]+'...'+date2commit[event.point.x];
311       else
312         window.location = "https://github.com/TUD-OS/NUL/commit/"+date2commit[event.point.x];
313     }""")
314
315 def make_int_keys(json):
316     r = re.compile('"(\d+)"(.*)')
317     s = ''
318     for match in r.finditer(json):
319         s += match.expand('\\1\\2')
320     return s
321
322 print("var date2commit = {%s};" % ",\n".join(["%d: '%s'" % (k, date2commit[k]) for k in sorted(date2commit.keys())]))
323 print("var commit2msg = %s;" % json.dumps(commit2msg, indent=True))
324 # for d in sorted(date2commit.keys()):
325 #     v = commits[d];
326 #     print('\t%d: { msg: "%s", hash: "%s" },' % (1000*time.mktime(d), v[0].replace('"', '\\"'), str(v[1]).replace('"', '\\"')))
327 print("""
328         </script>
329         <script type="text/javascript" src="js/highstock.js"></script>
330     </head>
331
332     <body>
333         <h1>NUL Performance Plots</h1>
334     <p>The graphs below show performance numbers from various
335     benchmarks that run nightly on NUL repository.</p>
336     <p>Table of content:</p>
337         <ul>
338 """)
339 for graph in graphs:
340     print("     <li><a href='#%s'>%s</a></li>" % (graph.title, graph.title))
341 print("    </ul>")
342 for graph in graphs:
343     print("""
344     <h2><a name='%(title)s'>%(title)s</a></h2>
345         <div id="%(id)s" style="height: 400px"></div>
346     <script>
347 $.getJSON('%(dataname)s', function(data) {
348     var options = %(options)s
349     for (var i=0; i < data.length; i++)
350         options['series'][i]['data'] = data[i];
351     $("#%(id)s").highcharts("StockChart", options);
352 });
353 </script>
354 """ % {'id': graph.id,
355        'title': graph.title,
356        'dataname': graph.dataname,
357        'options': graph.options_json()})
358     print(graph.getData(), file=open(graph.dataname, 'w'))
359 print("""
360     </body>
361 </html>
362 """)
363
364 # Local Variables:
365 # compile-command: "make perf"
366 # End: