84:75dafcb54f2c
Anton Shestakov <av6@dwimlabs.net>, Fri, 21 Oct 2016 18:58:17 +0800
viewer: lighter color for hovered row

previous change 74:d016e96c0182

viewer.py

Permissions: -rwxr-xr-x

Other formats: Feeds:
#!/usr/bin/env python
import colorsys
import json
import logging
import os
import re
import sqlite3
from collections import OrderedDict
from subprocess import check_output, Popen, PIPE
from urllib import quote_plus
from tornado.escape import xhtml_escape
from tornado.ioloop import IOLoop
from tornado.options import define, options
from tornado.web import Application, RequestHandler
from settings import DBPATH, HG, TEMPLATES, TESTHGREPO
from bench import MARKS
define('listen', metavar='IP', default='127.0.0.1')
define('port', metavar='PORT', default=8065, type=int)
define('xheaders', metavar='True|False', default=False, type=bool)
define('debug', metavar='True|False', default=False, type=bool)
def isrevrange(rev):
if ':' in rev or '..' in rev:
return True
if re.search(r'(?<!rev)\(.*\)', rev) is not None:
return True
return False
def getinfo(revset):
output = check_output([HG, 'log', '-R', TESTHGREPO, '-T', 'json', '-r', revset])
return json.loads(output)
def showuser(user, short=False, domainre=re.compile(r'@[^ >]+')):
user = domainre.sub('@...', user)
if short:
return user.partition(' <')[0]
return user
def urlencode(**kwargs):
def joinparams(params):
return ','.join(quote_plus(i, '(),') for i in params)
items = sorted(kwargs.items())
return '&'.join(quote_plus(k) + '=' + joinparams(v) for k, v in items)
class BaseHandler(RequestHandler):
def prepare(self):
self.conn = sqlite3.connect(DBPATH)
super(BaseHandler, self).prepare()
def on_finish(self):
self.conn.close()
class IndexHandler(BaseHandler):
def get(self):
self.redirect('results.html')
class ResultsHandler(BaseHandler):
def get(self, page, ext):
self.set_etag_header()
if self.check_etag_header():
self.set_status(304)
return
if page == 'fancy':
self.fancy_html()
elif ext == 'tsv':
self.results_tsv()
elif ext == 'asc':
self.results_asc()
elif ext == 'html':
self.results_html()
def compute_etag(self):
dbmt = os.stat(DBPATH).st_mtime
changelog = os.path.join(TESTHGREPO, '.hg', 'store', '00changelog.i')
repomt = os.stat(changelog).st_mtime
return 'W/"{},{}"'.format(dbmt, repomt)
def getmarks(self):
chosen = self.get_argument('marks', None)
if chosen is not None:
chosen = chosen.split(',')
marks = OrderedDict((mark, MARKS[mark]) for mark in chosen if mark in MARKS)
moremarks = OrderedDict((mark, desc) for mark, desc in MARKS.items() if mark not in chosen)
else:
marks = MARKS
moremarks = {}
return marks, moremarks
def getchangesets(self):
rev = self.get_argument('rev', 'tip')
revcount = 120
if isrevrange(rev):
revset = 'reverse({})'.format(rev)
else:
revset = '{}:0'.format(rev, revcount)
return getinfo('first({}, {})'.format(revset, revcount))
def getlimits(self, marks, changesets=None):
query = (
'SELECT MIN(time), MAX(time) FROM results'
' WHERE mark = ? AND cache = ?')
extra = []
if changesets:
query += ' AND node IN (' + ','.join('?' * len(changesets)) + ')'
extra = [cset['node'] for cset in changesets]
return {
mark:
self.conn.execute(query, [mark, False] + extra).fetchone()
+
self.conn.execute(query, [mark, True] + extra).fetchone()
for mark in marks
}
def getresults(self, changesets, marks, local=False, colors=True):
results = {}
colormap = {}
limits = self.getlimits(marks, changesets if local else None)
resultsq = self.conn.execute(
'SELECT node, mark, time, cache FROM results'
' WHERE node IN (' + ','.join('?' * len(changesets)) + ')'
' AND mark IN (' + ','.join('?' * len(marks)) + ')',
[cset['node'] for cset in changesets] + marks.keys())
for node, mark, time, cache in resultsq:
results.setdefault(node, {}).setdefault(mark, [None, None, None, None])
if not cache:
color = green_to_red(limits[mark][0:2], time) if time is not None else None
results[node][mark][0:2] = [time, color]
else:
color = green_to_red(limits[mark][2:4], time) if time is not None else None
results[node][mark][2:4] = [time, color]
if colors and color not in colormap:
colormap[color] = 'c{}'.format(len(colormap))
return results, colormap
def results_tsv(self):
self.set_header('Content-Type', 'text/plain; charset=UTF-8')
changesets = self.getchangesets()
marks, _ = self.getmarks()
results, _ = self.getresults(changesets, marks, colors=False)
self.write('rev\tnode')
for mark in marks:
self.write('\t{0} (without cache)\t{0} (with cache)'.format(mark))
self.write('\n')
for cset in changesets:
self.write('{rev}\t{node}'.format(**cset))
for mark in marks:
values = results.get(cset['node'], {}).get(mark, ('', '', ''))
self.write('\t{0}\t{2}'.format(*values))
self.write('\n')
self.finish()
def results_asc(self):
self.set_header('Content-Type', 'text/plain; charset=UTF-8')
rev = self.get_argument('rev', 'tip')
revcount = 120
regex = re.compile(r'^[-\\|/+ ]+([0-9a-f]{40})$')
template = '{rev}:{node|short} {tags} {date|isodate} {author|user} {desc|firstline|strip}\n {node}\n\n'
revset = 'first({}:0, {})'.format(rev, revcount)
cmd = [HG, 'log', '-R', TESTHGREPO, '-G', '-T', template, '-r', revset]
graph = Popen(cmd, stdout=PIPE)
while graph.poll() is None:
line = graph.stdout.readline()
matches = regex.findall(line)
for match in matches:
msg = []
rows = self.conn.execute(
'SELECT mark, time, cache FROM results WHERE node = ?',
(match,))
matchtimes = {}
for mark, time, cache in rows:
matchtimes.setdefault(mark, [None, None])
matchtimes[mark][1 if cache else 0] = time
for mark in MARKS:
msg.append('{mark}: {0:.2f}s/{1:.2f}s'.format(*matchtimes[mark], mark=mark))
line = line.replace(match, ' '.join(msg))
self.write(line)
self.finish()
def setlocal(self, value=None):
kwargs = self.request.arguments.copy()
if value is None:
try:
kwargs.pop('local')
except KeyError:
pass
else:
kwargs['local'] = [str(value)]
if kwargs:
return '?' + urlencode(**kwargs)
else:
return self.request.path
def setrev(self, value=None):
kwargs = self.request.arguments.copy()
if value is None:
try:
kwargs.pop('rev')
except KeyError:
pass
else:
kwargs['rev'] = ['rev({})'.format(value)]
if kwargs:
return '?' + urlencode(**kwargs)
else:
return self.request.path
def setmarks(self, marks):
def fn(value=None, add=None, remove=None):
kwargs = self.request.arguments.copy()
if add is not None:
kwargs['marks'] = marks.keys() + [add]
elif remove is not None:
kwargs['marks'] = [mark for mark in marks if mark != remove]
elif value is not None:
kwargs['marks'] = value
else:
try:
kwargs.pop('marks')
except KeyError:
pass
if kwargs:
return '?' + urlencode(**kwargs)
else:
return self.request.path
return fn
def results_html(self):
changesets = self.getchangesets()
marks, moremarks = self.getmarks()
local = self.get_argument('local', False)
results, colormap = self.getresults(changesets, marks, local=local)
context = {
'changesets': changesets,
'marks': marks,
'moremarks': moremarks,
'results': results,
'local': local,
'colormap': colormap,
'setlocal': self.setlocal,
'setrev': self.setrev,
'setmarks': self.setmarks(marks),
'showuser': showuser
}
self.render('results.html', **context)
def fancy_html(self):
changesets = self.getchangesets()
marks, moremarks = self.getmarks()
results, _ = self.getresults(changesets, marks, colors=False)
json = []
csetmap = {}
for cset in changesets:
item = {'rev': -cset['rev']}
for mark in marks:
values = results.get(cset['node'], {}).get(mark, None)
if values is not None:
item[mark] = values[0]
json.append(item)
extra = [cset['branch']] + cset['tags']
if len(cset['parents']) > 1:
extra.append('(merge)')
csetmap[cset['rev']] = {
'rev': cset['rev'],
'branch': cset['branch'],
'tags': cset['tags'],
'merge': 'M' if len(cset['parents']) > 1 else '',
'shortnode': cset['node'][:12],
'firstline': xhtml_escape(cset['desc'].partition('\n')[0])
}
data = {
'names': marks,
'x': 'rev',
'json': json,
'keys': {'x': 'rev', 'value': marks.keys()},
'csetmap': csetmap
}
context = {
'changesets': changesets,
'marks': marks,
'moremarks': moremarks,
'setrev': self.setrev,
'setmarks': self.setmarks(marks),
'data': data
}
self.render('fancy.html', **context)
def green_to_red(limits, value):
low, high = limits
hue = (value - low) / (high - low) if high != low else 0.5
r, g, b = colorsys.hsv_to_rgb((1 - hue) * 0.3, 1, 1)
return (int(r * 255), int(g * 255), int(b * 255))
class Viewer(Application):
def __init__(self):
handlers = [
(r'/', IndexHandler),
(r'/(results)\.(html|tsv|asc)', ResultsHandler),
(r'/(fancy)\.(html)', ResultsHandler),
]
settings = dict(
template_path=TEMPLATES,
debug=options.debug
)
super(Viewer, self).__init__(handlers, **settings)
def listen(self, port, address='', **kwargs):
name = self.__class__.__name__
logging.info('%s is serving on http://%s:%d/', name, address, port)
super(Viewer, self).listen(port, address, **kwargs)
def main():
options.parse_command_line()
application = Viewer()
application.listen(options.port, options.listen, xheaders=options.xheaders)
IOLoop.current().start()
if __name__ == '__main__':
main()