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