• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
6This defines several classes that are used to generate, analyze and print
7tables.
8
9Example usage:
10
11  from cros_utils import tabulator
12
13  data = [["benchmark1", "33", "44"],["benchmark2", "44", "33"]]
14  tabulator.GetSimpleTable(data)
15
16You could also use it to generate more complex tables with analysis such as
17p-values, custom colors, etc. Tables are generated by TableGenerator and
18analyzed/formatted by TableFormatter. TableFormatter can take in a list of
19columns with custom result computation and coloring, and will compare values in
20each row according to taht scheme. Here is a complex example on printing a
21table:
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
63from __future__ import print_function
64
65import getpass
66import math
67import sys
68import numpy
69
70from email_sender import EmailSender
71import misc
72
73
74def _AllFloat(values):
75  return all([misc.IsFloat(v) for v in values])
76
77
78def _GetFloats(values):
79  return [float(v) for v in values]
80
81
82def _StripNone(results):
83  res = []
84  for result in results:
85    if result is not None:
86      res.append(result)
87  return res
88
89
90class 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
211class 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
289class 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
303class 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
333class 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
343class AmeanResult(StringMeanResult):
344  """Arithmetic mean."""
345
346  def _ComputeFloat(self, cell, values, baseline_values):
347    cell.value = numpy.mean(values)
348
349
350class RawResult(Result):
351  """Raw result."""
352  pass
353
354
355class 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
368class 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
381class NumericalResult(Result):
382  """Numerical result."""
383
384  def _ComputeString(self, cell, values, baseline_values):
385    cell.value = '?'
386
387
388class StdResult(NumericalResult):
389  """Standard deviation."""
390
391  def _ComputeFloat(self, cell, values, baseline_values):
392    cell.value = numpy.std(values)
393
394
395class 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
406class 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
428class 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
442class 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
482class 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
496class 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
508class 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
551class 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
605class 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
619class 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
641class 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
658class 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
671class 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
684class 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
702class 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
745class 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
768class 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
960class 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
1130def 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
1166def 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
1196if __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