Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(101)

Side by Side Diff: scripts/slave/recipe_modules/perf_try/api.py

Issue 1573293002: Change auto_bisect to post results to perf dashboard. (Closed) Base URL: https://chromium.googlesource.com/chromium/tools/build.git@master
Patch Set: address comments Created 4 years, 11 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 # Copyright 2015 The Chromium Authors. All rights reserved. 1 # Copyright 2015 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be 2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file. 3 # found in the LICENSE file.
4 4
5 """API for the perf try job recipe module. 5 """API for the perf try job recipe module.
6 6
7 This API is meant to enable the perf try job recipe on any chromium-supported 7 This API is meant to enable the perf try job recipe on any chromium-supported
8 platform for any test that can be run via buildbot, perf or otherwise. 8 platform for any test that can be run via buildbot, perf or otherwise.
9 """ 9 """
10 10
11 import re 11 import re
12 import urllib
12 13
13 from recipe_engine import recipe_api 14 from recipe_engine import recipe_api
14 15
15
16 PERF_CONFIG_FILE = 'tools/run-perf-test.cfg' 16 PERF_CONFIG_FILE = 'tools/run-perf-test.cfg'
17 WEBKIT_PERF_CONFIG_FILE = 'third_party/WebKit/Tools/run-perf-test.cfg' 17 WEBKIT_PERF_CONFIG_FILE = 'third_party/WebKit/Tools/run-perf-test.cfg'
18 PERF_BENCHMARKS_PATH = 'tools/perf/benchmarks' 18 PERF_BENCHMARKS_PATH = 'tools/perf/benchmarks'
19 PERF_MEASUREMENTS_PATH = 'tools/perf/measurements' 19 PERF_MEASUREMENTS_PATH = 'tools/perf/measurements'
20 BUILDBOT_BUILDERNAME = 'BUILDBOT_BUILDERNAME' 20 BUILDBOT_BUILDERNAME = 'BUILDBOT_BUILDERNAME'
21 BENCHMARKS_JSON_FILE = 'benchmarks.json' 21 BENCHMARKS_JSON_FILE = 'benchmarks.json'
22 22
23 CLOUD_RESULTS_LINK = (r'\s(?P<VALUES>http://storage.googleapis.com/' 23 CLOUD_RESULTS_LINK = (r'\s(?P<VALUES>http://storage.googleapis.com/'
24 'chromium-telemetry/html-results/results-[a-z0-9-_]+)\s') 24 'chromium-telemetry/html-results/results-[a-z0-9-_]+)\s')
25 PROFILER_RESULTS_LINK = (r'\s(?P<VALUES>https://console.developers.google.com/' 25 PROFILER_RESULTS_LINK = (r'\s(?P<VALUES>https://console.developers.google.com/'
(...skipping 56 matching lines...) Expand 10 before | Expand all | Expand 10 after
82 upload_on_last_run=True, 82 upload_on_last_run=True,
83 results_label='TOT' if r[1] is None else r[1], 83 results_label='TOT' if r[1] is None else r[1],
84 allow_flakes=False) 84 allow_flakes=False)
85 85
86 labels = { 86 labels = {
87 'profiler_link1': ('%s - Profiler Data' % 'With Patch' 87 'profiler_link1': ('%s - Profiler Data' % 'With Patch'
88 if r[0] is None else r[0]), 88 if r[0] is None else r[0]),
89 'profiler_link2': ('%s - Profiler Data' % 'Without Patch' 89 'profiler_link2': ('%s - Profiler Data' % 'Without Patch'
90 if r[1] is None else r[1]) 90 if r[1] is None else r[1])
91 } 91 }
92
93 # TODO(chrisphan): Deprecate this. perf_dashboard.post_bisect below
94 # already outputs data in json format.
92 self._compare_and_present_results( 95 self._compare_and_present_results(
93 test_cfg, results_without_patch, results_with_patch, labels) 96 test_cfg, results_without_patch, results_with_patch, labels)
94 97
98 bisect_results = self.get_result(
99 test_cfg, results_without_patch, results_with_patch, labels)
100 self.m.perf_dashboard.set_default_config()
101 self.m.perf_dashboard.post_bisect(bisect_results, halt_on_failure=True)
102
95 def run_cq_job(self, update_step, master_dict, files_in_patch): 103 def run_cq_job(self, update_step, master_dict, files_in_patch):
96 """Runs benchmarks affected by a CL on CQ.""" 104 """Runs benchmarks affected by a CL on CQ."""
97 buildername = self.m.properties['buildername'] 105 buildername = self.m.properties['buildername']
98 affected_benchmarks = self._get_affected_benchmarks(files_in_patch) 106 affected_benchmarks = self._get_affected_benchmarks(files_in_patch)
99 if not affected_benchmarks: 107 if not affected_benchmarks:
100 step_result = self.m.step('Results', []) 108 step_result = self.m.step('Results', [])
101 step_result.presentation.step_text = ( 109 step_result.presentation.step_text = (
102 'There are no modifications to Telemetry benchmarks,' 110 'There are no modifications to Telemetry benchmarks,'
103 ' aborting the try job.') 111 ' aborting the try job.')
104 return 112 return
(...skipping 171 matching lines...) Expand 10 before | Expand all | Expand 10 after
276 self._get_hash(config.get('good_revision'))) 284 self._get_hash(config.get('good_revision')))
277 285
278 def _compare_and_present_results( 286 def _compare_and_present_results(
279 self, cfg, results_without_patch, results_with_patch, labels): 287 self, cfg, results_without_patch, results_with_patch, labels):
280 """Parses results and creates Results step.""" 288 """Parses results and creates Results step."""
281 output_with_patch = results_with_patch.get('output') 289 output_with_patch = results_with_patch.get('output')
282 output_without_patch = results_without_patch.get('output') 290 output_without_patch = results_without_patch.get('output')
283 values_with_patch = results_with_patch.get('results').get('values') 291 values_with_patch = results_with_patch.get('results').get('values')
284 values_without_patch = results_without_patch.get('results').get('values') 292 values_without_patch = results_without_patch.get('results').get('values')
285 293
286 cloud_links_without_patch = _parse_cloud_links(output_without_patch) 294 cloud_links_without_patch = self.parse_cloud_links(output_without_patch)
287 cloud_links_with_patch = _parse_cloud_links(output_with_patch) 295 cloud_links_with_patch = self.parse_cloud_links(output_with_patch)
288 296
289 results_link = (cloud_links_without_patch['html'][0] 297 results_link = (cloud_links_without_patch['html'][0]
290 if cloud_links_without_patch['html'] else '') 298 if cloud_links_without_patch['html'] else '')
291 299
292 if not values_with_patch or not values_without_patch: 300 if not values_with_patch or not values_without_patch:
293 step_result = self.m.step('Results', []) 301 step_result = self.m.step('Results', [])
294 step_result.presentation.step_text = ( 302 step_result.presentation.step_text = (
295 'No values from test with patch, or none from test without patch.\n' 303 'No values from test with patch, or none from test without patch.\n'
296 'Output with patch:\n%s\n\nOutput without patch:\n%s' % ( 304 'Output with patch:\n%s\n\nOutput without patch:\n%s' % (
297 output_with_patch, output_without_patch)) 305 output_with_patch, output_without_patch))
(...skipping 48 matching lines...) Expand 10 before | Expand all | Expand 10 after
346 step_result.presentation.links.update({ 354 step_result.presentation.links.update({
347 '%s[%d]' % ( 355 '%s[%d]' % (
348 labels.get('profiler_link1'), i): profiler_with_patch[i] 356 labels.get('profiler_link1'), i): profiler_with_patch[i]
349 }) 357 })
350 for i in xrange(len(profiler_without_patch)): # pragma: no cover 358 for i in xrange(len(profiler_without_patch)): # pragma: no cover
351 step_result.presentation.links.update({ 359 step_result.presentation.links.update({
352 '%s[%d]' % ( 360 '%s[%d]' % (
353 labels.get('profiler_link2'), i): profiler_without_patch[i] 361 labels.get('profiler_link2'), i): profiler_without_patch[i]
354 }) 362 })
355 363
364 def parse_cloud_links(self, output):
365 html_results_pattern = re.compile(CLOUD_RESULTS_LINK, re.MULTILINE)
366 profiler_pattern = re.compile(PROFILER_RESULTS_LINK, re.MULTILINE)
356 367
357 def _parse_cloud_links(output): 368 results = {
358 html_results_pattern = re.compile(CLOUD_RESULTS_LINK, re.MULTILINE) 369 'html': html_results_pattern.findall(output),
359 profiler_pattern = re.compile(PROFILER_RESULTS_LINK, re.MULTILINE) 370 'profiler': profiler_pattern.findall(output),
371 }
372 return results
360 373
361 results = {
362 'html': html_results_pattern.findall(output),
363 'profiler': profiler_pattern.findall(output),
364 }
365 374
366 return results 375 def get_result(self, config, results_without_patch, results_with_patch,
376 labels):
377 """Returns the results as a dict."""
378 output_with_patch = results_with_patch.get('output')
379 output_without_patch = results_without_patch.get('output')
380 values_with_patch = results_with_patch.get('results').get('values')
381 values_without_patch = results_without_patch.get('results').get('values')
382
383 cloud_links_without_patch = self.parse_cloud_links(output_without_patch)
384 cloud_links_with_patch = self.parse_cloud_links(output_with_patch)
385
386 cloud_link = (cloud_links_without_patch['html'][0]
387 if cloud_links_without_patch['html'] else '')
388
389 results = {
390 'try_job_id': config.get('try_job_id'),
391 'status': 'completed', # TODO(chrisphan) Get partial results state.
392 'buildbot_log_url': self._get_build_url(),
393 'bisect_bot': self.m.properties.get('buildername', 'Not found'),
394 'command': config.get('command'),
395 'metric': config.get('metric'),
396 'cloud_link': cloud_link,
397 }
398
399 if not values_with_patch or not values_without_patch:
400 results['warnings'] = ['No values from test with patch, or none '
401 'from test without patch.\n Output with patch:\n%s\n\nOutput without '
402 'patch:\n%s' % (output_with_patch, output_without_patch)]
403 return results
404
405 mean_with_patch = self.m.math_utils.mean(values_with_patch)
406 mean_without_patch = self.m.math_utils.mean(values_without_patch)
407
408 stderr_with_patch = self.m.math_utils.standard_error(values_with_patch)
409 stderr_without_patch = self.m.math_utils.standard_error(
410 values_without_patch)
411
412 profiler_with_patch = cloud_links_with_patch['profiler']
413 profiler_without_patch = cloud_links_without_patch['profiler']
414
415 # Calculate the % difference in the means of the 2 runs.
416 relative_change = None
417 std_err = None
418 if mean_with_patch and values_with_patch:
419 relative_change = self.m.math_utils.relative_change(
420 mean_without_patch, mean_with_patch) * 100
421 std_err = self.m.math_utils.pooled_standard_error(
422 [values_with_patch, values_without_patch])
423
424 if relative_change is not None and std_err is not None:
425 data = [
426 ['Revision', 'Mean', 'Std.Error'],
427 ['Patch', str(mean_with_patch), str(stderr_with_patch)],
428 ['No Patch', str(mean_without_patch), str(stderr_without_patch)]
429 ]
430 results['change'] = relative_change
431 results['std_err'] = std_err
432 results['result'] = _pretty_table(data)
433
434 profiler_links = []
435 if profiler_with_patch and profiler_without_patch:
436 for i in xrange(len(profiler_with_patch)): # pragma: no cover
437 profiler_links.append({
438 'title': '%s[%d]' % (labels.get('profiler_link1'), i),
439 'link': profiler_with_patch[i]
440 })
441 for i in xrange(len(profiler_without_patch)): # pragma: no cover
442 profiler_links.append({
443 'title': '%s[%d]' % (labels.get('profiler_link2'), i),
444 'link': profiler_without_patch[i]
445 })
446 results['profiler_links'] = profiler_links
447
448 return results
449
450 def _get_build_url(self):
451 properties = self.m.properties
452 bot_url = properties.get('buildbotURL',
453 'http://build.chromium.org/p/chromium/')
454 builder_name = urllib.quote(properties.get('buildername', ''))
455 builder_number = str(properties.get('buildnumber', ''))
456 return '%sbuilders/%s/builds/%s' % (bot_url, builder_name, builder_number)
367 457
368 458
369 def _validate_perf_config(config_contents, required_parameters): 459 def _validate_perf_config(config_contents, required_parameters):
370 """Validates the perf config file contents. 460 """Validates the perf config file contents.
371 461
372 This is used when we're doing a perf try job, the config file is called 462 This is used when we're doing a perf try job, the config file is called
373 run-perf-test.cfg by default. 463 run-perf-test.cfg by default.
374 464
375 The parameters checked are the required parameters; any additional optional 465 The parameters checked are the required parameters; any additional optional
376 parameters won't be checked and validation will still pass. 466 parameters won't be checked and validation will still pass.
(...skipping 28 matching lines...) Expand all
405 def _is_benchmark_match(benchmark, affected_benchmarks): 495 def _is_benchmark_match(benchmark, affected_benchmarks):
406 # TODO(prasadv): We should make more robust logic to determine if a 496 # TODO(prasadv): We should make more robust logic to determine if a
407 # which benchmark to run on CQ. Right now it just compares the file name 497 # which benchmark to run on CQ. Right now it just compares the file name
408 # with the benchmark name, which isn't necessarily correct. crbug.com/510925. 498 # with the benchmark name, which isn't necessarily correct. crbug.com/510925.
409 for b in affected_benchmarks: 499 for b in affected_benchmarks:
410 if benchmark.startswith(b): 500 if benchmark.startswith(b):
411 return True 501 return True
412 return False 502 return False
413 503
414 504
415 # TODO(prasadv): This method already exists in auto_bisect module,
416 # we need to identify a common location move this there, so that recipe modules
417 # share them.
418 def _pretty_table(data): 505 def _pretty_table(data):
419 """Arrange a matrix of strings into an ascii table. 506 results = []
420 507 for row in data:
421 This function was ripped off directly from somewhere in skia. It is 508 results.append(('%-12s' * len(row) % tuple(row)).rstrip())
422 inefficient and so, should be avoided for large data sets. 509 return '\n'.join(results)
423
424 Args:
425 data (list): A list of lists of strings containing the data to tabulate. It
426 is expected to be rectangular.
427
428 Returns:
429 A multi-line string containing the data arranged in a tabular manner.
430 """
431 result = ''
432 column_widths = [0] * len(data[0])
433 for line in data:
434 column_widths = [max(longest_len, len(prop)) for
435 longest_len, prop in zip(column_widths, line)]
436 for line in data:
437 for prop, width in zip(line, column_widths):
438 result += prop.ljust(width + 1)
439 result += '\n'
440 return result
441 510
442 511
443 def _prepend_src_to_path_in_command(test_cfg): 512 def _prepend_src_to_path_in_command(test_cfg):
444 command_to_run = [] 513 command_to_run = []
445 for v in test_cfg.get('command').split(): 514 for v in test_cfg.get('command').split():
446 if v in ['./tools/perf/run_benchmark', 515 if v in ['./tools/perf/run_benchmark',
447 'tools/perf/run_benchmark', 516 'tools/perf/run_benchmark',
448 'tools\\perf\\run_benchmark']: 517 'tools\\perf\\run_benchmark']:
449 v = 'src/tools/perf/run_benchmark' 518 v = 'src/tools/perf/run_benchmark'
450 command_to_run.append(v) 519 command_to_run.append(v)
451 test_cfg.update({'command': ' '.join(command_to_run)}) 520 test_cfg.update({'command': ' '.join(command_to_run)})
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698