xref: /aosp_15_r20/external/autotest/client/cros/power/power_dashboard.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1*9c5db199SXin Li# Lint as: python2, python3
2*9c5db199SXin Li# Copyright (c) 2017 The Chromium OS Authors. All rights reserved.
3*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be
4*9c5db199SXin Li# found in the LICENSE file.
5*9c5db199SXin Li
6*9c5db199SXin Lifrom __future__ import absolute_import
7*9c5db199SXin Lifrom __future__ import division
8*9c5db199SXin Lifrom __future__ import print_function
9*9c5db199SXin Li
10*9c5db199SXin Liimport collections
11*9c5db199SXin Liimport json
12*9c5db199SXin Liimport logging
13*9c5db199SXin Liimport numpy
14*9c5db199SXin Liimport operator
15*9c5db199SXin Liimport os
16*9c5db199SXin Liimport re
17*9c5db199SXin Liimport time
18*9c5db199SXin Lifrom six.moves import range
19*9c5db199SXin Lifrom six.moves import urllib
20*9c5db199SXin Li
21*9c5db199SXin Lifrom autotest_lib.client.bin import utils
22*9c5db199SXin Lifrom autotest_lib.client.common_lib import error
23*9c5db199SXin Lifrom autotest_lib.client.common_lib import lsbrelease_utils
24*9c5db199SXin Lifrom autotest_lib.client.common_lib.cros import retry
25*9c5db199SXin Lifrom autotest_lib.client.cros.power import power_status
26*9c5db199SXin Lifrom autotest_lib.client.cros.power import power_utils
27*9c5db199SXin Lifrom six.moves import zip
28*9c5db199SXin Li
29*9c5db199SXin Li_HTML_CHART_STR = '''
30*9c5db199SXin Li<!DOCTYPE html>
31*9c5db199SXin Li<html>
32*9c5db199SXin Li<head>
33*9c5db199SXin Li<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js">
34*9c5db199SXin Li</script>
35*9c5db199SXin Li<script type="text/javascript">
36*9c5db199SXin Li    google.charts.load('current', {{'packages':['corechart', 'table']}});
37*9c5db199SXin Li    google.charts.setOnLoadCallback(drawChart);
38*9c5db199SXin Li    function drawChart() {{
39*9c5db199SXin Li        var dataArray = [
40*9c5db199SXin Li{data}
41*9c5db199SXin Li        ];
42*9c5db199SXin Li        var data = google.visualization.arrayToDataTable(dataArray);
43*9c5db199SXin Li        var numDataCols = data.getNumberOfColumns() - 1;
44*9c5db199SXin Li        var unit = '{unit}';
45*9c5db199SXin Li        var options = {{
46*9c5db199SXin Li            width: 1600,
47*9c5db199SXin Li            height: 1200,
48*9c5db199SXin Li            lineWidth: 1,
49*9c5db199SXin Li            legend: {{ position: 'top', maxLines: 3 }},
50*9c5db199SXin Li            vAxis: {{ viewWindow: {{min: 0}}, title: '{type} ({unit})' }},
51*9c5db199SXin Li            hAxis: {{ viewWindow: {{min: 0}}, title: 'time (second)' }},
52*9c5db199SXin Li        }};
53*9c5db199SXin Li        var element = document.getElementById('{type}');
54*9c5db199SXin Li        var chart;
55*9c5db199SXin Li        if (unit == 'percent') {{
56*9c5db199SXin Li            options['isStacked'] = true;
57*9c5db199SXin Li            if (numDataCols == 2) {{
58*9c5db199SXin Li                options['colors'] = ['#d32f2f', '#43a047']
59*9c5db199SXin Li            }} else if (numDataCols <= 4) {{
60*9c5db199SXin Li                options['colors'] = ['#d32f2f', '#f4c7c3', '#cddc39','#43a047'];
61*9c5db199SXin Li            }} else if (numDataCols <= 9) {{
62*9c5db199SXin Li                options['colors'] = ['#d32f2f', '#e57373', '#f4c7c3', '#ffccbc',
63*9c5db199SXin Li                        '#f0f4c3', '#c8e6c9', '#cddc39', '#81c784', '#43a047'];
64*9c5db199SXin Li            }}
65*9c5db199SXin Li            chart = new google.visualization.SteppedAreaChart(element);
66*9c5db199SXin Li        }} else if (data.getNumberOfRows() == 2 && unit == 'point') {{
67*9c5db199SXin Li            var newArray = [['key', 'value']];
68*9c5db199SXin Li            for (var i = 1; i < dataArray[0].length; i++) {{
69*9c5db199SXin Li                newArray.push([dataArray[0][i], dataArray[1][i]]);
70*9c5db199SXin Li            }}
71*9c5db199SXin Li            data = google.visualization.arrayToDataTable(newArray);
72*9c5db199SXin Li            delete options.width;
73*9c5db199SXin Li            delete options.height;
74*9c5db199SXin Li            chart = new google.visualization.Table(element);
75*9c5db199SXin Li        }} else {{
76*9c5db199SXin Li            chart = new google.visualization.LineChart(element);
77*9c5db199SXin Li        }}
78*9c5db199SXin Li        chart.draw(data, options);
79*9c5db199SXin Li    }}
80*9c5db199SXin Li</script>
81*9c5db199SXin Li</head>
82*9c5db199SXin Li<body>
83*9c5db199SXin Li<div id="{type}"></div>
84*9c5db199SXin Li</body>
85*9c5db199SXin Li</html>
86*9c5db199SXin Li'''
87*9c5db199SXin Li
88*9c5db199SXin Li_HWID_LINK_STR = '''
89*9c5db199SXin Li<a href="http://goto.google.com/pdash-hwid?query={hwid}">
90*9c5db199SXin Li  Link to hwid lookup.
91*9c5db199SXin Li</a><br />
92*9c5db199SXin Li'''
93*9c5db199SXin Li
94*9c5db199SXin Li_PDASH_LINK_STR = '''
95*9c5db199SXin Li<a href="http://chrome-power.appspot.com/dashboard?board={board}&test={test}&datetime={datetime}">
96*9c5db199SXin Li  Link to power dashboard.
97*9c5db199SXin Li</a><br />
98*9c5db199SXin Li'''
99*9c5db199SXin Li
100*9c5db199SXin Li_TDASH_LINK_STR = '''
101*9c5db199SXin Li<a href="http://chrome-power.appspot.com/thermal_dashboard?note={note}">
102*9c5db199SXin Li  Link to thermal dashboard.
103*9c5db199SXin Li</a><br />
104*9c5db199SXin Li'''
105*9c5db199SXin Li
106*9c5db199SXin Li# Global variable to avoid duplicate dashboard link in BaseDashboard._save_html
107*9c5db199SXin Ligenerated_dashboard_link = False
108*9c5db199SXin Li
109*9c5db199SXin Li
110*9c5db199SXin Liclass BaseDashboard(object):
111*9c5db199SXin Li    """Base class that implements method for prepare and upload data to power
112*9c5db199SXin Li    dashboard.
113*9c5db199SXin Li    """
114*9c5db199SXin Li
115*9c5db199SXin Li    def __init__(self, logger, testname, start_ts=None, resultsdir=None,
116*9c5db199SXin Li                 uploadurl=None):
117*9c5db199SXin Li        """Create BaseDashboard objects.
118*9c5db199SXin Li
119*9c5db199SXin Li        Args:
120*9c5db199SXin Li            logger: object that store the log. This will get convert to
121*9c5db199SXin Li                    dictionary by self._convert()
122*9c5db199SXin Li            testname: name of current test
123*9c5db199SXin Li            start_ts: timestamp of when test started in seconds since epoch
124*9c5db199SXin Li            resultsdir: directory to save the power json
125*9c5db199SXin Li            uploadurl: url to upload power data
126*9c5db199SXin Li        """
127*9c5db199SXin Li        self._logger = logger
128*9c5db199SXin Li        self._testname = testname
129*9c5db199SXin Li        self._start_ts = start_ts if start_ts else time.time()
130*9c5db199SXin Li        self._resultsdir = resultsdir
131*9c5db199SXin Li        self._uploadurl = uploadurl
132*9c5db199SXin Li
133*9c5db199SXin Li    def _create_powerlog_dict(self, raw_measurement):
134*9c5db199SXin Li        """Create powerlog dictionary from raw measurement data
135*9c5db199SXin Li        Data format in go/power-dashboard-data.
136*9c5db199SXin Li
137*9c5db199SXin Li        Args:
138*9c5db199SXin Li            raw_measurement: dictionary contains raw measurement data
139*9c5db199SXin Li
140*9c5db199SXin Li        Returns:
141*9c5db199SXin Li            A dictionary of powerlog
142*9c5db199SXin Li        """
143*9c5db199SXin Li        powerlog_dict = {
144*9c5db199SXin Li                'format_version': 6,
145*9c5db199SXin Li                'timestamp': self._start_ts,
146*9c5db199SXin Li                'test': self._testname,
147*9c5db199SXin Li                'dut': self._create_dut_info_dict(
148*9c5db199SXin Li                        list(raw_measurement['data'].keys())),
149*9c5db199SXin Li                'power': raw_measurement,
150*9c5db199SXin Li        }
151*9c5db199SXin Li
152*9c5db199SXin Li        return powerlog_dict
153*9c5db199SXin Li
154*9c5db199SXin Li    def _create_dut_info_dict(self, power_rails):
155*9c5db199SXin Li        """Create a dictionary that contain information of the DUT.
156*9c5db199SXin Li
157*9c5db199SXin Li        MUST be implemented in subclass.
158*9c5db199SXin Li
159*9c5db199SXin Li        Args:
160*9c5db199SXin Li            power_rails: list of measured power rails
161*9c5db199SXin Li
162*9c5db199SXin Li        Returns:
163*9c5db199SXin Li            DUT info dictionary
164*9c5db199SXin Li        """
165*9c5db199SXin Li        raise NotImplementedError
166*9c5db199SXin Li
167*9c5db199SXin Li    def _save_json(self, powerlog_dict, resultsdir, filename='power_log.json'):
168*9c5db199SXin Li        """Convert powerlog dict to human readable formatted JSON and
169*9c5db199SXin Li        append to <resultsdir>/<filename>.
170*9c5db199SXin Li
171*9c5db199SXin Li        Args:
172*9c5db199SXin Li            powerlog_dict: dictionary of power data
173*9c5db199SXin Li            resultsdir: directory to save formatted JSON object
174*9c5db199SXin Li            filename: filename to append to
175*9c5db199SXin Li        """
176*9c5db199SXin Li        if not os.path.exists(resultsdir):
177*9c5db199SXin Li            raise error.TestError('resultsdir %s does not exist.' % resultsdir)
178*9c5db199SXin Li        filename = os.path.join(resultsdir, filename)
179*9c5db199SXin Li        json_str = json.dumps(powerlog_dict, indent=4, separators=(',', ': '),
180*9c5db199SXin Li                              ensure_ascii=False)
181*9c5db199SXin Li        json_str = utils.strip_non_printable(json_str)
182*9c5db199SXin Li        with open(filename, 'a') as f:
183*9c5db199SXin Li            f.write(json_str)
184*9c5db199SXin Li
185*9c5db199SXin Li    def _generate_dashboard_link(self, powerlog_dict):
186*9c5db199SXin Li        """Generate link to power and thermal dashboard"""
187*9c5db199SXin Li        # Use global variable to generate this only once.
188*9c5db199SXin Li        global generated_dashboard_link
189*9c5db199SXin Li        if generated_dashboard_link:
190*9c5db199SXin Li            return ''
191*9c5db199SXin Li        generated_dashboard_link = True
192*9c5db199SXin Li
193*9c5db199SXin Li        board = powerlog_dict['dut']['board']
194*9c5db199SXin Li        test = powerlog_dict['test']
195*9c5db199SXin Li        datetime = time.strftime('%Y%m%d%H%M',
196*9c5db199SXin Li                                 time.gmtime(powerlog_dict['timestamp']))
197*9c5db199SXin Li        hwid = powerlog_dict['dut']['sku']['hwid']
198*9c5db199SXin Li        note = powerlog_dict['dut']['note']
199*9c5db199SXin Li
200*9c5db199SXin Li        html_str = '<!DOCTYPE html><html><body>'
201*9c5db199SXin Li        html_str += _HWID_LINK_STR.format(hwid=hwid)
202*9c5db199SXin Li        html_str += _PDASH_LINK_STR.format(board=board,
203*9c5db199SXin Li                                           test=test,
204*9c5db199SXin Li                                           datetime=datetime)
205*9c5db199SXin Li
206*9c5db199SXin Li        if re.match('ThermalQual.(full|lab).*', note):
207*9c5db199SXin Li            html_str += _TDASH_LINK_STR.format(note=note)
208*9c5db199SXin Li
209*9c5db199SXin Li        html_str += '</body></html>'
210*9c5db199SXin Li
211*9c5db199SXin Li        return html_str
212*9c5db199SXin Li
213*9c5db199SXin Li    def _save_html(self, powerlog_dict, resultsdir, filename='power_log.html'):
214*9c5db199SXin Li        """Convert powerlog dict to chart in HTML page and append to
215*9c5db199SXin Li        <resultsdir>/<filename>.
216*9c5db199SXin Li
217*9c5db199SXin Li        Note that this results in multiple HTML objects in one file but Chrome
218*9c5db199SXin Li        can render all of it in one page.
219*9c5db199SXin Li
220*9c5db199SXin Li        Args:
221*9c5db199SXin Li            powerlog_dict: dictionary of power data
222*9c5db199SXin Li            resultsdir: directory to save HTML page
223*9c5db199SXin Li            filename: filename to append to
224*9c5db199SXin Li        """
225*9c5db199SXin Li        html_str = self._generate_dashboard_link(powerlog_dict)
226*9c5db199SXin Li
227*9c5db199SXin Li        # Create dict from type to sorted list of rail names.
228*9c5db199SXin Li        rail_type = collections.defaultdict(list)
229*9c5db199SXin Li        for r, t in powerlog_dict['power']['type'].items():
230*9c5db199SXin Li            rail_type[t].append(r)
231*9c5db199SXin Li        for t in rail_type:
232*9c5db199SXin Li            rail_type[t] = sorted(rail_type[t])
233*9c5db199SXin Li
234*9c5db199SXin Li        row_indent = ' ' * 12
235*9c5db199SXin Li        for t in rail_type:
236*9c5db199SXin Li            data_str_list = []
237*9c5db199SXin Li
238*9c5db199SXin Li            # Generate rail name data string.
239*9c5db199SXin Li            header = ['time'] + rail_type[t]
240*9c5db199SXin Li            header_str = row_indent + "['" + "', '".join(header) + "']"
241*9c5db199SXin Li            data_str_list.append(header_str)
242*9c5db199SXin Li
243*9c5db199SXin Li            # Generate measurements data string.
244*9c5db199SXin Li            for i in range(powerlog_dict['power']['sample_count']):
245*9c5db199SXin Li                row = [str(i * powerlog_dict['power']['sample_duration'])]
246*9c5db199SXin Li                for r in rail_type[t]:
247*9c5db199SXin Li                    row.append(str(powerlog_dict['power']['data'][r][i]))
248*9c5db199SXin Li                row_str = row_indent + '[' + ', '.join(row) + ']'
249*9c5db199SXin Li                data_str_list.append(row_str)
250*9c5db199SXin Li
251*9c5db199SXin Li            data_str = ',\n'.join(data_str_list)
252*9c5db199SXin Li            unit = powerlog_dict['power']['unit'][rail_type[t][0]]
253*9c5db199SXin Li            html_str += _HTML_CHART_STR.format(data=data_str, unit=unit, type=t)
254*9c5db199SXin Li
255*9c5db199SXin Li        if not os.path.exists(resultsdir):
256*9c5db199SXin Li            raise error.TestError('resultsdir %s does not exist.' % resultsdir)
257*9c5db199SXin Li        filename = os.path.join(resultsdir, filename)
258*9c5db199SXin Li        with open(filename, 'a') as f:
259*9c5db199SXin Li            f.write(html_str)
260*9c5db199SXin Li
261*9c5db199SXin Li    def _upload(self, powerlog_dict, uploadurl):
262*9c5db199SXin Li        """Convert powerlog dict to minimal size JSON and upload to dashboard.
263*9c5db199SXin Li
264*9c5db199SXin Li        Args:
265*9c5db199SXin Li            powerlog_dict: dictionary of power data
266*9c5db199SXin Li            uploadurl: url to upload the power data
267*9c5db199SXin Li        """
268*9c5db199SXin Li        json_str = json.dumps(powerlog_dict, ensure_ascii=False)
269*9c5db199SXin Li        data_obj = {'data': utils.strip_non_printable(json_str)}
270*9c5db199SXin Li        encoded = urllib.parse.urlencode(data_obj).encode('utf-8')
271*9c5db199SXin Li        req = urllib.request.Request(uploadurl, encoded)
272*9c5db199SXin Li
273*9c5db199SXin Li        @retry.retry(urllib.error.URLError,
274*9c5db199SXin Li                     raiselist=[urllib.error.HTTPError],
275*9c5db199SXin Li                     timeout_min=5.0,
276*9c5db199SXin Li                     delay_sec=1,
277*9c5db199SXin Li                     backoff=2)
278*9c5db199SXin Li        def _do_upload():
279*9c5db199SXin Li            urllib.request.urlopen(req)
280*9c5db199SXin Li
281*9c5db199SXin Li        _do_upload()
282*9c5db199SXin Li
283*9c5db199SXin Li    def _create_checkpoint_dict(self):
284*9c5db199SXin Li        """Create dictionary for checkpoint.
285*9c5db199SXin Li
286*9c5db199SXin Li        @returns a dictionary of tags to their corresponding intervals in the
287*9c5db199SXin Li                 following format:
288*9c5db199SXin Li                 {
289*9c5db199SXin Li                      tag1: [(start1, end1), (start2, end2), ...],
290*9c5db199SXin Li                      tag2: [(start3, end3), (start4, end4), ...],
291*9c5db199SXin Li                      ...
292*9c5db199SXin Li                 }
293*9c5db199SXin Li        """
294*9c5db199SXin Li        raise NotImplementedError
295*9c5db199SXin Li
296*9c5db199SXin Li    def _tag_with_checkpoint(self, power_dict):
297*9c5db199SXin Li        """Tag power_dict with checkpoint data.
298*9c5db199SXin Li
299*9c5db199SXin Li        This function translates the checkpoint intervals into a list of tags
300*9c5db199SXin Li        for each data point.
301*9c5db199SXin Li
302*9c5db199SXin Li        @param power_dict: a dictionary with power data; assume this dictionary
303*9c5db199SXin Li                           has attributes 'sample_count' and 'sample_duration'.
304*9c5db199SXin Li        """
305*9c5db199SXin Li        checkpoint_dict = self._create_checkpoint_dict()
306*9c5db199SXin Li
307*9c5db199SXin Li        # Create list of check point event tuple.
308*9c5db199SXin Li        # Tuple format: (checkpoint_name:str, event_time:float, is_start:bool)
309*9c5db199SXin Li        checkpoint_event_list = []
310*9c5db199SXin Li        for name, intervals in checkpoint_dict.items():
311*9c5db199SXin Li            for start, finish in intervals:
312*9c5db199SXin Li                checkpoint_event_list.append((name, start, True))
313*9c5db199SXin Li                checkpoint_event_list.append((name, finish, False))
314*9c5db199SXin Li
315*9c5db199SXin Li        checkpoint_event_list = sorted(checkpoint_event_list,
316*9c5db199SXin Li                                       key=operator.itemgetter(1))
317*9c5db199SXin Li
318*9c5db199SXin Li        # Add placeholder check point at 1e9 seconds.
319*9c5db199SXin Li        checkpoint_event_list.append(('dummy', 1e9, True))
320*9c5db199SXin Li
321*9c5db199SXin Li        interval_set = set()
322*9c5db199SXin Li        event_index = 0
323*9c5db199SXin Li        checkpoint_list = []
324*9c5db199SXin Li        for i in range(power_dict['sample_count']):
325*9c5db199SXin Li            curr_time = i * power_dict['sample_duration']
326*9c5db199SXin Li
327*9c5db199SXin Li            # Process every checkpoint event until current point of time
328*9c5db199SXin Li            while checkpoint_event_list[event_index][1] <= curr_time:
329*9c5db199SXin Li                name, _, is_start = checkpoint_event_list[event_index]
330*9c5db199SXin Li                if is_start:
331*9c5db199SXin Li                    interval_set.add(name)
332*9c5db199SXin Li                else:
333*9c5db199SXin Li                    interval_set.discard(name)
334*9c5db199SXin Li                event_index += 1
335*9c5db199SXin Li
336*9c5db199SXin Li            checkpoint_list.append(list(interval_set))
337*9c5db199SXin Li        power_dict['checkpoint'] = checkpoint_list
338*9c5db199SXin Li
339*9c5db199SXin Li    def _convert(self):
340*9c5db199SXin Li        """Convert data from self._logger object to raw power measurement
341*9c5db199SXin Li        dictionary.
342*9c5db199SXin Li
343*9c5db199SXin Li        MUST be implemented in subclass.
344*9c5db199SXin Li
345*9c5db199SXin Li        Return:
346*9c5db199SXin Li            raw measurement dictionary
347*9c5db199SXin Li        """
348*9c5db199SXin Li        raise NotImplementedError
349*9c5db199SXin Li
350*9c5db199SXin Li    def upload(self):
351*9c5db199SXin Li        """Upload powerlog to dashboard and save data to results directory.
352*9c5db199SXin Li        """
353*9c5db199SXin Li        raw_measurement = self._convert()
354*9c5db199SXin Li        if raw_measurement is None:
355*9c5db199SXin Li            return
356*9c5db199SXin Li
357*9c5db199SXin Li        powerlog_dict = self._create_powerlog_dict(raw_measurement)
358*9c5db199SXin Li        if self._resultsdir is not None:
359*9c5db199SXin Li            self._save_json(powerlog_dict, self._resultsdir)
360*9c5db199SXin Li            self._save_html(powerlog_dict, self._resultsdir)
361*9c5db199SXin Li        if self._uploadurl is not None:
362*9c5db199SXin Li            self._upload(powerlog_dict, self._uploadurl)
363*9c5db199SXin Li
364*9c5db199SXin Li
365*9c5db199SXin Liclass ClientTestDashboard(BaseDashboard):
366*9c5db199SXin Li    """Dashboard class for autotests that run on client side.
367*9c5db199SXin Li    """
368*9c5db199SXin Li
369*9c5db199SXin Li    def __init__(self, logger, testname, start_ts, resultsdir, uploadurl, note):
370*9c5db199SXin Li        """Create BaseDashboard objects.
371*9c5db199SXin Li
372*9c5db199SXin Li        Args:
373*9c5db199SXin Li            logger: object that store the log. This will get convert to
374*9c5db199SXin Li                    dictionary by self._convert()
375*9c5db199SXin Li            testname: name of current test
376*9c5db199SXin Li            start_ts: timestamp of when test started in seconds since epoch
377*9c5db199SXin Li            resultsdir: directory to save the power json
378*9c5db199SXin Li            uploadurl: url to upload power data
379*9c5db199SXin Li            note: note for current test run
380*9c5db199SXin Li        """
381*9c5db199SXin Li        super(ClientTestDashboard, self).__init__(logger, testname, start_ts,
382*9c5db199SXin Li                                                  resultsdir, uploadurl)
383*9c5db199SXin Li        self._note = note
384*9c5db199SXin Li
385*9c5db199SXin Li
386*9c5db199SXin Li    def _create_dut_info_dict(self, power_rails):
387*9c5db199SXin Li        """Create a dictionary that contain information of the DUT.
388*9c5db199SXin Li
389*9c5db199SXin Li        Args:
390*9c5db199SXin Li            power_rails: list of measured power rails
391*9c5db199SXin Li
392*9c5db199SXin Li        Returns:
393*9c5db199SXin Li            DUT info dictionary
394*9c5db199SXin Li        """
395*9c5db199SXin Li        board = utils.get_board()
396*9c5db199SXin Li        platform = utils.get_platform()
397*9c5db199SXin Li
398*9c5db199SXin Li        if not platform.startswith(board):
399*9c5db199SXin Li            board += '_' + platform
400*9c5db199SXin Li
401*9c5db199SXin Li        if power_utils.has_hammer():
402*9c5db199SXin Li            board += '_hammer'
403*9c5db199SXin Li
404*9c5db199SXin Li        dut_info_dict = {
405*9c5db199SXin Li                'board': board,
406*9c5db199SXin Li                'version': {
407*9c5db199SXin Li                        'hw': utils.get_hardware_revision(),
408*9c5db199SXin Li                        'milestone':
409*9c5db199SXin Li                        lsbrelease_utils.get_chromeos_release_milestone(),
410*9c5db199SXin Li                        'os': lsbrelease_utils.get_chromeos_release_version(),
411*9c5db199SXin Li                        'channel': lsbrelease_utils.get_chromeos_channel(),
412*9c5db199SXin Li                        'firmware': utils.get_firmware_version(),
413*9c5db199SXin Li                        'ec': utils.get_ec_version(),
414*9c5db199SXin Li                        'kernel': utils.get_kernel_version(),
415*9c5db199SXin Li                },
416*9c5db199SXin Li                'sku': {
417*9c5db199SXin Li                        'cpu': utils.get_cpu_name(),
418*9c5db199SXin Li                        'memory_size': utils.get_mem_total_gb(),
419*9c5db199SXin Li                        'storage_size':
420*9c5db199SXin Li                        utils.get_disk_size_gb(utils.get_root_device()),
421*9c5db199SXin Li                        'display_resolution': utils.get_screen_resolution(),
422*9c5db199SXin Li                        'hwid': utils.get_hardware_id(),
423*9c5db199SXin Li                },
424*9c5db199SXin Li                'ina': {
425*9c5db199SXin Li                        'version': 0,
426*9c5db199SXin Li                        'ina': power_rails,
427*9c5db199SXin Li                },
428*9c5db199SXin Li                'note': self._note,
429*9c5db199SXin Li        }
430*9c5db199SXin Li
431*9c5db199SXin Li        if power_utils.has_battery():
432*9c5db199SXin Li            status = power_status.get_status()
433*9c5db199SXin Li            if status.battery:
434*9c5db199SXin Li                # Round the battery size to nearest tenth because it is
435*9c5db199SXin Li                # fluctuated for platform without battery nominal voltage data.
436*9c5db199SXin Li                dut_info_dict['sku']['battery_size'] = round(
437*9c5db199SXin Li                        status.battery.energy_full_design, 1)
438*9c5db199SXin Li                dut_info_dict['sku']['battery_shutdown_percent'] = \
439*9c5db199SXin Li                        power_utils.get_low_battery_shutdown_percent()
440*9c5db199SXin Li        return dut_info_dict
441*9c5db199SXin Li
442*9c5db199SXin Li
443*9c5db199SXin Liclass MeasurementLoggerDashboard(ClientTestDashboard):
444*9c5db199SXin Li    """Dashboard class for power_status.MeasurementLogger.
445*9c5db199SXin Li    """
446*9c5db199SXin Li
447*9c5db199SXin Li    def __init__(self, logger, testname, resultsdir, uploadurl, note):
448*9c5db199SXin Li        super(MeasurementLoggerDashboard, self).__init__(logger, testname, None,
449*9c5db199SXin Li                                                         resultsdir, uploadurl,
450*9c5db199SXin Li                                                         note)
451*9c5db199SXin Li        self._unit = None
452*9c5db199SXin Li        self._type = None
453*9c5db199SXin Li        self._padded_domains = None
454*9c5db199SXin Li
455*9c5db199SXin Li    def _create_powerlog_dict(self, raw_measurement):
456*9c5db199SXin Li        """Create powerlog dictionary from raw measurement data
457*9c5db199SXin Li        Data format in go/power-dashboard-data.
458*9c5db199SXin Li
459*9c5db199SXin Li        Args:
460*9c5db199SXin Li            raw_measurement: dictionary contains raw measurement data
461*9c5db199SXin Li
462*9c5db199SXin Li        Returns:
463*9c5db199SXin Li            A dictionary of powerlog
464*9c5db199SXin Li        """
465*9c5db199SXin Li        powerlog_dict = \
466*9c5db199SXin Li                super(MeasurementLoggerDashboard, self)._create_powerlog_dict(
467*9c5db199SXin Li                        raw_measurement)
468*9c5db199SXin Li
469*9c5db199SXin Li        # Using start time of the logger as the timestamp of powerlog dict.
470*9c5db199SXin Li        powerlog_dict['timestamp'] = self._logger.times[0]
471*9c5db199SXin Li
472*9c5db199SXin Li        return powerlog_dict
473*9c5db199SXin Li
474*9c5db199SXin Li    def _create_padded_domains(self):
475*9c5db199SXin Li        """Pad the domains name for dashboard to make the domain name better
476*9c5db199SXin Li        sorted in alphabetical order"""
477*9c5db199SXin Li        pass
478*9c5db199SXin Li
479*9c5db199SXin Li    def _create_checkpoint_dict(self):
480*9c5db199SXin Li        """Create dictionary for checkpoint.
481*9c5db199SXin Li        """
482*9c5db199SXin Li        start_time = self._logger.times[0]
483*9c5db199SXin Li        return self._logger._checkpoint_logger.convert_relative(start_time)
484*9c5db199SXin Li
485*9c5db199SXin Li    def _convert(self):
486*9c5db199SXin Li        """Convert data from power_status.MeasurementLogger object to raw
487*9c5db199SXin Li        power measurement dictionary.
488*9c5db199SXin Li
489*9c5db199SXin Li        Return:
490*9c5db199SXin Li            raw measurement dictionary or None if no readings
491*9c5db199SXin Li        """
492*9c5db199SXin Li        if len(self._logger.readings) == 0:
493*9c5db199SXin Li            logging.warning('No readings in logger ... ignoring')
494*9c5db199SXin Li            return None
495*9c5db199SXin Li
496*9c5db199SXin Li        power_dict = collections.defaultdict(dict, {
497*9c5db199SXin Li            'sample_count': len(self._logger.readings),
498*9c5db199SXin Li            'sample_duration': 0,
499*9c5db199SXin Li            'average': dict(),
500*9c5db199SXin Li            'data': dict(),
501*9c5db199SXin Li        })
502*9c5db199SXin Li        if power_dict['sample_count'] > 1:
503*9c5db199SXin Li            total_duration = self._logger.times[-1] - self._logger.times[0]
504*9c5db199SXin Li            power_dict['sample_duration'] = \
505*9c5db199SXin Li                    1.0 * total_duration / (power_dict['sample_count'] - 1)
506*9c5db199SXin Li
507*9c5db199SXin Li        self._create_padded_domains()
508*9c5db199SXin Li        for i, domain_readings in enumerate(zip(*self._logger.readings)):
509*9c5db199SXin Li            if self._padded_domains:
510*9c5db199SXin Li                domain = self._padded_domains[i]
511*9c5db199SXin Li            else:
512*9c5db199SXin Li                domain = self._logger.domains[i]
513*9c5db199SXin Li            power_dict['data'][domain] = domain_readings
514*9c5db199SXin Li            power_dict['average'][domain] = \
515*9c5db199SXin Li                    numpy.average(power_dict['data'][domain])
516*9c5db199SXin Li            if self._unit:
517*9c5db199SXin Li                power_dict['unit'][domain] = self._unit
518*9c5db199SXin Li            if self._type:
519*9c5db199SXin Li                power_dict['type'][domain] = self._type
520*9c5db199SXin Li
521*9c5db199SXin Li        self._tag_with_checkpoint(power_dict)
522*9c5db199SXin Li        return power_dict
523*9c5db199SXin Li
524*9c5db199SXin Li
525*9c5db199SXin Liclass PowerLoggerDashboard(MeasurementLoggerDashboard):
526*9c5db199SXin Li    """Dashboard class for power_status.PowerLogger.
527*9c5db199SXin Li    """
528*9c5db199SXin Li
529*9c5db199SXin Li    def __init__(self, logger, testname, resultsdir, uploadurl, note):
530*9c5db199SXin Li        super(PowerLoggerDashboard, self).__init__(logger, testname, resultsdir,
531*9c5db199SXin Li                                                   uploadurl, note)
532*9c5db199SXin Li        self._unit = 'watt'
533*9c5db199SXin Li        self._type = 'power'
534*9c5db199SXin Li
535*9c5db199SXin Li
536*9c5db199SXin Liclass TempLoggerDashboard(MeasurementLoggerDashboard):
537*9c5db199SXin Li    """Dashboard class for power_status.TempLogger.
538*9c5db199SXin Li    """
539*9c5db199SXin Li
540*9c5db199SXin Li    def __init__(self, logger, testname, resultsdir, uploadurl, note):
541*9c5db199SXin Li        super(TempLoggerDashboard, self).__init__(logger, testname, resultsdir,
542*9c5db199SXin Li                                                  uploadurl, note)
543*9c5db199SXin Li        self._unit = 'celsius'
544*9c5db199SXin Li        self._type = 'temperature'
545*9c5db199SXin Li
546*9c5db199SXin Li
547*9c5db199SXin Liclass KeyvalLogger(power_status.MeasurementLogger):
548*9c5db199SXin Li    """Class for logging custom keyval data to power dashboard.
549*9c5db199SXin Li
550*9c5db199SXin Li    Each key should be unique and only map to one value.
551*9c5db199SXin Li    See power_SpeedoMeter2 for implementation example.
552*9c5db199SXin Li    """
553*9c5db199SXin Li
554*9c5db199SXin Li    def __init__(self, start_ts, end_ts=None):
555*9c5db199SXin Li        # Do not call parent constructor to avoid making a new thread.
556*9c5db199SXin Li        self.times = [start_ts]
557*9c5db199SXin Li        self._start_ts = start_ts
558*9c5db199SXin Li        self._fixed_end_ts = end_ts  # prefer this (end time set by tests)
559*9c5db199SXin Li        self._updating_end_ts = time.time()  # updated when a new item is added
560*9c5db199SXin Li        self.keys = []
561*9c5db199SXin Li        self.values = []
562*9c5db199SXin Li        self.units = []
563*9c5db199SXin Li        self.types = []
564*9c5db199SXin Li
565*9c5db199SXin Li    def is_unit_valid(self, unit):
566*9c5db199SXin Li        """Make sure that unit of the data is supported unit."""
567*9c5db199SXin Li        pattern = re.compile(r'^((kilo|mega|giga)hertz|'
568*9c5db199SXin Li                             r'percent|celsius|fps|rpm|point|'
569*9c5db199SXin Li                             r'(milli|micro)?(watt|volt|amp))$')
570*9c5db199SXin Li        return pattern.match(unit) is not None
571*9c5db199SXin Li
572*9c5db199SXin Li    def add_item(self, key, value, unit, type_):
573*9c5db199SXin Li        """Add a data point to the logger.
574*9c5db199SXin Li
575*9c5db199SXin Li        @param key: string, key of the data.
576*9c5db199SXin Li        @param value: float, measurement value.
577*9c5db199SXin Li        @param unit: string, unit for the data.
578*9c5db199SXin Li        @param type: string, type of the data.
579*9c5db199SXin Li        """
580*9c5db199SXin Li        if not self.is_unit_valid(unit):
581*9c5db199SXin Li            raise error.TestError(
582*9c5db199SXin Li                    'Unit %s is not support in power dashboard.' % unit)
583*9c5db199SXin Li        self.keys.append(key)
584*9c5db199SXin Li        self.values.append(value)
585*9c5db199SXin Li        self.units.append(unit)
586*9c5db199SXin Li        self.types.append(type_)
587*9c5db199SXin Li        self._updating_end_ts = time.time()
588*9c5db199SXin Li
589*9c5db199SXin Li    def set_end(self, end_ts):
590*9c5db199SXin Li        """Set the end timestamp.
591*9c5db199SXin Li
592*9c5db199SXin Li        If the end timestamp is not set explicitly by tests, use the timestamp
593*9c5db199SXin Li        of the last added item instead.
594*9c5db199SXin Li
595*9c5db199SXin Li        @param end_ts: end timestamp for KeyvalLogger.
596*9c5db199SXin Li        """
597*9c5db199SXin Li        self._fixed_end_ts = end_ts
598*9c5db199SXin Li
599*9c5db199SXin Li    def calc(self, mtype=None):
600*9c5db199SXin Li        return {}
601*9c5db199SXin Li
602*9c5db199SXin Li    def save_results(self, resultsdir=None, fname_prefix=None):
603*9c5db199SXin Li        pass
604*9c5db199SXin Li
605*9c5db199SXin Li
606*9c5db199SXin Liclass KeyvalLoggerDashboard(MeasurementLoggerDashboard):
607*9c5db199SXin Li    """Dashboard class for custom keyval data in KeyvalLogger class."""
608*9c5db199SXin Li
609*9c5db199SXin Li    def _convert(self):
610*9c5db199SXin Li        """Convert KeyvalLogger data to power dict."""
611*9c5db199SXin Li        power_dict = {
612*9c5db199SXin Li                # 2 samples to show flat value spanning across duration of the test.
613*9c5db199SXin Li                'sample_count':
614*9c5db199SXin Li                2,
615*9c5db199SXin Li                'sample_duration':
616*9c5db199SXin Li                (self._logger._fixed_end_ts -
617*9c5db199SXin Li                 self._logger._start_ts) if self._logger._fixed_end_ts else
618*9c5db199SXin Li                (self._logger._updating_end_ts - self._logger._start_ts),
619*9c5db199SXin Li                'average':
620*9c5db199SXin Li                dict(list(zip(self._logger.keys, self._logger.values))),
621*9c5db199SXin Li                'data':
622*9c5db199SXin Li                dict(
623*9c5db199SXin Li                        list(
624*9c5db199SXin Li                                zip(self._logger.keys,
625*9c5db199SXin Li                                    ([v, v] for v in self._logger.values)))),
626*9c5db199SXin Li                'unit':
627*9c5db199SXin Li                dict(list(zip(self._logger.keys, self._logger.units))),
628*9c5db199SXin Li                'type':
629*9c5db199SXin Li                dict(list(zip(self._logger.keys, self._logger.types))),
630*9c5db199SXin Li                'checkpoint': [[self._testname], [self._testname]],
631*9c5db199SXin Li        }
632*9c5db199SXin Li        return power_dict
633*9c5db199SXin Li
634*9c5db199SXin Li
635*9c5db199SXin Liclass CPUStatsLoggerDashboard(MeasurementLoggerDashboard):
636*9c5db199SXin Li    """Dashboard class for power_status.CPUStatsLogger.
637*9c5db199SXin Li    """
638*9c5db199SXin Li    @staticmethod
639*9c5db199SXin Li    def _split_domain(domain):
640*9c5db199SXin Li        """Return domain_type and domain_name for given domain.
641*9c5db199SXin Li
642*9c5db199SXin Li        Example: Split ................... to ........... and .......
643*9c5db199SXin Li                       cpuidle_C1E-SKL        cpuidle         C1E-SKL
644*9c5db199SXin Li                       cpuidle_0_3_C0         cpuidle_0_3     C0
645*9c5db199SXin Li                       cpupkg_C0_C1           cpupkg          C0_C1
646*9c5db199SXin Li                       cpufreq_0_3_1512000    cpufreq_0_3     1512000
647*9c5db199SXin Li
648*9c5db199SXin Li        Args:
649*9c5db199SXin Li            domain: cpu stat domain name to split
650*9c5db199SXin Li
651*9c5db199SXin Li        Return:
652*9c5db199SXin Li            tuple of domain_type and domain_name
653*9c5db199SXin Li        """
654*9c5db199SXin Li        # Regex explanation
655*9c5db199SXin Li        # .*?           matches type non-greedily                 (cpuidle)
656*9c5db199SXin Li        # (?:_\d+)*     matches cpu part, ?: makes it not a group (_0_1_2_3)
657*9c5db199SXin Li        # .*            matches name greedily                     (C0_C1)
658*9c5db199SXin Li        return re.match(r'(.*?(?:_\d+)*)_(.*)', domain).groups()
659*9c5db199SXin Li
660*9c5db199SXin Li    def _convert(self):
661*9c5db199SXin Li        power_dict = super(CPUStatsLoggerDashboard, self)._convert()
662*9c5db199SXin Li        if not power_dict or not power_dict['data']:
663*9c5db199SXin Li            return None
664*9c5db199SXin Li        remove_rail = []
665*9c5db199SXin Li        for rail in power_dict['data']:
666*9c5db199SXin Li            if rail.startswith('wavg_cpu'):
667*9c5db199SXin Li                power_dict['type'][rail] = 'cpufreq_wavg'
668*9c5db199SXin Li                power_dict['unit'][rail] = 'kilohertz'
669*9c5db199SXin Li            elif rail.startswith('wavg_gpu'):
670*9c5db199SXin Li                power_dict['type'][rail] = 'gpufreq_wavg'
671*9c5db199SXin Li                power_dict['unit'][rail] = 'megahertz'
672*9c5db199SXin Li            else:
673*9c5db199SXin Li                # Remove all aggregate stats, only 'non-c0' and 'non-C0_C1' now
674*9c5db199SXin Li                if self._split_domain(rail)[1].startswith('non'):
675*9c5db199SXin Li                    remove_rail.append(rail)
676*9c5db199SXin Li                    continue
677*9c5db199SXin Li                power_dict['type'][rail] = self._split_domain(rail)[0]
678*9c5db199SXin Li                power_dict['unit'][rail] = 'percent'
679*9c5db199SXin Li        for rail in remove_rail:
680*9c5db199SXin Li            del power_dict['data'][rail]
681*9c5db199SXin Li            del power_dict['average'][rail]
682*9c5db199SXin Li        return power_dict
683*9c5db199SXin Li
684*9c5db199SXin Li    def _create_padded_domains(self):
685*9c5db199SXin Li        """Padded number in the domain name with dot to make it sorted
686*9c5db199SXin Li        alphabetically.
687*9c5db199SXin Li
688*9c5db199SXin Li        Example:
689*9c5db199SXin Li        cpuidle_C1-SKL, cpuidle_C1E-SKL, cpuidle_C2-SKL, cpuidle_C10-SKL
690*9c5db199SXin Li        will be changed to
691*9c5db199SXin Li        cpuidle_C.1-SKL, cpuidle_C.1E-SKL, cpuidle_C.2-SKL, cpuidle_C10-SKL
692*9c5db199SXin Li        which make it in alphabetically order.
693*9c5db199SXin Li        """
694*9c5db199SXin Li        longest = collections.defaultdict(int)
695*9c5db199SXin Li        searcher = re.compile(r'\d+')
696*9c5db199SXin Li        number_strs = []
697*9c5db199SXin Li        splitted_domains = \
698*9c5db199SXin Li                [self._split_domain(domain) for domain in self._logger.domains]
699*9c5db199SXin Li        for domain_type, domain_name in splitted_domains:
700*9c5db199SXin Li            result = searcher.search(domain_name)
701*9c5db199SXin Li            if not result:
702*9c5db199SXin Li                number_strs.append('')
703*9c5db199SXin Li                continue
704*9c5db199SXin Li            number_str = result.group(0)
705*9c5db199SXin Li            number_strs.append(number_str)
706*9c5db199SXin Li            longest[domain_type] = max(longest[domain_type], len(number_str))
707*9c5db199SXin Li
708*9c5db199SXin Li        self._padded_domains = []
709*9c5db199SXin Li        for i in range(len(self._logger.domains)):
710*9c5db199SXin Li            if not number_strs[i]:
711*9c5db199SXin Li                self._padded_domains.append(self._logger.domains[i])
712*9c5db199SXin Li                continue
713*9c5db199SXin Li
714*9c5db199SXin Li            domain_type, domain_name = splitted_domains[i]
715*9c5db199SXin Li            formatter_component = '{:.>%ds}' % longest[domain_type]
716*9c5db199SXin Li
717*9c5db199SXin Li            # Change "cpuidle_C1E-SKL" to "cpuidle_C{:.>2s}E-SKL"
718*9c5db199SXin Li            formatter_str = domain_type + '_' + \
719*9c5db199SXin Li                    searcher.sub(formatter_component, domain_name, count=1)
720*9c5db199SXin Li
721*9c5db199SXin Li            # Run "cpuidle_C{:_>2s}E-SKL".format("1") to get "cpuidle_C.1E-SKL"
722*9c5db199SXin Li            self._padded_domains.append(formatter_str.format(number_strs[i]))
723*9c5db199SXin Li
724*9c5db199SXin Li
725*9c5db199SXin Liclass VideoFpsLoggerDashboard(MeasurementLoggerDashboard):
726*9c5db199SXin Li    """Dashboard class for power_status.VideoFpsLogger."""
727*9c5db199SXin Li
728*9c5db199SXin Li    def __init__(self, logger, testname, resultsdir, uploadurl, note):
729*9c5db199SXin Li        super(VideoFpsLoggerDashboard, self).__init__(
730*9c5db199SXin Li            logger, testname, resultsdir, uploadurl, note)
731*9c5db199SXin Li        self._unit = 'fps'
732*9c5db199SXin Li        self._type = 'fps'
733*9c5db199SXin Li
734*9c5db199SXin Li
735*9c5db199SXin Liclass FanRpmLoggerDashboard(MeasurementLoggerDashboard):
736*9c5db199SXin Li    """Dashboard class for power_status.FanRpmLogger."""
737*9c5db199SXin Li
738*9c5db199SXin Li    def __init__(self, logger, testname, resultsdir, uploadurl, note):
739*9c5db199SXin Li        super(FanRpmLoggerDashboard, self).__init__(
740*9c5db199SXin Li            logger, testname, resultsdir, uploadurl, note)
741*9c5db199SXin Li        self._unit = 'rpm'
742*9c5db199SXin Li        self._type = 'fan'
743*9c5db199SXin Li
744*9c5db199SXin Li
745*9c5db199SXin Liclass FreeMemoryLoggerDashboard(MeasurementLoggerDashboard):
746*9c5db199SXin Li    """Dashboard class for power_status.FreeMemoryLogger."""
747*9c5db199SXin Li
748*9c5db199SXin Li    def __init__(self, logger, testname, resultsdir, uploadurl, note):
749*9c5db199SXin Li        # Don't upload to dashboard
750*9c5db199SXin Li        uploadurl = None
751*9c5db199SXin Li        super(FreeMemoryLoggerDashboard,
752*9c5db199SXin Li              self).__init__(logger, testname, resultsdir, uploadurl, note)
753*9c5db199SXin Li        self._unit = 'point'
754*9c5db199SXin Li        self._type = 'mem'
755*9c5db199SXin Li
756*9c5db199SXin Li
757*9c5db199SXin Lidashboard_factory = None
758*9c5db199SXin Lidef get_dashboard_factory():
759*9c5db199SXin Li    global dashboard_factory
760*9c5db199SXin Li    if not dashboard_factory:
761*9c5db199SXin Li        dashboard_factory = LoggerDashboardFactory()
762*9c5db199SXin Li    return dashboard_factory
763*9c5db199SXin Li
764*9c5db199SXin Liclass LoggerDashboardFactory(object):
765*9c5db199SXin Li    """Class to generate client test dashboard object from logger."""
766*9c5db199SXin Li
767*9c5db199SXin Li    loggerToDashboardDict = {
768*9c5db199SXin Li            power_status.CPUStatsLogger: CPUStatsLoggerDashboard,
769*9c5db199SXin Li            power_status.PowerLogger: PowerLoggerDashboard,
770*9c5db199SXin Li            power_status.TempLogger: TempLoggerDashboard,
771*9c5db199SXin Li            power_status.VideoFpsLogger: VideoFpsLoggerDashboard,
772*9c5db199SXin Li            power_status.FanRpmLogger: FanRpmLoggerDashboard,
773*9c5db199SXin Li            power_status.FreeMemoryLogger: FreeMemoryLoggerDashboard,
774*9c5db199SXin Li            KeyvalLogger: KeyvalLoggerDashboard,
775*9c5db199SXin Li    }
776*9c5db199SXin Li
777*9c5db199SXin Li    def registerDataType(self, logger_type, dashboard_type):
778*9c5db199SXin Li        """Register new type of dashboard to the factory
779*9c5db199SXin Li
780*9c5db199SXin Li        @param logger_type: Type of logger to register
781*9c5db199SXin Li        @param dashboard_type: Type of dashboard to register
782*9c5db199SXin Li        """
783*9c5db199SXin Li        self.loggerToDashboardDict[logger_type] = dashboard_type
784*9c5db199SXin Li
785*9c5db199SXin Li    def createDashboard(self, logger, testname, resultsdir=None,
786*9c5db199SXin Li                        uploadurl=None, note=''):
787*9c5db199SXin Li        """Create dashboard object"""
788*9c5db199SXin Li        if uploadurl is None:
789*9c5db199SXin Li            uploadurl = 'http://chrome-power.appspot.com/rapl'
790*9c5db199SXin Li        dashboard = self.loggerToDashboardDict[type(logger)]
791*9c5db199SXin Li        return dashboard(logger, testname, resultsdir, uploadurl, note)
792*9c5db199SXin Li
793*9c5db199SXin Li
794*9c5db199SXin Lidef generate_parallax_report(output_dir):
795*9c5db199SXin Li    """Generate parallax report in the result directory."""
796*9c5db199SXin Li    parallax_url = 'http://crospower.page.link/parallax'
797*9c5db199SXin Li    local_dir = '/usr/local'
798*9c5db199SXin Li    parallax_tar = os.path.join(local_dir, 'parallax.tar.xz')
799*9c5db199SXin Li    parallax_dir = os.path.join(local_dir, 'report_analysis')
800*9c5db199SXin Li    parallax_exe = os.path.join(parallax_dir, 'process.py')
801*9c5db199SXin Li    results_dir = os.path.join(output_dir, 'results')
802*9c5db199SXin Li    parallax_html = os.path.join(results_dir, 'parallax.html')
803*9c5db199SXin Li
804*9c5db199SXin Li    # Download the source
805*9c5db199SXin Li    cmd = ' '.join(['wget', parallax_url, '-O', parallax_tar])
806*9c5db199SXin Li    utils.run(cmd)
807*9c5db199SXin Li
808*9c5db199SXin Li    # Extract the tool
809*9c5db199SXin Li    cmd = ' '.join(['tar', 'xf', parallax_tar, '-C', local_dir])
810*9c5db199SXin Li    utils.run(cmd)
811*9c5db199SXin Li
812*9c5db199SXin Li    # Run the tool
813*9c5db199SXin Li    cmd = ' '.join([
814*9c5db199SXin Li            'python', parallax_exe, '-t', 'PowerQual', '-p', output_dir, '-o',
815*9c5db199SXin Li            parallax_html
816*9c5db199SXin Li    ])
817*9c5db199SXin Li    utils.run(cmd)
818*9c5db199SXin Li
819*9c5db199SXin Li    # Clean up the tool
820*9c5db199SXin Li    cmd = ' '.join(['rm', '-rf', parallax_tar, parallax_dir])
821*9c5db199SXin Li    utils.run(cmd)
822