blob: 2c26ccadfd2d2e4fc6b87683f15877b3e965c9db [file] [log] [blame]
# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Table generating, analyzing and printing functions.
This defines several classes that are used to generate, analyze and print
tables.
Example usage:
from cros_utils import tabulator
data = [["benchmark1", "33", "44"],["benchmark2", "44", "33"]]
tabulator.GetSimpleTable(data)
You could also use it to generate more complex tables with analysis such as
p-values, custom colors, etc. Tables are generated by TableGenerator and
analyzed/formatted by TableFormatter. TableFormatter can take in a list of
columns with custom result computation and coloring, and will compare values in
each row according to taht scheme. Here is a complex example on printing a
table:
from cros_utils import tabulator
runs = [[{"k1": "10", "k2": "12", "k5": "40", "k6": "40",
"ms_1": "20", "k7": "FAIL", "k8": "PASS", "k9": "PASS",
"k10": "0"},
{"k1": "13", "k2": "14", "k3": "15", "ms_1": "10", "k8": "PASS",
"k9": "FAIL", "k10": "0"}],
[{"k1": "50", "k2": "51", "k3": "52", "k4": "53", "k5": "35", "k6":
"45", "ms_1": "200", "ms_2": "20", "k7": "FAIL", "k8": "PASS", "k9":
"PASS"}]]
labels = ["vanilla", "modified"]
tg = TableGenerator(runs, labels, TableGenerator.SORT_BY_VALUES_DESC)
table = tg.GetTable()
columns = [Column(LiteralResult(),
Format(),
"Literal"),
Column(AmeanResult(),
Format()),
Column(StdResult(),
Format()),
Column(CoeffVarResult(),
CoeffVarFormat()),
Column(NonEmptyCountResult(),
Format()),
Column(AmeanRatioResult(),
PercentFormat()),
Column(AmeanRatioResult(),
RatioFormat()),
Column(GmeanRatioResult(),
RatioFormat()),
Column(PValueResult(),
PValueFormat()),
]
tf = TableFormatter(table, columns)
cell_table = tf.GetCellTable()
tp = TablePrinter(cell_table, out_to)
print tp.Print()
"""
from __future__ import print_function
import getpass
import math
import sys
import numpy
import colortrans
from email_sender import EmailSender
import misc
def _AllFloat(values):
return all([misc.IsFloat(v) for v in values])
def _GetFloats(values):
return [float(v) for v in values]
def _StripNone(results):
res = []
for result in results:
if result is not None:
res.append(result)
return res
class TableGenerator(object):
"""Creates a table from a list of list of dicts.
The main public function is called GetTable().
"""
SORT_BY_KEYS = 0
SORT_BY_KEYS_DESC = 1
SORT_BY_VALUES = 2
SORT_BY_VALUES_DESC = 3
MISSING_VALUE = 'x'
def __init__(self, d, l, sort=SORT_BY_KEYS, key_name='keys'):
self._runs = d
self._labels = l
self._sort = sort
self._key_name = key_name
def _AggregateKeys(self):
keys = set([])
for run_list in self._runs:
for run in run_list:
keys = keys.union(run.keys())
return keys
def _GetHighestValue(self, key):
values = []
for run_list in self._runs:
for run in run_list:
if key in run:
values.append(run[key])
values = _StripNone(values)
if _AllFloat(values):
values = _GetFloats(values)
return max(values)
def _GetLowestValue(self, key):
values = []
for run_list in self._runs:
for run in run_list:
if key in run:
values.append(run[key])
values = _StripNone(values)
if _AllFloat(values):
values = _GetFloats(values)
return min(values)
def _SortKeys(self, keys):
if self._sort == self.SORT_BY_KEYS:
return sorted(keys)
elif self._sort == self.SORT_BY_VALUES:
# pylint: disable=unnecessary-lambda
return sorted(keys, key=lambda x: self._GetLowestValue(x))
elif self._sort == self.SORT_BY_VALUES_DESC:
# pylint: disable=unnecessary-lambda
return sorted(keys, key=lambda x: self._GetHighestValue(x), reverse=True)
else:
assert 0, 'Unimplemented sort %s' % self._sort
def _GetKeys(self):
keys = self._AggregateKeys()
return self._SortKeys(keys)
def GetTable(self, number_of_rows=sys.maxint):
"""Returns a table from a list of list of dicts.
The list of list of dicts is passed into the constructor of TableGenerator.
This method converts that into a canonical list of lists which represents a
table of values.
Args:
number_of_rows: Maximum number of rows to return from the table.
Returns:
A list of lists which is the table.
Example:
We have the following runs:
[[{"k1": "v1", "k2": "v2"}, {"k1": "v3"}],
[{"k1": "v4", "k4": "v5"}]]
and the following labels:
["vanilla", "modified"]
it will return:
[["Key", "vanilla", "modified"]
["k1", ["v1", "v3"], ["v4"]]
["k2", ["v2"], []]
["k4", [], ["v5"]]]
The returned table can then be processed further by other classes in this
module.
"""
keys = self._GetKeys()
header = [self._key_name] + self._labels
table = [header]
rows = 0
for k in keys:
row = [k]
unit = None
for run_list in self._runs:
v = []
for run in run_list:
if k in run:
if type(run[k]) is list:
val = run[k][0]
unit = run[k][1]
else:
val = run[k]
v.append(val)
else:
v.append(None)
row.append(v)
# If we got a 'unit' value, append the units name to the key name.
if unit:
keyname = row[0] + ' (%s) ' % unit
row[0] = keyname
table.append(row)
rows += 1
if rows == number_of_rows:
break
return table
class Result(object):
"""A class that respresents a single result.
This single result is obtained by condensing the information from a list of
runs and a list of baseline runs.
"""
def __init__(self):
pass
def _AllStringsSame(self, values):
values_set = set(values)
return len(values_set) == 1
def NeedsBaseline(self):
return False
# pylint: disable=unused-argument
def _Literal(self, cell, values, baseline_values):
cell.value = ' '.join([str(v) for v in values])
def _ComputeFloat(self, cell, values, baseline_values):
self._Literal(cell, values, baseline_values)
def _ComputeString(self, cell, values, baseline_values):
self._Literal(cell, values, baseline_values)
def _InvertIfLowerIsBetter(self, cell):
pass
def _GetGmean(self, values):
if not values:
return float('nan')
if any([v < 0 for v in values]):
return float('nan')
if any([v == 0 for v in values]):
return 0.0
log_list = [math.log(v) for v in values]
gmean_log = sum(log_list) / len(log_list)
return math.exp(gmean_log)
def Compute(self, cell, values, baseline_values):
"""Compute the result given a list of values and baseline values.
Args:
cell: A cell data structure to populate.
values: List of values.
baseline_values: List of baseline values. Can be none if this is the
baseline itself.
"""
all_floats = True
values = _StripNone(values)
if not values:
cell.value = ''
return
if _AllFloat(values):
float_values = _GetFloats(values)
else:
all_floats = False
if baseline_values:
baseline_values = _StripNone(baseline_values)
if baseline_values:
if _AllFloat(baseline_values):
float_baseline_values = _GetFloats(baseline_values)
else:
all_floats = False
else:
if self.NeedsBaseline():
cell.value = ''
return
float_baseline_values = None
if all_floats:
self._ComputeFloat(cell, float_values, float_baseline_values)
self._InvertIfLowerIsBetter(cell)
else:
self._ComputeString(cell, values, baseline_values)
class LiteralResult(Result):
"""A literal result."""
def __init__(self, iteration=0):
super(LiteralResult, self).__init__()
self.iteration = iteration
def Compute(self, cell, values, baseline_values):
try:
cell.value = values[self.iteration]
except IndexError:
cell.value = '-'
class NonEmptyCountResult(Result):
"""A class that counts the number of non-empty results.
The number of non-empty values will be stored in the cell.
"""
def Compute(self, cell, values, baseline_values):
"""Put the number of non-empty values in the cell result.
Args:
cell: Put the result in cell.value.
values: A list of values for the row.
baseline_values: A list of baseline values for the row.
"""
cell.value = len(_StripNone(values))
if not baseline_values:
return
base_value = len(_StripNone(baseline_values))
if cell.value == base_value:
return
f = ColorBoxFormat()
len_values = len(values)
len_baseline_values = len(baseline_values)
tmp_cell = Cell()
tmp_cell.value = 1.0 + (float(cell.value - base_value) /
(max(len_values, len_baseline_values)))
f.Compute(tmp_cell)
cell.bgcolor = tmp_cell.bgcolor
class StringMeanResult(Result):
"""Mean of string values."""
def _ComputeString(self, cell, values, baseline_values):
if self._AllStringsSame(values):
cell.value = str(values[0])
else:
cell.value = '?'
class AmeanResult(StringMeanResult):
"""Arithmetic mean."""
def _ComputeFloat(self, cell, values, baseline_values):
cell.value = numpy.mean(values)
class RawResult(Result):
"""Raw result."""
pass
class MinResult(Result):
"""Minimum."""
def _ComputeFloat(self, cell, values, baseline_values):
cell.value = min(values)
def _ComputeString(self, cell, values, baseline_values):
if values:
cell.value = min(values)
else:
cell.value = ''
class MaxResult(Result):
"""Maximum."""
def _ComputeFloat(self, cell, values, baseline_values):
cell.value = max(values)
def _ComputeString(self, cell, values, baseline_values):
if values:
cell.value = max(values)
else:
cell.value = ''
class NumericalResult(Result):
"""Numerical result."""
def _ComputeString(self, cell, values, baseline_values):
cell.value = '?'
class StdResult(NumericalResult):
"""Standard deviation."""
def _ComputeFloat(self, cell, values, baseline_values):
cell.value = numpy.std(values)
class CoeffVarResult(NumericalResult):
"""Standard deviation / Mean"""
def _ComputeFloat(self, cell, values, baseline_values):
if numpy.mean(values) != 0.0:
noise = numpy.abs(numpy.std(values) / numpy.mean(values))
else:
noise = 0.0
cell.value = noise
class ComparisonResult(Result):
"""Same or Different."""
def NeedsBaseline(self):
return True
def _ComputeString(self, cell, values, baseline_values):
value = None
baseline_value = None
if self._AllStringsSame(values):
value = values[0]
if self._AllStringsSame(baseline_values):
baseline_value = baseline_values[0]
if value is not None and baseline_value is not None:
if value == baseline_value:
cell.value = 'SAME'
else:
cell.value = 'DIFFERENT'
else:
cell.value = '?'
class PValueResult(ComparisonResult):
"""P-value."""
def _ComputeFloat(self, cell, values, baseline_values):
if len(values) < 2 or len(baseline_values) < 2:
cell.value = float('nan')
return
import stats
_, cell.value = stats.lttest_ind(values, baseline_values)
def _ComputeString(self, cell, values, baseline_values):
return float('nan')
class KeyAwareComparisonResult(ComparisonResult):
"""Automatic key aware comparison."""
def _IsLowerBetter(self, key):
# TODO(llozano): Trying to guess direction by looking at the name of the
# test does not seem like a good idea. Test frameworks should provide this
# info explicitly. I believe Telemetry has this info. Need to find it out.
#
# Below are some test names for which we are not sure what the
# direction is.
#
# For these we dont know what the direction is. But, since we dont
# specify anything, crosperf will assume higher is better:
# --percent_impl_scrolled--percent_impl_scrolled--percent
# --solid_color_tiles_analyzed--solid_color_tiles_analyzed--count
# --total_image_cache_hit_count--total_image_cache_hit_count--count
# --total_texture_upload_time_by_url
#
# About these we are doubtful but we made a guess:
# --average_num_missing_tiles_by_url--*--units (low is good)
# --experimental_mean_frame_time_by_url--*--units (low is good)
# --experimental_median_frame_time_by_url--*--units (low is good)
# --texture_upload_count--texture_upload_count--count (high is good)
# --total_deferred_image_decode_count--count (low is good)
# --total_tiles_analyzed--total_tiles_analyzed--count (high is good)
lower_is_better_keys = ['milliseconds', 'ms_', 'seconds_', 'KB', 'rdbytes',
'wrbytes', 'dropped_percent', '(ms)', '(seconds)',
'--ms', '--average_num_missing_tiles',
'--experimental_jank', '--experimental_mean_frame',
'--experimental_median_frame_time',
'--total_deferred_image_decode_count', '--seconds']
return any([l in key for l in lower_is_better_keys])
def _InvertIfLowerIsBetter(self, cell):
if self._IsLowerBetter(cell.name):
if cell.value:
cell.value = 1.0 / cell.value
class AmeanRatioResult(KeyAwareComparisonResult):
"""Ratio of arithmetic means of values vs. baseline values."""
def _ComputeFloat(self, cell, values, baseline_values):
if numpy.mean(baseline_values) != 0:
cell.value = numpy.mean(values) / numpy.mean(baseline_values)
elif numpy.mean(values) != 0:
cell.value = 0.00
# cell.value = 0 means the values and baseline_values have big difference
else:
cell.value = 1.00
# no difference if both values and baseline_values are 0
class GmeanRatioResult(KeyAwareComparisonResult):
"""Ratio of geometric means of values vs. baseline values."""
def _ComputeFloat(self, cell, values, baseline_values):
if self._GetGmean(baseline_values) != 0:
cell.value = self._GetGmean(values) / self._GetGmean(baseline_values)
elif self._GetGmean(values) != 0:
cell.value = 0.00
else:
cell.value = 1.00
class Color(object):
"""Class that represents color in RGBA format."""
def __init__(self, r=0, g=0, b=0, a=0):
self.r = r
self.g = g
self.b = b
self.a = a
def __str__(self):
return 'r: %s g: %s: b: %s: a: %s' % (self.r, self.g, self.b, self.a)
def Round(self):
"""Round RGBA values to the nearest integer."""
self.r = int(self.r)
self.g = int(self.g)
self.b = int(self.b)
self.a = int(self.a)
def GetRGB(self):
"""Get a hex representation of the color."""
return '%02x%02x%02x' % (self.r, self.g, self.b)
@classmethod
def Lerp(cls, ratio, a, b):
"""Perform linear interpolation between two colors.
Args:
ratio: The ratio to use for linear polation.
a: The first color object (used when ratio is 0).
b: The second color object (used when ratio is 1).
Returns:
Linearly interpolated color.
"""
ret = cls()
ret.r = (b.r - a.r) * ratio + a.r
ret.g = (b.g - a.g) * ratio + a.g
ret.b = (b.b - a.b) * ratio + a.b
ret.a = (b.a - a.a) * ratio + a.a
return ret
class Format(object):
"""A class that represents the format of a column."""
def __init__(self):
pass
def Compute(self, cell):
"""Computes the attributes of a cell based on its value.
Attributes typically are color, width, etc.
Args:
cell: The cell whose attributes are to be populated.
"""
if cell.value is None:
cell.string_value = ''
if isinstance(cell.value, float):
self._ComputeFloat(cell)
else:
self._ComputeString(cell)
def _ComputeFloat(self, cell):
cell.string_value = '{0:.2f}'.format(cell.value)
def _ComputeString(self, cell):
cell.string_value = str(cell.value)
def _GetColor(self, value, low, mid, high, power=6, mid_value=1.0):
min_value = 0.0
max_value = 2.0
if math.isnan(value):
return mid
if value > mid_value:
value = max_value - mid_value / value
return self._GetColorBetweenRange(value, min_value, mid_value, max_value,
low, mid, high, power)
def _GetColorBetweenRange(self, value, min_value, mid_value, max_value,
low_color, mid_color, high_color, power):
assert value <= max_value
assert value >= min_value
if value > mid_value:
value = (max_value - value) / (max_value - mid_value)
value **= power
ret = Color.Lerp(value, high_color, mid_color)
else:
value = (value - min_value) / (mid_value - min_value)
value **= power
ret = Color.Lerp(value, low_color, mid_color)
ret.Round()
return ret
class PValueFormat(Format):
"""Formatting for p-value."""
def _ComputeFloat(self, cell):
cell.string_value = '%0.2f' % float(cell.value)
if float(cell.value) < 0.05:
cell.bgcolor = self._GetColor(cell.value,
Color(255, 255, 0, 0),
Color(255, 255, 255, 0),
Color(255, 255, 255, 0),
mid_value=0.05,
power=1)
class StorageFormat(Format):
"""Format the cell as a storage number.
Example:
If the cell contains a value of 1024, the string_value will be 1.0K.
"""
def _ComputeFloat(self, cell):
base = 1024
suffices = ['K', 'M', 'G']
v = float(cell.value)
current = 0
while v >= base**(current + 1) and current < len(suffices):
current += 1
if current:
divisor = base**current
cell.string_value = '%1.1f%s' % ((v / divisor), suffices[current - 1])
else:
cell.string_value = str(cell.value)
class CoeffVarFormat(Format):
"""Format the cell as a percent.
Example:
If the cell contains a value of 1.5, the string_value will be +150%.
"""
def _ComputeFloat(self, cell):
cell.string_value = '%1.1f%%' % (float(cell.value) * 100)
cell.color = self._GetColor(cell.value,
Color(0, 255, 0, 0),
Color(0, 0, 0, 0),
Color(255, 0, 0, 0),
mid_value=0.02,
power=1)
class PercentFormat(Format):
"""Format the cell as a percent.
Example:
If the cell contains a value of 1.5, the string_value will be +50%.
"""
def _ComputeFloat(self, cell):
cell.string_value = '%+1.1f%%' % ((float(cell.value) - 1) * 100)
cell.color = self._GetColor(cell.value, Color(255, 0, 0, 0),
Color(0, 0, 0, 0), Color(0, 255, 0, 0))
class RatioFormat(Format):
"""Format the cell as a ratio.
Example:
If the cell contains a value of 1.5642, the string_value will be 1.56.
"""
def _ComputeFloat(self, cell):
cell.string_value = '%+1.1f%%' % ((cell.value - 1) * 100)
cell.color = self._GetColor(cell.value, Color(255, 0, 0, 0),
Color(0, 0, 0, 0), Color(0, 255, 0, 0))
class ColorBoxFormat(Format):
"""Format the cell as a color box.
Example:
If the cell contains a value of 1.5, it will get a green color.
If the cell contains a value of 0.5, it will get a red color.
The intensity of the green/red will be determined by how much above or below
1.0 the value is.
"""
def _ComputeFloat(self, cell):
cell.string_value = '--'
bgcolor = self._GetColor(cell.value, Color(255, 0, 0, 0),
Color(255, 255, 255, 0), Color(0, 255, 0, 0))
cell.bgcolor = bgcolor
cell.color = bgcolor
class Cell(object):
"""A class to represent a cell in a table.
Attributes:
value: The raw value of the cell.
color: The color of the cell.
bgcolor: The background color of the cell.
string_value: The string value of the cell.
suffix: A string suffix to be attached to the value when displaying.
prefix: A string prefix to be attached to the value when displaying.
color_row: Indicates whether the whole row is to inherit this cell's color.
bgcolor_row: Indicates whether the whole row is to inherit this cell's
bgcolor.
width: Optional specifier to make a column narrower than the usual width.
The usual width of a column is the max of all its cells widths.
colspan: Set the colspan of the cell in the HTML table, this is used for
table headers. Default value is 1.
name: the test name of the cell.
header: Whether this is a header in html.
"""
def __init__(self):
self.value = None
self.color = None
self.bgcolor = None
self.string_value = None
self.suffix = None
self.prefix = None
# Entire row inherits this color.
self.color_row = False
self.bgcolor_row = False
self.width = None
self.colspan = 1
self.name = None
self.header = False
def __str__(self):
l = []
l.append('value: %s' % self.value)
l.append('string_value: %s' % self.string_value)
return ' '.join(l)
class Column(object):
"""Class representing a column in a table.
Attributes:
result: an object of the Result class.
fmt: an object of the Format class.
"""
def __init__(self, result, fmt, name=''):
self.result = result
self.fmt = fmt
self.name = name
# Takes in:
# ["Key", "Label1", "Label2"]
# ["k", ["v", "v2"], [v3]]
# etc.
# Also takes in a format string.
# Returns a table like:
# ["Key", "Label1", "Label2"]
# ["k", avg("v", "v2"), stddev("v", "v2"), etc.]]
# according to format string
class TableFormatter(object):
"""Class to convert a plain table into a cell-table.
This class takes in a table generated by TableGenerator and a list of column
formats to apply to the table and returns a table of cells.
"""
def __init__(self, table, columns):
"""The constructor takes in a table and a list of columns.
Args:
table: A list of lists of values.
columns: A list of column containing what to produce and how to format it.
"""
self._table = table
self._columns = columns
self._table_columns = []
self._out_table = []
def GenerateCellTable(self, table_type):
row_index = 0
all_failed = False
for row in self._table[1:]:
# It does not make sense to put retval in the summary table.
if str(row[0]) == 'retval' and table_type == 'summary':
# Check to see if any runs passed, and update all_failed.
all_failed = True
for values in row[1:]:
if 0 in values:
all_failed = False
continue
key = Cell()
key.string_value = str(row[0])
out_row = [key]
baseline = None
for values in row[1:]:
for column in self._columns:
cell = Cell()
cell.name = key.string_value
if column.result.NeedsBaseline():
if baseline is not None:
column.result.Compute(cell, values, baseline)
column.fmt.Compute(cell)
out_row.append(cell)
if not row_index:
self._table_columns.append(column)
else:
column.result.Compute(cell, values, baseline)
column.fmt.Compute(cell)
out_row.append(cell)
if not row_index:
self._table_columns.append(column)
if baseline is None:
baseline = values
self._out_table.append(out_row)
row_index += 1
# If this is a summary table, and the only row in it is 'retval', and
# all the test runs failed, we need to a 'Results' row to the output
# table.
if table_type == 'summary' and all_failed and len(self._table) == 2:
labels_row = self._table[0]
key = Cell()
key.string_value = 'Results'
out_row = [key]
baseline = None
for _ in labels_row[1:]:
for column in self._columns:
cell = Cell()
cell.name = key.string_value
column.result.Compute(cell, ['Fail'], baseline)
column.fmt.Compute(cell)
out_row.append(cell)
if not row_index:
self._table_columns.append(column)
self._out_table.append(out_row)
def AddColumnName(self):
"""Generate Column name at the top of table."""
key = Cell()
key.header = True
key.string_value = 'Keys'
header = [key]
for column in self._table_columns:
cell = Cell()
cell.header = True
if column.name:
cell.string_value = column.name
else:
result_name = column.result.__class__.__name__
format_name = column.fmt.__class__.__name__
cell.string_value = '%s %s' % (result_name.replace('Result', ''),
format_name.replace('Format', ''))
header.append(cell)
self._out_table = [header] + self._out_table
def AddHeader(self, s):
"""Put additional string on the top of the table."""
cell = Cell()
cell.header = True
cell.string_value = str(s)
header = [cell]
colspan = max(1, max(len(row) for row in self._table))
cell.colspan = colspan
self._out_table = [header] + self._out_table
def GetPassesAndFails(self, values):
passes = 0
fails = 0
for val in values:
if val == 0:
passes = passes + 1
else:
fails = fails + 1
return passes, fails
def AddLabelName(self):
"""Put label on the top of the table."""
top_header = []
base_colspan = len([c for c in self._columns if not c.result.NeedsBaseline()
])
compare_colspan = len(self._columns)
# Find the row with the key 'retval', if it exists. This
# will be used to calculate the number of iterations that passed and
# failed for each image label.
retval_row = None
for row in self._table:
if row[0] == 'retval':
retval_row = row
# The label is organized as follows
# "keys" label_base, label_comparison1, label_comparison2
# The first cell has colspan 1, the second is base_colspan
# The others are compare_colspan
column_position = 0
for label in self._table[0]:
cell = Cell()
cell.header = True
# Put the number of pass/fail iterations in the image label header.
if column_position > 0 and retval_row:
retval_values = retval_row[column_position]
if type(retval_values) is list:
passes, fails = self.GetPassesAndFails(retval_values)
cell.string_value = str(label) + ' (pass:%d fail:%d)' % (passes,
fails)
else:
cell.string_value = str(label)
else:
cell.string_value = str(label)
if top_header:
cell.colspan = base_colspan
if len(top_header) > 1:
cell.colspan = compare_colspan
top_header.append(cell)
column_position = column_position + 1
self._out_table = [top_header] + self._out_table
def _PrintOutTable(self):
o = ''
for row in self._out_table:
for cell in row:
o += str(cell) + ' '
o += '\n'
print(o)
def GetCellTable(self, table_type='full', headers=True):
"""Function to return a table of cells.
The table (list of lists) is converted into a table of cells by this
function.
Args:
table_type: Can be 'full' or 'summary'
headers: A boolean saying whether we want default headers
Returns:
A table of cells with each cell having the properties and string values as
requiested by the columns passed in the constructor.
"""
# Generate the cell table, creating a list of dynamic columns on the fly.
if not self._out_table:
self.GenerateCellTable(table_type)
if headers:
self.AddColumnName()
self.AddLabelName()
return self._out_table
class TablePrinter(object):
"""Class to print a cell table to the console, file or html."""
PLAIN = 0
CONSOLE = 1
HTML = 2
TSV = 3
EMAIL = 4
def __init__(self, table, output_type):
"""Constructor that stores the cell table and output type."""
self._table = table
self._output_type = output_type
self._row_styles = []
self._column_styles = []
# Compute whole-table properties like max-size, etc.
def _ComputeStyle(self):
self._row_styles = []
for row in self._table:
row_style = Cell()
for cell in row:
if cell.color_row:
assert cell.color, 'Cell color not set but color_row set!'
assert not row_style.color, 'Multiple row_style.colors found!'
row_style.color = cell.color
if cell.bgcolor_row:
assert cell.bgcolor, 'Cell bgcolor not set but bgcolor_row set!'
assert not row_style.bgcolor, 'Multiple row_style.bgcolors found!'
row_style.bgcolor = cell.bgcolor
self._row_styles.append(row_style)
self._column_styles = []
if len(self._table) < 2:
return
for i in range(max(len(row) for row in self._table)):
column_style = Cell()
for row in self._table:
if not any([cell.colspan != 1 for cell in row]):
column_style.width = max(column_style.width, len(row[i].string_value))
self._column_styles.append(column_style)
def _GetBGColorFix(self, color):
if self._output_type == self.CONSOLE:
rgb = color.GetRGB()
prefix, _ = colortrans.rgb2short(rgb)
# pylint: disable=anomalous-backslash-in-string
prefix = '\033[48;5;%sm' % prefix
suffix = '\033[0m'
elif self._output_type in [self.EMAIL, self.HTML]:
rgb = color.GetRGB()
prefix = ("<FONT style=\"BACKGROUND-COLOR:#{0}\">".format(rgb))
suffix = '</FONT>'
elif self._output_type in [self.PLAIN, self.TSV]:
prefix = ''
suffix = ''
return prefix, suffix
def _GetColorFix(self, color):
if self._output_type == self.CONSOLE:
rgb = color.GetRGB()
prefix, _ = colortrans.rgb2short(rgb)
# pylint: disable=anomalous-backslash-in-string
prefix = '\033[38;5;%sm' % prefix
suffix = '\033[0m'
elif self._output_type in [self.EMAIL, self.HTML]:
rgb = color.GetRGB()
prefix = '<FONT COLOR=#{0}>'.format(rgb)
suffix = '</FONT>'
elif self._output_type in [self.PLAIN, self.TSV]:
prefix = ''
suffix = ''
return prefix, suffix
def Print(self):
"""Print the table to a console, html, etc.
Returns:
A string that contains the desired representation of the table.
"""
self._ComputeStyle()
return self._GetStringValue()
def _GetCellValue(self, i, j):
cell = self._table[i][j]
out = cell.string_value
raw_width = len(out)
if cell.color:
p, s = self._GetColorFix(cell.color)
out = '%s%s%s' % (p, out, s)
if cell.bgcolor:
p, s = self._GetBGColorFix(cell.bgcolor)
out = '%s%s%s' % (p, out, s)
if self._output_type in [self.PLAIN, self.CONSOLE, self.EMAIL]:
if cell.width:
width = cell.width
else:
if self._column_styles:
width = self._column_styles[j].width
else:
width = len(cell.string_value)
if cell.colspan > 1:
width = 0
start = 0
for k in range(j):
start += self._table[i][k].colspan
for k in range(cell.colspan):
width += self._column_styles[start + k].width
if width > raw_width:
padding = ('%' + str(width - raw_width) + 's') % ''
out = padding + out
if self._output_type == self.HTML:
if cell.header:
tag = 'th'
else:
tag = 'td'
out = "<{0} colspan = \"{2}\"> {1} </{0}>".format(tag, out, cell.colspan)
return out
def _GetHorizontalSeparator(self):
if self._output_type in [self.CONSOLE, self.PLAIN, self.EMAIL]:
return ' '
if self._output_type == self.HTML:
return ''
if self._output_type == self.TSV:
return '\t'
def _GetVerticalSeparator(self):
if self._output_type in [self.PLAIN, self.CONSOLE, self.TSV, self.EMAIL]:
return '\n'
if self._output_type == self.HTML:
return '</tr>\n<tr>'
def _GetPrefix(self):
if self._output_type in [self.PLAIN, self.CONSOLE, self.TSV, self.EMAIL]:
return ''
if self._output_type == self.HTML:
return "<p></p><table id=\"box-table-a\">\n<tr>"
def _GetSuffix(self):
if self._output_type in [self.PLAIN, self.CONSOLE, self.TSV, self.EMAIL]:
return ''
if self._output_type == self.HTML:
return '</tr>\n</table>'
def _GetStringValue(self):
o = ''
o += self._GetPrefix()
for i in range(len(self._table)):
row = self._table[i]
# Apply row color and bgcolor.
p = s = bgp = bgs = ''
if self._row_styles[i].bgcolor:
bgp, bgs = self._GetBGColorFix(self._row_styles[i].bgcolor)
if self._row_styles[i].color:
p, s = self._GetColorFix(self._row_styles[i].color)
o += p + bgp
for j in range(len(row)):
out = self._GetCellValue(i, j)
o += out + self._GetHorizontalSeparator()
o += s + bgs
o += self._GetVerticalSeparator()
o += self._GetSuffix()
return o
# Some common drivers
def GetSimpleTable(table, out_to=TablePrinter.CONSOLE):
"""Prints a simple table.
This is used by code that has a very simple list-of-lists and wants to produce
a table with ameans, a percentage ratio of ameans and a colorbox.
Args:
table: a list of lists.
out_to: specify the fomat of output. Currently it supports HTML and CONSOLE.
Returns:
A string version of the table that can be printed to the console.
Example:
GetSimpleConsoleTable([["binary", "b1", "b2"],["size", "300", "400"]])
will produce a colored table that can be printed to the console.
"""
columns = [
Column(AmeanResult(), Format()),
Column(AmeanRatioResult(), PercentFormat()),
Column(AmeanRatioResult(), ColorBoxFormat()),
]
our_table = [table[0]]
for row in table[1:]:
our_row = [row[0]]
for v in row[1:]:
our_row.append([v])
our_table.append(our_row)
tf = TableFormatter(our_table, columns)
cell_table = tf.GetCellTable()
tp = TablePrinter(cell_table, out_to)
return tp.Print()
# pylint: disable=redefined-outer-name
def GetComplexTable(runs, labels, out_to=TablePrinter.CONSOLE):
"""Prints a complex table.
This can be used to generate a table with arithmetic mean, standard deviation,
coefficient of variation, p-values, etc.
Args:
runs: A list of lists with data to tabulate.
labels: A list of labels that correspond to the runs.
out_to: specifies the format of the table (example CONSOLE or HTML).
Returns:
A string table that can be printed to the console or put in an HTML file.
"""
tg = TableGenerator(runs, labels, TableGenerator.SORT_BY_VALUES_DESC)
table = tg.GetTable()
columns = [Column(LiteralResult(), Format(), 'Literal'),
Column(AmeanResult(), Format()), Column(StdResult(), Format()),
Column(CoeffVarResult(), CoeffVarFormat()),
Column(NonEmptyCountResult(), Format()),
Column(AmeanRatioResult(), PercentFormat()),
Column(AmeanRatioResult(), RatioFormat()),
Column(GmeanRatioResult(), RatioFormat()),
Column(PValueResult(), PValueFormat())]
tf = TableFormatter(table, columns)
cell_table = tf.GetCellTable()
tp = TablePrinter(cell_table, out_to)
return tp.Print()
if __name__ == '__main__':
# Run a few small tests here.
runs = [[{'k1': '10',
'k2': '12',
'k5': '40',
'k6': '40',
'ms_1': '20',
'k7': 'FAIL',
'k8': 'PASS',
'k9': 'PASS',
'k10': '0'}, {'k1': '13',
'k2': '14',
'k3': '15',
'ms_1': '10',
'k8': 'PASS',
'k9': 'FAIL',
'k10': '0'}], [{'k1': '50',
'k2': '51',
'k3': '52',
'k4': '53',
'k5': '35',
'k6': '45',
'ms_1': '200',
'ms_2': '20',
'k7': 'FAIL',
'k8': 'PASS',
'k9': 'PASS'}]]
labels = ['vanilla', 'modified']
t = GetComplexTable(runs, labels, TablePrinter.CONSOLE)
print(t)
email = GetComplexTable(runs, labels, TablePrinter.EMAIL)
runs = [[{'k1': '1'}, {'k1': '1.1'}, {'k1': '1.2'}],
[{'k1': '5'}, {'k1': '5.1'}, {'k1': '5.2'}]]
t = GetComplexTable(runs, labels, TablePrinter.CONSOLE)
print(t)
simple_table = [
['binary', 'b1', 'b2', 'b3'],
['size', 100, 105, 108],
['rodata', 100, 80, 70],
['data', 100, 100, 100],
['debug', 100, 140, 60],
]
t = GetSimpleTable(simple_table)
print(t)
email += GetSimpleTable(simple_table, TablePrinter.HTML)
email_to = [getpass.getuser()]
email = "<pre style='font-size: 13px'>%s</pre>" % email
EmailSender().SendEmail(email_to, 'SimpleTableTest', email, msg_type='html')