blob: 24b55eb48db0b8a9a08fba8ac7d79851ecfb2d28 [file] [log] [blame]
Dennis Jeffreyf9bef6c2013-08-05 11:01:27 -07001# 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
Quinten Yearsley5a66aea2015-04-14 12:40:25 -070016import httplib, json, math, os, re, urllib, urllib2
Dennis Jeffreyf9bef6c2013-08-05 11:01:27 -070017
18import common
Hsinyu Chaoe0b08e62015-08-11 10:50:37 +000019from autotest_lib.client.cros import constants
Dennis Jeffreyf9bef6c2013-08-05 11:01:27 -070020from autotest_lib.tko import utils as tko_utils
21
22_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
23_PRESENTATION_CONFIG_FILE = os.path.join(
24 _ROOT_DIR, 'perf_dashboard_config.json')
Dennis Jeffreyf9bef6c2013-08-05 11:01:27 -070025_DASHBOARD_UPLOAD_URL = 'https://chromeperf.appspot.com/add_point'
26
Quinten Yearsley5a66aea2015-04-14 12:40:25 -070027# Format for Chrome and Chrome OS version strings.
28VERSION_REGEXP = r'^(\d+)\.(\d+)\.(\d+)\.(\d+)$'
Dennis Jeffreyf9bef6c2013-08-05 11:01:27 -070029
Fang Deng947502e2014-05-07 11:59:07 -070030class PerfUploadingError(Exception):
31 """Exception raised in perf_uploader"""
32 pass
33
34
Dennis Jeffreyf9bef6c2013-08-05 11:01:27 -070035def _aggregate_iterations(perf_values):
36 """Aggregate same measurements from multiple iterations.
37
38 Each perf measurement may exist multiple times across multiple iterations
39 of a test. Here, the results for each unique measured perf metric are
40 aggregated across multiple iterations.
41
42 @param perf_values: A list of tko.models.perf_value_iteration objects.
43
44 @return A dictionary mapping each unique measured perf value (keyed by
Puthikorn Voravootivatad2c1e62014-06-10 18:04:03 -070045 tuple of its description and graph name) to information about that
46 perf value (in particular, the value is a list of values
47 for each iteration).
Dennis Jeffreyf9bef6c2013-08-05 11:01:27 -070048
49 """
50 perf_data = {}
51 for perf_iteration in perf_values:
52 for perf_dict in perf_iteration.perf_measurements:
Puthikorn Voravootivatad2c1e62014-06-10 18:04:03 -070053 key = (perf_dict['description'], perf_dict['graph'])
54 if key not in perf_data:
55 perf_data[key] = {
Dennis Jeffreyf9bef6c2013-08-05 11:01:27 -070056 'units': perf_dict['units'],
57 'higher_is_better': perf_dict['higher_is_better'],
Fang Deng7f24f0b2013-11-12 11:22:16 -080058 'graph': perf_dict['graph'],
Dennis Jeffreyf9bef6c2013-08-05 11:01:27 -070059 'value': [perf_dict['value']], # Note: a list of values.
60 'stddev': perf_dict['stddev']
61 }
62 else:
Puthikorn Voravootivatad2c1e62014-06-10 18:04:03 -070063 perf_data[key]['value'].append(perf_dict['value'])
Dennis Jeffreyf9bef6c2013-08-05 11:01:27 -070064 # Note: the stddev will be recomputed later when the results
65 # from each of the multiple iterations are averaged together.
66 return perf_data
67
68
69def _mean_and_stddev(data, precision=4):
70 """Computes mean and standard deviation from a list of numbers.
71
72 Assumes that the list contains at least 2 numbers.
73
74 @param data: A list of numeric values.
75 @param precision: The integer number of decimal places to which to
76 round the results.
77
78 @return A 2-tuple (mean, standard_deviation), in which each value is
79 rounded to |precision| decimal places.
80
81 """
82 n = len(data)
83 mean = float(sum(data)) / n
84 # Divide by n-1 to compute "sample standard deviation".
85 variance = sum([(elem - mean) ** 2 for elem in data]) / (n - 1)
86 return round(mean, precision), round(math.sqrt(variance), precision)
87
88
89def _compute_avg_stddev(perf_data):
90 """Compute average and standard deviations as needed for perf measurements.
91
92 For any perf measurement that exists in multiple iterations (has more than
93 one measured value), compute the average and standard deviation for it and
94 then store the updated information in the dictionary.
95
96 @param perf_data: A dictionary of measured perf data as computed by
97 _aggregate_iterations(), except each value is now a single value, not a
98 list of values.
99
100 """
101 for perf_dict in perf_data.itervalues():
102 if len(perf_dict['value']) > 1:
103 perf_dict['value'], perf_dict['stddev'] = (
104 _mean_and_stddev(map(float, perf_dict['value'])))
105 else:
106 perf_dict['value'] = perf_dict['value'][0] # Take out of list.
107
108
109def _parse_config_file():
110 """Parses a presentation config file and stores the info into a dict.
111
112 The config file contains information about how to present the perf data
113 on the perf dashboard. This is required if the default presentation
114 settings aren't desired for certain tests.
115
Fang Deng947502e2014-05-07 11:59:07 -0700116 @returns A dictionary mapping each unique autotest name to a dictionary
Dennis Jeffreyf9bef6c2013-08-05 11:01:27 -0700117 of presentation config information.
118
Fang Deng947502e2014-05-07 11:59:07 -0700119 @raises PerfUploadingError if config data or master name for the test
120 is missing from the config file.
121
Dennis Jeffreyf9bef6c2013-08-05 11:01:27 -0700122 """
123 json_obj = []
124 if os.path.exists(_PRESENTATION_CONFIG_FILE):
125 with open(_PRESENTATION_CONFIG_FILE, 'r') as fp:
126 json_obj = json.load(fp)
127 config_dict = {}
128 for entry in json_obj:
129 config_dict[entry['autotest_name']] = entry
130 return config_dict
131
132
133def _gather_presentation_info(config_data, test_name):
134 """Gathers presentation info from config data for the given test name.
135
136 @param config_data: A dictionary of dashboard presentation info for all
137 tests, as returned by _parse_config_file(). Info is keyed by autotest
138 name.
139 @param test_name: The name of an autotest.
140
141 @return A dictionary containing presentation information extracted from
142 |config_data| for the given autotest name.
Quinten Yearsley5a66aea2015-04-14 12:40:25 -0700143
144 @raises PerfUploadingError if some required data is missing.
Dennis Jeffreyf9bef6c2013-08-05 11:01:27 -0700145 """
Fang Deng947502e2014-05-07 11:59:07 -0700146 if not test_name in config_data:
147 raise PerfUploadingError(
148 'No config data is specified for test %s in %s.' %
149 (test_name, _PRESENTATION_CONFIG_FILE))
150
151 presentation_dict = config_data[test_name]
152 try:
153 master_name = presentation_dict['master_name']
154 except KeyError:
155 raise PerfUploadingError(
156 'No master name is specified for test %s in %s.' %
157 (test_name, _PRESENTATION_CONFIG_FILE))
158 if 'dashboard_test_name' in presentation_dict:
159 test_name = presentation_dict['dashboard_test_name']
Fang Deng7f24f0b2013-11-12 11:22:16 -0800160 return {'master_name': master_name, 'test_name': test_name}
Dennis Jeffreyf9bef6c2013-08-05 11:01:27 -0700161
162
Keith Haddowac0b1642014-11-20 10:26:44 -0800163def _format_for_upload(platform_name, cros_version, chrome_version,
Kris Rambisheb04ae42015-02-04 16:28:44 -0800164 hardware_id, variant_name, hardware_hostname,
165 perf_data, presentation_info):
Dennis Jeffreyf9bef6c2013-08-05 11:01:27 -0700166 """Formats perf data suitably to upload to the perf dashboard.
167
168 The perf dashboard expects perf data to be uploaded as a
169 specially-formatted JSON string. In particular, the JSON object must be a
170 dictionary with key "data", and value being a list of dictionaries where
171 each dictionary contains all the information associated with a single
172 measured perf value: master name, bot name, test name, perf value, error
173 value, units, and build version numbers.
174
175 @param platform_name: The string name of the platform.
176 @param cros_version: The string chromeOS version number.
177 @param chrome_version: The string chrome version number.
Keith Haddowac0b1642014-11-20 10:26:44 -0800178 @param hardware_id: String that identifies the type of hardware the test was
179 executed on.
Kris Rambisheb04ae42015-02-04 16:28:44 -0800180 @param variant_name: String that identifies the variant name of the board.
“Keith390e85c2015-01-09 14:28:07 -0800181 @param hardware_hostname: String that identifies the name of the device the
182 test was executed on.
Dennis Jeffreyf9bef6c2013-08-05 11:01:27 -0700183 @param perf_data: A dictionary of measured perf data as computed by
184 _compute_avg_stddev().
185 @param presentation_info: A dictionary of dashboard presentation info for
186 the given test, as identified by _gather_presentation_info().
187
188 @return A dictionary containing the formatted information ready to upload
189 to the performance dashboard.
190
191 """
192 dash_entries = []
Kris Rambisheb04ae42015-02-04 16:28:44 -0800193 if variant_name:
194 platform_name += '-' + variant_name
Ilja Friedel890cab32014-11-21 00:07:07 +0000195 for (desc, graph), data in perf_data.iteritems():
Dennis Jeffreyf9bef6c2013-08-05 11:01:27 -0700196 # Each perf metric is named by a path that encodes the test name,
197 # a graph name (if specified), and a description. This must be defined
198 # according to rules set by the Chrome team, as implemented in:
199 # chromium/tools/build/scripts/slave/results_dashboard.py.
Dennis Jeffreyf9bef6c2013-08-05 11:01:27 -0700200 if desc.endswith('_ref'):
201 desc = 'ref'
202 desc = desc.replace('_by_url', '')
203 desc = desc.replace('/', '_')
Fang Deng7f24f0b2013-11-12 11:22:16 -0800204 if data['graph']:
Dennis Jeffreyf9bef6c2013-08-05 11:01:27 -0700205 test_path = '%s/%s/%s' % (presentation_info['test_name'],
Fang Deng7f24f0b2013-11-12 11:22:16 -0800206 data['graph'], desc)
Dennis Jeffreyf9bef6c2013-08-05 11:01:27 -0700207 else:
208 test_path = '%s/%s' % (presentation_info['test_name'], desc)
209
210 new_dash_entry = {
211 'master': presentation_info['master_name'],
212 'bot': 'cros-' + platform_name, # Prefix to clarify it's chromeOS.
213 'test': test_path,
Fang Deng7f24f0b2013-11-12 11:22:16 -0800214 'value': data['value'],
215 'error': data['stddev'],
216 'units': data['units'],
Puthikorn Voravootivatfd03bcb2014-05-16 16:35:55 -0700217 'higher_is_better': data['higher_is_better'],
Quinten Yearsley5a66aea2015-04-14 12:40:25 -0700218 'revision': _get_id_from_version(chrome_version, cros_version),
Dennis Jeffreyf9bef6c2013-08-05 11:01:27 -0700219 'supplemental_columns': {
220 'r_cros_version': cros_version,
221 'r_chrome_version': chrome_version,
Quinten Yearsley5a66aea2015-04-14 12:40:25 -0700222 'a_default_rev': 'r_chrome_version',
Keith Haddowac0b1642014-11-20 10:26:44 -0800223 'a_hardware_identifier': hardware_id,
“Keith390e85c2015-01-09 14:28:07 -0800224 'a_hardware_hostname': hardware_hostname,
Dennis Jeffreyf9bef6c2013-08-05 11:01:27 -0700225 }
226 }
227
228 dash_entries.append(new_dash_entry)
229
230 json_string = json.dumps(dash_entries)
231 return {'data': json_string}
232
233
Quinten Yearsley5a66aea2015-04-14 12:40:25 -0700234def _get_version_numbers(test_attributes):
235 """Gets the version numbers from the test attributes and validates them.
236
237 @param test_attributes: The attributes property (which is a dict) of an
238 autotest tko.models.test object.
239
240 @return A pair of strings (Chrome OS version, Chrome version).
241
242 @raises PerfUploadingError if a version isn't formatted as expected.
243 """
244 chrome_version = test_attributes.get('CHROME_VERSION', '')
245 cros_version = test_attributes.get('CHROMEOS_RELEASE_VERSION', '')
246 # Prefix the ChromeOS version number with the Chrome milestone.
247 cros_version = chrome_version[:chrome_version.find('.') + 1] + cros_version
248 if not re.match(VERSION_REGEXP, cros_version):
249 raise PerfUploadingError('CrOS version "%s" does not match expected '
250 'format.' % cros_version)
251 if not re.match(VERSION_REGEXP, chrome_version):
252 raise PerfUploadingError('Chrome version "%s" does not match expected '
253 'format.' % chrome_version)
254 return (cros_version, chrome_version)
255
256
257def _get_id_from_version(chrome_version, cros_version):
258 """Computes the point ID to use, from Chrome and ChromeOS version numbers.
259
260 For ChromeOS row data, data values are associated with both a Chrome
261 version number and a ChromeOS version number (unlike for Chrome row data
262 that is associated with a single revision number). This function takes
263 both version numbers as input, then computes a single, unique integer ID
264 from them, which serves as a 'fake' revision number that can uniquely
265 identify each ChromeOS data point, and which will allow ChromeOS data points
266 to be sorted by Chrome version number, with ties broken by ChromeOS version
267 number.
268
269 To compute the integer ID, we take the portions of each version number that
270 serve as the shortest unambiguous names for each (as described here:
271 http://www.chromium.org/developers/version-numbers). We then force each
272 component of each portion to be a fixed width (padded by zeros if needed),
273 concatenate all digits together (with those coming from the Chrome version
274 number first), and convert the entire string of digits into an integer.
275 We ensure that the total number of digits does not exceed that which is
276 allowed by AppEngine NDB for an integer (64-bit signed value).
277
278 For example:
279 Chrome version: 27.0.1452.2 (shortest unambiguous name: 1452.2)
280 ChromeOS version: 27.3906.0.0 (shortest unambiguous name: 3906.0.0)
281 concatenated together with padding for fixed-width columns:
282 ('01452' + '002') + ('03906' + '000' + '00') = '014520020390600000'
283 Final integer ID: 14520020390600000
284
285 @param chrome_ver: The Chrome version number as a string.
286 @param cros_ver: The ChromeOS version number as a string.
287
288 @return A unique integer ID associated with the two given version numbers.
289
290 """
291
292 # Number of digits to use from each part of the version string for Chrome
293 # and Chrome OS versions when building a point ID out of these two versions.
294 chrome_version_col_widths = [0, 0, 5, 3]
295 cros_version_col_widths = [0, 5, 3, 2]
296
297 def get_digits_from_version(version_num, column_widths):
298 if re.match(VERSION_REGEXP, version_num):
299 computed_string = ''
300 version_parts = version_num.split('.')
301 for i, version_part in enumerate(version_parts):
302 if column_widths[i]:
303 computed_string += version_part.zfill(column_widths[i])
304 return computed_string
305 else:
306 return None
307
308 chrome_digits = get_digits_from_version(
309 chrome_version, chrome_version_col_widths)
310 cros_digits = get_digits_from_version(
311 cros_version, cros_version_col_widths)
312 if not chrome_digits or not cros_digits:
313 return None
314 result_digits = chrome_digits + cros_digits
315 max_digits = sum(chrome_version_col_widths + cros_version_col_widths)
316 if len(result_digits) > max_digits:
317 return None
318 return int(result_digits)
319
320
Dennis Jeffreyf9bef6c2013-08-05 11:01:27 -0700321def _send_to_dashboard(data_obj):
322 """Sends formatted perf data to the perf dashboard.
323
324 @param data_obj: A formatted data object as returned by
325 _format_for_upload().
326
Fang Deng947502e2014-05-07 11:59:07 -0700327 @raises PerfUploadingError if an exception was raised when uploading.
Dennis Jeffreyf9bef6c2013-08-05 11:01:27 -0700328
329 """
330 encoded = urllib.urlencode(data_obj)
331 req = urllib2.Request(_DASHBOARD_UPLOAD_URL, encoded)
332 try:
333 urllib2.urlopen(req)
Fang Deng947502e2014-05-07 11:59:07 -0700334 except urllib2.HTTPError as e:
335 raise PerfUploadingError('HTTPError: %d %s for JSON %s\n' % (
336 e.code, e.msg, data_obj['data']))
337 except urllib2.URLError as e:
338 raise PerfUploadingError(
339 'URLError: %s for JSON %s\n' %
340 (str(e.reason), data_obj['data']))
Dennis Jeffreyf9bef6c2013-08-05 11:01:27 -0700341 except httplib.HTTPException:
Fang Deng947502e2014-05-07 11:59:07 -0700342 raise PerfUploadingError(
343 'HTTPException for JSON %s\n' % data_obj['data'])
Dennis Jeffreyf9bef6c2013-08-05 11:01:27 -0700344
345
346def upload_test(job, test):
347 """Uploads any perf data associated with a test to the perf dashboard.
348
349 @param job: An autotest tko.models.job object that is associated with the
350 given |test|.
351 @param test: An autotest tko.models.test object that may or may not be
352 associated with measured perf data.
353
354 """
355 if not test.perf_values:
356 return
357
358 # Aggregate values from multiple iterations together.
359 perf_data = _aggregate_iterations(test.perf_values)
360
361 # Compute averages and standard deviations as needed for measured perf
362 # values that exist in multiple iterations. Ultimately, we only upload a
363 # single measurement (with standard deviation) for every unique measured
364 # perf metric.
365 _compute_avg_stddev(perf_data)
366
367 # Format the perf data for the upload, then upload it.
368 test_name = test.testname
369 platform_name = job.machine_group
Keith Haddowac0b1642014-11-20 10:26:44 -0800370 hardware_id = test.attributes.get('hwid', '')
“Keith390e85c2015-01-09 14:28:07 -0800371 hardware_hostname = test.machine
Kris Rambisheb04ae42015-02-04 16:28:44 -0800372 variant_name = test.attributes.get(constants.VARIANT_KEY, None)
Dennis Jeffreyf9bef6c2013-08-05 11:01:27 -0700373 config_data = _parse_config_file()
Fang Deng947502e2014-05-07 11:59:07 -0700374 try:
Quinten Yearsley5a66aea2015-04-14 12:40:25 -0700375 cros_version, chrome_version = _get_version_numbers(test.attributes)
Fang Deng947502e2014-05-07 11:59:07 -0700376 presentation_info = _gather_presentation_info(config_data, test_name)
377 formatted_data = _format_for_upload(
Keith Haddowac0b1642014-11-20 10:26:44 -0800378 platform_name, cros_version, chrome_version, hardware_id,
Kris Rambisheb04ae42015-02-04 16:28:44 -0800379 variant_name, hardware_hostname, perf_data, presentation_info)
Fang Deng947502e2014-05-07 11:59:07 -0700380 _send_to_dashboard(formatted_data)
381 except PerfUploadingError as e:
Dennis Jeffreyf9bef6c2013-08-05 11:01:27 -0700382 tko_utils.dprint('Error when uploading perf data to the perf '
Fang Deng947502e2014-05-07 11:59:07 -0700383 'dashboard for test %s: %s' % (test_name, e))
Dennis Jeffreyf9bef6c2013-08-05 11:01:27 -0700384 else:
385 tko_utils.dprint('Successfully uploaded perf data to the perf '
386 'dashboard for test %s.' % test_name)