| OLD | NEW |
| 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 Loading... |
| 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 Loading... |
| 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 Loading... |
| 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 Loading... |
| 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)}) |
| OLD | NEW |