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 3# found in the LICENSE file. 4 5"""URL endpoint containing server-side functionality for bisect try jobs.""" 6 7import difflib 8import hashlib 9import json 10import logging 11 12import httplib2 13 14from google.appengine.api import users 15from google.appengine.api import app_identity 16 17from dashboard import buildbucket_job 18from dashboard import buildbucket_service 19from dashboard import can_bisect 20from dashboard import namespaced_stored_object 21from dashboard import quick_logger 22from dashboard import request_handler 23from dashboard import rietveld_service 24from dashboard import stored_object 25from dashboard import utils 26from dashboard.models import graph_data 27from dashboard.models import try_job 28 29 30# Path to the perf bisect script config file, relative to chromium/src. 31_BISECT_CONFIG_PATH = 'tools/auto_bisect/bisect.cfg' 32 33# Path to the perf trybot config file, relative to chromium/src. 34_PERF_CONFIG_PATH = 'tools/run-perf-test.cfg' 35 36_PATCH_HEADER = """Index: %(filename)s 37diff --git a/%(filename_a)s b/%(filename_b)s 38index %(hash_a)s..%(hash_b)s 100644 39""" 40 41_BOT_BROWSER_MAP_KEY = 'bot_browser_map' 42_INTERNAL_MASTERS_KEY = 'internal_masters' 43_BUILDER_TYPES_KEY = 'bisect_builder_types' 44_TESTER_DIRECTOR_MAP_KEY = 'recipe_tester_director_map' 45_MASTER_TRY_SERVER_MAP_KEY = 'master_try_server_map' 46 47_NON_TELEMETRY_TEST_COMMANDS = { 48 'angle_perftests': [ 49 './out/Release/angle_perftests', 50 '--test-launcher-print-test-stdio=always', 51 '--test-launcher-jobs=1', 52 ], 53 'cc_perftests': [ 54 './out/Release/cc_perftests', 55 '--test-launcher-print-test-stdio=always', 56 ], 57 'idb_perf': [ 58 './out/Release/performance_ui_tests', 59 '--gtest_filter=IndexedDBTest.Perf', 60 ], 61 'load_library_perf_tests': [ 62 './out/Release/load_library_perf_tests', 63 '--single-process-tests', 64 ], 65 'media_perftests': [ 66 './out/Release/media_perftests', 67 '--single-process-tests', 68 ], 69 'performance_browser_tests': [ 70 './out/Release/performance_browser_tests', 71 '--test-launcher-print-test-stdio=always', 72 '--enable-gpu', 73 ], 74} 75 76 77class StartBisectHandler(request_handler.RequestHandler): 78 """URL endpoint for AJAX requests for bisect config handling. 79 80 Requests are made to this end-point by bisect and trace forms. This handler 81 does several different types of things depending on what is given as the 82 value of the "step" parameter: 83 "prefill-info": Returns JSON with some info to fill into the form. 84 "perform-bisect": Triggers a bisect job. 85 "perform-perf-try": Triggers a perf try job. 86 """ 87 88 def post(self): 89 """Performs one of several bisect-related actions depending on parameters. 90 91 The only required parameter is "step", which indicates what to do. 92 93 This end-point should always output valid JSON with different contents 94 depending on the value of "step". 95 """ 96 user = users.get_current_user() 97 if not utils.IsValidSheriffUser(): 98 message = 'User "%s" not authorized.' % user 99 self.response.out.write(json.dumps({'error': message})) 100 return 101 102 step = self.request.get('step') 103 104 if step == 'prefill-info': 105 result = _PrefillInfo(self.request.get('test_path')) 106 elif step == 'perform-bisect': 107 result = self._PerformBisectStep(user) 108 elif step == 'perform-perf-try': 109 result = self._PerformPerfTryStep(user) 110 else: 111 result = {'error': 'Invalid parameters.'} 112 113 self.response.write(json.dumps(result)) 114 115 def _PerformBisectStep(self, user): 116 """Gathers the parameters for a bisect job and triggers the job.""" 117 bug_id = int(self.request.get('bug_id', -1)) 118 master_name = self.request.get('master', 'ChromiumPerf') 119 internal_only = self.request.get('internal_only') == 'true' 120 bisect_bot = self.request.get('bisect_bot') 121 bypass_no_repro_check = self.request.get('bypass_no_repro_check') == 'true' 122 use_recipe = bool(GetBisectDirectorForTester(bisect_bot)) 123 124 bisect_config = GetBisectConfig( 125 bisect_bot=bisect_bot, 126 master_name=master_name, 127 suite=self.request.get('suite'), 128 metric=self.request.get('metric'), 129 good_revision=self.request.get('good_revision'), 130 bad_revision=self.request.get('bad_revision'), 131 repeat_count=self.request.get('repeat_count', 10), 132 max_time_minutes=self.request.get('max_time_minutes', 20), 133 bug_id=bug_id, 134 use_archive=self.request.get('use_archive'), 135 bisect_mode=self.request.get('bisect_mode', 'mean'), 136 use_buildbucket=use_recipe, 137 bypass_no_repro_check=bypass_no_repro_check) 138 139 if 'error' in bisect_config: 140 return bisect_config 141 142 config_python_string = 'config = %s\n' % json.dumps( 143 bisect_config, sort_keys=True, indent=2, separators=(',', ': ')) 144 145 bisect_job = try_job.TryJob( 146 bot=bisect_bot, 147 config=config_python_string, 148 bug_id=bug_id, 149 email=user.email(), 150 master_name=master_name, 151 internal_only=internal_only, 152 job_type='bisect', 153 use_buildbucket=use_recipe) 154 155 try: 156 results = PerformBisect(bisect_job) 157 except request_handler.InvalidInputError as iie: 158 results = {'error': iie.message} 159 if 'error' in results and bisect_job.key: 160 bisect_job.key.delete() 161 return results 162 163 def _PerformPerfTryStep(self, user): 164 """Gathers the parameters required for a perf try job and starts the job.""" 165 perf_config = _GetPerfTryConfig( 166 bisect_bot=self.request.get('bisect_bot'), 167 suite=self.request.get('suite'), 168 good_revision=self.request.get('good_revision'), 169 bad_revision=self.request.get('bad_revision'), 170 rerun_option=self.request.get('rerun_option')) 171 172 if 'error' in perf_config: 173 return perf_config 174 175 config_python_string = 'config = %s\n' % json.dumps( 176 perf_config, sort_keys=True, indent=2, separators=(',', ': ')) 177 178 perf_job = try_job.TryJob( 179 bot=self.request.get('bisect_bot'), 180 config=config_python_string, 181 bug_id=-1, 182 email=user.email(), 183 job_type='perf-try') 184 185 results = _PerformPerfTryJob(perf_job) 186 if 'error' in results and perf_job.key: 187 perf_job.key.delete() 188 return results 189 190 191def _PrefillInfo(test_path): 192 """Pre-fills some best guesses config form based on the test path. 193 194 Args: 195 test_path: Test path string. 196 197 Returns: 198 A dictionary indicating the result. If successful, this should contain the 199 the fields "suite", "email", "all_metrics", and "default_metric". If not 200 successful this will contain the field "error". 201 """ 202 if not test_path: 203 return {'error': 'No test specified'} 204 205 suite_path = '/'.join(test_path.split('/')[:3]) 206 suite = utils.TestKey(suite_path).get() 207 if not suite: 208 return {'error': 'Invalid test %s' % test_path} 209 210 graph_path = '/'.join(test_path.split('/')[:4]) 211 graph_key = utils.TestKey(graph_path) 212 213 info = {'suite': suite.key.string_id()} 214 info['master'] = suite.master_name 215 info['internal_only'] = suite.internal_only 216 info['use_archive'] = _CanDownloadBuilds(suite.master_name) 217 218 info['all_bots'] = _GetAvailableBisectBots(suite.master_name) 219 info['bisect_bot'] = GuessBisectBot(suite.master_name, suite.bot_name) 220 221 user = users.get_current_user() 222 if not user: 223 return {'error': 'User not logged in.'} 224 225 # Secondary check for bisecting internal only tests. 226 if suite.internal_only and not utils.IsInternalUser(): 227 return {'error': 'Unauthorized access, please use corp account to login.'} 228 229 info['email'] = user.email() 230 231 info['all_metrics'] = [] 232 metric_keys_query = graph_data.Test.query( 233 graph_data.Test.has_rows == True, ancestor=graph_key) 234 metric_keys = metric_keys_query.fetch(keys_only=True) 235 for metric_key in metric_keys: 236 metric_path = utils.TestPath(metric_key) 237 if metric_path.endswith('/ref') or metric_path.endswith('_ref'): 238 continue 239 info['all_metrics'].append(GuessMetric(metric_path)) 240 info['default_metric'] = GuessMetric(test_path) 241 242 return info 243 244 245def GetBisectConfig( 246 bisect_bot, master_name, suite, metric, good_revision, bad_revision, 247 repeat_count, max_time_minutes, bug_id, use_archive=None, 248 bisect_mode='mean', use_buildbucket=False, bypass_no_repro_check=False): 249 """Fills in a JSON response with the filled-in config file. 250 251 Args: 252 bisect_bot: Bisect bot name. (This should be either a legacy bisector or a 253 recipe-enabled tester). 254 master_name: Master name of the test being bisected. 255 suite: Test suite name of the test being bisected. 256 metric: Bisect bot "metric" parameter, in the form "chart/trace". 257 good_revision: Known good revision number. 258 bad_revision: Known bad revision number. 259 repeat_count: Number of times to repeat the test. 260 max_time_minutes: Max time to run the test. 261 bug_id: The Chromium issue tracker bug ID. 262 use_archive: Specifies whether to use build archives or not to bisect. 263 If this is not empty or None, then we want to use archived builds. 264 bisect_mode: What aspect of the test run to bisect on; possible options are 265 "mean", "std_dev", and "return_code". 266 use_buildbucket: Whether this job will started using buildbucket, 267 this should be used for bisects using the bisect recipe. 268 269 Returns: 270 A dictionary with the result; if successful, this will contain "config", 271 which is a config string; if there's an error, this will contain "error". 272 """ 273 command = GuessCommand( 274 bisect_bot, suite, metric=metric, use_buildbucket=use_buildbucket) 275 if not command: 276 return {'error': 'Could not guess command for %r.' % suite} 277 278 try: 279 repeat_count = int(repeat_count) 280 max_time_minutes = int(max_time_minutes) 281 bug_id = int(bug_id) 282 except ValueError: 283 return {'error': 'repeat count, max time and bug_id must be integers.'} 284 285 if not can_bisect.IsValidRevisionForBisect(good_revision): 286 return {'error': 'Invalid "good" revision "%s".' % good_revision} 287 if not can_bisect.IsValidRevisionForBisect(bad_revision): 288 return {'error': 'Invalid "bad" revision "%s".' % bad_revision} 289 290 config_dict = { 291 'command': command, 292 'good_revision': str(good_revision), 293 'bad_revision': str(bad_revision), 294 'metric': metric, 295 'repeat_count': str(repeat_count), 296 'max_time_minutes': str(max_time_minutes), 297 'bug_id': str(bug_id), 298 'builder_type': _BuilderType(master_name, use_archive), 299 'target_arch': GuessTargetArch(bisect_bot), 300 'bisect_mode': bisect_mode, 301 } 302 if use_buildbucket: 303 config_dict['recipe_tester_name'] = bisect_bot 304 if bypass_no_repro_check: 305 config_dict['required_initial_confidence'] = '0' 306 return config_dict 307 308 309def _BuilderType(master_name, use_archive): 310 """Returns the builder_type string to use in the bisect config. 311 312 Args: 313 master_name: The test master name. 314 use_archive: Whether or not to use archived builds. 315 316 Returns: 317 A string which indicates where the builds should be obtained from. 318 """ 319 if not use_archive: 320 return '' 321 builder_types = namespaced_stored_object.Get(_BUILDER_TYPES_KEY) 322 if not builder_types or master_name not in builder_types: 323 return 'perf' 324 return builder_types[master_name] 325 326 327def GuessTargetArch(bisect_bot): 328 """Returns target architecture for the bisect job.""" 329 if 'x64' in bisect_bot or 'win64' in bisect_bot: 330 return 'x64' 331 elif bisect_bot in ['android_nexus9_perf_bisect']: 332 return 'arm64' 333 else: 334 return 'ia32' 335 336 337def _GetPerfTryConfig( 338 bisect_bot, suite, good_revision, bad_revision, rerun_option=None): 339 """Fills in a JSON response with the filled-in config file. 340 341 Args: 342 bisect_bot: Bisect bot name. 343 suite: Test suite name. 344 good_revision: Known good revision number. 345 bad_revision: Known bad revision number. 346 rerun_option: Optional rerun command line parameter. 347 348 Returns: 349 A dictionary with the result; if successful, this will contain "config", 350 which is a config string; if there's an error, this will contain "error". 351 """ 352 command = GuessCommand(bisect_bot, suite, rerun_option=rerun_option) 353 if not command: 354 return {'error': 'Only Telemetry is supported at the moment.'} 355 356 if not can_bisect.IsValidRevisionForBisect(good_revision): 357 return {'error': 'Invalid "good" revision "%s".' % good_revision} 358 if not can_bisect.IsValidRevisionForBisect(bad_revision): 359 return {'error': 'Invalid "bad" revision "%s".' % bad_revision} 360 361 config_dict = { 362 'command': command, 363 'good_revision': str(good_revision), 364 'bad_revision': str(bad_revision), 365 'repeat_count': '1', 366 'max_time_minutes': '60', 367 } 368 return config_dict 369 370 371def _GetAvailableBisectBots(master_name): 372 """Gets all available bisect bots corresponding to a master name.""" 373 bisect_bot_map = namespaced_stored_object.Get(can_bisect.BISECT_BOT_MAP_KEY) 374 for master, platform_bot_pairs in bisect_bot_map.iteritems(): 375 if master_name.startswith(master): 376 return sorted({bot for _, bot in platform_bot_pairs}) 377 return [] 378 379 380def _CanDownloadBuilds(master_name): 381 """Checks whether bisecting using archives is supported.""" 382 return master_name.startswith('ChromiumPerf') 383 384 385def GuessBisectBot(master_name, bot_name): 386 """Returns a bisect bot name based on |bot_name| (perf_id) string.""" 387 fallback = 'linux_perf_bisect' 388 bisect_bot_map = namespaced_stored_object.Get(can_bisect.BISECT_BOT_MAP_KEY) 389 if not bisect_bot_map: 390 return fallback 391 bot_name = bot_name.lower() 392 for master, platform_bot_pairs in bisect_bot_map.iteritems(): 393 # Treat ChromiumPerfFyi (etc.) the same as ChromiumPerf. 394 if master_name.startswith(master): 395 for platform, bisect_bot in platform_bot_pairs: 396 if platform.lower() in bot_name: 397 return bisect_bot 398 # Nothing was found; log a warning and return a fall-back name. 399 logging.warning('No bisect bot for %s/%s.', master_name, bot_name) 400 return fallback 401 402 403def GuessCommand( 404 bisect_bot, suite, metric=None, rerun_option=None, use_buildbucket=False): 405 """Returns a command to use in the bisect configuration.""" 406 if suite in _NON_TELEMETRY_TEST_COMMANDS: 407 return _GuessCommandNonTelemetry(suite, bisect_bot, use_buildbucket) 408 return _GuessCommandTelemetry( 409 suite, bisect_bot, metric, rerun_option, use_buildbucket) 410 411 412def _GuessCommandNonTelemetry(suite, bisect_bot, use_buildbucket): 413 """Returns a command string to use for non-Telemetry tests.""" 414 if suite not in _NON_TELEMETRY_TEST_COMMANDS: 415 return None 416 if suite == 'cc_perftests' and bisect_bot.startswith('android'): 417 if use_buildbucket: 418 return 'src/build/android/test_runner.py gtest --release -s cc_perftests' 419 else: 420 return 'build/android/test_runner.py gtest --release -s cc_perftests' 421 422 command = list(_NON_TELEMETRY_TEST_COMMANDS[suite]) 423 424 if use_buildbucket and command[0].startswith('./out'): 425 command[0] = command[0].replace('./', './src/') 426 427 # For Windows x64, the compilation output is put in "out/Release_x64". 428 # Note that the legacy bisect script always extracts binaries into Release 429 # regardless of platform, so this change is only necessary for recipe bisect. 430 if use_buildbucket and _GuessBrowserName(bisect_bot) == 'release_x64': 431 command[0] = command[0].replace('/Release/', '/Release_x64/') 432 433 if bisect_bot.startswith('win'): 434 command[0] = command[0].replace('/', '\\') 435 command[0] += '.exe' 436 return ' '.join(command) 437 438 439def _GuessCommandTelemetry( 440 suite, bisect_bot, metric, # pylint: disable=unused-argument 441 rerun_option, use_buildbucket): 442 """Returns a command to use given that |suite| is a Telemetry benchmark.""" 443 # TODO(qyearsley): Use metric to add a --story-filter flag for Telemetry. 444 # See: http://crbug.com/448628 445 command = [] 446 if bisect_bot.startswith('win'): 447 command.append('python') 448 449 if use_buildbucket: 450 test_cmd = 'src/tools/perf/run_benchmark' 451 else: 452 test_cmd = 'tools/perf/run_benchmark' 453 454 command.extend([ 455 test_cmd, 456 '-v', 457 '--browser=%s' % _GuessBrowserName(bisect_bot), 458 '--output-format=%s' % ('chartjson' if use_buildbucket else 'buildbot'), 459 '--also-run-disabled-tests', 460 ]) 461 462 # Test command might be a little different from the test name on the bots. 463 if suite == 'blink_perf': 464 test_name = 'blink_perf.all' 465 elif suite == 'startup.cold.dirty.blank_page': 466 test_name = 'startup.cold.blank_page' 467 elif suite == 'startup.warm.dirty.blank_page': 468 test_name = 'startup.warm.blank_page' 469 else: 470 test_name = suite 471 command.append(test_name) 472 473 if rerun_option: 474 command.append(rerun_option) 475 476 return ' '.join(command) 477 478 479def _GuessBrowserName(bisect_bot): 480 """Returns a browser name string for Telemetry to use.""" 481 default = 'release' 482 browser_map = namespaced_stored_object.Get(_BOT_BROWSER_MAP_KEY) 483 if not browser_map: 484 return default 485 for bot_name_prefix, browser_name in browser_map: 486 if bisect_bot.startswith(bot_name_prefix): 487 return browser_name 488 return default 489 490 491def GuessMetric(test_path): 492 """Returns a "metric" string to use in the bisect config. 493 494 Args: 495 test_path: The slash-separated test path used by the dashboard. 496 497 Returns: 498 A "metric" string of the form "chart/trace". If there is an 499 interaction record name, then it is included in the chart name; 500 if we're looking at the summary result, then the trace name is 501 the chart name. 502 """ 503 chart = None 504 trace = None 505 parts = test_path.split('/') 506 if len(parts) == 4: 507 # master/bot/benchmark/chart 508 chart = parts[3] 509 elif len(parts) == 5 and _HasChildTest(test_path): 510 # master/bot/benchmark/chart/interaction 511 # Here we're assuming that this test is a Telemetry test that uses 512 # interaction labels, and we're bisecting on the summary metric. 513 # Seeing whether there is a child test is a naive way of guessing 514 # whether this is a story-level test or interaction-level test with 515 # story-level children. 516 # TODO(qyearsley): When a more reliable way of telling is available 517 # (e.g. a property on the Test entity), use that instead. 518 chart = '%s-%s' % (parts[4], parts[3]) 519 elif len(parts) == 5: 520 # master/bot/benchmark/chart/trace 521 chart = parts[3] 522 trace = parts[4] 523 elif len(parts) == 6: 524 # master/bot/benchmark/chart/interaction/trace 525 chart = '%s-%s' % (parts[4], parts[3]) 526 trace = parts[5] 527 else: 528 logging.error('Cannot guess metric for test %s', test_path) 529 530 if trace is None: 531 trace = chart 532 return '%s/%s' % (chart, trace) 533 534 535def _HasChildTest(test_path): 536 key = utils.TestKey(test_path) 537 child = graph_data.Test.query(graph_data.Test.parent_test == key).get() 538 return bool(child) 539 540 541def _CreatePatch(base_config, config_changes, config_path): 542 """Takes the base config file and the changes and generates a patch. 543 544 Args: 545 base_config: The whole contents of the base config file. 546 config_changes: The new config string. This will replace the part of the 547 base config file that starts with "config = {" and ends with "}". 548 config_path: Path to the config file to use. 549 550 Returns: 551 A triple with the patch string, the base md5 checksum, and the "base 552 hashes", which normally might contain checksums for multiple files, but 553 in our case just contains the base checksum and base filename. 554 """ 555 # Compute git SHA1 hashes for both the original and new config. See: 556 # http://git-scm.com/book/en/Git-Internals-Git-Objects#Object-Storage 557 base_checksum = hashlib.md5(base_config).hexdigest() 558 base_hashes = '%s:%s' % (base_checksum, config_path) 559 base_header = 'blob %d\0' % len(base_config) 560 base_sha = hashlib.sha1(base_header + base_config).hexdigest() 561 562 # Replace part of the base config to get the new config. 563 new_config = (base_config[:base_config.rfind('config')] + 564 config_changes + 565 base_config[base_config.rfind('}') + 2:]) 566 567 # The client sometimes adds extra '\r' chars; remove them. 568 new_config = new_config.replace('\r', '') 569 new_header = 'blob %d\0' % len(new_config) 570 new_sha = hashlib.sha1(new_header + new_config).hexdigest() 571 diff = difflib.unified_diff(base_config.split('\n'), 572 new_config.split('\n'), 573 'a/%s' % config_path, 574 'b/%s' % config_path, 575 lineterm='') 576 patch_header = _PATCH_HEADER % { 577 'filename': config_path, 578 'filename_a': config_path, 579 'filename_b': config_path, 580 'hash_a': base_sha, 581 'hash_b': new_sha, 582 } 583 patch = patch_header + '\n'.join(diff) 584 patch = patch.rstrip() + '\n' 585 return (patch, base_checksum, base_hashes) 586 587 588def PerformBisect(bisect_job): 589 """Starts the bisect job. 590 591 This creates a patch, uploads it, then tells Rietveld to try the patch. 592 593 TODO(qyearsley): If we want to use other tryservers sometimes in the future, 594 then we need to have some way to decide which one to use. This could 595 perhaps be passed as part of the bisect bot name, or guessed from the bisect 596 bot name. 597 598 Args: 599 bisect_job: A TryJob entity. 600 601 Returns: 602 A dictionary containing the result; if successful, this dictionary contains 603 the field "issue_id" and "issue_url", otherwise it contains "error". 604 605 Raises: 606 AssertionError: Bot or config not set as expected. 607 request_handler.InvalidInputError: Some property of the bisect job 608 is invalid. 609 """ 610 assert bisect_job.bot and bisect_job.config 611 if not bisect_job.key: 612 bisect_job.put() 613 614 if bisect_job.use_buildbucket: 615 result = _PerformBuildbucketBisect(bisect_job) 616 else: 617 result = _PerformLegacyBisect(bisect_job) 618 if 'error' in result: 619 bisect_job.run_count += 1 620 bisect_job.SetFailed() 621 return result 622 623 624def _PerformLegacyBisect(bisect_job): 625 bot = bisect_job.bot 626 email = bisect_job.email 627 bug_id = bisect_job.bug_id 628 629 config_dict = bisect_job.GetConfigDict() 630 config_dict['try_job_id'] = bisect_job.key.id() 631 bisect_job.config = utils.BisectConfigPythonString(config_dict) 632 633 # Get the base config file contents and make a patch. 634 base_config = utils.DownloadChromiumFile(_BISECT_CONFIG_PATH) 635 if not base_config: 636 return {'error': 'Error downloading base config'} 637 patch, base_checksum, base_hashes = _CreatePatch( 638 base_config, bisect_job.config, _BISECT_CONFIG_PATH) 639 640 # Check if bisect is for internal only tests. 641 bisect_internal = _IsBisectInternalOnly(bisect_job) 642 643 # Upload the patch to Rietveld. 644 server = rietveld_service.RietveldService(bisect_internal) 645 646 subject = 'Perf bisect for bug %s on behalf of %s' % (bug_id, email) 647 issue_id, patchset_id = server.UploadPatch(subject, 648 patch, 649 base_checksum, 650 base_hashes, 651 base_config, 652 _BISECT_CONFIG_PATH) 653 654 if not issue_id: 655 return {'error': 'Error uploading patch to rietveld_service.'} 656 657 if bisect_internal: 658 # Internal server URL has '/bots', that cannot be accessed via browser, 659 # therefore strip this path from internal server URL. 660 issue_url = '%s/%s' % (server.Config().internal_server_url.strip('/bots'), 661 issue_id) 662 else: 663 issue_url = '%s/%s' % (server.Config().server_url.strip('/bots'), issue_id) 664 665 # Tell Rietveld to try the patch. 666 master = _GetTryServerMaster(bisect_job) 667 trypatch_success = server.TryPatch(master, issue_id, patchset_id, bot) 668 if trypatch_success: 669 # Create TryJob entity. update_bug_with_results and auto_bisect 670 # cron job will be tracking/starting/restarting bisect. 671 if bug_id and bug_id > 0: 672 bisect_job.rietveld_issue_id = int(issue_id) 673 bisect_job.rietveld_patchset_id = int(patchset_id) 674 bisect_job.SetStarted() 675 bug_comment = ('Bisect started; track progress at ' 676 '<a href="%s">%s</a>' % (issue_url, issue_url)) 677 LogBisectResult(bisect_job, bug_comment) 678 return {'issue_id': issue_id, 'issue_url': issue_url} 679 680 return {'error': 'Error starting try job. Try to fix at %s' % issue_url} 681 682 683def _IsBisectInternalOnly(bisect_job): 684 """Checks if the bisect is for an internal-only test.""" 685 internal_masters = namespaced_stored_object.Get(_INTERNAL_MASTERS_KEY) 686 return internal_masters and bisect_job.master_name in internal_masters 687 688 689def _GetTryServerMaster(bisect_job): 690 """Returns the try server master to be used for bisecting.""" 691 try_server_map = namespaced_stored_object.Get(_MASTER_TRY_SERVER_MAP_KEY) 692 default = 'tryserver.chromium.perf' 693 if not try_server_map: 694 logging.warning('Could not get master to try server map, using default.') 695 return default 696 return try_server_map.get(bisect_job.master_name, default) 697 698 699def _PerformPerfTryJob(perf_job): 700 """Performs the perf try job on the try bot. 701 702 This creates a patch, uploads it, then tells Rietveld to try the patch. 703 704 Args: 705 perf_job: TryJob entity with initialized bot name and config. 706 707 Returns: 708 A dictionary containing the result; if successful, this dictionary contains 709 the field "issue_id", otherwise it contains "error". 710 """ 711 assert perf_job.bot and perf_job.config 712 713 if not perf_job.key: 714 perf_job.put() 715 716 bot = perf_job.bot 717 email = perf_job.email 718 719 config_dict = perf_job.GetConfigDict() 720 config_dict['try_job_id'] = perf_job.key.id() 721 perf_job.config = utils.BisectConfigPythonString(config_dict) 722 723 # Get the base config file contents and make a patch. 724 base_config = utils.DownloadChromiumFile(_PERF_CONFIG_PATH) 725 if not base_config: 726 return {'error': 'Error downloading base config'} 727 patch, base_checksum, base_hashes = _CreatePatch( 728 base_config, perf_job.config, _PERF_CONFIG_PATH) 729 730 # Upload the patch to Rietveld. 731 server = rietveld_service.RietveldService() 732 subject = 'Perf Try Job on behalf of %s' % email 733 issue_id, patchset_id = server.UploadPatch(subject, 734 patch, 735 base_checksum, 736 base_hashes, 737 base_config, 738 _PERF_CONFIG_PATH) 739 740 if not issue_id: 741 return {'error': 'Error uploading patch to rietveld_service.'} 742 url = 'https://codereview.chromium.org/%s/' % issue_id 743 744 # Tell Rietveld to try the patch. 745 master = 'tryserver.chromium.perf' 746 trypatch_success = server.TryPatch(master, issue_id, patchset_id, bot) 747 if trypatch_success: 748 # Create TryJob entity. The update_bug_with_results and auto_bisect 749 # cron jobs will be tracking, or restarting the job. 750 perf_job.rietveld_issue_id = int(issue_id) 751 perf_job.rietveld_patchset_id = int(patchset_id) 752 perf_job.SetStarted() 753 return {'issue_id': issue_id} 754 return {'error': 'Error starting try job. Try to fix at %s' % url} 755 756 757def LogBisectResult(job, comment): 758 """Adds an entry to the bisect result log for a particular bug.""" 759 if not job.bug_id or job.bug_id < 0: 760 return 761 formatter = quick_logger.Formatter() 762 logger = quick_logger.QuickLogger('bisect_result', job.bug_id, formatter) 763 if job.log_record_id: 764 logger.Log(comment, record_id=job.log_record_id) 765 logger.Save() 766 else: 767 job.log_record_id = logger.Log(comment) 768 logger.Save() 769 job.put() 770 771 772def _MakeBuildbucketBisectJob(bisect_job): 773 """Creates a bisect job object that the buildbucket service can use. 774 775 Args: 776 bisect_job: The entity (try_job.TryJob) off of which to create the 777 buildbucket job. 778 779 Returns: 780 A buildbucket_job.BisectJob object populated with the necessary attributes 781 to pass it to the buildbucket service to start the job. 782 """ 783 config = bisect_job.GetConfigDict() 784 if bisect_job.job_type not in ['bisect', 'bisect-fyi']: 785 raise request_handler.InvalidInputError( 786 'Recipe only supports bisect jobs at this time.') 787 if not bisect_job.master_name.startswith('ChromiumPerf'): 788 raise request_handler.InvalidInputError( 789 'Recipe is only implemented for tests run on chromium.perf ' 790 '(and chromium.perf.fyi).') 791 792 # Recipe bisect supports 'perf' and 'return_code' test types only. 793 # TODO (prasadv): Update bisect form on dashboard to support test_types. 794 test_type = 'perf' 795 if config.get('bisect_mode') == 'return_code': 796 test_type = config['bisect_mode'] 797 798 # Tester name is a required parameter for recipe bisects. 799 tester_name = config['recipe_tester_name'] 800 801 return buildbucket_job.BisectJob( 802 try_job_id=bisect_job.key.id(), 803 bisect_director=GetBisectDirectorForTester(tester_name), 804 good_revision=config['good_revision'], 805 bad_revision=config['bad_revision'], 806 test_command=config['command'], 807 metric=config['metric'], 808 repeats=config['repeat_count'], 809 timeout_minutes=config['max_time_minutes'], 810 bug_id=bisect_job.bug_id, 811 gs_bucket='chrome-perf', 812 recipe_tester_name=tester_name, 813 test_type=test_type, 814 required_initial_confidence=config.get('required_initial_confidence') 815 ) 816 817 818def _PerformBuildbucketBisect(bisect_job): 819 config_dict = bisect_job.GetConfigDict() 820 if 'recipe_tester_name' not in config_dict: 821 logging.error('"recipe_tester_name" required in bisect jobs ' 822 'that use buildbucket. Config: %s', config_dict) 823 return {'error': 'No "recipe_tester_name" given.'} 824 825 try: 826 bisect_job.buildbucket_job_id = buildbucket_service.PutJob( 827 _MakeBuildbucketBisectJob(bisect_job)) 828 bisect_job.SetStarted() 829 hostname = app_identity.get_default_version_hostname() 830 job_id = bisect_job.buildbucket_job_id 831 issue_url = 'https://%s/buildbucket_job_status/%s' % (hostname, job_id) 832 bug_comment = ('Bisect started; track progress at ' 833 '<a href="%s">%s</a>' % (issue_url, issue_url)) 834 LogBisectResult(bisect_job, bug_comment) 835 return { 836 'issue_id': job_id, 837 'issue_url': issue_url, 838 } 839 except httplib2.HttpLib2Error as e: 840 return { 841 'error': ('Could not start job because of the following exception: ' + 842 e.message), 843 } 844 845 846def GetBisectDirectorForTester(bot): 847 """Maps the name of a tester bot to its corresponding bisect director. 848 849 Args: 850 bot (str): The name of the tester bot in the tryserver.chromium.perf 851 waterfall. (e.g. 'linux_perf_tester'). 852 853 Returns: 854 The name of the bisect director that can use the given tester (e.g. 855 'linux_perf_bisector') 856 """ 857 recipe_tester_director_mapping = stored_object.Get( 858 _TESTER_DIRECTOR_MAP_KEY) 859 return recipe_tester_director_mapping.get(bot) 860