Home | History | Annotate | Download | only in cros_utils
      1 # Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 """Table generating, analyzing and printing functions.
      5 
      6 This defines several classes that are used to generate, analyze and print
      7 tables.
      8 
      9 Example usage:
     10 
     11   from cros_utils import tabulator
     12 
     13   data = [["benchmark1", "33", "44"],["benchmark2", "44", "33"]]
     14   tabulator.GetSimpleTable(data)
     15 
     16 You could also use it to generate more complex tables with analysis such as
     17 p-values, custom colors, etc. Tables are generated by TableGenerator and
     18 analyzed/formatted by TableFormatter. TableFormatter can take in a list of
     19 columns with custom result computation and coloring, and will compare values in
     20 each row according to taht scheme. Here is a complex example on printing a
     21 table:
     22 
     23   from cros_utils import tabulator
     24 
     25   runs = [[{"k1": "10", "k2": "12", "k5": "40", "k6": "40",
     26             "ms_1": "20", "k7": "FAIL", "k8": "PASS", "k9": "PASS",
     27             "k10": "0"},
     28            {"k1": "13", "k2": "14", "k3": "15", "ms_1": "10", "k8": "PASS",
     29             "k9": "FAIL", "k10": "0"}],
     30           [{"k1": "50", "k2": "51", "k3": "52", "k4": "53", "k5": "35", "k6":
     31             "45", "ms_1": "200", "ms_2": "20", "k7": "FAIL", "k8": "PASS", "k9":
     32             "PASS"}]]
     33   labels = ["vanilla", "modified"]
     34   tg = TableGenerator(runs, labels, TableGenerator.SORT_BY_VALUES_DESC)
     35   table = tg.GetTable()
     36   columns = [Column(LiteralResult(),
     37                     Format(),
     38                     "Literal"),
     39              Column(AmeanResult(),
     40                     Format()),
     41              Column(StdResult(),
     42                     Format()),
     43              Column(CoeffVarResult(),
     44                     CoeffVarFormat()),
     45              Column(NonEmptyCountResult(),
     46                     Format()),
     47              Column(AmeanRatioResult(),
     48                     PercentFormat()),
     49              Column(AmeanRatioResult(),
     50                     RatioFormat()),
     51              Column(GmeanRatioResult(),
     52                     RatioFormat()),
     53              Column(PValueResult(),
     54                     PValueFormat()),
     55             ]
     56   tf = TableFormatter(table, columns)
     57   cell_table = tf.GetCellTable()
     58   tp = TablePrinter(cell_table, out_to)
     59   print tp.Print()
     60 
     61 """
     62 
     63 from __future__ import print_function
     64 
     65 import getpass
     66 import math
     67 import sys
     68 import numpy
     69 
     70 from email_sender import EmailSender
     71 import misc
     72 
     73 
     74 def _AllFloat(values):
     75   return all([misc.IsFloat(v) for v in values])
     76 
     77 
     78 def _GetFloats(values):
     79   return [float(v) for v in values]
     80 
     81 
     82 def _StripNone(results):
     83   res = []
     84   for result in results:
     85     if result is not None:
     86       res.append(result)
     87   return res
     88 
     89 
     90 class TableGenerator(object):
     91   """Creates a table from a list of list of dicts.
     92 
     93   The main public function is called GetTable().
     94   """
     95   SORT_BY_KEYS = 0
     96   SORT_BY_KEYS_DESC = 1
     97   SORT_BY_VALUES = 2
     98   SORT_BY_VALUES_DESC = 3
     99 
    100   MISSING_VALUE = 'x'
    101 
    102   def __init__(self, d, l, sort=SORT_BY_KEYS, key_name='keys'):
    103     self._runs = d
    104     self._labels = l
    105     self._sort = sort
    106     self._key_name = key_name
    107 
    108   def _AggregateKeys(self):
    109     keys = set([])
    110     for run_list in self._runs:
    111       for run in run_list:
    112         keys = keys.union(run.keys())
    113     return keys
    114 
    115   def _GetHighestValue(self, key):
    116     values = []
    117     for run_list in self._runs:
    118       for run in run_list:
    119         if key in run:
    120           values.append(run[key])
    121     values = _StripNone(values)
    122     if _AllFloat(values):
    123       values = _GetFloats(values)
    124     return max(values)
    125 
    126   def _GetLowestValue(self, key):
    127     values = []
    128     for run_list in self._runs:
    129       for run in run_list:
    130         if key in run:
    131           values.append(run[key])
    132     values = _StripNone(values)
    133     if _AllFloat(values):
    134       values = _GetFloats(values)
    135     return min(values)
    136 
    137   def _SortKeys(self, keys):
    138     if self._sort == self.SORT_BY_KEYS:
    139       return sorted(keys)
    140     elif self._sort == self.SORT_BY_VALUES:
    141       # pylint: disable=unnecessary-lambda
    142       return sorted(keys, key=lambda x: self._GetLowestValue(x))
    143     elif self._sort == self.SORT_BY_VALUES_DESC:
    144       # pylint: disable=unnecessary-lambda
    145       return sorted(keys, key=lambda x: self._GetHighestValue(x), reverse=True)
    146     else:
    147       assert 0, 'Unimplemented sort %s' % self._sort
    148 
    149   def _GetKeys(self):
    150     keys = self._AggregateKeys()
    151     return self._SortKeys(keys)
    152 
    153   def GetTable(self, number_of_rows=sys.maxint):
    154     """Returns a table from a list of list of dicts.
    155 
    156     The list of list of dicts is passed into the constructor of TableGenerator.
    157     This method converts that into a canonical list of lists which represents a
    158     table of values.
    159 
    160     Args:
    161       number_of_rows: Maximum number of rows to return from the table.
    162 
    163     Returns:
    164       A list of lists which is the table.
    165 
    166     Example:
    167       We have the following runs:
    168         [[{"k1": "v1", "k2": "v2"}, {"k1": "v3"}],
    169          [{"k1": "v4", "k4": "v5"}]]
    170       and the following labels:
    171         ["vanilla", "modified"]
    172       it will return:
    173         [["Key", "vanilla", "modified"]
    174          ["k1", ["v1", "v3"], ["v4"]]
    175          ["k2", ["v2"], []]
    176          ["k4", [], ["v5"]]]
    177       The returned table can then be processed further by other classes in this
    178       module.
    179     """
    180     keys = self._GetKeys()
    181     header = [self._key_name] + self._labels
    182     table = [header]
    183     rows = 0
    184     for k in keys:
    185       row = [k]
    186       unit = None
    187       for run_list in self._runs:
    188         v = []
    189         for run in run_list:
    190           if k in run:
    191             if type(run[k]) is list:
    192               val = run[k][0]
    193               unit = run[k][1]
    194             else:
    195               val = run[k]
    196             v.append(val)
    197           else:
    198             v.append(None)
    199         row.append(v)
    200       # If we got a 'unit' value, append the units name to the key name.
    201       if unit:
    202         keyname = row[0] + ' (%s) ' % unit
    203         row[0] = keyname
    204       table.append(row)
    205       rows += 1
    206       if rows == number_of_rows:
    207         break
    208     return table
    209 
    210 
    211 class Result(object):
    212   """A class that respresents a single result.
    213 
    214   This single result is obtained by condensing the information from a list of
    215   runs and a list of baseline runs.
    216   """
    217 
    218   def __init__(self):
    219     pass
    220 
    221   def _AllStringsSame(self, values):
    222     values_set = set(values)
    223     return len(values_set) == 1
    224 
    225   def NeedsBaseline(self):
    226     return False
    227 
    228   # pylint: disable=unused-argument
    229   def _Literal(self, cell, values, baseline_values):
    230     cell.value = ' '.join([str(v) for v in values])
    231 
    232   def _ComputeFloat(self, cell, values, baseline_values):
    233     self._Literal(cell, values, baseline_values)
    234 
    235   def _ComputeString(self, cell, values, baseline_values):
    236     self._Literal(cell, values, baseline_values)
    237 
    238   def _InvertIfLowerIsBetter(self, cell):
    239     pass
    240 
    241   def _GetGmean(self, values):
    242     if not values:
    243       return float('nan')
    244     if any([v < 0 for v in values]):
    245       return float('nan')
    246     if any([v == 0 for v in values]):
    247       return 0.0
    248     log_list = [math.log(v) for v in values]
    249     gmean_log = sum(log_list) / len(log_list)
    250     return math.exp(gmean_log)
    251 
    252   def Compute(self, cell, values, baseline_values):
    253     """Compute the result given a list of values and baseline values.
    254 
    255     Args:
    256       cell: A cell data structure to populate.
    257       values: List of values.
    258       baseline_values: List of baseline values. Can be none if this is the
    259       baseline itself.
    260     """
    261     all_floats = True
    262     values = _StripNone(values)
    263     if not values:
    264       cell.value = ''
    265       return
    266     if _AllFloat(values):
    267       float_values = _GetFloats(values)
    268     else:
    269       all_floats = False
    270     if baseline_values:
    271       baseline_values = _StripNone(baseline_values)
    272     if baseline_values:
    273       if _AllFloat(baseline_values):
    274         float_baseline_values = _GetFloats(baseline_values)
    275       else:
    276         all_floats = False
    277     else:
    278       if self.NeedsBaseline():
    279         cell.value = ''
    280         return
    281       float_baseline_values = None
    282     if all_floats:
    283       self._ComputeFloat(cell, float_values, float_baseline_values)
    284       self._InvertIfLowerIsBetter(cell)
    285     else:
    286       self._ComputeString(cell, values, baseline_values)
    287 
    288 
    289 class LiteralResult(Result):
    290   """A literal result."""
    291 
    292   def __init__(self, iteration=0):
    293     super(LiteralResult, self).__init__()
    294     self.iteration = iteration
    295 
    296   def Compute(self, cell, values, baseline_values):
    297     try:
    298       cell.value = values[self.iteration]
    299     except IndexError:
    300       cell.value = '-'
    301 
    302 
    303 class NonEmptyCountResult(Result):
    304   """A class that counts the number of non-empty results.
    305 
    306   The number of non-empty values will be stored in the cell.
    307   """
    308 
    309   def Compute(self, cell, values, baseline_values):
    310     """Put the number of non-empty values in the cell result.
    311 
    312     Args:
    313       cell: Put the result in cell.value.
    314       values: A list of values for the row.
    315       baseline_values: A list of baseline values for the row.
    316     """
    317     cell.value = len(_StripNone(values))
    318     if not baseline_values:
    319       return
    320     base_value = len(_StripNone(baseline_values))
    321     if cell.value == base_value:
    322       return
    323     f = ColorBoxFormat()
    324     len_values = len(values)
    325     len_baseline_values = len(baseline_values)
    326     tmp_cell = Cell()
    327     tmp_cell.value = 1.0 + (float(cell.value - base_value) /
    328                             (max(len_values, len_baseline_values)))
    329     f.Compute(tmp_cell)
    330     cell.bgcolor = tmp_cell.bgcolor
    331 
    332 
    333 class StringMeanResult(Result):
    334   """Mean of string values."""
    335 
    336   def _ComputeString(self, cell, values, baseline_values):
    337     if self._AllStringsSame(values):
    338       cell.value = str(values[0])
    339     else:
    340       cell.value = '?'
    341 
    342 
    343 class AmeanResult(StringMeanResult):
    344   """Arithmetic mean."""
    345 
    346   def _ComputeFloat(self, cell, values, baseline_values):
    347     cell.value = numpy.mean(values)
    348 
    349 
    350 class RawResult(Result):
    351   """Raw result."""
    352   pass
    353 
    354 
    355 class MinResult(Result):
    356   """Minimum."""
    357 
    358   def _ComputeFloat(self, cell, values, baseline_values):
    359     cell.value = min(values)
    360 
    361   def _ComputeString(self, cell, values, baseline_values):
    362     if values:
    363       cell.value = min(values)
    364     else:
    365       cell.value = ''
    366 
    367 
    368 class MaxResult(Result):
    369   """Maximum."""
    370 
    371   def _ComputeFloat(self, cell, values, baseline_values):
    372     cell.value = max(values)
    373 
    374   def _ComputeString(self, cell, values, baseline_values):
    375     if values:
    376       cell.value = max(values)
    377     else:
    378       cell.value = ''
    379 
    380 
    381 class NumericalResult(Result):
    382   """Numerical result."""
    383 
    384   def _ComputeString(self, cell, values, baseline_values):
    385     cell.value = '?'
    386 
    387 
    388 class StdResult(NumericalResult):
    389   """Standard deviation."""
    390 
    391   def _ComputeFloat(self, cell, values, baseline_values):
    392     cell.value = numpy.std(values)
    393 
    394 
    395 class CoeffVarResult(NumericalResult):
    396   """Standard deviation / Mean"""
    397 
    398   def _ComputeFloat(self, cell, values, baseline_values):
    399     if numpy.mean(values) != 0.0:
    400       noise = numpy.abs(numpy.std(values) / numpy.mean(values))
    401     else:
    402       noise = 0.0
    403     cell.value = noise
    404 
    405 
    406 class ComparisonResult(Result):
    407   """Same or Different."""
    408 
    409   def NeedsBaseline(self):
    410     return True
    411 
    412   def _ComputeString(self, cell, values, baseline_values):
    413     value = None
    414     baseline_value = None
    415     if self._AllStringsSame(values):
    416       value = values[0]
    417     if self._AllStringsSame(baseline_values):
    418       baseline_value = baseline_values[0]
    419     if value is not None and baseline_value is not None:
    420       if value == baseline_value:
    421         cell.value = 'SAME'
    422       else:
    423         cell.value = 'DIFFERENT'
    424     else:
    425       cell.value = '?'
    426 
    427 
    428 class PValueResult(ComparisonResult):
    429   """P-value."""
    430 
    431   def _ComputeFloat(self, cell, values, baseline_values):
    432     if len(values) < 2 or len(baseline_values) < 2:
    433       cell.value = float('nan')
    434       return
    435     import stats
    436     _, cell.value = stats.lttest_ind(values, baseline_values)
    437 
    438   def _ComputeString(self, cell, values, baseline_values):
    439     return float('nan')
    440 
    441 
    442 class KeyAwareComparisonResult(ComparisonResult):
    443   """Automatic key aware comparison."""
    444 
    445   def _IsLowerBetter(self, key):
    446     # TODO(llozano): Trying to guess direction by looking at the name of the
    447     # test does not seem like a good idea. Test frameworks should provide this
    448     # info explicitly. I believe Telemetry has this info. Need to find it out.
    449     #
    450     # Below are some test names for which we are not sure what the
    451     # direction is.
    452     #
    453     # For these we dont know what the direction is. But, since we dont
    454     # specify anything, crosperf will assume higher is better:
    455     # --percent_impl_scrolled--percent_impl_scrolled--percent
    456     # --solid_color_tiles_analyzed--solid_color_tiles_analyzed--count
    457     # --total_image_cache_hit_count--total_image_cache_hit_count--count
    458     # --total_texture_upload_time_by_url
    459     #
    460     # About these we are doubtful but we made a guess:
    461     # --average_num_missing_tiles_by_url--*--units (low is good)
    462     # --experimental_mean_frame_time_by_url--*--units (low is good)
    463     # --experimental_median_frame_time_by_url--*--units (low is good)
    464     # --texture_upload_count--texture_upload_count--count (high is good)
    465     # --total_deferred_image_decode_count--count (low is good)
    466     # --total_tiles_analyzed--total_tiles_analyzed--count (high is good)
    467     lower_is_better_keys = ['milliseconds', 'ms_', 'seconds_', 'KB', 'rdbytes',
    468                             'wrbytes', 'dropped_percent', '(ms)', '(seconds)',
    469                             '--ms', '--average_num_missing_tiles',
    470                             '--experimental_jank', '--experimental_mean_frame',
    471                             '--experimental_median_frame_time',
    472                             '--total_deferred_image_decode_count', '--seconds']
    473 
    474     return any([l in key for l in lower_is_better_keys])
    475 
    476   def _InvertIfLowerIsBetter(self, cell):
    477     if self._IsLowerBetter(cell.name):
    478       if cell.value:
    479         cell.value = 1.0 / cell.value
    480 
    481 
    482 class AmeanRatioResult(KeyAwareComparisonResult):
    483   """Ratio of arithmetic means of values vs. baseline values."""
    484 
    485   def _ComputeFloat(self, cell, values, baseline_values):
    486     if numpy.mean(baseline_values) != 0:
    487       cell.value = numpy.mean(values) / numpy.mean(baseline_values)
    488     elif numpy.mean(values) != 0:
    489       cell.value = 0.00
    490       # cell.value = 0 means the values and baseline_values have big difference
    491     else:
    492       cell.value = 1.00
    493       # no difference if both values and baseline_values are 0
    494 
    495 
    496 class GmeanRatioResult(KeyAwareComparisonResult):
    497   """Ratio of geometric means of values vs. baseline values."""
    498 
    499   def _ComputeFloat(self, cell, values, baseline_values):
    500     if self._GetGmean(baseline_values) != 0:
    501       cell.value = self._GetGmean(values) / self._GetGmean(baseline_values)
    502     elif self._GetGmean(values) != 0:
    503       cell.value = 0.00
    504     else:
    505       cell.value = 1.00
    506 
    507 
    508 class Color(object):
    509   """Class that represents color in RGBA format."""
    510 
    511   def __init__(self, r=0, g=0, b=0, a=0):
    512     self.r = r
    513     self.g = g
    514     self.b = b
    515     self.a = a
    516 
    517   def __str__(self):
    518     return 'r: %s g: %s: b: %s: a: %s' % (self.r, self.g, self.b, self.a)
    519 
    520   def Round(self):
    521     """Round RGBA values to the nearest integer."""
    522     self.r = int(self.r)
    523     self.g = int(self.g)
    524     self.b = int(self.b)
    525     self.a = int(self.a)
    526 
    527   def GetRGB(self):
    528     """Get a hex representation of the color."""
    529     return '%02x%02x%02x' % (self.r, self.g, self.b)
    530 
    531   @classmethod
    532   def Lerp(cls, ratio, a, b):
    533     """Perform linear interpolation between two colors.
    534 
    535     Args:
    536       ratio: The ratio to use for linear polation.
    537       a: The first color object (used when ratio is 0).
    538       b: The second color object (used when ratio is 1).
    539 
    540     Returns:
    541       Linearly interpolated color.
    542     """
    543     ret = cls()
    544     ret.r = (b.r - a.r) * ratio + a.r
    545     ret.g = (b.g - a.g) * ratio + a.g
    546     ret.b = (b.b - a.b) * ratio + a.b
    547     ret.a = (b.a - a.a) * ratio + a.a
    548     return ret
    549 
    550 
    551 class Format(object):
    552   """A class that represents the format of a column."""
    553 
    554   def __init__(self):
    555     pass
    556 
    557   def Compute(self, cell):
    558     """Computes the attributes of a cell based on its value.
    559 
    560     Attributes typically are color, width, etc.
    561 
    562     Args:
    563       cell: The cell whose attributes are to be populated.
    564     """
    565     if cell.value is None:
    566       cell.string_value = ''
    567     if isinstance(cell.value, float):
    568       self._ComputeFloat(cell)
    569     else:
    570       self._ComputeString(cell)
    571 
    572   def _ComputeFloat(self, cell):
    573     cell.string_value = '{0:.2f}'.format(cell.value)
    574 
    575   def _ComputeString(self, cell):
    576     cell.string_value = str(cell.value)
    577 
    578   def _GetColor(self, value, low, mid, high, power=6, mid_value=1.0):
    579     min_value = 0.0
    580     max_value = 2.0
    581     if math.isnan(value):
    582       return mid
    583     if value > mid_value:
    584       value = max_value - mid_value / value
    585 
    586     return self._GetColorBetweenRange(value, min_value, mid_value, max_value,
    587                                       low, mid, high, power)
    588 
    589   def _GetColorBetweenRange(self, value, min_value, mid_value, max_value,
    590                             low_color, mid_color, high_color, power):
    591     assert value <= max_value
    592     assert value >= min_value
    593     if value > mid_value:
    594       value = (max_value - value) / (max_value - mid_value)
    595       value **= power
    596       ret = Color.Lerp(value, high_color, mid_color)
    597     else:
    598       value = (value - min_value) / (mid_value - min_value)
    599       value **= power
    600       ret = Color.Lerp(value, low_color, mid_color)
    601     ret.Round()
    602     return ret
    603 
    604 
    605 class PValueFormat(Format):
    606   """Formatting for p-value."""
    607 
    608   def _ComputeFloat(self, cell):
    609     cell.string_value = '%0.2f' % float(cell.value)
    610     if float(cell.value) < 0.05:
    611       cell.bgcolor = self._GetColor(cell.value,
    612                                     Color(255, 255, 0, 0),
    613                                     Color(255, 255, 255, 0),
    614                                     Color(255, 255, 255, 0),
    615                                     mid_value=0.05,
    616                                     power=1)
    617 
    618 
    619 class StorageFormat(Format):
    620   """Format the cell as a storage number.
    621 
    622   Example:
    623     If the cell contains a value of 1024, the string_value will be 1.0K.
    624   """
    625 
    626   def _ComputeFloat(self, cell):
    627     base = 1024
    628     suffices = ['K', 'M', 'G']
    629     v = float(cell.value)
    630     current = 0
    631     while v >= base**(current + 1) and current < len(suffices):
    632       current += 1
    633 
    634     if current:
    635       divisor = base**current
    636       cell.string_value = '%1.1f%s' % ((v / divisor), suffices[current - 1])
    637     else:
    638       cell.string_value = str(cell.value)
    639 
    640 
    641 class CoeffVarFormat(Format):
    642   """Format the cell as a percent.
    643 
    644   Example:
    645     If the cell contains a value of 1.5, the string_value will be +150%.
    646   """
    647 
    648   def _ComputeFloat(self, cell):
    649     cell.string_value = '%1.1f%%' % (float(cell.value) * 100)
    650     cell.color = self._GetColor(cell.value,
    651                                 Color(0, 255, 0, 0),
    652                                 Color(0, 0, 0, 0),
    653                                 Color(255, 0, 0, 0),
    654                                 mid_value=0.02,
    655                                 power=1)
    656 
    657 
    658 class PercentFormat(Format):
    659   """Format the cell as a percent.
    660 
    661   Example:
    662     If the cell contains a value of 1.5, the string_value will be +50%.
    663   """
    664 
    665   def _ComputeFloat(self, cell):
    666     cell.string_value = '%+1.1f%%' % ((float(cell.value) - 1) * 100)
    667     cell.color = self._GetColor(cell.value, Color(255, 0, 0, 0),
    668                                 Color(0, 0, 0, 0), Color(0, 255, 0, 0))
    669 
    670 
    671 class RatioFormat(Format):
    672   """Format the cell as a ratio.
    673 
    674   Example:
    675     If the cell contains a value of 1.5642, the string_value will be 1.56.
    676   """
    677 
    678   def _ComputeFloat(self, cell):
    679     cell.string_value = '%+1.1f%%' % ((cell.value - 1) * 100)
    680     cell.color = self._GetColor(cell.value, Color(255, 0, 0, 0),
    681                                 Color(0, 0, 0, 0), Color(0, 255, 0, 0))
    682 
    683 
    684 class ColorBoxFormat(Format):
    685   """Format the cell as a color box.
    686 
    687   Example:
    688     If the cell contains a value of 1.5, it will get a green color.
    689     If the cell contains a value of 0.5, it will get a red color.
    690     The intensity of the green/red will be determined by how much above or below
    691     1.0 the value is.
    692   """
    693 
    694   def _ComputeFloat(self, cell):
    695     cell.string_value = '--'
    696     bgcolor = self._GetColor(cell.value, Color(255, 0, 0, 0),
    697                              Color(255, 255, 255, 0), Color(0, 255, 0, 0))
    698     cell.bgcolor = bgcolor
    699     cell.color = bgcolor
    700 
    701 
    702 class Cell(object):
    703   """A class to represent a cell in a table.
    704 
    705   Attributes:
    706     value: The raw value of the cell.
    707     color: The color of the cell.
    708     bgcolor: The background color of the cell.
    709     string_value: The string value of the cell.
    710     suffix: A string suffix to be attached to the value when displaying.
    711     prefix: A string prefix to be attached to the value when displaying.
    712     color_row: Indicates whether the whole row is to inherit this cell's color.
    713     bgcolor_row: Indicates whether the whole row is to inherit this cell's
    714     bgcolor.
    715     width: Optional specifier to make a column narrower than the usual width.
    716     The usual width of a column is the max of all its cells widths.
    717     colspan: Set the colspan of the cell in the HTML table, this is used for
    718     table headers. Default value is 1.
    719     name: the test name of the cell.
    720     header: Whether this is a header in html.
    721   """
    722 
    723   def __init__(self):
    724     self.value = None
    725     self.color = None
    726     self.bgcolor = None
    727     self.string_value = None
    728     self.suffix = None
    729     self.prefix = None
    730     # Entire row inherits this color.
    731     self.color_row = False
    732     self.bgcolor_row = False
    733     self.width = None
    734     self.colspan = 1
    735     self.name = None
    736     self.header = False
    737 
    738   def __str__(self):
    739     l = []
    740     l.append('value: %s' % self.value)
    741     l.append('string_value: %s' % self.string_value)
    742     return ' '.join(l)
    743 
    744 
    745 class Column(object):
    746   """Class representing a column in a table.
    747 
    748   Attributes:
    749     result: an object of the Result class.
    750     fmt: an object of the Format class.
    751   """
    752 
    753   def __init__(self, result, fmt, name=''):
    754     self.result = result
    755     self.fmt = fmt
    756     self.name = name
    757 
    758 
    759 # Takes in:
    760 # ["Key", "Label1", "Label2"]
    761 # ["k", ["v", "v2"], [v3]]
    762 # etc.
    763 # Also takes in a format string.
    764 # Returns a table like:
    765 # ["Key", "Label1", "Label2"]
    766 # ["k", avg("v", "v2"), stddev("v", "v2"), etc.]]
    767 # according to format string
    768 class TableFormatter(object):
    769   """Class to convert a plain table into a cell-table.
    770 
    771   This class takes in a table generated by TableGenerator and a list of column
    772   formats to apply to the table and returns a table of cells.
    773   """
    774 
    775   def __init__(self, table, columns):
    776     """The constructor takes in a table and a list of columns.
    777 
    778     Args:
    779       table: A list of lists of values.
    780       columns: A list of column containing what to produce and how to format it.
    781     """
    782     self._table = table
    783     self._columns = columns
    784     self._table_columns = []
    785     self._out_table = []
    786 
    787   def GenerateCellTable(self, table_type):
    788     row_index = 0
    789     all_failed = False
    790 
    791     for row in self._table[1:]:
    792       # It does not make sense to put retval in the summary table.
    793       if str(row[0]) == 'retval' and table_type == 'summary':
    794         # Check to see if any runs passed, and update all_failed.
    795         all_failed = True
    796         for values in row[1:]:
    797           if 0 in values:
    798             all_failed = False
    799         continue
    800       key = Cell()
    801       key.string_value = str(row[0])
    802       out_row = [key]
    803       baseline = None
    804       for values in row[1:]:
    805         for column in self._columns:
    806           cell = Cell()
    807           cell.name = key.string_value
    808           if column.result.NeedsBaseline():
    809             if baseline is not None:
    810               column.result.Compute(cell, values, baseline)
    811               column.fmt.Compute(cell)
    812               out_row.append(cell)
    813               if not row_index:
    814                 self._table_columns.append(column)
    815           else:
    816             column.result.Compute(cell, values, baseline)
    817             column.fmt.Compute(cell)
    818             out_row.append(cell)
    819             if not row_index:
    820               self._table_columns.append(column)
    821 
    822         if baseline is None:
    823           baseline = values
    824       self._out_table.append(out_row)
    825       row_index += 1
    826 
    827     # If this is a summary table, and the only row in it is 'retval', and
    828     # all the test runs failed, we need to a 'Results' row to the output
    829     # table.
    830     if table_type == 'summary' and all_failed and len(self._table) == 2:
    831       labels_row = self._table[0]
    832       key = Cell()
    833       key.string_value = 'Results'
    834       out_row = [key]
    835       baseline = None
    836       for _ in labels_row[1:]:
    837         for column in self._columns:
    838           cell = Cell()
    839           cell.name = key.string_value
    840           column.result.Compute(cell, ['Fail'], baseline)
    841           column.fmt.Compute(cell)
    842           out_row.append(cell)
    843           if not row_index:
    844             self._table_columns.append(column)
    845       self._out_table.append(out_row)
    846 
    847   def AddColumnName(self):
    848     """Generate Column name at the top of table."""
    849     key = Cell()
    850     key.header = True
    851     key.string_value = 'Keys'
    852     header = [key]
    853     for column in self._table_columns:
    854       cell = Cell()
    855       cell.header = True
    856       if column.name:
    857         cell.string_value = column.name
    858       else:
    859         result_name = column.result.__class__.__name__
    860         format_name = column.fmt.__class__.__name__
    861 
    862         cell.string_value = '%s %s' % (result_name.replace('Result', ''),
    863                                        format_name.replace('Format', ''))
    864 
    865       header.append(cell)
    866 
    867     self._out_table = [header] + self._out_table
    868 
    869   def AddHeader(self, s):
    870     """Put additional string on the top of the table."""
    871     cell = Cell()
    872     cell.header = True
    873     cell.string_value = str(s)
    874     header = [cell]
    875     colspan = max(1, max(len(row) for row in self._table))
    876     cell.colspan = colspan
    877     self._out_table = [header] + self._out_table
    878 
    879   def GetPassesAndFails(self, values):
    880     passes = 0
    881     fails = 0
    882     for val in values:
    883       if val == 0:
    884         passes = passes + 1
    885       else:
    886         fails = fails + 1
    887     return passes, fails
    888 
    889   def AddLabelName(self):
    890     """Put label on the top of the table."""
    891     top_header = []
    892     base_colspan = len([c for c in self._columns if not c.result.NeedsBaseline()
    893                        ])
    894     compare_colspan = len(self._columns)
    895     # Find the row with the key 'retval', if it exists.  This
    896     # will be used to calculate the number of iterations that passed and
    897     # failed for each image label.
    898     retval_row = None
    899     for row in self._table:
    900       if row[0] == 'retval':
    901         retval_row = row
    902     # The label is organized as follows
    903     # "keys" label_base, label_comparison1, label_comparison2
    904     # The first cell has colspan 1, the second is base_colspan
    905     # The others are compare_colspan
    906     column_position = 0
    907     for label in self._table[0]:
    908       cell = Cell()
    909       cell.header = True
    910       # Put the number of pass/fail iterations in the image label header.
    911       if column_position > 0 and retval_row:
    912         retval_values = retval_row[column_position]
    913         if type(retval_values) is list:
    914           passes, fails = self.GetPassesAndFails(retval_values)
    915           cell.string_value = str(label) + '  (pass:%d fail:%d)' % (passes,
    916                                                                     fails)
    917         else:
    918           cell.string_value = str(label)
    919       else:
    920         cell.string_value = str(label)
    921       if top_header:
    922         cell.colspan = base_colspan
    923       if len(top_header) > 1:
    924         cell.colspan = compare_colspan
    925       top_header.append(cell)
    926       column_position = column_position + 1
    927     self._out_table = [top_header] + self._out_table
    928 
    929   def _PrintOutTable(self):
    930     o = ''
    931     for row in self._out_table:
    932       for cell in row:
    933         o += str(cell) + ' '
    934       o += '\n'
    935     print(o)
    936 
    937   def GetCellTable(self, table_type='full', headers=True):
    938     """Function to return a table of cells.
    939 
    940     The table (list of lists) is converted into a table of cells by this
    941     function.
    942 
    943     Args:
    944       table_type: Can be 'full' or 'summary'
    945       headers: A boolean saying whether we want default headers
    946 
    947     Returns:
    948       A table of cells with each cell having the properties and string values as
    949       requiested by the columns passed in the constructor.
    950     """
    951     # Generate the cell table, creating a list of dynamic columns on the fly.
    952     if not self._out_table:
    953       self.GenerateCellTable(table_type)
    954     if headers:
    955       self.AddColumnName()
    956       self.AddLabelName()
    957     return self._out_table
    958 
    959 
    960 class TablePrinter(object):
    961   """Class to print a cell table to the console, file or html."""
    962   PLAIN = 0
    963   CONSOLE = 1
    964   HTML = 2
    965   TSV = 3
    966   EMAIL = 4
    967 
    968   def __init__(self, table, output_type):
    969     """Constructor that stores the cell table and output type."""
    970     self._table = table
    971     self._output_type = output_type
    972     self._row_styles = []
    973     self._column_styles = []
    974 
    975   # Compute whole-table properties like max-size, etc.
    976   def _ComputeStyle(self):
    977     self._row_styles = []
    978     for row in self._table:
    979       row_style = Cell()
    980       for cell in row:
    981         if cell.color_row:
    982           assert cell.color, 'Cell color not set but color_row set!'
    983           assert not row_style.color, 'Multiple row_style.colors found!'
    984           row_style.color = cell.color
    985         if cell.bgcolor_row:
    986           assert cell.bgcolor, 'Cell bgcolor not set but bgcolor_row set!'
    987           assert not row_style.bgcolor, 'Multiple row_style.bgcolors found!'
    988           row_style.bgcolor = cell.bgcolor
    989       self._row_styles.append(row_style)
    990 
    991     self._column_styles = []
    992     if len(self._table) < 2:
    993       return
    994 
    995     for i in range(max(len(row) for row in self._table)):
    996       column_style = Cell()
    997       for row in self._table:
    998         if not any([cell.colspan != 1 for cell in row]):
    999           column_style.width = max(column_style.width, len(row[i].string_value))
   1000       self._column_styles.append(column_style)
   1001 
   1002   def _GetBGColorFix(self, color):
   1003     if self._output_type == self.CONSOLE:
   1004       prefix = misc.rgb2short(color.r, color.g, color.b)
   1005       # pylint: disable=anomalous-backslash-in-string
   1006       prefix = '\033[48;5;%sm' % prefix
   1007       suffix = '\033[0m'
   1008     elif self._output_type in [self.EMAIL, self.HTML]:
   1009       rgb = color.GetRGB()
   1010       prefix = ("<FONT style=\"BACKGROUND-COLOR:#{0}\">".format(rgb))
   1011       suffix = '</FONT>'
   1012     elif self._output_type in [self.PLAIN, self.TSV]:
   1013       prefix = ''
   1014       suffix = ''
   1015     return prefix, suffix
   1016 
   1017   def _GetColorFix(self, color):
   1018     if self._output_type == self.CONSOLE:
   1019       prefix = misc.rgb2short(color.r, color.g, color.b)
   1020       # pylint: disable=anomalous-backslash-in-string
   1021       prefix = '\033[38;5;%sm' % prefix
   1022       suffix = '\033[0m'
   1023     elif self._output_type in [self.EMAIL, self.HTML]:
   1024       rgb = color.GetRGB()
   1025       prefix = '<FONT COLOR=#{0}>'.format(rgb)
   1026       suffix = '</FONT>'
   1027     elif self._output_type in [self.PLAIN, self.TSV]:
   1028       prefix = ''
   1029       suffix = ''
   1030     return prefix, suffix
   1031 
   1032   def Print(self):
   1033     """Print the table to a console, html, etc.
   1034 
   1035     Returns:
   1036       A string that contains the desired representation of the table.
   1037     """
   1038     self._ComputeStyle()
   1039     return self._GetStringValue()
   1040 
   1041   def _GetCellValue(self, i, j):
   1042     cell = self._table[i][j]
   1043     out = cell.string_value
   1044     raw_width = len(out)
   1045 
   1046     if cell.color:
   1047       p, s = self._GetColorFix(cell.color)
   1048       out = '%s%s%s' % (p, out, s)
   1049 
   1050     if cell.bgcolor:
   1051       p, s = self._GetBGColorFix(cell.bgcolor)
   1052       out = '%s%s%s' % (p, out, s)
   1053 
   1054     if self._output_type in [self.PLAIN, self.CONSOLE, self.EMAIL]:
   1055       if cell.width:
   1056         width = cell.width
   1057       else:
   1058         if self._column_styles:
   1059           width = self._column_styles[j].width
   1060         else:
   1061           width = len(cell.string_value)
   1062       if cell.colspan > 1:
   1063         width = 0
   1064         start = 0
   1065         for k in range(j):
   1066           start += self._table[i][k].colspan
   1067         for k in range(cell.colspan):
   1068           width += self._column_styles[start + k].width
   1069       if width > raw_width:
   1070         padding = ('%' + str(width - raw_width) + 's') % ''
   1071         out = padding + out
   1072 
   1073     if self._output_type == self.HTML:
   1074       if cell.header:
   1075         tag = 'th'
   1076       else:
   1077         tag = 'td'
   1078       out = "<{0} colspan = \"{2}\"> {1} </{0}>".format(tag, out, cell.colspan)
   1079 
   1080     return out
   1081 
   1082   def _GetHorizontalSeparator(self):
   1083     if self._output_type in [self.CONSOLE, self.PLAIN, self.EMAIL]:
   1084       return ' '
   1085     if self._output_type == self.HTML:
   1086       return ''
   1087     if self._output_type == self.TSV:
   1088       return '\t'
   1089 
   1090   def _GetVerticalSeparator(self):
   1091     if self._output_type in [self.PLAIN, self.CONSOLE, self.TSV, self.EMAIL]:
   1092       return '\n'
   1093     if self._output_type == self.HTML:
   1094       return '</tr>\n<tr>'
   1095 
   1096   def _GetPrefix(self):
   1097     if self._output_type in [self.PLAIN, self.CONSOLE, self.TSV, self.EMAIL]:
   1098       return ''
   1099     if self._output_type == self.HTML:
   1100       return "<p></p><table id=\"box-table-a\">\n<tr>"
   1101 
   1102   def _GetSuffix(self):
   1103     if self._output_type in [self.PLAIN, self.CONSOLE, self.TSV, self.EMAIL]:
   1104       return ''
   1105     if self._output_type == self.HTML:
   1106       return '</tr>\n</table>'
   1107 
   1108   def _GetStringValue(self):
   1109     o = ''
   1110     o += self._GetPrefix()
   1111     for i in range(len(self._table)):
   1112       row = self._table[i]
   1113       # Apply row color and bgcolor.
   1114       p = s = bgp = bgs = ''
   1115       if self._row_styles[i].bgcolor:
   1116         bgp, bgs = self._GetBGColorFix(self._row_styles[i].bgcolor)
   1117       if self._row_styles[i].color:
   1118         p, s = self._GetColorFix(self._row_styles[i].color)
   1119       o += p + bgp
   1120       for j in range(len(row)):
   1121         out = self._GetCellValue(i, j)
   1122         o += out + self._GetHorizontalSeparator()
   1123       o += s + bgs
   1124       o += self._GetVerticalSeparator()
   1125     o += self._GetSuffix()
   1126     return o
   1127 
   1128 
   1129 # Some common drivers
   1130 def GetSimpleTable(table, out_to=TablePrinter.CONSOLE):
   1131   """Prints a simple table.
   1132 
   1133   This is used by code that has a very simple list-of-lists and wants to produce
   1134   a table with ameans, a percentage ratio of ameans and a colorbox.
   1135 
   1136   Args:
   1137     table: a list of lists.
   1138     out_to: specify the fomat of output. Currently it supports HTML and CONSOLE.
   1139 
   1140   Returns:
   1141     A string version of the table that can be printed to the console.
   1142 
   1143   Example:
   1144     GetSimpleConsoleTable([["binary", "b1", "b2"],["size", "300", "400"]])
   1145     will produce a colored table that can be printed to the console.
   1146   """
   1147   columns = [
   1148       Column(AmeanResult(), Format()),
   1149       Column(AmeanRatioResult(), PercentFormat()),
   1150       Column(AmeanRatioResult(), ColorBoxFormat()),
   1151   ]
   1152   our_table = [table[0]]
   1153   for row in table[1:]:
   1154     our_row = [row[0]]
   1155     for v in row[1:]:
   1156       our_row.append([v])
   1157     our_table.append(our_row)
   1158 
   1159   tf = TableFormatter(our_table, columns)
   1160   cell_table = tf.GetCellTable()
   1161   tp = TablePrinter(cell_table, out_to)
   1162   return tp.Print()
   1163 
   1164 
   1165 # pylint: disable=redefined-outer-name
   1166 def GetComplexTable(runs, labels, out_to=TablePrinter.CONSOLE):
   1167   """Prints a complex table.
   1168 
   1169   This can be used to generate a table with arithmetic mean, standard deviation,
   1170   coefficient of variation, p-values, etc.
   1171 
   1172   Args:
   1173     runs: A list of lists with data to tabulate.
   1174     labels: A list of labels that correspond to the runs.
   1175     out_to: specifies the format of the table (example CONSOLE or HTML).
   1176 
   1177   Returns:
   1178     A string table that can be printed to the console or put in an HTML file.
   1179   """
   1180   tg = TableGenerator(runs, labels, TableGenerator.SORT_BY_VALUES_DESC)
   1181   table = tg.GetTable()
   1182   columns = [Column(LiteralResult(), Format(), 'Literal'),
   1183              Column(AmeanResult(), Format()), Column(StdResult(), Format()),
   1184              Column(CoeffVarResult(), CoeffVarFormat()),
   1185              Column(NonEmptyCountResult(), Format()),
   1186              Column(AmeanRatioResult(), PercentFormat()),
   1187              Column(AmeanRatioResult(), RatioFormat()),
   1188              Column(GmeanRatioResult(), RatioFormat()),
   1189              Column(PValueResult(), PValueFormat())]
   1190   tf = TableFormatter(table, columns)
   1191   cell_table = tf.GetCellTable()
   1192   tp = TablePrinter(cell_table, out_to)
   1193   return tp.Print()
   1194 
   1195 
   1196 if __name__ == '__main__':
   1197   # Run a few small tests here.
   1198   runs = [[{'k1': '10',
   1199             'k2': '12',
   1200             'k5': '40',
   1201             'k6': '40',
   1202             'ms_1': '20',
   1203             'k7': 'FAIL',
   1204             'k8': 'PASS',
   1205             'k9': 'PASS',
   1206             'k10': '0'}, {'k1': '13',
   1207                           'k2': '14',
   1208                           'k3': '15',
   1209                           'ms_1': '10',
   1210                           'k8': 'PASS',
   1211                           'k9': 'FAIL',
   1212                           'k10': '0'}], [{'k1': '50',
   1213                                           'k2': '51',
   1214                                           'k3': '52',
   1215                                           'k4': '53',
   1216                                           'k5': '35',
   1217                                           'k6': '45',
   1218                                           'ms_1': '200',
   1219                                           'ms_2': '20',
   1220                                           'k7': 'FAIL',
   1221                                           'k8': 'PASS',
   1222                                           'k9': 'PASS'}]]
   1223   labels = ['vanilla', 'modified']
   1224   t = GetComplexTable(runs, labels, TablePrinter.CONSOLE)
   1225   print(t)
   1226   email = GetComplexTable(runs, labels, TablePrinter.EMAIL)
   1227 
   1228   runs = [[{'k1': '1'}, {'k1': '1.1'}, {'k1': '1.2'}],
   1229           [{'k1': '5'}, {'k1': '5.1'}, {'k1': '5.2'}]]
   1230   t = GetComplexTable(runs, labels, TablePrinter.CONSOLE)
   1231   print(t)
   1232 
   1233   simple_table = [
   1234       ['binary', 'b1', 'b2', 'b3'],
   1235       ['size', 100, 105, 108],
   1236       ['rodata', 100, 80, 70],
   1237       ['data', 100, 100, 100],
   1238       ['debug', 100, 140, 60],
   1239   ]
   1240   t = GetSimpleTable(simple_table)
   1241   print(t)
   1242   email += GetSimpleTable(simple_table, TablePrinter.HTML)
   1243   email_to = [getpass.getuser()]
   1244   email = "<pre style='font-size: 13px'>%s</pre>" % email
   1245   EmailSender().SendEmail(email_to, 'SimpleTableTest', email, msg_type='html')
   1246