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