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