• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Uploads performance data to the performance dashboard.
6
7Performance tests may output data that needs to be displayed on the performance
8dashboard.  The autotest TKO parser invokes this module with each test
9associated with a job.  If a test has performance data associated with it, it
10is uploaded to the performance dashboard.  The performance dashboard is owned
11by Chrome team and is available here: https://chromeperf.appspot.com/.  Users
12must be logged in with an @google.com account to view chromeOS perf data there.
13
14"""
15
16import httplib
17import json
18import os
19import re
20import urllib
21import urllib2
22
23import common
24from autotest_lib.client.cros import constants
25from autotest_lib.tko import utils as tko_utils
26
27_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
28_PRESENTATION_CONFIG_FILE = os.path.join(
29        _ROOT_DIR, 'perf_dashboard_config.json')
30_PRESENTATION_SHADOW_CONFIG_FILE = os.path.join(
31        _ROOT_DIR, 'perf_dashboard_shadow_config.json')
32_DASHBOARD_UPLOAD_URL = 'https://chromeperf.appspot.com/add_point'
33
34# Format for Chrome and Chrome OS version strings.
35VERSION_REGEXP = r'^(\d+)\.(\d+)\.(\d+)\.(\d+)$'
36
37class PerfUploadingError(Exception):
38    """Exception raised in perf_uploader"""
39    pass
40
41
42def _parse_config_file(config_file):
43    """Parses a presentation config file and stores the info into a dict.
44
45    The config file contains information about how to present the perf data
46    on the perf dashboard.  This is required if the default presentation
47    settings aren't desired for certain tests.
48
49    @param config_file: Path to the configuration file to be parsed.
50
51    @returns A dictionary mapping each unique autotest name to a dictionary
52        of presentation config information.
53
54    @raises PerfUploadingError if config data or master name for the test
55        is missing from the config file.
56
57    """
58    json_obj = []
59    if os.path.exists(config_file):
60        with open(config_file, 'r') as fp:
61            json_obj = json.load(fp)
62    config_dict = {}
63    for entry in json_obj:
64        config_dict[entry['autotest_name']] = entry
65    return config_dict
66
67
68def _gather_presentation_info(config_data, test_name):
69    """Gathers presentation info from config data for the given test name.
70
71    @param config_data: A dictionary of dashboard presentation info for all
72        tests, as returned by _parse_config_file().  Info is keyed by autotest
73        name.
74    @param test_name: The name of an autotest.
75
76    @return A dictionary containing presentation information extracted from
77        |config_data| for the given autotest name.
78
79    @raises PerfUploadingError if some required data is missing.
80    """
81    if not test_name in config_data:
82        raise PerfUploadingError(
83                'No config data is specified for test %s in %s.' %
84                (test_name, _PRESENTATION_CONFIG_FILE))
85
86    presentation_dict = config_data[test_name]
87    try:
88        master_name = presentation_dict['master_name']
89    except KeyError:
90        raise PerfUploadingError(
91                'No master name is specified for test %s in %s.' %
92                (test_name, _PRESENTATION_CONFIG_FILE))
93    if 'dashboard_test_name' in presentation_dict:
94        test_name = presentation_dict['dashboard_test_name']
95    return {'master_name': master_name, 'test_name': test_name}
96
97
98def _format_for_upload(platform_name, cros_version, chrome_version,
99                       hardware_id, variant_name, hardware_hostname,
100                       perf_data, presentation_info, jobname):
101    """Formats perf data suitable to upload to the perf dashboard.
102
103    The perf dashboard expects perf data to be uploaded as a
104    specially-formatted JSON string.  In particular, the JSON object must be a
105    dictionary with key "data", and value being a list of dictionaries where
106    each dictionary contains all the information associated with a single
107    measured perf value: master name, bot name, test name, perf value, error
108    value, units, and build version numbers.
109
110    @param platform_name: The string name of the platform.
111    @param cros_version: The string chromeOS version number.
112    @param chrome_version: The string chrome version number.
113    @param hardware_id: String that identifies the type of hardware the test was
114            executed on.
115    @param variant_name: String that identifies the variant name of the board.
116    @param hardware_hostname: String that identifies the name of the device the
117            test was executed on.
118    @param perf_data: A dictionary of measured perf data as computed by
119            _compute_avg_stddev().
120    @param presentation_info: A dictionary of dashboard presentation info for
121            the given test, as identified by _gather_presentation_info().
122    @param jobname: A string uniquely identifying the test run, this enables
123            linking back from a test result to the logs of the test run.
124
125    @return A dictionary containing the formatted information ready to upload
126        to the performance dashboard.
127
128    """
129    if variant_name:
130        platform_name += '-' + variant_name
131
132    perf_values = perf_data
133    # Client side case - server side comes with its own charts data section.
134    if 'charts' not in perf_values:
135        perf_values = {
136          'format_version': '1.0',
137          'benchmark_name': presentation_info['test_name'],
138          'charts': perf_data,
139        }
140
141    dash_entry = {
142        'master': presentation_info['master_name'],
143        'bot': 'cros-' + platform_name,  # Prefix to clarify it's ChromeOS.
144        'point_id': _get_id_from_version(chrome_version, cros_version),
145        'versions': {
146            'cros_version': cros_version,
147            'chrome_version': chrome_version,
148        },
149        'supplemental': {
150            'default_rev': 'r_cros_version',
151            'hardware_identifier': hardware_id,
152            'hardware_hostname': hardware_hostname,
153            'variant_name': variant_name,
154            'jobname': jobname,
155        },
156        'chart_data': perf_values,
157    }
158    return {'data': json.dumps(dash_entry)}
159
160
161def _get_version_numbers(test_attributes):
162    """Gets the version numbers from the test attributes and validates them.
163
164    @param test_attributes: The attributes property (which is a dict) of an
165        autotest tko.models.test object.
166
167    @return A pair of strings (Chrome OS version, Chrome version).
168
169    @raises PerfUploadingError if a version isn't formatted as expected.
170    """
171    chrome_version = test_attributes.get('CHROME_VERSION', '')
172    cros_version = test_attributes.get('CHROMEOS_RELEASE_VERSION', '')
173    cros_milestone = test_attributes.get('CHROMEOS_RELEASE_CHROME_MILESTONE')
174    # Use the release milestone as the milestone if present, othewise prefix the
175    # cros version with the with the Chrome browser milestone.
176    if cros_milestone:
177      cros_version = "%s.%s" % (cros_milestone, cros_version)
178    else:
179      cros_version = chrome_version[:chrome_version.find('.') + 1] + cros_version
180    if not re.match(VERSION_REGEXP, cros_version):
181        raise PerfUploadingError('CrOS version "%s" does not match expected '
182                                 'format.' % cros_version)
183    if not re.match(VERSION_REGEXP, chrome_version):
184        raise PerfUploadingError('Chrome version "%s" does not match expected '
185                                 'format.' % chrome_version)
186    return (cros_version, chrome_version)
187
188
189def _get_id_from_version(chrome_version, cros_version):
190    """Computes the point ID to use, from Chrome and ChromeOS version numbers.
191
192    For ChromeOS row data, data values are associated with both a Chrome
193    version number and a ChromeOS version number (unlike for Chrome row data
194    that is associated with a single revision number).  This function takes
195    both version numbers as input, then computes a single, unique integer ID
196    from them, which serves as a 'fake' revision number that can uniquely
197    identify each ChromeOS data point, and which will allow ChromeOS data points
198    to be sorted by Chrome version number, with ties broken by ChromeOS version
199    number.
200
201    To compute the integer ID, we take the portions of each version number that
202    serve as the shortest unambiguous names for each (as described here:
203    http://www.chromium.org/developers/version-numbers).  We then force each
204    component of each portion to be a fixed width (padded by zeros if needed),
205    concatenate all digits together (with those coming from the Chrome version
206    number first), and convert the entire string of digits into an integer.
207    We ensure that the total number of digits does not exceed that which is
208    allowed by AppEngine NDB for an integer (64-bit signed value).
209
210    For example:
211      Chrome version: 27.0.1452.2 (shortest unambiguous name: 1452.2)
212      ChromeOS version: 27.3906.0.0 (shortest unambiguous name: 3906.0.0)
213      concatenated together with padding for fixed-width columns:
214          ('01452' + '002') + ('03906' + '000' + '00') = '014520020390600000'
215      Final integer ID: 14520020390600000
216
217    @param chrome_ver: The Chrome version number as a string.
218    @param cros_ver: The ChromeOS version number as a string.
219
220    @return A unique integer ID associated with the two given version numbers.
221
222    """
223
224    # Number of digits to use from each part of the version string for Chrome
225    # and Chrome OS versions when building a point ID out of these two versions.
226    chrome_version_col_widths = [0, 0, 5, 3]
227    cros_version_col_widths = [0, 5, 3, 2]
228
229    def get_digits_from_version(version_num, column_widths):
230        if re.match(VERSION_REGEXP, version_num):
231            computed_string = ''
232            version_parts = version_num.split('.')
233            for i, version_part in enumerate(version_parts):
234                if column_widths[i]:
235                    computed_string += version_part.zfill(column_widths[i])
236            return computed_string
237        else:
238            return None
239
240    chrome_digits = get_digits_from_version(
241            chrome_version, chrome_version_col_widths)
242    cros_digits = get_digits_from_version(
243            cros_version, cros_version_col_widths)
244    if not chrome_digits or not cros_digits:
245        return None
246    result_digits = chrome_digits + cros_digits
247    max_digits = sum(chrome_version_col_widths + cros_version_col_widths)
248    if len(result_digits) > max_digits:
249        return None
250    return int(result_digits)
251
252
253def _send_to_dashboard(data_obj):
254    """Sends formatted perf data to the perf dashboard.
255
256    @param data_obj: A formatted data object as returned by
257        _format_for_upload().
258
259    @raises PerfUploadingError if an exception was raised when uploading.
260
261    """
262    encoded = urllib.urlencode(data_obj)
263    req = urllib2.Request(_DASHBOARD_UPLOAD_URL, encoded)
264    try:
265        urllib2.urlopen(req)
266    except urllib2.HTTPError as e:
267        raise PerfUploadingError('HTTPError: %d %s for JSON %s\n' % (
268                e.code, e.msg, data_obj['data']))
269    except urllib2.URLError as e:
270        raise PerfUploadingError(
271                'URLError: %s for JSON %s\n' %
272                (str(e.reason), data_obj['data']))
273    except httplib.HTTPException:
274        raise PerfUploadingError(
275                'HTTPException for JSON %s\n' % data_obj['data'])
276
277
278def upload_test(job, test, jobname):
279    """Uploads any perf data associated with a test to the perf dashboard.
280
281    @param job: An autotest tko.models.job object that is associated with the
282        given |test|.
283    @param test: An autotest tko.models.test object that may or may not be
284        associated with measured perf data.
285    @param jobname: A string uniquely identifying the test run, this enables
286            linking back from a test result to the logs of the test run.
287
288    """
289
290    # Format the perf data for the upload, then upload it.
291    test_name = test.testname
292    platform_name = job.machine_group
293    # Append the platform name with '.arc' if the suffix of the control
294    # filename is '.arc'.
295    if job.label and re.match('.*\.arc$', job.label):
296        platform_name += '.arc'
297    hardware_id = test.attributes.get('hwid', '')
298    hardware_hostname = test.machine
299    variant_name = test.attributes.get(constants.VARIANT_KEY, None)
300    config_data = _parse_config_file(_PRESENTATION_CONFIG_FILE)
301    try:
302        shadow_config_data = _parse_config_file(_PRESENTATION_SHADOW_CONFIG_FILE)
303        config_data.update(shadow_config_data)
304    except ValueError as e:
305        tko_utils.dprint('Failed to parse config file %s: %s.' %
306                         (_PRESENTATION_SHADOW_CONFIG_FILE, e))
307    try:
308        cros_version, chrome_version = _get_version_numbers(test.attributes)
309        presentation_info = _gather_presentation_info(config_data, test_name)
310        formatted_data = _format_for_upload(
311                platform_name, cros_version, chrome_version, hardware_id,
312                variant_name, hardware_hostname, test.perf_values,
313                presentation_info, jobname)
314        _send_to_dashboard(formatted_data)
315    except PerfUploadingError as e:
316        tko_utils.dprint('Error when uploading perf data to the perf '
317                         'dashboard for test %s: %s' % (test_name, e))
318    else:
319        tko_utils.dprint('Successfully uploaded perf data to the perf '
320                         'dashboard for test %s.' % test_name)
321
322