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