1# Copyright (c) 2017 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 5import collections 6import json 7import logging 8import numpy 9import operator 10import os 11import re 12import time 13import urllib 14import urllib2 15 16from autotest_lib.client.bin import utils 17from autotest_lib.client.common_lib import error 18from autotest_lib.client.common_lib import lsbrelease_utils 19from autotest_lib.client.common_lib.cros import retry 20from autotest_lib.client.cros.power import power_status 21from autotest_lib.client.cros.power import power_utils 22 23_HTML_CHART_STR = ''' 24<!DOCTYPE html> 25<html> 26<head> 27<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"> 28</script> 29<script type="text/javascript"> 30 google.charts.load('current', {{'packages':['corechart']}}); 31 google.charts.setOnLoadCallback(drawChart); 32 function drawChart() {{ 33 var data = google.visualization.arrayToDataTable([ 34{data} 35 ]); 36 var numDataCols = data.getNumberOfColumns() - 1; 37 var unit = '{unit}'; 38 var options = {{ 39 width: 1600, 40 height: 1200, 41 lineWidth: 1, 42 legend: {{ position: 'top', maxLines: 3 }}, 43 vAxis: {{ viewWindow: {{min: 0}}, title: '{type} ({unit})' }}, 44 hAxis: {{ viewWindow: {{min: 0}}, title: 'time (second)' }}, 45 }}; 46 var element = document.getElementById('{type}'); 47 var chart; 48 if (unit == 'percent') {{ 49 options['isStacked'] = true; 50 if (numDataCols == 2) {{ 51 options['colors'] = ['#d32f2f', '#43a047'] 52 }} else if (numDataCols <= 4) {{ 53 options['colors'] = ['#d32f2f', '#f4c7c3', '#cddc39','#43a047']; 54 }} else if (numDataCols <= 9) {{ 55 options['colors'] = ['#d32f2f', '#e57373', '#f4c7c3', '#ffccbc', 56 '#f0f4c3', '#c8e6c9', '#cddc39', '#81c784', '#43a047']; 57 }} 58 chart = new google.visualization.SteppedAreaChart(element); 59 }} else {{ 60 chart = new google.visualization.LineChart(element); 61 }} 62 chart.draw(data, options); 63 }} 64</script> 65</head> 66<body> 67<div id="{type}"></div> 68</body> 69</html> 70''' 71 72 73class BaseDashboard(object): 74 """Base class that implements method for prepare and upload data to power 75 dashboard. 76 """ 77 78 def __init__(self, logger, testname, start_ts=None, resultsdir=None, 79 uploadurl=None): 80 """Create BaseDashboard objects. 81 82 Args: 83 logger: object that store the log. This will get convert to 84 dictionary by self._convert() 85 testname: name of current test 86 start_ts: timestamp of when test started in seconds since epoch 87 resultsdir: directory to save the power json 88 uploadurl: url to upload power data 89 """ 90 self._logger = logger 91 self._testname = testname 92 self._start_ts = start_ts if start_ts else time.time() 93 self._resultsdir = resultsdir 94 self._uploadurl = uploadurl 95 96 def _create_powerlog_dict(self, raw_measurement): 97 """Create powerlog dictionary from raw measurement data 98 Data format in go/power-dashboard-data. 99 100 Args: 101 raw_measurement: dictionary contains raw measurement data 102 103 Returns: 104 A dictionary of powerlog 105 """ 106 powerlog_dict = { 107 'format_version': 5, 108 'timestamp': self._start_ts, 109 'test': self._testname, 110 'dut': self._create_dut_info_dict(raw_measurement['data'].keys()), 111 'power': raw_measurement, 112 } 113 114 return powerlog_dict 115 116 def _create_dut_info_dict(self, power_rails): 117 """Create a dictionary that contain information of the DUT. 118 119 MUST be implemented in subclass. 120 121 Args: 122 power_rails: list of measured power rails 123 124 Returns: 125 DUT info dictionary 126 """ 127 raise NotImplementedError 128 129 def _save_json(self, powerlog_dict, resultsdir, filename='power_log.json'): 130 """Convert powerlog dict to human readable formatted JSON and 131 append to <resultsdir>/<filename>. 132 133 Args: 134 powerlog_dict: dictionary of power data 135 resultsdir: directory to save formatted JSON object 136 filename: filename to append to 137 """ 138 if not os.path.exists(resultsdir): 139 raise error.TestError('resultsdir %s does not exist.' % resultsdir) 140 filename = os.path.join(resultsdir, filename) 141 json_str = json.dumps(powerlog_dict, indent=4, separators=(',', ': '), 142 ensure_ascii=False) 143 json_str = utils.strip_non_printable(json_str) 144 with file(filename, 'a') as f: 145 f.write(json_str) 146 147 def _save_html(self, powerlog_dict, resultsdir, filename='power_log.html'): 148 """Convert powerlog dict to chart in HTML page and append to 149 <resultsdir>/<filename>. 150 151 Note that this results in multiple HTML objects in one file but Chrome 152 can render all of it in one page. 153 154 Args: 155 powerlog_dict: dictionary of power data 156 resultsdir: directory to save HTML page 157 filename: filename to append to 158 """ 159 # Create dict from type to sorted list of rail names. 160 rail_type = collections.defaultdict(list) 161 for r, t in powerlog_dict['power']['type'].iteritems(): 162 rail_type[t].append(r) 163 for t in rail_type: 164 rail_type[t] = sorted(rail_type[t]) 165 166 html_str = '' 167 row_indent = ' ' * 12 168 for t in rail_type: 169 data_str_list = [] 170 171 # Generate rail name data string. 172 header = ['time'] + rail_type[t] 173 header_str = row_indent + "['" + "', '".join(header) + "']" 174 data_str_list.append(header_str) 175 176 # Generate measurements data string. 177 for i in range(powerlog_dict['power']['sample_count']): 178 row = [str(i * powerlog_dict['power']['sample_duration'])] 179 for r in rail_type[t]: 180 row.append(str(powerlog_dict['power']['data'][r][i])) 181 row_str = row_indent + '[' + ', '.join(row) + ']' 182 data_str_list.append(row_str) 183 184 data_str = ',\n'.join(data_str_list) 185 unit = powerlog_dict['power']['unit'][rail_type[t][0]] 186 html_str += _HTML_CHART_STR.format(data=data_str, unit=unit, type=t) 187 188 if not os.path.exists(resultsdir): 189 raise error.TestError('resultsdir %s does not exist.' % resultsdir) 190 filename = os.path.join(resultsdir, filename) 191 with file(filename, 'a') as f: 192 f.write(html_str) 193 194 def _upload(self, powerlog_dict, uploadurl): 195 """Convert powerlog dict to minimal size JSON and upload to dashboard. 196 197 Args: 198 powerlog_dict: dictionary of power data 199 uploadurl: url to upload the power data 200 """ 201 json_str = json.dumps(powerlog_dict, ensure_ascii=False) 202 data_obj = {'data': utils.strip_non_printable(json_str)} 203 encoded = urllib.urlencode(data_obj) 204 req = urllib2.Request(uploadurl, encoded) 205 206 @retry.retry(urllib2.URLError, blacklist=[urllib2.HTTPError], 207 timeout_min=5.0, delay_sec=1, backoff=2) 208 def _do_upload(): 209 urllib2.urlopen(req) 210 211 _do_upload() 212 213 def _create_checkpoint_dict(self): 214 """Create dictionary for checkpoint. 215 216 @returns a dictionary of tags to their corresponding intervals in the 217 following format: 218 { 219 tag1: [(start1, end1), (start2, end2), ...], 220 tag2: [(start3, end3), (start4, end4), ...], 221 ... 222 } 223 """ 224 raise NotImplementedError 225 226 def _tag_with_checkpoint(self, power_dict): 227 """Tag power_dict with checkpoint data. 228 229 This function translates the checkpoint intervals into a list of tags 230 for each data point. 231 232 @param power_dict: a dictionary with power data; assume this dictionary 233 has attributes 'sample_count' and 'sample_duration'. 234 """ 235 checkpoint_dict = self._create_checkpoint_dict() 236 237 # Create list of check point event tuple. 238 # Tuple format: (checkpoint_name:str, event_time:float, is_start:bool) 239 checkpoint_event_list = [] 240 for name, intervals in checkpoint_dict.iteritems(): 241 for start, finish in intervals: 242 checkpoint_event_list.append((name, start, True)) 243 checkpoint_event_list.append((name, finish, False)) 244 245 checkpoint_event_list = sorted(checkpoint_event_list, 246 key=operator.itemgetter(1)) 247 248 # Add dummy check point at 1e9 seconds. 249 checkpoint_event_list.append(('dummy', 1e9, True)) 250 251 interval_set = set() 252 event_index = 0 253 checkpoint_list = [] 254 for i in range(power_dict['sample_count']): 255 curr_time = i * power_dict['sample_duration'] 256 257 # Process every checkpoint event until current point of time 258 while checkpoint_event_list[event_index][1] <= curr_time: 259 name, _, is_start = checkpoint_event_list[event_index] 260 if is_start: 261 interval_set.add(name) 262 else: 263 interval_set.discard(name) 264 event_index += 1 265 266 checkpoint_list.append(list(interval_set)) 267 power_dict['checkpoint'] = checkpoint_list 268 269 def _convert(self): 270 """Convert data from self._logger object to raw power measurement 271 dictionary. 272 273 MUST be implemented in subclass. 274 275 Return: 276 raw measurement dictionary 277 """ 278 raise NotImplementedError 279 280 def upload(self): 281 """Upload powerlog to dashboard and save data to results directory. 282 """ 283 raw_measurement = self._convert() 284 if raw_measurement is None: 285 return 286 287 powerlog_dict = self._create_powerlog_dict(raw_measurement) 288 if self._resultsdir is not None: 289 self._save_json(powerlog_dict, self._resultsdir) 290 self._save_html(powerlog_dict, self._resultsdir) 291 if self._uploadurl is not None: 292 self._upload(powerlog_dict, self._uploadurl) 293 294 295class ClientTestDashboard(BaseDashboard): 296 """Dashboard class for autotests that run on client side. 297 """ 298 299 def __init__(self, logger, testname, start_ts=None, resultsdir=None, 300 uploadurl=None, note=''): 301 """Create BaseDashboard objects. 302 303 Args: 304 logger: object that store the log. This will get convert to 305 dictionary by self._convert() 306 testname: name of current test 307 start_ts: timestamp of when test started in seconds since epoch 308 resultsdir: directory to save the power json 309 uploadurl: url to upload power data 310 note: note for current test run 311 """ 312 super(ClientTestDashboard, self).__init__(logger, testname, start_ts, 313 resultsdir, uploadurl) 314 self._note = note 315 316 317 def _create_dut_info_dict(self, power_rails): 318 """Create a dictionary that contain information of the DUT. 319 320 Args: 321 power_rails: list of measured power rails 322 323 Returns: 324 DUT info dictionary 325 """ 326 board = utils.get_board() 327 platform = utils.get_platform() 328 329 if not platform.startswith(board): 330 board += '_' + platform 331 332 if power_utils.has_hammer(): 333 board += '_hammer' 334 335 dut_info_dict = { 336 'board': board, 337 'version': { 338 'hw': utils.get_hardware_revision(), 339 'milestone': lsbrelease_utils.get_chromeos_release_milestone(), 340 'os': lsbrelease_utils.get_chromeos_release_version(), 341 'channel': lsbrelease_utils.get_chromeos_channel(), 342 'firmware': utils.get_firmware_version(), 343 'ec': utils.get_ec_version(), 344 'kernel': utils.get_kernel_version(), 345 }, 346 'sku': { 347 'cpu': utils.get_cpu_name(), 348 'memory_size': utils.get_mem_total_gb(), 349 'storage_size': utils.get_disk_size_gb(utils.get_root_device()), 350 'display_resolution': utils.get_screen_resolution(), 351 }, 352 'ina': { 353 'version': 0, 354 'ina': power_rails, 355 }, 356 'note': self._note, 357 } 358 359 if power_utils.has_battery(): 360 status = power_status.get_status() 361 if status.battery: 362 # Round the battery size to nearest tenth because it is 363 # fluctuated for platform without battery nominal voltage data. 364 dut_info_dict['sku']['battery_size'] = round( 365 status.battery.energy_full_design, 1) 366 dut_info_dict['sku']['battery_shutdown_percent'] = \ 367 power_utils.get_low_battery_shutdown_percent() 368 return dut_info_dict 369 370 371class MeasurementLoggerDashboard(ClientTestDashboard): 372 """Dashboard class for power_status.MeasurementLogger. 373 """ 374 375 def __init__(self, logger, testname, resultsdir=None, uploadurl=None, 376 note=''): 377 super(MeasurementLoggerDashboard, self).__init__(logger, testname, None, 378 resultsdir, uploadurl, 379 note) 380 self._unit = None 381 self._type = None 382 self._padded_domains = None 383 384 def _create_powerlog_dict(self, raw_measurement): 385 """Create powerlog dictionary from raw measurement data 386 Data format in go/power-dashboard-data. 387 388 Args: 389 raw_measurement: dictionary contains raw measurement data 390 391 Returns: 392 A dictionary of powerlog 393 """ 394 powerlog_dict = \ 395 super(MeasurementLoggerDashboard, self)._create_powerlog_dict( 396 raw_measurement) 397 398 # Using start time of the logger as the timestamp of powerlog dict. 399 powerlog_dict['timestamp'] = self._logger.times[0] 400 401 return powerlog_dict 402 403 def _create_padded_domains(self): 404 """Pad the domains name for dashboard to make the domain name better 405 sorted in alphabetical order""" 406 pass 407 408 def _create_checkpoint_dict(self): 409 """Create dictionary for checkpoint. 410 """ 411 start_time = self._logger.times[0] 412 return self._logger._checkpoint_logger.convert_relative(start_time) 413 414 def _convert(self): 415 """Convert data from power_status.MeasurementLogger object to raw 416 power measurement dictionary. 417 418 Return: 419 raw measurement dictionary or None if no readings 420 """ 421 if len(self._logger.readings) == 0: 422 logging.warn('No readings in logger ... ignoring') 423 return None 424 425 power_dict = collections.defaultdict(dict, { 426 'sample_count': len(self._logger.readings), 427 'sample_duration': 0, 428 'average': dict(), 429 'data': dict(), 430 }) 431 if power_dict['sample_count'] > 1: 432 total_duration = self._logger.times[-1] - self._logger.times[0] 433 power_dict['sample_duration'] = \ 434 1.0 * total_duration / (power_dict['sample_count'] - 1) 435 436 self._create_padded_domains() 437 for i, domain_readings in enumerate(zip(*self._logger.readings)): 438 if self._padded_domains: 439 domain = self._padded_domains[i] 440 else: 441 domain = self._logger.domains[i] 442 power_dict['data'][domain] = domain_readings 443 power_dict['average'][domain] = \ 444 numpy.average(power_dict['data'][domain]) 445 if self._unit: 446 power_dict['unit'][domain] = self._unit 447 if self._type: 448 power_dict['type'][domain] = self._type 449 450 self._tag_with_checkpoint(power_dict) 451 return power_dict 452 453 454class PowerLoggerDashboard(MeasurementLoggerDashboard): 455 """Dashboard class for power_status.PowerLogger. 456 """ 457 458 def __init__(self, logger, testname, resultsdir=None, uploadurl=None, 459 note=''): 460 if uploadurl is None: 461 uploadurl = 'http://chrome-power.appspot.com/rapl' 462 super(PowerLoggerDashboard, self).__init__(logger, testname, resultsdir, 463 uploadurl, note) 464 self._unit = 'watt' 465 self._type = 'power' 466 467 468class TempLoggerDashboard(MeasurementLoggerDashboard): 469 """Dashboard class for power_status.TempLogger. 470 """ 471 472 def __init__(self, logger, testname, resultsdir=None, uploadurl=None, 473 note=''): 474 if uploadurl is None: 475 uploadurl = 'http://chrome-power.appspot.com/rapl' 476 super(TempLoggerDashboard, self).__init__(logger, testname, resultsdir, 477 uploadurl, note) 478 self._unit = 'celsius' 479 self._type = 'temperature' 480 481 482class SimplePowerLoggerDashboard(ClientTestDashboard): 483 """Dashboard class for simple system power measurement taken and publishing 484 it to the dashboard. 485 """ 486 487 def __init__(self, duration_secs, power_watts, testname, start_ts, 488 resultsdir=None, uploadurl=None, note=''): 489 490 if uploadurl is None: 491 uploadurl = 'http://chrome-power.appspot.com/rapl' 492 super(SimplePowerLoggerDashboard, self).__init__( 493 None, testname, start_ts, resultsdir, uploadurl, note) 494 495 self._unit = 'watt' 496 self._type = 'power' 497 self._duration_secs = duration_secs 498 self._power_watts = power_watts 499 self._testname = testname 500 501 def _convert(self): 502 """Convert vbat to raw power measurement dictionary. 503 504 Return: 505 raw measurement dictionary 506 """ 507 power_dict = { 508 'sample_count': 1, 509 'sample_duration': self._duration_secs, 510 'average': {'system': self._power_watts}, 511 'data': {'system': [self._power_watts]}, 512 'unit': {'system': self._unit}, 513 'type': {'system': self._type}, 514 'checkpoint': [[self._testname]], 515 } 516 return power_dict 517 518 519class CPUStatsLoggerDashboard(MeasurementLoggerDashboard): 520 """Dashboard class for power_status.CPUStatsLogger. 521 """ 522 523 def __init__(self, logger, testname, resultsdir=None, uploadurl=None, 524 note=''): 525 if uploadurl is None: 526 uploadurl = 'http://chrome-power.appspot.com/rapl' 527 super(CPUStatsLoggerDashboard, self).__init__( 528 logger, testname, resultsdir, uploadurl, note) 529 530 @staticmethod 531 def _split_domain(domain): 532 """Return domain_type and domain_name for given domain. 533 534 Example: Split ................... to ........... and ....... 535 cpuidle_C1E-SKL cpuidle C1E-SKL 536 cpuidle_0_3_C0 cpuidle_0_3 C0 537 cpupkg_C0_C1 cpupkg C0_C1 538 cpufreq_0_3_1512000 cpufreq_0_3 1512000 539 540 Args: 541 domain: cpu stat domain name to split 542 543 Return: 544 tuple of domain_type and domain_name 545 """ 546 # Regex explanation 547 # .*? matches type non-greedily (cpuidle) 548 # (?:_\d+)* matches cpu part, ?: makes it not a group (_0_1_2_3) 549 # .* matches name greedily (C0_C1) 550 return re.match(r'(.*?(?:_\d+)*)_(.*)', domain).groups() 551 552 def _convert(self): 553 power_dict = super(CPUStatsLoggerDashboard, self)._convert() 554 remove_rail = [] 555 for rail in power_dict['data']: 556 if rail.startswith('wavg_cpu'): 557 power_dict['type'][rail] = 'cpufreq_wavg' 558 power_dict['unit'][rail] = 'kilohertz' 559 elif rail.startswith('wavg_gpu'): 560 power_dict['type'][rail] = 'gpufreq_wavg' 561 power_dict['unit'][rail] = 'megahertz' 562 else: 563 # Remove all aggregate stats, only 'non-c0' and 'non-C0_C1' now 564 if self._split_domain(rail)[1].startswith('non'): 565 remove_rail.append(rail) 566 continue 567 power_dict['type'][rail] = self._split_domain(rail)[0] 568 power_dict['unit'][rail] = 'percent' 569 for rail in remove_rail: 570 del power_dict['data'][rail] 571 del power_dict['average'][rail] 572 return power_dict 573 574 def _create_padded_domains(self): 575 """Padded number in the domain name with dot to make it sorted 576 alphabetically. 577 578 Example: 579 cpuidle_C1-SKL, cpuidle_C1E-SKL, cpuidle_C2-SKL, cpuidle_C10-SKL 580 will be changed to 581 cpuidle_C.1-SKL, cpuidle_C.1E-SKL, cpuidle_C.2-SKL, cpuidle_C10-SKL 582 which make it in alphabetically order. 583 """ 584 longest = collections.defaultdict(int) 585 searcher = re.compile(r'\d+') 586 number_strs = [] 587 splitted_domains = \ 588 [self._split_domain(domain) for domain in self._logger.domains] 589 for domain_type, domain_name in splitted_domains: 590 result = searcher.search(domain_name) 591 if not result: 592 number_strs.append('') 593 continue 594 number_str = result.group(0) 595 number_strs.append(number_str) 596 longest[domain_type] = max(longest[domain_type], len(number_str)) 597 598 self._padded_domains = [] 599 for i in range(len(self._logger.domains)): 600 if not number_strs[i]: 601 self._padded_domains.append(self._logger.domains[i]) 602 continue 603 604 domain_type, domain_name = splitted_domains[i] 605 formatter_component = '{:.>%ds}' % longest[domain_type] 606 607 # Change "cpuidle_C1E-SKL" to "cpuidle_C{:.>2s}E-SKL" 608 formatter_str = domain_type + '_' + \ 609 searcher.sub(formatter_component, domain_name, count=1) 610 611 # Run "cpuidle_C{:_>2s}E-SKL".format("1") to get "cpuidle_C.1E-SKL" 612 self._padded_domains.append(formatter_str.format(number_strs[i])) 613 614 615class VideoFpsLoggerDashboard(MeasurementLoggerDashboard): 616 """Dashboard class for power_status.VideoFpsLogger.""" 617 618 def __init__(self, logger, testname, resultsdir=None, uploadurl=None, 619 note=''): 620 if uploadurl is None: 621 uploadurl = 'http://chrome-power.appspot.com/rapl' 622 super(VideoFpsLoggerDashboard, self).__init__( 623 logger, testname, resultsdir, uploadurl, note) 624 self._unit = 'fps' 625 self._type = 'fps' 626