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

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: . Created 4 years, 10 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
« no previous file with comments | « scripts/slave/recipe_modules/perf_try/__init__.py ('k') | scripts/slave/recipes/bisect.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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_results 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_results(
102 bisect_results, halt_on_failure=True)
103
95 def run_cq_job(self, update_step, bot_db, files_in_patch): 104 def run_cq_job(self, update_step, bot_db, files_in_patch):
96 """Runs benchmarks affected by a CL on CQ.""" 105 """Runs benchmarks affected by a CL on CQ."""
97 buildername = self.m.properties['buildername'] 106 buildername = self.m.properties['buildername']
98 affected_benchmarks = self._get_affected_benchmarks(files_in_patch) 107 affected_benchmarks = self._get_affected_benchmarks(files_in_patch)
99 if not affected_benchmarks: 108 if not affected_benchmarks:
100 step_result = self.m.step('Results', []) 109 step_result = self.m.step('Results', [])
101 step_result.presentation.step_text = ( 110 step_result.presentation.step_text = (
102 'There are no modifications to Telemetry benchmarks,' 111 'There are no modifications to Telemetry benchmarks,'
103 ' aborting the try job.') 112 ' aborting the try job.')
104 return 113 return
(...skipping 166 matching lines...) Expand 10 before | Expand all | Expand 10 after
271 self._get_hash(config.get('good_revision'))) 280 self._get_hash(config.get('good_revision')))
272 281
273 def _compare_and_present_results( 282 def _compare_and_present_results(
274 self, cfg, results_without_patch, results_with_patch, labels): 283 self, cfg, results_without_patch, results_with_patch, labels):
275 """Parses results and creates Results step.""" 284 """Parses results and creates Results step."""
276 output_with_patch = results_with_patch.get('output') 285 output_with_patch = results_with_patch.get('output')
277 output_without_patch = results_without_patch.get('output') 286 output_without_patch = results_without_patch.get('output')
278 values_with_patch = results_with_patch.get('results').get('values') 287 values_with_patch = results_with_patch.get('results').get('values')
279 values_without_patch = results_without_patch.get('results').get('values') 288 values_without_patch = results_without_patch.get('results').get('values')
280 289
281 cloud_links_without_patch = _parse_cloud_links(output_without_patch) 290 cloud_links_without_patch = self.parse_cloud_links(output_without_patch)
282 cloud_links_with_patch = _parse_cloud_links(output_with_patch) 291 cloud_links_with_patch = self.parse_cloud_links(output_with_patch)
283 292
284 results_link = (cloud_links_without_patch['html'][0] 293 results_link = (cloud_links_without_patch['html'][0]
285 if cloud_links_without_patch['html'] else '') 294 if cloud_links_without_patch['html'] else '')
286 295
287 if not values_with_patch or not values_without_patch: 296 if not values_with_patch or not values_without_patch:
288 step_result = self.m.step('Results', []) 297 step_result = self.m.step('Results', [])
289 step_result.presentation.step_text = ( 298 step_result.presentation.step_text = (
290 'No values from test with patch, or none from test without patch.\n' 299 'No values from test with patch, or none from test without patch.\n'
291 'Output with patch:\n%s\n\nOutput without patch:\n%s' % ( 300 'Output with patch:\n%s\n\nOutput without patch:\n%s' % (
292 output_with_patch, output_without_patch)) 301 output_with_patch, output_without_patch))
(...skipping 48 matching lines...) Expand 10 before | Expand all | Expand 10 after
341 step_result.presentation.links.update({ 350 step_result.presentation.links.update({
342 '%s[%d]' % ( 351 '%s[%d]' % (
343 labels.get('profiler_link1'), i): profiler_with_patch[i] 352 labels.get('profiler_link1'), i): profiler_with_patch[i]
344 }) 353 })
345 for i in xrange(len(profiler_without_patch)): # pragma: no cover 354 for i in xrange(len(profiler_without_patch)): # pragma: no cover
346 step_result.presentation.links.update({ 355 step_result.presentation.links.update({
347 '%s[%d]' % ( 356 '%s[%d]' % (
348 labels.get('profiler_link2'), i): profiler_without_patch[i] 357 labels.get('profiler_link2'), i): profiler_without_patch[i]
349 }) 358 })
350 359
360 def parse_cloud_links(self, output):
361 html_results_pattern = re.compile(CLOUD_RESULTS_LINK, re.MULTILINE)
362 profiler_pattern = re.compile(PROFILER_RESULTS_LINK, re.MULTILINE)
351 363
352 def _parse_cloud_links(output): 364 results = {
353 html_results_pattern = re.compile(CLOUD_RESULTS_LINK, re.MULTILINE) 365 'html': html_results_pattern.findall(output),
354 profiler_pattern = re.compile(PROFILER_RESULTS_LINK, re.MULTILINE) 366 'profiler': profiler_pattern.findall(output),
367 }
368 return results
355 369
356 results = {
357 'html': html_results_pattern.findall(output),
358 'profiler': profiler_pattern.findall(output),
359 }
360 370
361 return results 371 def get_result(self, config, results_without_patch, results_with_patch,
372 labels):
373 """Returns the results as a dict."""
374 output_with_patch = results_with_patch.get('output')
375 output_without_patch = results_without_patch.get('output')
376 values_with_patch = results_with_patch.get('results').get('values')
377 values_without_patch = results_without_patch.get('results').get('values')
378
379 cloud_links_without_patch = self.parse_cloud_links(output_without_patch)
380 cloud_links_with_patch = self.parse_cloud_links(output_with_patch)
381
382 cloud_link = (cloud_links_without_patch['html'][0]
383 if cloud_links_without_patch['html'] else '')
384
385 results = {
386 'try_job_id': config.get('try_job_id'),
387 'status': 'completed', # TODO(chrisphan) Get partial results state.
388 'buildbot_log_url': self._get_build_url(),
389 'bisect_bot': self.m.properties.get('buildername', 'Not found'),
390 'command': config.get('command'),
391 'metric': config.get('metric'),
392 'cloud_link': cloud_link,
393 }
394
395 if not values_with_patch or not values_without_patch:
396 results['warnings'] = ['No values from test with patch, or none '
397 'from test without patch.\n Output with patch:\n%s\n\nOutput without '
398 'patch:\n%s' % (output_with_patch, output_without_patch)]
399 return results
400
401 mean_with_patch = self.m.math_utils.mean(values_with_patch)
402 mean_without_patch = self.m.math_utils.mean(values_without_patch)
403
404 stderr_with_patch = self.m.math_utils.standard_error(values_with_patch)
405 stderr_without_patch = self.m.math_utils.standard_error(
406 values_without_patch)
407
408 profiler_with_patch = cloud_links_with_patch['profiler']
409 profiler_without_patch = cloud_links_without_patch['profiler']
410
411 # Calculate the % difference in the means of the 2 runs.
412 relative_change = None
413 std_err = None
414 if mean_with_patch and values_with_patch:
415 relative_change = self.m.math_utils.relative_change(
416 mean_without_patch, mean_with_patch) * 100
417 std_err = self.m.math_utils.pooled_standard_error(
418 [values_with_patch, values_without_patch])
419
420 if relative_change is not None and std_err is not None:
421 data = [
422 ['Revision', 'Mean', 'Std.Error'],
423 ['Patch', str(mean_with_patch), str(stderr_with_patch)],
424 ['No Patch', str(mean_without_patch), str(stderr_without_patch)]
425 ]
426 results['change'] = relative_change
427 results['std_err'] = std_err
428 results['result'] = _pretty_table(data)
429
430 profiler_links = []
431 if profiler_with_patch and profiler_without_patch:
432 for i in xrange(len(profiler_with_patch)): # pragma: no cover
433 profiler_links.append({
434 'title': '%s[%d]' % (labels.get('profiler_link1'), i),
435 'link': profiler_with_patch[i]
436 })
437 for i in xrange(len(profiler_without_patch)): # pragma: no cover
438 profiler_links.append({
439 'title': '%s[%d]' % (labels.get('profiler_link2'), i),
440 'link': profiler_without_patch[i]
441 })
442 results['profiler_links'] = profiler_links
443
444 return results
445
446 def _get_build_url(self):
447 properties = self.m.properties
448 bot_url = properties.get('buildbotURL',
449 'http://build.chromium.org/p/chromium/')
450 builder_name = urllib.quote(properties.get('buildername', ''))
451 builder_number = str(properties.get('buildnumber', ''))
452 return '%sbuilders/%s/builds/%s' % (bot_url, builder_name, builder_number)
362 453
363 454
364 def _validate_perf_config(config_contents, required_parameters): 455 def _validate_perf_config(config_contents, required_parameters):
365 """Validates the perf config file contents. 456 """Validates the perf config file contents.
366 457
367 This is used when we're doing a perf try job, the config file is called 458 This is used when we're doing a perf try job, the config file is called
368 run-perf-test.cfg by default. 459 run-perf-test.cfg by default.
369 460
370 The parameters checked are the required parameters; any additional optional 461 The parameters checked are the required parameters; any additional optional
371 parameters won't be checked and validation will still pass. 462 parameters won't be checked and validation will still pass.
(...skipping 28 matching lines...) Expand all
400 def _is_benchmark_match(benchmark, affected_benchmarks): 491 def _is_benchmark_match(benchmark, affected_benchmarks):
401 # TODO(prasadv): We should make more robust logic to determine if a 492 # TODO(prasadv): We should make more robust logic to determine if a
402 # which benchmark to run on CQ. Right now it just compares the file name 493 # which benchmark to run on CQ. Right now it just compares the file name
403 # with the benchmark name, which isn't necessarily correct. crbug.com/510925. 494 # with the benchmark name, which isn't necessarily correct. crbug.com/510925.
404 for b in affected_benchmarks: 495 for b in affected_benchmarks:
405 if benchmark.startswith(b): 496 if benchmark.startswith(b):
406 return True 497 return True
407 return False 498 return False
408 499
409 500
410 # TODO(prasadv): This method already exists in auto_bisect module,
411 # we need to identify a common location move this there, so that recipe modules
412 # share them.
413 def _pretty_table(data): 501 def _pretty_table(data):
414 """Arrange a matrix of strings into an ascii table. 502 results = []
415 503 for row in data:
416 This function was ripped off directly from somewhere in skia. It is 504 results.append(('%-12s' * len(row) % tuple(row)).rstrip())
417 inefficient and so, should be avoided for large data sets. 505 return '\n'.join(results)
418
419 Args:
420 data (list): A list of lists of strings containing the data to tabulate. It
421 is expected to be rectangular.
422
423 Returns:
424 A multi-line string containing the data arranged in a tabular manner.
425 """
426 result = ''
427 column_widths = [0] * len(data[0])
428 for line in data:
429 column_widths = [max(longest_len, len(prop)) for
430 longest_len, prop in zip(column_widths, line)]
431 for line in data:
432 for prop, width in zip(line, column_widths):
433 result += prop.ljust(width + 1)
434 result += '\n'
435 return result
436 506
437 507
438 def _prepend_src_to_path_in_command(test_cfg): 508 def _prepend_src_to_path_in_command(test_cfg):
439 command_to_run = [] 509 command_to_run = []
440 for v in test_cfg.get('command').split(): 510 for v in test_cfg.get('command').split():
441 if v in ['./tools/perf/run_benchmark', 511 if v in ['./tools/perf/run_benchmark',
442 'tools/perf/run_benchmark', 512 'tools/perf/run_benchmark',
443 'tools\\perf\\run_benchmark']: 513 'tools\\perf\\run_benchmark']:
444 v = 'src/tools/perf/run_benchmark' 514 v = 'src/tools/perf/run_benchmark'
445 command_to_run.append(v) 515 command_to_run.append(v)
446 test_cfg.update({'command': ' '.join(command_to_run)}) 516 test_cfg.update({'command': ' '.join(command_to_run)})
OLDNEW
« no previous file with comments | « scripts/slave/recipe_modules/perf_try/__init__.py ('k') | scripts/slave/recipes/bisect.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698