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