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