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