You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1245 lines
37 KiB
1245 lines
37 KiB
# 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 |
|
|
|
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: |
|
prefix = misc.rgb2short(color.r, color.g, color.b) |
|
# 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: |
|
prefix = misc.rgb2short(color.r, color.g, color.b) |
|
# 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')
|
|
|