1*9c5db199SXin Li# Lint as: python2, python3 2*9c5db199SXin Li# Copyright (c) 2013 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 Li"""Uploads performance data to the performance dashboard. 7*9c5db199SXin Li 8*9c5db199SXin LiPerformance tests may output data that needs to be displayed on the performance 9*9c5db199SXin Lidashboard. The autotest TKO parser invokes this module with each test 10*9c5db199SXin Liassociated with a job. If a test has performance data associated with it, it 11*9c5db199SXin Liis uploaded to the performance dashboard. The performance dashboard is owned 12*9c5db199SXin Liby Chrome team and is available here: https://chromeperf.appspot.com/. Users 13*9c5db199SXin Limust be logged in with an @google.com account to view chromeOS perf data there. 14*9c5db199SXin Li 15*9c5db199SXin Li""" 16*9c5db199SXin Li 17*9c5db199SXin Liimport six.moves.http_client 18*9c5db199SXin Liimport json 19*9c5db199SXin Liimport os 20*9c5db199SXin Liimport re 21*9c5db199SXin Lifrom six.moves import urllib 22*9c5db199SXin Li 23*9c5db199SXin Liimport common 24*9c5db199SXin Lifrom autotest_lib.tko import utils as tko_utils 25*9c5db199SXin Li 26*9c5db199SXin Li_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) 27*9c5db199SXin Li 28*9c5db199SXin Li_OAUTH_IMPORT_OK = False 29*9c5db199SXin Li_OAUTH_CREDS = None 30*9c5db199SXin Litry: 31*9c5db199SXin Li from google.oauth2 import service_account 32*9c5db199SXin Li from google.auth.transport.requests import Request 33*9c5db199SXin Li from google.auth.exceptions import RefreshError 34*9c5db199SXin Li _OAUTH_IMPORT_OK = True 35*9c5db199SXin Liexcept ImportError as e: 36*9c5db199SXin Li tko_utils.dprint('Failed to import google-auth:\n%s' % e) 37*9c5db199SXin Li 38*9c5db199SXin Li_DASHBOARD_UPLOAD_URL = 'https://chromeperf.appspot.com/add_point' 39*9c5db199SXin Li_OAUTH_SCOPES = ['https://www.googleapis.com/auth/userinfo.email'] 40*9c5db199SXin Li_PRESENTATION_CONFIG_FILE = os.path.join( 41*9c5db199SXin Li _ROOT_DIR, 'perf_dashboard_config.json') 42*9c5db199SXin Li_PRESENTATION_SHADOW_CONFIG_FILE = os.path.join( 43*9c5db199SXin Li _ROOT_DIR, 'perf_dashboard_shadow_config.json') 44*9c5db199SXin Li_SERVICE_ACCOUNT_FILE = '/creds/service_accounts/skylab-drone.json' 45*9c5db199SXin Li 46*9c5db199SXin Li# Format for Chrome and ChromeOS version strings. 47*9c5db199SXin LiVERSION_REGEXP = r'^(\d+)\.(\d+)\.(\d+)\.(\d+)$' 48*9c5db199SXin Li 49*9c5db199SXin Li 50*9c5db199SXin Liclass PerfUploadingError(Exception): 51*9c5db199SXin Li """Exception raised in perf_uploader""" 52*9c5db199SXin Li pass 53*9c5db199SXin Li 54*9c5db199SXin Li 55*9c5db199SXin Lidef _parse_config_file(config_file): 56*9c5db199SXin Li """Parses a presentation config file and stores the info into a dict. 57*9c5db199SXin Li 58*9c5db199SXin Li The config file contains information about how to present the perf data 59*9c5db199SXin Li on the perf dashboard. This is required if the default presentation 60*9c5db199SXin Li settings aren't desired for certain tests. 61*9c5db199SXin Li 62*9c5db199SXin Li @param config_file: Path to the configuration file to be parsed. 63*9c5db199SXin Li 64*9c5db199SXin Li @returns A dictionary mapping each unique autotest name to a dictionary 65*9c5db199SXin Li of presentation config information. 66*9c5db199SXin Li 67*9c5db199SXin Li @raises PerfUploadingError if config data or main name for the test 68*9c5db199SXin Li is missing from the config file. 69*9c5db199SXin Li 70*9c5db199SXin Li """ 71*9c5db199SXin Li json_obj = [] 72*9c5db199SXin Li if os.path.exists(config_file): 73*9c5db199SXin Li with open(config_file, 'r') as fp: 74*9c5db199SXin Li json_obj = json.load(fp) 75*9c5db199SXin Li config_dict = {} 76*9c5db199SXin Li for entry in json_obj: 77*9c5db199SXin Li if 'autotest_regex' in entry: 78*9c5db199SXin Li config_dict[entry['autotest_regex']] = entry 79*9c5db199SXin Li else: 80*9c5db199SXin Li config_dict['^' + re.escape(entry['autotest_name']) + '$'] = entry 81*9c5db199SXin Li return config_dict 82*9c5db199SXin Li 83*9c5db199SXin Li 84*9c5db199SXin Lidef _gather_presentation_info(config_data, test_name): 85*9c5db199SXin Li """Gathers presentation info from config data for the given test name. 86*9c5db199SXin Li 87*9c5db199SXin Li @param config_data: A dictionary of dashboard presentation info for all 88*9c5db199SXin Li tests, as returned by _parse_config_file(). Info is keyed by autotest 89*9c5db199SXin Li name. 90*9c5db199SXin Li @param test_name: The name of an autotest. 91*9c5db199SXin Li 92*9c5db199SXin Li @return A dictionary containing presentation information extracted from 93*9c5db199SXin Li |config_data| for the given autotest name. 94*9c5db199SXin Li 95*9c5db199SXin Li @raises PerfUploadingError if some required data is missing. 96*9c5db199SXin Li """ 97*9c5db199SXin Li presentation_dict = None 98*9c5db199SXin Li for regex in config_data: 99*9c5db199SXin Li match = re.match(regex, test_name) 100*9c5db199SXin Li if match: 101*9c5db199SXin Li if presentation_dict: 102*9c5db199SXin Li raise PerfUploadingError('Duplicate config data refer to the ' 103*9c5db199SXin Li 'same test %s' % test_name) 104*9c5db199SXin Li presentation_dict = config_data[regex] 105*9c5db199SXin Li 106*9c5db199SXin Li if not presentation_dict: 107*9c5db199SXin Li raise PerfUploadingError( 108*9c5db199SXin Li 'No config data is specified for test %s in %s.' % 109*9c5db199SXin Li (test_name, _PRESENTATION_CONFIG_FILE)) 110*9c5db199SXin Li try: 111*9c5db199SXin Li main_name = presentation_dict['main_name'] 112*9c5db199SXin Li except KeyError: 113*9c5db199SXin Li raise PerfUploadingError( 114*9c5db199SXin Li 'No main name is specified for test %s in %s.' % 115*9c5db199SXin Li (test_name, _PRESENTATION_CONFIG_FILE)) 116*9c5db199SXin Li if 'dashboard_test_name' in presentation_dict: 117*9c5db199SXin Li test_name = presentation_dict['dashboard_test_name'] 118*9c5db199SXin Li return {'main_name': main_name, 'test_name': test_name} 119*9c5db199SXin Li 120*9c5db199SXin Li 121*9c5db199SXin Lidef _format_for_upload(board_name, cros_version, chrome_version, 122*9c5db199SXin Li hardware_id, hardware_hostname, perf_values, 123*9c5db199SXin Li presentation_info, jobname): 124*9c5db199SXin Li """Formats perf data suitable to upload to the perf dashboard. 125*9c5db199SXin Li 126*9c5db199SXin Li The perf dashboard expects perf data to be uploaded as a 127*9c5db199SXin Li specially-formatted JSON string. In particular, the JSON object must be a 128*9c5db199SXin Li dictionary with key "data", and value being a list of dictionaries where 129*9c5db199SXin Li each dictionary contains all the information associated with a single 130*9c5db199SXin Li measured perf value: main name, bot name, test name, perf value, error 131*9c5db199SXin Li value, units, and build version numbers. 132*9c5db199SXin Li 133*9c5db199SXin Li @param board_name: The string name of the image board name. 134*9c5db199SXin Li @param cros_version: The string chromeOS version number. 135*9c5db199SXin Li @param chrome_version: The string chrome version number. 136*9c5db199SXin Li @param hardware_id: String that identifies the type of hardware the test was 137*9c5db199SXin Li executed on. 138*9c5db199SXin Li @param hardware_hostname: String that identifies the name of the device the 139*9c5db199SXin Li test was executed on. 140*9c5db199SXin Li @param perf_values: A dictionary of measured perf data as computed by 141*9c5db199SXin Li _compute_avg_stddev(). 142*9c5db199SXin Li @param presentation_info: A dictionary of dashboard presentation info for 143*9c5db199SXin Li the given test, as identified by _gather_presentation_info(). 144*9c5db199SXin Li @param jobname: A string uniquely identifying the test run, this enables 145*9c5db199SXin Li linking back from a test result to the logs of the test run. 146*9c5db199SXin Li 147*9c5db199SXin Li @return A dictionary containing the formatted information ready to upload 148*9c5db199SXin Li to the performance dashboard. 149*9c5db199SXin Li 150*9c5db199SXin Li """ 151*9c5db199SXin Li # Client side case - server side comes with its own charts data section. 152*9c5db199SXin Li if 'charts' not in perf_values: 153*9c5db199SXin Li perf_values = { 154*9c5db199SXin Li 'format_version': '1.0', 155*9c5db199SXin Li 'benchmark_name': presentation_info['test_name'], 156*9c5db199SXin Li 'charts': perf_values, 157*9c5db199SXin Li } 158*9c5db199SXin Li 159*9c5db199SXin Li # TODO b:169251326 terms below are set outside of this codebase and 160*9c5db199SXin Li # should be updated when possible ("master" -> "main"). # nocheck 161*9c5db199SXin Li # see catapult-project/catapult/dashboard/dashboard/add_point.py 162*9c5db199SXin Li dash_entry = { 163*9c5db199SXin Li 'master': presentation_info['main_name'], # nocheck 164*9c5db199SXin Li 'bot': 'cros-' + board_name, # Prefix to clarify it's ChromeOS. 165*9c5db199SXin Li 'point_id': _get_id_from_version(chrome_version, cros_version), 166*9c5db199SXin Li 'versions': { 167*9c5db199SXin Li 'cros_version': cros_version, 168*9c5db199SXin Li 'chrome_version': chrome_version, 169*9c5db199SXin Li }, 170*9c5db199SXin Li 'supplemental': { 171*9c5db199SXin Li 'default_rev': 'r_cros_version', 172*9c5db199SXin Li 'hardware_identifier': hardware_id, 173*9c5db199SXin Li 'hardware_hostname': hardware_hostname, 174*9c5db199SXin Li 'jobname': jobname, 175*9c5db199SXin Li }, 176*9c5db199SXin Li 'chart_data': perf_values, 177*9c5db199SXin Li } 178*9c5db199SXin Li return {'data': json.dumps(dash_entry)} 179*9c5db199SXin Li 180*9c5db199SXin Li 181*9c5db199SXin Lidef _get_version_numbers(test_attributes): 182*9c5db199SXin Li """Gets the version numbers from the test attributes and validates them. 183*9c5db199SXin Li 184*9c5db199SXin Li @param test_attributes: The attributes property (which is a dict) of an 185*9c5db199SXin Li autotest tko.models.test object. 186*9c5db199SXin Li 187*9c5db199SXin Li @return A pair of strings (ChromeOS version, Chrome version). 188*9c5db199SXin Li 189*9c5db199SXin Li @raises PerfUploadingError if a version isn't formatted as expected. 190*9c5db199SXin Li """ 191*9c5db199SXin Li chrome_version = test_attributes.get('CHROME_VERSION', '') 192*9c5db199SXin Li cros_version = test_attributes.get('CHROMEOS_RELEASE_VERSION', '') 193*9c5db199SXin Li cros_milestone = test_attributes.get('CHROMEOS_RELEASE_CHROME_MILESTONE') 194*9c5db199SXin Li # Use the release milestone as the milestone if present, othewise prefix the 195*9c5db199SXin Li # cros version with the with the Chrome browser milestone. 196*9c5db199SXin Li if cros_milestone: 197*9c5db199SXin Li cros_version = "%s.%s" % (cros_milestone, cros_version) 198*9c5db199SXin Li else: 199*9c5db199SXin Li cros_version = chrome_version[:chrome_version.find('.') + 200*9c5db199SXin Li 1] + cros_version 201*9c5db199SXin Li if not re.match(VERSION_REGEXP, cros_version): 202*9c5db199SXin Li raise PerfUploadingError('CrOS version "%s" does not match expected ' 203*9c5db199SXin Li 'format.' % cros_version) 204*9c5db199SXin Li if not re.match(VERSION_REGEXP, chrome_version): 205*9c5db199SXin Li raise PerfUploadingError('Chrome version "%s" does not match expected ' 206*9c5db199SXin Li 'format.' % chrome_version) 207*9c5db199SXin Li return (cros_version, chrome_version) 208*9c5db199SXin Li 209*9c5db199SXin Li 210*9c5db199SXin Lidef _get_id_from_version(chrome_version, cros_version): 211*9c5db199SXin Li """Computes the point ID to use, from Chrome and ChromeOS version numbers. 212*9c5db199SXin Li 213*9c5db199SXin Li For ChromeOS row data, data values are associated with both a Chrome 214*9c5db199SXin Li version number and a ChromeOS version number (unlike for Chrome row data 215*9c5db199SXin Li that is associated with a single revision number). This function takes 216*9c5db199SXin Li both version numbers as input, then computes a single, unique integer ID 217*9c5db199SXin Li from them, which serves as a 'fake' revision number that can uniquely 218*9c5db199SXin Li identify each ChromeOS data point, and which will allow ChromeOS data points 219*9c5db199SXin Li to be sorted by Chrome version number, with ties broken by ChromeOS version 220*9c5db199SXin Li number. 221*9c5db199SXin Li 222*9c5db199SXin Li To compute the integer ID, we take the portions of each version number that 223*9c5db199SXin Li serve as the shortest unambiguous names for each (as described here: 224*9c5db199SXin Li http://www.chromium.org/developers/version-numbers). We then force each 225*9c5db199SXin Li component of each portion to be a fixed width (padded by zeros if needed), 226*9c5db199SXin Li concatenate all digits together (with those coming from the Chrome version 227*9c5db199SXin Li number first), and convert the entire string of digits into an integer. 228*9c5db199SXin Li We ensure that the total number of digits does not exceed that which is 229*9c5db199SXin Li allowed by AppEngine NDB for an integer (64-bit signed value). 230*9c5db199SXin Li 231*9c5db199SXin Li For example: 232*9c5db199SXin Li Chrome version: 27.0.1452.2 (shortest unambiguous name: 1452.2) 233*9c5db199SXin Li ChromeOS version: 27.3906.0.0 (shortest unambiguous name: 3906.0.0) 234*9c5db199SXin Li concatenated together with padding for fixed-width columns: 235*9c5db199SXin Li ('01452' + '002') + ('03906' + '000' + '00') = '014520020390600000' 236*9c5db199SXin Li Final integer ID: 14520020390600000 237*9c5db199SXin Li 238*9c5db199SXin Li @param chrome_ver: The Chrome version number as a string. 239*9c5db199SXin Li @param cros_ver: The ChromeOS version number as a string. 240*9c5db199SXin Li 241*9c5db199SXin Li @return A unique integer ID associated with the two given version numbers. 242*9c5db199SXin Li 243*9c5db199SXin Li """ 244*9c5db199SXin Li 245*9c5db199SXin Li # Number of digits to use from each part of the version string for Chrome 246*9c5db199SXin Li # and ChromeOS versions when building a point ID out of these two versions. 247*9c5db199SXin Li chrome_version_col_widths = [0, 0, 5, 3] 248*9c5db199SXin Li cros_version_col_widths = [0, 5, 3, 2] 249*9c5db199SXin Li 250*9c5db199SXin Li def get_digits_from_version(version_num, column_widths): 251*9c5db199SXin Li if re.match(VERSION_REGEXP, version_num): 252*9c5db199SXin Li computed_string = '' 253*9c5db199SXin Li version_parts = version_num.split('.') 254*9c5db199SXin Li for i, version_part in enumerate(version_parts): 255*9c5db199SXin Li if column_widths[i]: 256*9c5db199SXin Li computed_string += version_part.zfill(column_widths[i]) 257*9c5db199SXin Li return computed_string 258*9c5db199SXin Li else: 259*9c5db199SXin Li return None 260*9c5db199SXin Li 261*9c5db199SXin Li chrome_digits = get_digits_from_version( 262*9c5db199SXin Li chrome_version, chrome_version_col_widths) 263*9c5db199SXin Li cros_digits = get_digits_from_version( 264*9c5db199SXin Li cros_version, cros_version_col_widths) 265*9c5db199SXin Li if not chrome_digits or not cros_digits: 266*9c5db199SXin Li return None 267*9c5db199SXin Li result_digits = chrome_digits + cros_digits 268*9c5db199SXin Li max_digits = sum(chrome_version_col_widths + cros_version_col_widths) 269*9c5db199SXin Li if len(result_digits) > max_digits: 270*9c5db199SXin Li return None 271*9c5db199SXin Li return int(result_digits) 272*9c5db199SXin Li 273*9c5db199SXin Li 274*9c5db199SXin Lidef _initialize_oauth(): 275*9c5db199SXin Li """Initialize oauth using local credentials and scopes. 276*9c5db199SXin Li 277*9c5db199SXin Li @return A boolean if oauth is apparently ready to use. 278*9c5db199SXin Li """ 279*9c5db199SXin Li global _OAUTH_CREDS 280*9c5db199SXin Li if _OAUTH_CREDS: 281*9c5db199SXin Li return True 282*9c5db199SXin Li if not _OAUTH_IMPORT_OK: 283*9c5db199SXin Li return False 284*9c5db199SXin Li try: 285*9c5db199SXin Li _OAUTH_CREDS = (service_account.Credentials.from_service_account_file( 286*9c5db199SXin Li _SERVICE_ACCOUNT_FILE) 287*9c5db199SXin Li .with_scopes(_OAUTH_SCOPES)) 288*9c5db199SXin Li return True 289*9c5db199SXin Li except Exception as e: 290*9c5db199SXin Li tko_utils.dprint('Failed to initialize oauth credentials:\n%s' % e) 291*9c5db199SXin Li return False 292*9c5db199SXin Li 293*9c5db199SXin Li 294*9c5db199SXin Lidef _add_oauth_token(headers): 295*9c5db199SXin Li """Add support for oauth2 via service credentials. 296*9c5db199SXin Li 297*9c5db199SXin Li This is currently best effort, we will silently not add the token 298*9c5db199SXin Li for a number of possible reasons (missing service account, etc). 299*9c5db199SXin Li 300*9c5db199SXin Li TODO(engeg@): Once this is validated, make mandatory. 301*9c5db199SXin Li 302*9c5db199SXin Li @param headers: A map of request headers to add the token to. 303*9c5db199SXin Li """ 304*9c5db199SXin Li if _initialize_oauth(): 305*9c5db199SXin Li if not _OAUTH_CREDS.valid: 306*9c5db199SXin Li try: 307*9c5db199SXin Li _OAUTH_CREDS.refresh(Request()) 308*9c5db199SXin Li except RefreshError as e: 309*9c5db199SXin Li tko_utils.dprint('Failed to refresh oauth token:\n%s' % e) 310*9c5db199SXin Li return 311*9c5db199SXin Li _OAUTH_CREDS.apply(headers) 312*9c5db199SXin Li 313*9c5db199SXin Li 314*9c5db199SXin Lidef _send_to_dashboard(data_obj): 315*9c5db199SXin Li """Sends formatted perf data to the perf dashboard. 316*9c5db199SXin Li 317*9c5db199SXin Li @param data_obj: A formatted data object as returned by 318*9c5db199SXin Li _format_for_upload(). 319*9c5db199SXin Li 320*9c5db199SXin Li @raises PerfUploadingError if an exception was raised when uploading. 321*9c5db199SXin Li 322*9c5db199SXin Li """ 323*9c5db199SXin Li encoded = urllib.parse.urlencode(data_obj) 324*9c5db199SXin Li req = urllib.request.Request(_DASHBOARD_UPLOAD_URL, encoded) 325*9c5db199SXin Li _add_oauth_token(req.headers) 326*9c5db199SXin Li try: 327*9c5db199SXin Li urllib.request.urlopen(req) 328*9c5db199SXin Li except urllib.error.HTTPError as e: 329*9c5db199SXin Li raise PerfUploadingError('HTTPError: %d %s for JSON %s\n' % ( 330*9c5db199SXin Li e.code, e.msg, data_obj['data'])) 331*9c5db199SXin Li except urllib.error.URLError as e: 332*9c5db199SXin Li raise PerfUploadingError( 333*9c5db199SXin Li 'URLError: %s for JSON %s\n' % 334*9c5db199SXin Li (str(e.reason), data_obj['data'])) 335*9c5db199SXin Li except six.moves.http_client.HTTPException: 336*9c5db199SXin Li raise PerfUploadingError( 337*9c5db199SXin Li 'HTTPException for JSON %s\n' % data_obj['data']) 338*9c5db199SXin Li 339*9c5db199SXin Li 340*9c5db199SXin Lidef _get_image_board_name(platform, image): 341*9c5db199SXin Li """Returns the board name of the tested image. 342*9c5db199SXin Li 343*9c5db199SXin Li Note that it can be different from the board name of DUTs the test was 344*9c5db199SXin Li scheduled to. 345*9c5db199SXin Li 346*9c5db199SXin Li @param platform: The DUT platform in lab. eg. eve 347*9c5db199SXin Li @param image: The image installed in the DUT. eg. eve-arcnext-release. 348*9c5db199SXin Li @return: the image board name. 349*9c5db199SXin Li """ 350*9c5db199SXin Li # This is a hacky way to resolve the mixture of reports in chromeperf 351*9c5db199SXin Li # dashboard. This solution is copied from our other reporting 352*9c5db199SXin Li # pipeline. 353*9c5db199SXin Li image_board_name = platform 354*9c5db199SXin Li 355*9c5db199SXin Li suffixes = ['-arcnext', '-ndktranslation', '-arcvm', '-kernelnext'] 356*9c5db199SXin Li 357*9c5db199SXin Li for suffix in suffixes: 358*9c5db199SXin Li if not platform.endswith(suffix) and (suffix + '-') in image: 359*9c5db199SXin Li image_board_name += suffix 360*9c5db199SXin Li return image_board_name 361*9c5db199SXin Li 362*9c5db199SXin Li 363*9c5db199SXin Lidef upload_test(job, test, jobname): 364*9c5db199SXin Li """Uploads any perf data associated with a test to the perf dashboard. 365*9c5db199SXin Li 366*9c5db199SXin Li @param job: An autotest tko.models.job object that is associated with the 367*9c5db199SXin Li given |test|. 368*9c5db199SXin Li @param test: An autotest tko.models.test object that may or may not be 369*9c5db199SXin Li associated with measured perf data. 370*9c5db199SXin Li @param jobname: A string uniquely identifying the test run, this enables 371*9c5db199SXin Li linking back from a test result to the logs of the test run. 372*9c5db199SXin Li 373*9c5db199SXin Li """ 374*9c5db199SXin Li 375*9c5db199SXin Li # Format the perf data for the upload, then upload it. 376*9c5db199SXin Li test_name = test.testname 377*9c5db199SXin Li image_board_name = _get_image_board_name( 378*9c5db199SXin Li job.machine_group, job.keyval_dict.get('build', job.machine_group)) 379*9c5db199SXin Li # Append the platform name with '.arc' if the suffix of the control 380*9c5db199SXin Li # filename is '.arc'. 381*9c5db199SXin Li if job.label and re.match('.*\.arc$', job.label): 382*9c5db199SXin Li image_board_name += '.arc' 383*9c5db199SXin Li hardware_id = test.attributes.get('hwid', '') 384*9c5db199SXin Li hardware_hostname = test.machine 385*9c5db199SXin Li config_data = _parse_config_file(_PRESENTATION_CONFIG_FILE) 386*9c5db199SXin Li try: 387*9c5db199SXin Li shadow_config_data = _parse_config_file(_PRESENTATION_SHADOW_CONFIG_FILE) 388*9c5db199SXin Li config_data.update(shadow_config_data) 389*9c5db199SXin Li except ValueError as e: 390*9c5db199SXin Li tko_utils.dprint('Failed to parse config file %s: %s.' % 391*9c5db199SXin Li (_PRESENTATION_SHADOW_CONFIG_FILE, e)) 392*9c5db199SXin Li try: 393*9c5db199SXin Li cros_version, chrome_version = _get_version_numbers(test.attributes) 394*9c5db199SXin Li presentation_info = _gather_presentation_info(config_data, test_name) 395*9c5db199SXin Li formatted_data = _format_for_upload(image_board_name, cros_version, 396*9c5db199SXin Li chrome_version, hardware_id, 397*9c5db199SXin Li hardware_hostname, test.perf_values, 398*9c5db199SXin Li presentation_info, jobname) 399*9c5db199SXin Li _send_to_dashboard(formatted_data) 400*9c5db199SXin Li except PerfUploadingError as e: 401*9c5db199SXin Li tko_utils.dprint('Warning: unable to upload perf data to the perf ' 402*9c5db199SXin Li 'dashboard for test %s: %s' % (test_name, e)) 403*9c5db199SXin Li else: 404*9c5db199SXin Li tko_utils.dprint('Successfully uploaded perf data to the perf ' 405*9c5db199SXin Li 'dashboard for test %s.' % test_name) 406