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