xref: /aosp_15_r20/external/autotest/tko/perf_upload/perf_uploader.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
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