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