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