# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import csv
import json
import os
DEFAULT_PRECISION = 5
[docs]class BaseReporter(object):
'''Base class for all reporters'''
def __init__(self, precision=DEFAULT_PRECISION, **kwargs):
self.precision = precision
def init(self, runner):
self.runner = runner
[docs] def start(self):
'''Hook called once on run start'''
pass
[docs] def before_class(self, bench):
'''Hook called once before each benchmark class'''
pass
[docs] def after_class(self, bench):
'''Hook called once after each benchmark class'''
pass
[docs] def before_method(self, bench, method):
'''Hook called once before each benchmark method'''
pass
[docs] def after_method(self, bench, method):
'''Hook called once after each benchmark method'''
pass
[docs] def progress(self, bench, method, times):
'''Hook called after each benchmark method call'''
pass
[docs] def end(self):
'''Hook called once on run end'''
pass
[docs] def summary(self):
'''Compute the execution summary'''
out = {}
for bench in self.runner.runned:
key = self.key(bench)
runs = {}
for method, results in bench.results.items():
mean = results.total / bench.times
name = bench.label_for(method)
runs[method] = {
'name': name,
'total': results.total,
'mean': mean
}
out[key] = {
'name': bench.label,
'times': bench.times,
'runs': runs
}
return out
[docs] def key(self, bench):
'''Generate a report key from a benchmark instance'''
return '{bench.__class__.__name__}-{bench.times}'.format(bench=bench)
[docs]class FileReporter(BaseReporter):
'''A reporter dumping results into a file'''
def __init__(self, filename, **kwargs):
'''
:param filename: the output file name
:type filename: string
'''
self.filename = filename
super(FileReporter, self).__init__(**kwargs)
[docs] def end(self):
'''
Dump the report into the output file.
If the file directory does not exists, it will be created.
The open file is then given as parameter to :meth:`~minibench.report.FileReporter.output`.
'''
dirname = os.path.dirname(self.filename)
if dirname and not os.path.exists(dirname):
os.makedirs(dirname)
with open(self.filename, 'w') as out:
self.out = out
self.output(out)
self.out = None
[docs] def output(self, out):
'''
Serialize the report into the open file.
Child classes should implement this method.
:param out: an open file object to serialize into.
:type out: file
'''
raise NotImplementedError('You need to implement the output method')
[docs] def line(self, text=''):
'''A simple helper to write line with `\n`'''
self.out.write(text)
self.out.write('\n')
[docs]class JsonReporter(FileReporter):
'''A reporter dumping results into a JSON file'''
def output(self, out):
json.dump(self.summary(), out)
[docs]class CsvReporter(FileReporter):
'''
A reporter dumping results into a CSV file
The CSV will have the following format:
========= ====== ===== ========= ===========
Benchmark Method Times Total (s) Average (s)
========= ====== ===== ========= ===========
It uses `;` character as delimiter and `"` as delimiter.
All Strings are quoted.
'''
def output(self, out):
writer = csv.writer(out, delimiter=str(';'), quotechar=str('"'), quoting=csv.QUOTE_NONNUMERIC)
writer.writerow(('Benchmark', 'Method', 'Times', 'Total (s)', 'Average (s)',))
for row in self.summary().values():
for run in row['runs'].values():
writer.writerow((
row['name'],
run['name'],
row['times'],
run['total'],
run['mean'],
))
class FixedWidth(object):
'''A mixins with helpers for fixed width tables raporters'''
headers = ('Method', 'Times', 'Total (s)', 'Average (s)')
def with_sizes(self, *headers):
'''Compute the report summary and add the computed column sizes'''
if len(headers) != 5:
raise ValueError('You need to provide this headers: class, method, times, total, average')
summary = self.summary()
for row in summary.values():
sizes = [len(header) for header in headers]
# Benchmark/Class column
sizes[0] = max(sizes[0], len(row['name']))
# Method column
max_length = max(len(r['name']) for r in row['runs'].values())
sizes[1] = max(sizes[1], max_length)
# Times column
sizes[2] = max(sizes[2], len(str(row['times'])))
# Float columns
for idx, field in [(3, 'total'), (4, 'mean')]:
float_len = lambda r: len(self.float(r[field]))
max_length = max(float_len(r) for r in row['runs'].values())
sizes[idx] = max(sizes[idx], max_length)
row['sizes'] = sizes
return summary
def float(self, value):
return '{0:.{1}f}'.format(value, self.precision)
[docs]class MarkdownReporter(FileReporter, FixedWidth):
'''
A reporter rendering result as a markdown table.
Each benchmark will be rendered as a table with the following format:
====== ===== ========= ===========
Method Times Total (s) Average (s)
====== ===== ========= ===========
'''
def output(self, out):
for bench in self.with_sizes('', *self.headers).values():
# Bench label as title
self.line('# {0}'.format(bench['name']))
self.line()
# Table header
sizes = bench['sizes'][1:]
headers = [h.ljust(s) for h, s in zip(self.headers, sizes)]
self.row(headers)
separators = ['-' * size for size in sizes]
self.row(separators, ':')
# Table body
for run in bench['runs'].values():
values = (run['name'], bench['times'], self.float(run['total']), self.float(run['mean']))
values = [str(v).ljust(s) for v, s in zip(values, sizes)]
self.row(values)
self.line()
def row(self, values, char=' '):
self.line('|{c}{r[0]}{c}|{c}{r[1]}{c}|{c}{r[2]}{c}|{c}{r[3]}{c}|'.format(r=values, c=char))
[docs]class RstReporter(FileReporter, FixedWidth):
'''
A reporter rendering result as a reStructuredText table
Each benchmark will be rendered as a table with the following format:
====== ===== ========= ===========
Method Times Total (s) Average (s)
====== ===== ========= ===========
'''
row = '| {row[0]: <{sizes[0]}} | {row[1]: <{sizes[1]}} | {row[2]: <{sizes[2]}} | {row[3]: <{sizes[3]}} |'
def output(self, out):
for bench in self.with_sizes('', *self.headers).values():
# Bench label as title
self.line(bench['name'])
self.line('=' * len(bench['name']))
self.line()
# Table header
sizes = bench['sizes'][1:]
self.line(self.separator(sizes))
line = self.row.format(row=self.headers, sizes=sizes)
self.line(line)
self.line(self.separator(sizes, '='))
# Table body
for run in bench['runs'].values():
row = (run['name'], bench['times'], self.float(run['total']), self.float(run['mean']))
line = self.row.format(row=row, sizes=sizes)
self.line(line)
# Table footer
self.line(self.separator(sizes))
self.line()
def separator(self, sizes, char='-'):
line = '+'.join([char * (size + 2) for size in sizes])
return ''.join(('+', line, '+'))