• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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