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