ozone: evdev: Sync caps lock LED state to evdev
[chromium-blink-merge.git] / tools / auto_bisect / bisect_perf_regression.py
blob746952eb6cdca14463828fd2686c3ca7348f8aa7
1 #!/usr/bin/env python
2 # Copyright 2013 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
6 """Chromium auto-bisect tool
8 This script bisects a range of commits using binary search. It starts by getting
9 reference values for the specified "good" and "bad" commits. Then, for revisions
10 in between, it will get builds, run tests and classify intermediate revisions as
11 "good" or "bad" until an adjacent "good" and "bad" revision is found; this is
12 the culprit.
14 If the culprit is a roll of a depedency repository (e.g. v8), it will then
15 expand the revision range and continue the bisect until a culprit revision in
16 the dependency repository is found.
18 Example usage using git commit hashes, bisecting a performance test based on
19 the mean value of a particular metric:
21 ./tools/auto_bisect/bisect_perf_regression.py
22 --command "out/Release/performance_ui_tests \
23 --gtest_filter=ShutdownTest.SimpleUserQuit"\
24 --metric shutdown/simple-user-quit
25 --good_revision 1f6e67861535121c5c819c16a666f2436c207e7b\
26 --bad-revision b732f23b4f81c382db0b23b9035f3dadc7d925bb\
28 Example usage using git commit positions, bisecting a functional test based on
29 whether it passes or fails.
31 ./tools/auto_bisect/bisect_perf_regression.py\
32 --command "out/Release/content_unittests -single-process-tests \
33 --gtest_filter=GpuMemoryBufferImplTests"\
34 --good_revision 408222\
35 --bad_revision 408232\
36 --bisect_mode return_code\
37 --builder_type full
39 In practice, the auto-bisect tool is usually run on tryserver.chromium.perf
40 try bots, and is started by tools/run-bisect-perf-regression.py using
41 config parameters from tools/auto_bisect/bisect.cfg.
42 """
44 import copy
45 import errno
46 import hashlib
47 import logging
48 import argparse
49 import os
50 import re
51 import shlex
52 import shutil
53 import StringIO
54 import sys
55 import time
57 sys.path.append(os.path.join(
58 os.path.dirname(__file__), os.path.pardir, 'telemetry'))
60 from bisect_printer import BisectPrinter
61 from bisect_results import BisectResults
62 from bisect_state import BisectState
63 import bisect_utils
64 import builder
65 import fetch_build
66 import math_utils
67 import query_crbug
68 import request_build
69 import source_control
71 # The script is in chromium/src/tools/auto_bisect. Throughout this script,
72 # we use paths to other things in the chromium/src repository.
74 # Possible return values from BisectPerformanceMetrics.RunTest.
75 BUILD_RESULT_SUCCEED = 0
76 BUILD_RESULT_FAIL = 1
77 BUILD_RESULT_SKIPPED = 2
79 # The confidence percentage we require to consider the initial range a
80 # regression based on the test results of the initial good and bad revisions.
81 REGRESSION_CONFIDENCE = 80
82 # How many times to repeat the test on the last known good and first known bad
83 # revisions in order to assess a more accurate confidence score in the
84 # regression culprit.
85 BORDER_REVISIONS_EXTRA_RUNS = 2
87 # Patch template to add a new file, DEPS.sha under src folder.
88 # This file contains SHA1 value of the DEPS changes made while bisecting
89 # dependency repositories. This patch send along with DEPS patch to try server.
90 # When a build requested is posted with a patch, bisect builders on try server,
91 # once build is produced, it reads SHA value from this file and appends it
92 # to build archive filename.
93 DEPS_SHA_PATCH = """diff --git DEPS.sha DEPS.sha
94 new file mode 100644
95 --- /dev/null
96 +++ DEPS.sha
97 @@ -0,0 +1 @@
98 +%(deps_sha)s
99 """
101 REGRESSION_CONFIDENCE_ERROR_TEMPLATE = """
102 We could not reproduce the regression with this test/metric/platform combination
103 with enough confidence.
105 Here are the results for the given "good" and "bad" revisions:
106 "Good" revision: {good_rev}
107 \tMean: {good_mean}
108 \tStandard error: {good_std_err}
109 \tSample size: {good_sample_size}
111 "Bad" revision: {bad_rev}
112 \tMean: {bad_mean}
113 \tStandard error: {bad_std_err}
114 \tSample size: {bad_sample_size}
116 NOTE: There's still a chance that this is actually a regression, but you may
117 need to bisect a different platform."""
119 # Git branch name used to run bisect try jobs.
120 BISECT_TRYJOB_BRANCH = 'bisect-tryjob'
121 # Git master branch name.
122 BISECT_MASTER_BRANCH = 'master'
123 # File to store 'git diff' content.
124 BISECT_PATCH_FILE = 'deps_patch.txt'
125 # SVN repo where the bisect try jobs are submitted.
126 PERF_SVN_REPO_URL = 'svn://svn.chromium.org/chrome-try/try-perf'
127 FULL_SVN_REPO_URL = 'svn://svn.chromium.org/chrome-try/try'
130 class RunGitError(Exception):
132 def __str__(self):
133 return '%s\nError executing git command.' % self.args[0]
136 def GetSHA1HexDigest(contents):
137 """Returns SHA1 hex digest of the given string."""
138 return hashlib.sha1(contents).hexdigest()
141 def WriteStringToFile(text, file_name):
142 """Writes text to a file, raising an RuntimeError on failure."""
143 try:
144 with open(file_name, 'wb') as f:
145 f.write(text)
146 except IOError:
147 raise RuntimeError('Error writing to file [%s]' % file_name)
150 def ReadStringFromFile(file_name):
151 """Writes text to a file, raising an RuntimeError on failure."""
152 try:
153 with open(file_name) as f:
154 return f.read()
155 except IOError:
156 raise RuntimeError('Error reading file [%s]' % file_name)
159 def ChangeBackslashToSlashInPatch(diff_text):
160 """Formats file paths in the given patch text to Unix-style paths."""
161 if not diff_text:
162 return None
163 diff_lines = diff_text.split('\n')
164 for i in range(len(diff_lines)):
165 line = diff_lines[i]
166 if line.startswith('--- ') or line.startswith('+++ '):
167 diff_lines[i] = line.replace('\\', '/')
168 return '\n'.join(diff_lines)
171 def _ParseRevisionsFromDEPSFileManually(deps_file_contents):
172 """Parses the vars section of the DEPS file using regular expressions.
174 Args:
175 deps_file_contents: The DEPS file contents as a string.
177 Returns:
178 A dictionary in the format {depot: revision} if successful, otherwise None.
180 # We'll parse the "vars" section of the DEPS file.
181 rxp = re.compile('vars = {(?P<vars_body>[^}]+)', re.MULTILINE)
182 re_results = rxp.search(deps_file_contents)
184 if not re_results:
185 return None
187 # We should be left with a series of entries in the vars component of
188 # the DEPS file with the following format:
189 # 'depot_name': 'revision',
190 vars_body = re_results.group('vars_body')
191 rxp = re.compile(r"'(?P<depot_body>[\w_-]+)':[\s]+'(?P<rev_body>[\w@]+)'",
192 re.MULTILINE)
193 re_results = rxp.findall(vars_body)
195 return dict(re_results)
198 def _WaitUntilBuildIsReady(fetch_build_func, builder_name, builder_type,
199 build_request_id, max_timeout):
200 """Waits until build is produced by bisect builder on try server.
202 Args:
203 fetch_build_func: Function to check and download build from cloud storage.
204 builder_name: Builder bot name on try server.
205 builder_type: Builder type, e.g. "perf" or "full". Refer to the constants
206 |fetch_build| which determine the valid values that can be passed.
207 build_request_id: A unique ID of the build request posted to try server.
208 max_timeout: Maximum time to wait for the build.
210 Returns:
211 Downloaded archive file path if exists, otherwise None.
213 # Build number on the try server.
214 build_num = None
215 # Interval to check build on cloud storage.
216 poll_interval = 60
217 # Interval to check build status on try server in seconds.
218 status_check_interval = 600
219 last_status_check = time.time()
220 start_time = time.time()
221 while True:
222 # Checks for build on gs://chrome-perf and download if exists.
223 res = fetch_build_func()
224 if res:
225 return (res, 'Build successfully found')
226 elapsed_status_check = time.time() - last_status_check
227 # To avoid overloading try server with status check requests, we check
228 # build status for every 10 minutes.
229 if elapsed_status_check > status_check_interval:
230 last_status_check = time.time()
231 if not build_num:
232 # Get the build number on try server for the current build.
233 build_num = request_build.GetBuildNumFromBuilder(
234 build_request_id, builder_name, builder_type)
235 # Check the status of build using the build number.
236 # Note: Build is treated as PENDING if build number is not found
237 # on the the try server.
238 build_status, status_link = request_build.GetBuildStatus(
239 build_num, builder_name, builder_type)
240 if build_status == request_build.FAILED:
241 return (None, 'Failed to produce build, log: %s' % status_link)
242 elapsed_time = time.time() - start_time
243 if elapsed_time > max_timeout:
244 return (None, 'Timed out: %ss without build' % max_timeout)
246 logging.info('Time elapsed: %ss without build.', elapsed_time)
247 time.sleep(poll_interval)
248 # For some reason, mac bisect bots were not flushing stdout periodically.
249 # As a result buildbot command is timed-out. Flush stdout on all platforms
250 # while waiting for build.
251 sys.stdout.flush()
254 def _UpdateV8Branch(deps_content):
255 """Updates V8 branch in DEPS file to process v8_bleeding_edge.
257 Check for "v8_branch" in DEPS file if exists update its value
258 with v8_bleeding_edge branch. Note: "v8_branch" is added to DEPS
259 variable from DEPS revision 254916, therefore check for "src/v8":
260 <v8 source path> in DEPS in order to support prior DEPS revisions
261 and update it.
263 Args:
264 deps_content: DEPS file contents to be modified.
266 Returns:
267 Modified DEPS file contents as a string.
269 new_branch = r'branches/bleeding_edge'
270 v8_branch_pattern = re.compile(r'(?<="v8_branch": ")(.*)(?=")')
271 if re.search(v8_branch_pattern, deps_content):
272 deps_content = re.sub(v8_branch_pattern, new_branch, deps_content)
273 else:
274 # Replaces the branch assigned to "src/v8" key in DEPS file.
275 # Format of "src/v8" in DEPS:
276 # "src/v8":
277 # (Var("googlecode_url") % "v8") + "/trunk@" + Var("v8_revision"),
278 # So, "/trunk@" is replace with "/branches/bleeding_edge@"
279 v8_src_pattern = re.compile(
280 r'(?<="v8"\) \+ "/)(.*)(?=@" \+ Var\("v8_revision"\))', re.MULTILINE)
281 if re.search(v8_src_pattern, deps_content):
282 deps_content = re.sub(v8_src_pattern, new_branch, deps_content)
283 return deps_content
286 def _UpdateDEPSForAngle(revision, depot, deps_file):
287 """Updates DEPS file with new revision for Angle repository.
289 This is a hack for Angle depot case because, in DEPS file "vars" dictionary
290 variable contains "angle_revision" key that holds git hash instead of
291 SVN revision.
293 And sometimes "angle_revision" key is not specified in "vars" variable,
294 in such cases check "deps" dictionary variable that matches
295 angle.git@[a-fA-F0-9]{40}$ and replace git hash.
297 deps_var = bisect_utils.DEPOT_DEPS_NAME[depot]['deps_var']
298 try:
299 deps_contents = ReadStringFromFile(deps_file)
300 # Check whether the depot and revision pattern in DEPS file vars variable
301 # e.g. "angle_revision": "fa63e947cb3eccf463648d21a05d5002c9b8adfa".
302 angle_rev_pattern = re.compile(r'(?<="%s": ")([a-fA-F0-9]{40})(?=")' %
303 deps_var, re.MULTILINE)
304 match = re.search(angle_rev_pattern, deps_contents)
305 if match:
306 # Update the revision information for the given depot
307 new_data = re.sub(angle_rev_pattern, revision, deps_contents)
308 else:
309 # Check whether the depot and revision pattern in DEPS file deps
310 # variable. e.g.,
311 # "src/third_party/angle": Var("chromium_git") +
312 # "/angle/angle.git@fa63e947cb3eccf463648d21a05d5002c9b8adfa",.
313 angle_rev_pattern = re.compile(
314 r'(?<=angle\.git@)([a-fA-F0-9]{40})(?=")', re.MULTILINE)
315 match = re.search(angle_rev_pattern, deps_contents)
316 if not match:
317 logging.info('Could not find angle revision information in DEPS file.')
318 return False
319 new_data = re.sub(angle_rev_pattern, revision, deps_contents)
320 # Write changes to DEPS file
321 WriteStringToFile(new_data, deps_file)
322 return True
323 except IOError, e:
324 logging.warn('Something went wrong while updating DEPS file, %s', e)
325 return False
328 def _TryParseHistogramValuesFromOutput(metric, text):
329 """Attempts to parse a metric in the format HISTOGRAM <graph: <trace>.
331 Args:
332 metric: The metric as a list of [<trace>, <value>] strings.
333 text: The text to parse the metric values from.
335 Returns:
336 A list of floating point numbers found, [] if none were found.
338 metric_formatted = 'HISTOGRAM %s: %s= ' % (metric[0], metric[1])
340 text_lines = text.split('\n')
341 values_list = []
343 for current_line in text_lines:
344 if metric_formatted in current_line:
345 current_line = current_line[len(metric_formatted):]
347 try:
348 histogram_values = eval(current_line)
350 for b in histogram_values['buckets']:
351 average_for_bucket = float(b['high'] + b['low']) * 0.5
352 # Extends the list with N-elements with the average for that bucket.
353 values_list.extend([average_for_bucket] * b['count'])
354 except Exception:
355 pass
357 return values_list
360 def _TryParseResultValuesFromOutput(metric, text):
361 """Attempts to parse a metric in the format RESULT <graph>: <trace>= ...
363 Args:
364 metric: The metric as a list of [<trace>, <value>] string pairs.
365 text: The text to parse the metric values from.
367 Returns:
368 A list of floating point numbers found.
370 # Format is: RESULT <graph>: <trace>= <value> <units>
371 metric_re = re.escape('RESULT %s: %s=' % (metric[0], metric[1]))
373 # The log will be parsed looking for format:
374 # <*>RESULT <graph_name>: <trace_name>= <value>
375 single_result_re = re.compile(
376 metric_re + r'\s*(?P<VALUE>[-]?\d*(\.\d*)?)')
378 # The log will be parsed looking for format:
379 # <*>RESULT <graph_name>: <trace_name>= [<value>,value,value,...]
380 multi_results_re = re.compile(
381 metric_re + r'\s*\[\s*(?P<VALUES>[-]?[\d\., ]+)\s*\]')
383 # The log will be parsed looking for format:
384 # <*>RESULT <graph_name>: <trace_name>= {<mean>, <std deviation>}
385 mean_stddev_re = re.compile(
386 metric_re +
387 r'\s*\{\s*(?P<MEAN>[-]?\d*(\.\d*)?),\s*(?P<STDDEV>\d+(\.\d*)?)\s*\}')
389 text_lines = text.split('\n')
390 values_list = []
391 for current_line in text_lines:
392 # Parse the output from the performance test for the metric we're
393 # interested in.
394 single_result_match = single_result_re.search(current_line)
395 multi_results_match = multi_results_re.search(current_line)
396 mean_stddev_match = mean_stddev_re.search(current_line)
397 if (not single_result_match is None and
398 single_result_match.group('VALUE')):
399 values_list += [single_result_match.group('VALUE')]
400 elif (not multi_results_match is None and
401 multi_results_match.group('VALUES')):
402 metric_values = multi_results_match.group('VALUES')
403 values_list += metric_values.split(',')
404 elif (not mean_stddev_match is None and
405 mean_stddev_match.group('MEAN')):
406 values_list += [mean_stddev_match.group('MEAN')]
408 values_list = [float(v) for v in values_list
409 if bisect_utils.IsStringFloat(v)]
411 return values_list
414 def _ParseMetricValuesFromOutput(metric, text):
415 """Parses output from performance_ui_tests and retrieves the results for
416 a given metric.
418 Args:
419 metric: The metric as a list of [<trace>, <value>] strings.
420 text: The text to parse the metric values from.
422 Returns:
423 A list of floating point numbers found.
425 metric_values = _TryParseResultValuesFromOutput(metric, text)
427 if not metric_values:
428 metric_values = _TryParseHistogramValuesFromOutput(metric, text)
430 return metric_values
433 def _GenerateProfileIfNecessary(command_args):
434 """Checks the command line of the performance test for dependencies on
435 profile generation, and runs tools/perf/generate_profile as necessary.
437 Args:
438 command_args: Command line being passed to performance test, as a list.
440 Returns:
441 False if profile generation was necessary and failed, otherwise True.
443 if '--profile-dir' in ' '.join(command_args):
444 # If we were using python 2.7+, we could just use the argparse
445 # module's parse_known_args to grab --profile-dir. Since some of the
446 # bots still run 2.6, have to grab the arguments manually.
447 arg_dict = {}
448 args_to_parse = ['--profile-dir', '--browser']
450 for arg_to_parse in args_to_parse:
451 for i, current_arg in enumerate(command_args):
452 if arg_to_parse in current_arg:
453 current_arg_split = current_arg.split('=')
455 # Check 2 cases, --arg=<val> and --arg <val>
456 if len(current_arg_split) == 2:
457 arg_dict[arg_to_parse] = current_arg_split[1]
458 elif i + 1 < len(command_args):
459 arg_dict[arg_to_parse] = command_args[i+1]
461 path_to_generate = os.path.join('tools', 'perf', 'generate_profile')
463 if arg_dict.has_key('--profile-dir') and arg_dict.has_key('--browser'):
464 profile_path, profile_type = os.path.split(arg_dict['--profile-dir'])
465 return not bisect_utils.RunProcess(['python', path_to_generate,
466 '--profile-type-to-generate', profile_type,
467 '--browser', arg_dict['--browser'], '--output-dir', profile_path])
468 return False
469 return True
472 def _CheckRegressionConfidenceError(
473 good_revision,
474 bad_revision,
475 known_good_value,
476 known_bad_value):
477 """Checks whether we can be confident beyond a certain degree that the given
478 metrics represent a regression.
480 Args:
481 good_revision: string representing the commit considered 'good'
482 bad_revision: Same as above for 'bad'.
483 known_good_value: A dict with at least: 'values', 'mean' and 'std_err'
484 known_bad_value: Same as above.
486 Returns:
487 False if there is no error (i.e. we can be confident there's a regressioni),
488 a string containing the details of the lack of confidence otherwise.
490 error = False
491 # Adding good and bad values to a parameter list.
492 confidence_params = []
493 for l in [known_bad_value['values'], known_good_value['values']]:
494 # Flatten if needed, by averaging the values in each nested list
495 if isinstance(l, list) and all([isinstance(x, list) for x in l]):
496 averages = map(math_utils.Mean, l)
497 confidence_params.append(averages)
498 else:
499 confidence_params.append(l)
500 regression_confidence = BisectResults.ConfidenceScore(*confidence_params)
501 if regression_confidence < REGRESSION_CONFIDENCE:
502 error = REGRESSION_CONFIDENCE_ERROR_TEMPLATE.format(
503 good_rev=good_revision,
504 good_mean=known_good_value['mean'],
505 good_std_err=known_good_value['std_err'],
506 good_sample_size=len(known_good_value['values']),
507 bad_rev=bad_revision,
508 bad_mean=known_bad_value['mean'],
509 bad_std_err=known_bad_value['std_err'],
510 bad_sample_size=len(known_bad_value['values']))
511 return error
514 class DepotDirectoryRegistry(object):
516 def __init__(self, src_cwd):
517 self.depot_cwd = {}
518 for depot in bisect_utils.DEPOT_NAMES:
519 # The working directory of each depot is just the path to the depot, but
520 # since we're already in 'src', we can skip that part.
521 path_in_src = bisect_utils.DEPOT_DEPS_NAME[depot]['src'][4:]
522 self.SetDepotDir(depot, os.path.join(src_cwd, path_in_src))
524 self.SetDepotDir('chromium', src_cwd)
526 def SetDepotDir(self, depot_name, depot_dir):
527 self.depot_cwd[depot_name] = depot_dir
529 def GetDepotDir(self, depot_name):
530 if depot_name in self.depot_cwd:
531 return self.depot_cwd[depot_name]
532 else:
533 assert False, ('Unknown depot [ %s ] encountered. Possibly a new one '
534 'was added without proper support?' % depot_name)
536 def ChangeToDepotDir(self, depot_name):
537 """Given a depot, changes to the appropriate working directory.
539 Args:
540 depot_name: The name of the depot (see DEPOT_NAMES).
542 os.chdir(self.GetDepotDir(depot_name))
545 def _PrepareBisectBranch(parent_branch, new_branch):
546 """Creates a new branch to submit bisect try job.
548 Args:
549 parent_branch: Parent branch to be used to create new branch.
550 new_branch: New branch name.
552 current_branch, returncode = bisect_utils.RunGit(
553 ['rev-parse', '--abbrev-ref', 'HEAD'])
554 if returncode:
555 raise RunGitError('Must be in a git repository to send changes to trybots.')
557 current_branch = current_branch.strip()
558 # Make sure current branch is master.
559 if current_branch != parent_branch:
560 output, returncode = bisect_utils.RunGit(['checkout', '-f', parent_branch])
561 if returncode:
562 raise RunGitError('Failed to checkout branch: %s.' % output)
564 # Delete new branch if exists.
565 output, returncode = bisect_utils.RunGit(['branch', '--list'])
566 if new_branch in output:
567 output, returncode = bisect_utils.RunGit(['branch', '-D', new_branch])
568 if returncode:
569 raise RunGitError('Deleting branch failed, %s', output)
571 # Check if the tree is dirty: make sure the index is up to date and then
572 # run diff-index.
573 bisect_utils.RunGit(['update-index', '--refresh', '-q'])
574 output, returncode = bisect_utils.RunGit(['diff-index', 'HEAD'])
575 if output:
576 raise RunGitError('Cannot send a try job with a dirty tree.')
578 # Create/check out the telemetry-tryjob branch, and edit the configs
579 # for the tryjob there.
580 output, returncode = bisect_utils.RunGit(['checkout', '-b', new_branch])
581 if returncode:
582 raise RunGitError('Failed to checkout branch: %s.' % output)
584 output, returncode = bisect_utils.RunGit(
585 ['branch', '--set-upstream-to', parent_branch])
586 if returncode:
587 raise RunGitError('Error in git branch --set-upstream-to')
590 def _StartBuilderTryJob(
591 builder_type, git_revision, builder_name, job_name, patch=None):
592 """Attempts to run a try job from the current directory.
594 Args:
595 builder_type: One of the builder types in fetch_build, e.g. "perf".
596 git_revision: A git commit hash.
597 builder_name: Name of the bisect bot to be used for try job.
598 bisect_job_name: Try job name, used to identify which bisect
599 job was responsible for requesting a build.
600 patch: A DEPS patch (used while bisecting dependency repositories),
601 or None if we're bisecting the top-level repository.
603 # TODO(prasadv, qyearsley): Make this a method of BuildArchive
604 # (which may be renamed to BuilderTryBot or Builder).
605 try:
606 # Temporary branch for running tryjob.
607 _PrepareBisectBranch(BISECT_MASTER_BRANCH, BISECT_TRYJOB_BRANCH)
608 patch_content = '/dev/null'
609 # Create a temporary patch file.
610 if patch:
611 WriteStringToFile(patch, BISECT_PATCH_FILE)
612 patch_content = BISECT_PATCH_FILE
614 try_command = [
615 'try',
616 '--bot=%s' % builder_name,
617 '--revision=%s' % git_revision,
618 '--name=%s' % job_name,
619 '--svn_repo=%s' % _TryJobSvnRepo(builder_type),
620 '--diff=%s' % patch_content,
622 # Execute try job to build revision.
623 print try_command
624 output, return_code = bisect_utils.RunGit(try_command)
626 command_string = ' '.join(['git'] + try_command)
627 if return_code:
628 raise RunGitError('Could not execute tryjob: %s.\n'
629 'Error: %s' % (command_string, output))
630 logging.info('Try job successfully submitted.\n TryJob Details: %s\n%s',
631 command_string, output)
632 finally:
633 # Delete patch file if exists.
634 try:
635 os.remove(BISECT_PATCH_FILE)
636 except OSError as e:
637 if e.errno != errno.ENOENT:
638 raise
639 # Checkout master branch and delete bisect-tryjob branch.
640 bisect_utils.RunGit(['checkout', '-f', BISECT_MASTER_BRANCH])
641 bisect_utils.RunGit(['branch', '-D', BISECT_TRYJOB_BRANCH])
644 def _TryJobSvnRepo(builder_type):
645 """Returns an SVN repo to use for try jobs based on the builder type."""
646 if builder_type == fetch_build.PERF_BUILDER:
647 return PERF_SVN_REPO_URL
648 if builder_type == fetch_build.FULL_BUILDER:
649 return FULL_SVN_REPO_URL
650 raise NotImplementedError('Unknown builder type "%s".' % builder_type)
653 class BisectPerformanceMetrics(object):
654 """This class contains functionality to perform a bisection of a range of
655 revisions to narrow down where performance regressions may have occurred.
657 The main entry-point is the Run method.
660 def __init__(self, opts, src_cwd):
661 """Constructs a BisectPerformancesMetrics object.
663 Args:
664 opts: BisectOptions object containing parsed options.
665 src_cwd: Root src/ directory of the test repository (inside bisect/ dir).
667 super(BisectPerformanceMetrics, self).__init__()
669 self.opts = opts
670 self.src_cwd = src_cwd
671 self.depot_registry = DepotDirectoryRegistry(self.src_cwd)
672 self.printer = BisectPrinter(self.opts, self.depot_registry)
673 self.cleanup_commands = []
674 self.warnings = []
675 self.builder = builder.Builder.FromOpts(opts)
677 def PerformCleanup(self):
678 """Performs cleanup when script is finished."""
679 os.chdir(self.src_cwd)
680 for c in self.cleanup_commands:
681 if c[0] == 'mv':
682 shutil.move(c[1], c[2])
683 else:
684 assert False, 'Invalid cleanup command.'
686 def GetRevisionList(self, depot, bad_revision, good_revision):
687 """Retrieves a list of all the commits between the bad revision and
688 last known good revision."""
690 cwd = self.depot_registry.GetDepotDir(depot)
691 return source_control.GetRevisionList(bad_revision, good_revision, cwd=cwd)
693 def _ParseRevisionsFromDEPSFile(self, depot):
694 """Parses the local DEPS file to determine blink/skia/v8 revisions which may
695 be needed if the bisect recurses into those depots later.
697 Args:
698 depot: Name of depot being bisected.
700 Returns:
701 A dict in the format {depot:revision} if successful, otherwise None.
703 try:
704 deps_data = {
705 'Var': lambda _: deps_data["vars"][_],
706 'From': lambda *args: None,
709 deps_file = bisect_utils.FILE_DEPS_GIT
710 if not os.path.exists(deps_file):
711 deps_file = bisect_utils.FILE_DEPS
712 execfile(deps_file, {}, deps_data)
713 deps_data = deps_data['deps']
715 rxp = re.compile(".git@(?P<revision>[a-fA-F0-9]+)")
716 results = {}
717 for depot_name, depot_data in bisect_utils.DEPOT_DEPS_NAME.iteritems():
718 if (depot_data.get('platform') and
719 depot_data.get('platform') != os.name):
720 continue
722 if depot_data.get('recurse') and depot in depot_data.get('from'):
723 depot_data_src = depot_data.get('src') or depot_data.get('src_old')
724 src_dir = deps_data.get(depot_data_src)
725 if src_dir:
726 self.depot_registry.SetDepotDir(depot_name, os.path.join(
727 self.src_cwd, depot_data_src[4:]))
728 re_results = rxp.search(src_dir)
729 if re_results:
730 results[depot_name] = re_results.group('revision')
731 else:
732 warning_text = ('Could not parse revision for %s while bisecting '
733 '%s' % (depot_name, depot))
734 if not warning_text in self.warnings:
735 self.warnings.append(warning_text)
736 else:
737 results[depot_name] = None
738 return results
739 except ImportError:
740 deps_file_contents = ReadStringFromFile(deps_file)
741 parse_results = _ParseRevisionsFromDEPSFileManually(deps_file_contents)
742 results = {}
743 for depot_name, depot_revision in parse_results.iteritems():
744 depot_revision = depot_revision.strip('@')
745 logging.warn(depot_name, depot_revision)
746 for cur_name, cur_data in bisect_utils.DEPOT_DEPS_NAME.iteritems():
747 if (cur_data.has_key('deps_var') and
748 cur_data['deps_var'] == depot_name):
749 src_name = cur_name
750 results[src_name] = depot_revision
751 break
752 return results
754 def _Get3rdPartyRevisions(self, depot):
755 """Parses the DEPS file to determine WebKit/v8/etc... versions.
757 Args:
758 depot: A depot name. Should be in the DEPOT_NAMES list.
760 Returns:
761 A dict in the format {depot: revision} if successful, otherwise None.
763 cwd = os.getcwd()
764 self.depot_registry.ChangeToDepotDir(depot)
766 results = {}
768 if depot == 'chromium' or depot == 'android-chrome':
769 results = self._ParseRevisionsFromDEPSFile(depot)
770 os.chdir(cwd)
772 if depot == 'v8':
773 # We can't try to map the trunk revision to bleeding edge yet, because
774 # we don't know which direction to try to search in. Have to wait until
775 # the bisect has narrowed the results down to 2 v8 rolls.
776 results['v8_bleeding_edge'] = None
778 return results
780 def BackupOrRestoreOutputDirectory(self, restore=False, build_type='Release'):
781 """Backs up or restores build output directory based on restore argument.
783 Args:
784 restore: Indicates whether to restore or backup. Default is False(Backup)
785 build_type: Target build type ('Release', 'Debug', 'Release_x64' etc.)
787 Returns:
788 Path to backup or restored location as string. otherwise None if it fails.
790 build_dir = os.path.abspath(
791 builder.GetBuildOutputDirectory(self.opts, self.src_cwd))
792 source_dir = os.path.join(build_dir, build_type)
793 destination_dir = os.path.join(build_dir, '%s.bak' % build_type)
794 if restore:
795 source_dir, destination_dir = destination_dir, source_dir
796 if os.path.exists(source_dir):
797 RemoveDirectoryTree(destination_dir)
798 shutil.move(source_dir, destination_dir)
799 return destination_dir
800 return None
802 def _DownloadAndUnzipBuild(self, revision, depot, build_type='Release',
803 create_patch=False):
804 """Downloads the build archive for the given revision.
806 Args:
807 revision: The git revision to download.
808 depot: The name of a dependency repository. Should be in DEPOT_NAMES.
809 build_type: Target build type, e.g. Release', 'Debug', 'Release_x64' etc.
810 create_patch: Create a patch with any locally modified files.
812 Returns:
813 True if download succeeds, otherwise False.
815 patch = None
816 patch_sha = None
817 if depot != 'chromium':
818 # Create a DEPS patch with new revision for dependency repository.
819 self._CreateDEPSPatch(depot, revision)
820 create_patch = True
822 if create_patch:
823 revision, patch = self._CreatePatch(revision)
825 if patch:
826 # Get the SHA of the DEPS changes patch.
827 patch_sha = GetSHA1HexDigest(patch)
829 # Update the DEPS changes patch with a patch to create a new file named
830 # 'DEPS.sha' and add patch_sha evaluated above to it.
831 patch = '%s\n%s' % (patch, DEPS_SHA_PATCH % {'deps_sha': patch_sha})
833 build_dir = builder.GetBuildOutputDirectory(self.opts, self.src_cwd)
834 downloaded_file = self._WaitForBuildDownload(
835 revision, build_dir, deps_patch=patch, deps_patch_sha=patch_sha)
836 if not downloaded_file:
837 return False
838 return self._UnzipAndMoveBuildProducts(downloaded_file, build_dir,
839 build_type=build_type)
841 def _WaitForBuildDownload(self, revision, build_dir, deps_patch=None,
842 deps_patch_sha=None):
843 """Tries to download a zip archive for a build.
845 This involves seeing whether the archive is already available, and if not,
846 then requesting a build and waiting before downloading.
848 Args:
849 revision: A git commit hash.
850 build_dir: The directory to download the build into.
851 deps_patch: A patch which changes a dependency repository revision in
852 the DEPS, if applicable.
853 deps_patch_sha: The SHA1 hex digest of the above patch.
855 Returns:
856 File path of the downloaded file if successful, otherwise None.
858 bucket_name, remote_path = fetch_build.GetBucketAndRemotePath(
859 revision, builder_type=self.opts.builder_type,
860 target_arch=self.opts.target_arch,
861 target_platform=self.opts.target_platform,
862 deps_patch_sha=deps_patch_sha)
863 output_dir = os.path.abspath(build_dir)
864 fetch_build_func = lambda: fetch_build.FetchFromCloudStorage(
865 bucket_name, remote_path, output_dir)
867 is_available = fetch_build.BuildIsAvailable(bucket_name, remote_path)
868 if is_available:
869 return fetch_build_func()
871 # When build archive doesn't exist, make a request and wait.
872 return self._RequestBuildAndWait(
873 revision, fetch_build_func, deps_patch=deps_patch)
875 def _RequestBuildAndWait(self, git_revision, fetch_build_func,
876 deps_patch=None):
877 """Triggers a try job for a build job.
879 This function prepares and starts a try job for a builder, and waits for
880 the archive to be produced and archived. Once the build is ready it is
881 downloaded.
883 For performance tests, builders on the tryserver.chromium.perf are used.
885 TODO(qyearsley): Make this function take "builder_type" as a parameter
886 and make requests to different bot names based on that parameter.
888 Args:
889 git_revision: A git commit hash.
890 fetch_build_func: Function to check and download build from cloud storage.
891 deps_patch: DEPS patch string, used when bisecting dependency repos.
893 Returns:
894 Downloaded archive file path when requested build exists and download is
895 successful, otherwise None.
897 if not fetch_build_func:
898 return None
900 # Create a unique ID for each build request posted to try server builders.
901 # This ID is added to "Reason" property of the build.
902 build_request_id = GetSHA1HexDigest(
903 '%s-%s-%s' % (git_revision, deps_patch, time.time()))
905 # Revert any changes to DEPS file.
906 bisect_utils.CheckRunGit(['reset', '--hard', 'HEAD'], cwd=self.src_cwd)
908 builder_name, build_timeout = fetch_build.GetBuilderNameAndBuildTime(
909 builder_type=self.opts.builder_type,
910 target_arch=self.opts.target_arch,
911 target_platform=self.opts.target_platform)
913 try:
914 _StartBuilderTryJob(self.opts.builder_type, git_revision, builder_name,
915 job_name=build_request_id, patch=deps_patch)
916 except RunGitError as e:
917 logging.warn('Failed to post builder try job for revision: [%s].\n'
918 'Error: %s', git_revision, e)
919 return None
921 archive_filename, error_msg = _WaitUntilBuildIsReady(
922 fetch_build_func, builder_name, self.opts.builder_type,
923 build_request_id, build_timeout)
924 if not archive_filename:
925 logging.warn('%s [revision: %s]', error_msg, git_revision)
926 return archive_filename
928 def _UnzipAndMoveBuildProducts(self, downloaded_file, build_dir,
929 build_type='Release'):
930 """Unzips the build archive and moves it to the build output directory.
932 The build output directory is wherever the binaries are expected to
933 be in order to start Chrome and run tests.
935 TODO: Simplify and clarify this method if possible.
937 Args:
938 downloaded_file: File path of the downloaded zip file.
939 build_dir: Directory where the the zip file was downloaded to.
940 build_type: "Release" or "Debug".
942 Returns:
943 True if successful, False otherwise.
945 abs_build_dir = os.path.abspath(build_dir)
946 output_dir = os.path.join(abs_build_dir, self.GetZipFileBuildDirName())
947 logging.info('EXPERIMENTAL RUN, _UnzipAndMoveBuildProducts locals %s',
948 str(locals()))
950 try:
951 RemoveDirectoryTree(output_dir)
952 self.BackupOrRestoreOutputDirectory(restore=False)
953 # Build output directory based on target(e.g. out/Release, out/Debug).
954 target_build_output_dir = os.path.join(abs_build_dir, build_type)
956 logging.info('Extracting "%s" to "%s"', downloaded_file, abs_build_dir)
957 fetch_build.Unzip(downloaded_file, abs_build_dir)
959 if not os.path.exists(output_dir):
960 # Due to recipe changes, the builds extract folder contains
961 # out/Release instead of full-build-<platform>/Release.
962 if os.path.exists(os.path.join(abs_build_dir, 'out', build_type)):
963 output_dir = os.path.join(abs_build_dir, 'out', build_type)
964 else:
965 raise IOError('Missing extracted folder %s ' % output_dir)
967 logging.info('Moving build from %s to %s',
968 output_dir, target_build_output_dir)
969 shutil.move(output_dir, target_build_output_dir)
970 return True
971 except Exception as e:
972 logging.info('Something went wrong while extracting archive file: %s', e)
973 self.BackupOrRestoreOutputDirectory(restore=True)
974 # Cleanup any leftovers from unzipping.
975 if os.path.exists(output_dir):
976 RemoveDirectoryTree(output_dir)
977 finally:
978 # Delete downloaded archive
979 if os.path.exists(downloaded_file):
980 os.remove(downloaded_file)
981 return False
983 @staticmethod
984 def GetZipFileBuildDirName():
985 """Gets the base file name of the zip file.
987 After extracting the zip file, this is the name of the directory where
988 the build files are expected to be. Possibly.
990 TODO: Make sure that this returns the actual directory name where the
991 Release or Debug directory is inside of the zip files. This probably
992 depends on the builder recipe, and may depend on whether the builder is
993 a perf builder or full builder.
995 Returns:
996 The name of the directory inside a build archive which is expected to
997 contain a Release or Debug directory.
999 if bisect_utils.IsWindowsHost():
1000 return 'full-build-win32'
1001 if bisect_utils.IsLinuxHost():
1002 return 'full-build-linux'
1003 if bisect_utils.IsMacHost():
1004 return 'full-build-mac'
1005 raise NotImplementedError('Unknown platform "%s".' % sys.platform)
1007 def IsDownloadable(self, depot):
1008 """Checks if build can be downloaded based on target platform and depot."""
1009 if (self.opts.target_platform in ['chromium', 'android']
1010 and self.opts.builder_type):
1011 return (depot == 'chromium' or
1012 'chromium' in bisect_utils.DEPOT_DEPS_NAME[depot]['from'] or
1013 'v8' in bisect_utils.DEPOT_DEPS_NAME[depot]['from'])
1014 return False
1016 def UpdateDepsContents(self, deps_contents, depot, git_revision, deps_key):
1017 """Returns modified version of DEPS file contents.
1019 Args:
1020 deps_contents: DEPS file content.
1021 depot: Current depot being bisected.
1022 git_revision: A git hash to be updated in DEPS.
1023 deps_key: Key in vars section of DEPS file to be searched.
1025 Returns:
1026 Updated DEPS content as string if deps key is found, otherwise None.
1028 # Check whether the depot and revision pattern in DEPS file vars
1029 # e.g. for webkit the format is "webkit_revision": "12345".
1030 deps_revision = re.compile(r'(?<="%s": ")([0-9]+)(?=")' % deps_key,
1031 re.MULTILINE)
1032 new_data = None
1033 if re.search(deps_revision, deps_contents):
1034 commit_position = source_control.GetCommitPosition(
1035 git_revision, self.depot_registry.GetDepotDir(depot))
1036 if not commit_position:
1037 logging.warn('Could not determine commit position for %s', git_revision)
1038 return None
1039 # Update the revision information for the given depot
1040 new_data = re.sub(deps_revision, str(commit_position), deps_contents)
1041 else:
1042 # Check whether the depot and revision pattern in DEPS file vars
1043 # e.g. for webkit the format is "webkit_revision": "559a6d4ab7a84c539..".
1044 deps_revision = re.compile(
1045 r'(?<=["\']%s["\']: ["\'])([a-fA-F0-9]{40})(?=["\'])' % deps_key,
1046 re.MULTILINE)
1047 if re.search(deps_revision, deps_contents):
1048 new_data = re.sub(deps_revision, git_revision, deps_contents)
1049 if new_data:
1050 # For v8_bleeding_edge revisions change V8 branch in order
1051 # to fetch bleeding edge revision.
1052 if depot == 'v8_bleeding_edge':
1053 new_data = _UpdateV8Branch(new_data)
1054 if not new_data:
1055 return None
1056 return new_data
1058 def UpdateDeps(self, revision, depot, deps_file):
1059 """Updates DEPS file with new revision of dependency repository.
1061 This method search DEPS for a particular pattern in which depot revision
1062 is specified (e.g "webkit_revision": "123456"). If a match is found then
1063 it resolves the given git hash to SVN revision and replace it in DEPS file.
1065 Args:
1066 revision: A git hash revision of the dependency repository.
1067 depot: Current depot being bisected.
1068 deps_file: Path to DEPS file.
1070 Returns:
1071 True if DEPS file is modified successfully, otherwise False.
1073 if not os.path.exists(deps_file):
1074 return False
1076 deps_var = bisect_utils.DEPOT_DEPS_NAME[depot]['deps_var']
1077 # Don't update DEPS file if deps_var is not set in DEPOT_DEPS_NAME.
1078 if not deps_var:
1079 logging.warn('DEPS update not supported for Depot: %s', depot)
1080 return False
1082 # Hack for Angle repository. In the DEPS file, "vars" dictionary variable
1083 # contains "angle_revision" key that holds git hash instead of SVN revision.
1084 # And sometime "angle_revision" key is not specified in "vars" variable.
1085 # In such cases check, "deps" dictionary variable that matches
1086 # angle.git@[a-fA-F0-9]{40}$ and replace git hash.
1087 if depot == 'angle':
1088 return _UpdateDEPSForAngle(revision, depot, deps_file)
1090 try:
1091 deps_contents = ReadStringFromFile(deps_file)
1092 updated_deps_content = self.UpdateDepsContents(
1093 deps_contents, depot, revision, deps_var)
1094 # Write changes to DEPS file
1095 if updated_deps_content:
1096 WriteStringToFile(updated_deps_content, deps_file)
1097 return True
1098 except IOError, e:
1099 logging.warn('Something went wrong while updating DEPS file. [%s]', e)
1100 return False
1102 def _CreateDEPSPatch(self, depot, revision):
1103 """Checks out the DEPS file at the specified revision and modifies it.
1105 Args:
1106 depot: Current depot being bisected.
1107 revision: A git hash revision of the dependency repository.
1109 deps_file_path = os.path.join(self.src_cwd, bisect_utils.FILE_DEPS)
1110 if not os.path.exists(deps_file_path):
1111 raise RuntimeError('DEPS file does not exists.[%s]' % deps_file_path)
1112 # Get current chromium revision (git hash).
1113 cmd = ['rev-parse', 'HEAD']
1114 chromium_sha = bisect_utils.CheckRunGit(cmd).strip()
1115 if not chromium_sha:
1116 raise RuntimeError('Failed to determine Chromium revision for %s' %
1117 revision)
1118 if ('chromium' in bisect_utils.DEPOT_DEPS_NAME[depot]['from'] or
1119 'v8' in bisect_utils.DEPOT_DEPS_NAME[depot]['from']):
1120 # Checkout DEPS file for the current chromium revision.
1121 if not source_control.CheckoutFileAtRevision(
1122 bisect_utils.FILE_DEPS, chromium_sha, cwd=self.src_cwd):
1123 raise RuntimeError(
1124 'DEPS checkout Failed for chromium revision : [%s]' % chromium_sha)
1126 if not self.UpdateDeps(revision, depot, deps_file_path):
1127 raise RuntimeError(
1128 'Failed to update DEPS file for chromium: [%s]' % chromium_sha)
1130 def _CreatePatch(self, revision):
1131 """Creates a patch from currently modified files.
1133 Args:
1134 depot: Current depot being bisected.
1135 revision: A git hash revision of the dependency repository.
1137 Returns:
1138 A tuple with git hash of chromium revision and DEPS patch text.
1140 # Get current chromium revision (git hash).
1141 chromium_sha = bisect_utils.CheckRunGit(['rev-parse', 'HEAD']).strip()
1142 if not chromium_sha:
1143 raise RuntimeError('Failed to determine Chromium revision for %s' %
1144 revision)
1145 # Checkout DEPS file for the current chromium revision.
1146 diff_command = [
1147 'diff',
1148 '--src-prefix=',
1149 '--dst-prefix=',
1150 '--no-ext-diff',
1151 'HEAD',
1153 diff_text = bisect_utils.CheckRunGit(diff_command)
1154 return (chromium_sha, ChangeBackslashToSlashInPatch(diff_text))
1156 def ObtainBuild(
1157 self, depot, revision=None, create_patch=False):
1158 """Obtains a build by either downloading or building directly.
1160 Args:
1161 depot: Dependency repository name.
1162 revision: A git commit hash. If None is given, the currently checked-out
1163 revision is built.
1164 create_patch: Create a patch with any locally modified files.
1166 Returns:
1167 True for success.
1169 if self.opts.debug_ignore_build:
1170 return True
1172 build_success = False
1173 cwd = os.getcwd()
1174 os.chdir(self.src_cwd)
1175 # Fetch build archive for the given revision from the cloud storage when
1176 # the storage bucket is passed.
1177 if self.IsDownloadable(depot) and revision:
1178 build_success = self._DownloadAndUnzipBuild(
1179 revision, depot, build_type='Release', create_patch=create_patch)
1180 else:
1181 # Print the current environment set on the machine.
1182 print 'Full Environment:'
1183 for key, value in sorted(os.environ.items()):
1184 print '%s: %s' % (key, value)
1185 # Print the environment before proceeding with compile.
1186 sys.stdout.flush()
1187 build_success = self.builder.Build(depot, self.opts)
1188 os.chdir(cwd)
1189 return build_success
1191 def RunGClientHooks(self):
1192 """Runs gclient with runhooks command.
1194 Returns:
1195 True if gclient reports no errors.
1197 if self.opts.debug_ignore_build:
1198 return True
1199 return not bisect_utils.RunGClient(['runhooks'], cwd=self.src_cwd)
1201 def _IsBisectModeUsingMetric(self):
1202 return self.opts.bisect_mode in [bisect_utils.BISECT_MODE_MEAN,
1203 bisect_utils.BISECT_MODE_STD_DEV]
1205 def _IsBisectModeReturnCode(self):
1206 return self.opts.bisect_mode in [bisect_utils.BISECT_MODE_RETURN_CODE]
1208 def _IsBisectModeStandardDeviation(self):
1209 return self.opts.bisect_mode in [bisect_utils.BISECT_MODE_STD_DEV]
1211 def GetCompatibleCommand(self, command_to_run, revision, depot):
1212 """Return a possibly modified test command depending on the revision.
1214 Prior to crrev.com/274857 *only* android-chromium-testshell
1215 Then until crrev.com/276628 *both* (android-chromium-testshell and
1216 android-chrome-shell) work. After that rev 276628 *only*
1217 android-chrome-shell works. The bisect_perf_regression.py script should
1218 handle these cases and set appropriate browser type based on revision.
1220 if self.opts.target_platform in ['android']:
1221 # When its a third_party depot, get the chromium revision.
1222 if depot != 'chromium':
1223 revision = bisect_utils.CheckRunGit(
1224 ['rev-parse', 'HEAD'], cwd=self.src_cwd).strip()
1225 commit_position = source_control.GetCommitPosition(revision,
1226 cwd=self.src_cwd)
1227 if not commit_position:
1228 return command_to_run
1229 cmd_re = re.compile(r'--browser=(?P<browser_type>\S+)')
1230 matches = cmd_re.search(command_to_run)
1231 if bisect_utils.IsStringInt(commit_position) and matches:
1232 cmd_browser = matches.group('browser_type')
1233 if commit_position <= 274857 and cmd_browser == 'android-chrome-shell':
1234 return command_to_run.replace(cmd_browser,
1235 'android-chromium-testshell')
1236 elif (commit_position >= 276628 and
1237 cmd_browser == 'android-chromium-testshell'):
1238 return command_to_run.replace(cmd_browser,
1239 'android-chrome-shell')
1240 return command_to_run
1242 def RunPerformanceTestAndParseResults(
1243 self, command_to_run, metric, reset_on_first_run=False,
1244 upload_on_last_run=False, results_label=None, test_run_multiplier=1):
1245 """Runs a performance test on the current revision and parses the results.
1247 Args:
1248 command_to_run: The command to be run to execute the performance test.
1249 metric: The metric to parse out from the results of the performance test.
1250 This is the result chart name and trace name, separated by slash.
1251 May be None for perf try jobs.
1252 reset_on_first_run: If True, pass the flag --reset-results on first run.
1253 upload_on_last_run: If True, pass the flag --upload-results on last run.
1254 results_label: A value for the option flag --results-label.
1255 The arguments reset_on_first_run, upload_on_last_run and results_label
1256 are all ignored if the test is not a Telemetry test.
1257 test_run_multiplier: Factor by which to multiply the number of test runs
1258 and the timeout period specified in self.opts.
1260 Returns:
1261 (values dict, 0) if --debug_ignore_perf_test was passed.
1262 (values dict, 0, test output) if the test was run successfully.
1263 (error message, -1) if the test couldn't be run.
1264 (error message, -1, test output) if the test ran but there was an error.
1266 success_code, failure_code = 0, -1
1268 if self.opts.debug_ignore_perf_test:
1269 fake_results = {
1270 'mean': 0.0,
1271 'std_err': 0.0,
1272 'std_dev': 0.0,
1273 'values': [0.0]
1276 # When debug_fake_test_mean is set, its value is returned as the mean
1277 # and the flag is cleared so that further calls behave as if it wasn't
1278 # set (returning the fake_results dict as defined above).
1279 if self.opts.debug_fake_first_test_mean:
1280 fake_results['mean'] = float(self.opts.debug_fake_first_test_mean)
1281 self.opts.debug_fake_first_test_mean = 0
1283 return (fake_results, success_code)
1285 # For Windows platform set posix=False, to parse windows paths correctly.
1286 # On Windows, path separators '\' or '\\' are replace by '' when posix=True,
1287 # refer to http://bugs.python.org/issue1724822. By default posix=True.
1288 args = shlex.split(command_to_run, posix=not bisect_utils.IsWindowsHost())
1290 if not _GenerateProfileIfNecessary(args):
1291 err_text = 'Failed to generate profile for performance test.'
1292 return (err_text, failure_code)
1294 is_telemetry = bisect_utils.IsTelemetryCommand(command_to_run)
1296 start_time = time.time()
1298 metric_values = []
1299 output_of_all_runs = ''
1300 repeat_count = self.opts.repeat_test_count * test_run_multiplier
1301 for i in xrange(repeat_count):
1302 # Can ignore the return code since if the tests fail, it won't return 0.
1303 current_args = copy.copy(args)
1304 if is_telemetry:
1305 if i == 0 and reset_on_first_run:
1306 current_args.append('--reset-results')
1307 if i == self.opts.repeat_test_count - 1 and upload_on_last_run:
1308 current_args.append('--upload-results')
1309 if results_label:
1310 current_args.append('--results-label=%s' % results_label)
1311 try:
1312 output, return_code = bisect_utils.RunProcessAndRetrieveOutput(
1313 current_args, cwd=self.src_cwd)
1314 except OSError, e:
1315 if e.errno == errno.ENOENT:
1316 err_text = ('Something went wrong running the performance test. '
1317 'Please review the command line:\n\n')
1318 if 'src/' in ' '.join(args):
1319 err_text += ('Check that you haven\'t accidentally specified a '
1320 'path with src/ in the command.\n\n')
1321 err_text += ' '.join(args)
1322 err_text += '\n'
1324 return (err_text, failure_code)
1325 raise
1327 output_of_all_runs += output
1328 if self.opts.output_buildbot_annotations:
1329 print output
1331 if metric and self._IsBisectModeUsingMetric():
1332 parsed_metric = _ParseMetricValuesFromOutput(metric, output)
1333 if parsed_metric:
1334 metric_values.append(math_utils.Mean(parsed_metric))
1335 # If we're bisecting on a metric (ie, changes in the mean or
1336 # standard deviation) and no metric values are produced, bail out.
1337 if not metric_values:
1338 break
1339 elif self._IsBisectModeReturnCode():
1340 metric_values.append(return_code)
1342 elapsed_minutes = (time.time() - start_time) / 60.0
1343 time_limit = self.opts.max_time_minutes * test_run_multiplier
1344 if elapsed_minutes >= time_limit:
1345 break
1347 if metric and len(metric_values) == 0:
1348 err_text = 'Metric %s was not found in the test output.' % metric
1349 # TODO(qyearsley): Consider also getting and displaying a list of metrics
1350 # that were found in the output here.
1351 return (err_text, failure_code, output_of_all_runs)
1353 # If we're bisecting on return codes, we're really just looking for zero vs
1354 # non-zero.
1355 values = {}
1356 if self._IsBisectModeReturnCode():
1357 # If any of the return codes is non-zero, output 1.
1358 overall_return_code = 0 if (
1359 all(current_value == 0 for current_value in metric_values)) else 1
1361 values = {
1362 'mean': overall_return_code,
1363 'std_err': 0.0,
1364 'std_dev': 0.0,
1365 'values': metric_values,
1368 print 'Results of performance test: Command returned with %d' % (
1369 overall_return_code)
1370 print
1371 elif metric:
1372 # Need to get the average value if there were multiple values.
1373 truncated_mean = math_utils.TruncatedMean(
1374 metric_values, self.opts.truncate_percent)
1375 standard_err = math_utils.StandardError(metric_values)
1376 standard_dev = math_utils.StandardDeviation(metric_values)
1378 if self._IsBisectModeStandardDeviation():
1379 metric_values = [standard_dev]
1381 values = {
1382 'mean': truncated_mean,
1383 'std_err': standard_err,
1384 'std_dev': standard_dev,
1385 'values': metric_values,
1388 print 'Results of performance test: %12f %12f' % (
1389 truncated_mean, standard_err)
1390 print
1391 return (values, success_code, output_of_all_runs)
1393 def PerformPreBuildCleanup(self):
1394 """Performs cleanup between runs."""
1395 print 'Cleaning up between runs.'
1396 print
1398 # Leaving these .pyc files around between runs may disrupt some perf tests.
1399 for (path, _, files) in os.walk(self.src_cwd):
1400 for cur_file in files:
1401 if cur_file.endswith('.pyc'):
1402 path_to_file = os.path.join(path, cur_file)
1403 os.remove(path_to_file)
1405 def _RunPostSync(self, _depot):
1406 """Performs any work after syncing.
1408 Args:
1409 depot: Depot name.
1411 Returns:
1412 True if successful.
1414 if 'android' in self.opts.target_platform:
1415 if not builder.SetupAndroidBuildEnvironment(self.opts,
1416 path_to_src=self.src_cwd):
1417 return False
1419 return self.RunGClientHooks()
1421 @staticmethod
1422 def ShouldSkipRevision(depot, revision):
1423 """Checks whether a particular revision can be safely skipped.
1425 Some commits can be safely skipped (such as a DEPS roll for the repos
1426 still using .DEPS.git), since the tool is git based those changes
1427 would have no effect.
1429 Args:
1430 depot: The depot being bisected.
1431 revision: Current revision we're synced to.
1433 Returns:
1434 True if we should skip building/testing this revision.
1436 # Skips revisions with DEPS on android-chrome.
1437 if depot == 'android-chrome':
1438 cmd = ['diff-tree', '--no-commit-id', '--name-only', '-r', revision]
1439 output = bisect_utils.CheckRunGit(cmd)
1441 files = output.splitlines()
1443 if len(files) == 1 and files[0] == 'DEPS':
1444 return True
1446 return False
1448 def RunTest(self, revision, depot, command, metric, skippable=False,
1449 skip_sync=False, create_patch=False, force_build=False,
1450 test_run_multiplier=1):
1451 """Performs a full sync/build/run of the specified revision.
1453 Args:
1454 revision: The revision to sync to.
1455 depot: The depot that's being used at the moment (src, webkit, etc.)
1456 command: The command to execute the performance test.
1457 metric: The performance metric being tested.
1458 skip_sync: Skip the sync step.
1459 create_patch: Create a patch with any locally modified files.
1460 force_build: Force a local build.
1461 test_run_multiplier: Factor by which to multiply the given number of runs
1462 and the set timeout period.
1464 Returns:
1465 On success, a tuple containing the results of the performance test.
1466 Otherwise, a tuple with the error message.
1468 logging.info('Running RunTest with rev "%s", command "%s"',
1469 revision, command)
1470 # Decide which sync program to use.
1471 sync_client = None
1472 if depot == 'chromium' or depot == 'android-chrome':
1473 sync_client = 'gclient'
1475 # Do the syncing for all depots.
1476 if not (self.opts.debug_ignore_sync or skip_sync):
1477 if not self._SyncRevision(depot, revision, sync_client):
1478 return ('Failed to sync: [%s]' % str(revision), BUILD_RESULT_FAIL)
1480 # Try to do any post-sync steps. This may include "gclient runhooks".
1481 if not self._RunPostSync(depot):
1482 return ('Failed to run [gclient runhooks].', BUILD_RESULT_FAIL)
1484 # Skip this revision if it can be skipped.
1485 if skippable and self.ShouldSkipRevision(depot, revision):
1486 return ('Skipped revision: [%s]' % str(revision),
1487 BUILD_RESULT_SKIPPED)
1489 # Obtain a build for this revision. This may be done by requesting a build
1490 # from another builder, waiting for it and downloading it.
1491 start_build_time = time.time()
1492 revision_to_build = revision if not force_build else None
1493 build_success = self.ObtainBuild(
1494 depot, revision=revision_to_build, create_patch=create_patch)
1495 if not build_success:
1496 return ('Failed to build revision: [%s]' % str(revision),
1497 BUILD_RESULT_FAIL)
1498 after_build_time = time.time()
1500 # Possibly alter the command.
1501 command = self.GetCompatibleCommand(command, revision, depot)
1503 # Run the command and get the results.
1504 results = self.RunPerformanceTestAndParseResults(
1505 command, metric, test_run_multiplier=test_run_multiplier)
1507 # Restore build output directory once the tests are done, to avoid
1508 # any discrepancies.
1509 if self.IsDownloadable(depot) and revision:
1510 self.BackupOrRestoreOutputDirectory(restore=True)
1512 # A value other than 0 indicates that the test couldn't be run, and results
1513 # should also include an error message.
1514 if results[1] != 0:
1515 return results
1517 external_revisions = self._Get3rdPartyRevisions(depot)
1519 if not external_revisions is None:
1520 return (results[0], results[1], external_revisions,
1521 time.time() - after_build_time, after_build_time -
1522 start_build_time)
1523 else:
1524 return ('Failed to parse DEPS file for external revisions.',
1525 BUILD_RESULT_FAIL)
1527 def _SyncRevision(self, depot, revision, sync_client):
1528 """Syncs depot to particular revision.
1530 Args:
1531 depot: The depot that's being used at the moment (src, webkit, etc.)
1532 revision: The revision to sync to.
1533 sync_client: Program used to sync, e.g. "gclient". Can be None.
1535 Returns:
1536 True if successful, False otherwise.
1538 self.depot_registry.ChangeToDepotDir(depot)
1540 if sync_client:
1541 self.PerformPreBuildCleanup()
1543 # When using gclient to sync, you need to specify the depot you
1544 # want so that all the dependencies sync properly as well.
1545 # i.e. gclient sync src@<SHA1>
1546 if sync_client == 'gclient' and revision:
1547 revision = '%s@%s' % (bisect_utils.DEPOT_DEPS_NAME[depot]['src'],
1548 revision)
1549 if depot == 'chromium' and self.opts.target_platform == 'android-chrome':
1550 return self._SyncRevisionsForAndroidChrome(revision)
1552 return source_control.SyncToRevision(revision, sync_client)
1554 def _SyncRevisionsForAndroidChrome(self, revision):
1555 """Syncs android-chrome and chromium repos to particular revision.
1557 This is a special case for android-chrome as the gclient sync for chromium
1558 overwrites the android-chrome revision to TOT. Therefore both the repos
1559 are synced to known revisions.
1561 Args:
1562 revision: Git hash of the Chromium to sync.
1564 Returns:
1565 True if successful, False otherwise.
1567 revisions_list = [revision]
1568 current_android_rev = source_control.GetCurrentRevision(
1569 self.depot_registry.GetDepotDir('android-chrome'))
1570 revisions_list.append(
1571 '%s@%s' % (bisect_utils.DEPOT_DEPS_NAME['android-chrome']['src'],
1572 current_android_rev))
1573 return not bisect_utils.RunGClientAndSync(revisions_list)
1575 def _CheckIfRunPassed(self, current_value, known_good_value, known_bad_value):
1576 """Given known good and bad values, decide if the current_value passed
1577 or failed.
1579 Args:
1580 current_value: The value of the metric being checked.
1581 known_bad_value: The reference value for a "failed" run.
1582 known_good_value: The reference value for a "passed" run.
1584 Returns:
1585 True if the current_value is closer to the known_good_value than the
1586 known_bad_value.
1588 if self.opts.bisect_mode == bisect_utils.BISECT_MODE_STD_DEV:
1589 dist_to_good_value = abs(current_value['std_dev'] -
1590 known_good_value['std_dev'])
1591 dist_to_bad_value = abs(current_value['std_dev'] -
1592 known_bad_value['std_dev'])
1593 else:
1594 dist_to_good_value = abs(current_value['mean'] - known_good_value['mean'])
1595 dist_to_bad_value = abs(current_value['mean'] - known_bad_value['mean'])
1597 return dist_to_good_value < dist_to_bad_value
1599 def _GetV8BleedingEdgeFromV8TrunkIfMappable(
1600 self, revision, bleeding_edge_branch):
1601 """Gets v8 bleeding edge revision mapped to v8 revision in trunk.
1603 Args:
1604 revision: A trunk V8 revision mapped to bleeding edge revision.
1605 bleeding_edge_branch: Branch used to perform lookup of bleeding edge
1606 revision.
1607 Return:
1608 A mapped bleeding edge revision if found, otherwise None.
1610 commit_position = source_control.GetCommitPosition(revision)
1612 if bisect_utils.IsStringInt(commit_position):
1613 # V8 is tricky to bisect, in that there are only a few instances when
1614 # we can dive into bleeding_edge and get back a meaningful result.
1615 # Try to detect a V8 "business as usual" case, which is when:
1616 # 1. trunk revision N has description "Version X.Y.Z"
1617 # 2. bleeding_edge revision (N-1) has description "Prepare push to
1618 # trunk. Now working on X.Y.(Z+1)."
1620 # As of 01/24/2014, V8 trunk descriptions are formatted:
1621 # "Version 3.X.Y (based on bleeding_edge revision rZ)"
1622 # So we can just try parsing that out first and fall back to the old way.
1623 v8_dir = self.depot_registry.GetDepotDir('v8')
1624 v8_bleeding_edge_dir = self.depot_registry.GetDepotDir('v8_bleeding_edge')
1626 revision_info = source_control.QueryRevisionInfo(revision, cwd=v8_dir)
1627 version_re = re.compile("Version (?P<values>[0-9,.]+)")
1628 regex_results = version_re.search(revision_info['subject'])
1629 if regex_results:
1630 git_revision = None
1631 if 'based on bleeding_edge' in revision_info['subject']:
1632 try:
1633 bleeding_edge_revision = revision_info['subject'].split(
1634 'bleeding_edge revision r')[1]
1635 bleeding_edge_revision = int(bleeding_edge_revision.split(')')[0])
1636 bleeding_edge_url = ('https://v8.googlecode.com/svn/branches/'
1637 'bleeding_edge@%s' % bleeding_edge_revision)
1638 cmd = ['log',
1639 '--format=%H',
1640 '--grep',
1641 bleeding_edge_url,
1642 '-1',
1643 bleeding_edge_branch]
1644 output = bisect_utils.CheckRunGit(cmd, cwd=v8_dir)
1645 if output:
1646 git_revision = output.strip()
1647 return git_revision
1648 except (IndexError, ValueError):
1649 pass
1650 else:
1651 # V8 rolls description changed after V8 git migration, new description
1652 # includes "Version 3.X.Y (based on <git hash>)"
1653 try:
1654 rxp = re.compile('based on (?P<git_revision>[a-fA-F0-9]+)')
1655 re_results = rxp.search(revision_info['subject'])
1656 if re_results:
1657 return re_results.group('git_revision')
1658 except (IndexError, ValueError):
1659 pass
1660 if not git_revision:
1661 # Wasn't successful, try the old way of looking for "Prepare push to"
1662 git_revision = source_control.ResolveToRevision(
1663 int(commit_position) - 1, 'v8_bleeding_edge',
1664 bisect_utils.DEPOT_DEPS_NAME, -1, cwd=v8_bleeding_edge_dir)
1666 if git_revision:
1667 revision_info = source_control.QueryRevisionInfo(git_revision,
1668 cwd=v8_bleeding_edge_dir)
1670 if 'Prepare push to trunk' in revision_info['subject']:
1671 return git_revision
1672 return None
1674 def _GetNearestV8BleedingEdgeFromTrunk(
1675 self, revision, v8_branch, bleeding_edge_branch, search_forward=True):
1676 """Gets the nearest V8 roll and maps to bleeding edge revision.
1678 V8 is a bit tricky to bisect since it isn't just rolled out like blink.
1679 Each revision on trunk might just be whatever was in bleeding edge, rolled
1680 directly out. Or it could be some mixture of previous v8 trunk versions,
1681 with bits and pieces cherry picked out from bleeding edge. In order to
1682 bisect, we need both the before/after versions on trunk v8 to be just pushes
1683 from bleeding edge. With the V8 git migration, the branches got switched.
1684 a) master (external/v8) == candidates (v8/v8)
1685 b) bleeding_edge (external/v8) == master (v8/v8)
1687 Args:
1688 revision: A V8 revision to get its nearest bleeding edge revision
1689 search_forward: Searches forward if True, otherwise search backward.
1691 Return:
1692 A mapped bleeding edge revision if found, otherwise None.
1694 cwd = self.depot_registry.GetDepotDir('v8')
1695 cmd = ['log', '--format=%ct', '-1', revision]
1696 output = bisect_utils.CheckRunGit(cmd, cwd=cwd)
1697 commit_time = int(output)
1698 commits = []
1699 if search_forward:
1700 cmd = ['log',
1701 '--format=%H',
1702 '--after=%d' % commit_time,
1703 v8_branch,
1704 '--reverse']
1705 output = bisect_utils.CheckRunGit(cmd, cwd=cwd)
1706 output = output.split()
1707 commits = output
1708 #Get 10 git hashes immediately after the given commit.
1709 commits = commits[:10]
1710 else:
1711 cmd = ['log',
1712 '--format=%H',
1713 '-10',
1714 '--before=%d' % commit_time,
1715 v8_branch]
1716 output = bisect_utils.CheckRunGit(cmd, cwd=cwd)
1717 output = output.split()
1718 commits = output
1720 bleeding_edge_revision = None
1722 for c in commits:
1723 bleeding_edge_revision = self._GetV8BleedingEdgeFromV8TrunkIfMappable(
1724 c, bleeding_edge_branch)
1725 if bleeding_edge_revision:
1726 break
1728 return bleeding_edge_revision
1730 def _FillInV8BleedingEdgeInfo(self, min_revision_state, max_revision_state):
1731 cwd = self.depot_registry.GetDepotDir('v8')
1732 # when "remote.origin.url" is https://chromium.googlesource.com/v8/v8.git
1733 v8_branch = 'origin/candidates'
1734 bleeding_edge_branch = 'origin/master'
1736 # Support for the chromium revisions with external V8 repo.
1737 # ie https://chromium.googlesource.com/external/v8.git
1738 cmd = ['config', '--get', 'remote.origin.url']
1739 v8_repo_url = bisect_utils.CheckRunGit(cmd, cwd=cwd)
1741 if 'external/v8.git' in v8_repo_url:
1742 v8_branch = 'origin/master'
1743 bleeding_edge_branch = 'origin/bleeding_edge'
1745 r1 = self._GetNearestV8BleedingEdgeFromTrunk(min_revision_state.revision,
1746 v8_branch, bleeding_edge_branch, search_forward=True)
1747 r2 = self._GetNearestV8BleedingEdgeFromTrunk(max_revision_state.revision,
1748 v8_branch, bleeding_edge_branch, search_forward=False)
1749 min_revision_state.external['v8_bleeding_edge'] = r1
1750 max_revision_state.external['v8_bleeding_edge'] = r2
1752 if (not self._GetV8BleedingEdgeFromV8TrunkIfMappable(
1753 min_revision_state.revision, bleeding_edge_branch)
1754 or not self._GetV8BleedingEdgeFromV8TrunkIfMappable(
1755 max_revision_state.revision, bleeding_edge_branch)):
1756 self.warnings.append(
1757 'Trunk revisions in V8 did not map directly to bleeding_edge. '
1758 'Attempted to expand the range to find V8 rolls which did map '
1759 'directly to bleeding_edge revisions, but results might not be '
1760 'valid.')
1762 def _FindNextDepotToBisect(
1763 self, current_depot, min_revision_state, max_revision_state):
1764 """Decides which depot the script should dive into next (if any).
1766 Args:
1767 current_depot: Current depot being bisected.
1768 min_revision_state: State of the earliest revision in the bisect range.
1769 max_revision_state: State of the latest revision in the bisect range.
1771 Returns:
1772 Name of the depot to bisect next, or None.
1774 external_depot = None
1775 for next_depot in bisect_utils.DEPOT_NAMES:
1776 if bisect_utils.DEPOT_DEPS_NAME[next_depot].has_key('platform'):
1777 if bisect_utils.DEPOT_DEPS_NAME[next_depot]['platform'] != os.name:
1778 continue
1780 if not (bisect_utils.DEPOT_DEPS_NAME[next_depot]['recurse']
1781 and min_revision_state.depot
1782 in bisect_utils.DEPOT_DEPS_NAME[next_depot]['from']):
1783 continue
1785 if current_depot == 'v8':
1786 # We grab the bleeding_edge info here rather than earlier because we
1787 # finally have the revision range. From that we can search forwards and
1788 # backwards to try to match trunk revisions to bleeding_edge.
1789 self._FillInV8BleedingEdgeInfo(min_revision_state, max_revision_state)
1791 if (min_revision_state.external.get(next_depot) ==
1792 max_revision_state.external.get(next_depot)):
1793 continue
1795 if (min_revision_state.external.get(next_depot) and
1796 max_revision_state.external.get(next_depot)):
1797 external_depot = next_depot
1798 break
1800 return external_depot
1802 def PrepareToBisectOnDepot(
1803 self, current_depot, start_revision, end_revision, previous_revision):
1804 """Changes to the appropriate directory and gathers a list of revisions
1805 to bisect between |start_revision| and |end_revision|.
1807 Args:
1808 current_depot: The depot we want to bisect.
1809 start_revision: Start of the revision range.
1810 end_revision: End of the revision range.
1811 previous_revision: The last revision we synced to on |previous_depot|.
1813 Returns:
1814 A list containing the revisions between |start_revision| and
1815 |end_revision| inclusive.
1817 # Change into working directory of external library to run
1818 # subsequent commands.
1819 self.depot_registry.ChangeToDepotDir(current_depot)
1821 # V8 (and possibly others) is merged in periodically. Bisecting
1822 # this directory directly won't give much good info.
1823 if bisect_utils.DEPOT_DEPS_NAME[current_depot].has_key('custom_deps'):
1824 config_path = os.path.join(self.src_cwd, '..')
1825 if bisect_utils.RunGClientAndCreateConfig(
1826 self.opts, bisect_utils.DEPOT_DEPS_NAME[current_depot]['custom_deps'],
1827 cwd=config_path):
1828 return []
1829 if bisect_utils.RunGClient(
1830 ['sync', '--revision', previous_revision], cwd=self.src_cwd):
1831 return []
1833 if current_depot == 'v8_bleeding_edge':
1834 self.depot_registry.ChangeToDepotDir('chromium')
1836 shutil.move('v8', 'v8.bak')
1837 shutil.move('v8_bleeding_edge', 'v8')
1839 self.cleanup_commands.append(['mv', 'v8', 'v8_bleeding_edge'])
1840 self.cleanup_commands.append(['mv', 'v8.bak', 'v8'])
1842 self.depot_registry.SetDepotDir('v8_bleeding_edge',
1843 os.path.join(self.src_cwd, 'v8'))
1844 self.depot_registry.SetDepotDir('v8', os.path.join(self.src_cwd,
1845 'v8.bak'))
1847 self.depot_registry.ChangeToDepotDir(current_depot)
1849 depot_revision_list = self.GetRevisionList(current_depot,
1850 end_revision,
1851 start_revision)
1853 self.depot_registry.ChangeToDepotDir('chromium')
1855 return depot_revision_list
1857 def GatherReferenceValues(self, good_rev, bad_rev, cmd, metric, target_depot):
1858 """Gathers reference values by running the performance tests on the
1859 known good and bad revisions.
1861 Args:
1862 good_rev: The last known good revision where the performance regression
1863 has not occurred yet.
1864 bad_rev: A revision where the performance regression has already occurred.
1865 cmd: The command to execute the performance test.
1866 metric: The metric being tested for regression.
1868 Returns:
1869 A tuple with the results of building and running each revision.
1871 bad_run_results = self.RunTest(bad_rev, target_depot, cmd, metric)
1873 good_run_results = None
1875 if not bad_run_results[1]:
1876 good_run_results = self.RunTest(good_rev, target_depot, cmd, metric)
1878 return (bad_run_results, good_run_results)
1880 def PrintRevisionsToBisectMessage(self, revision_list, depot):
1881 if self.opts.output_buildbot_annotations:
1882 step_name = 'Bisection Range: [%s:%s - %s]' % (depot, revision_list[-1],
1883 revision_list[0])
1884 bisect_utils.OutputAnnotationStepStart(step_name)
1886 print
1887 print 'Revisions to bisect on [%s]:' % depot
1888 for revision_id in revision_list:
1889 print ' -> %s' % (revision_id, )
1890 print
1892 if self.opts.output_buildbot_annotations:
1893 bisect_utils.OutputAnnotationStepClosed()
1895 def NudgeRevisionsIfDEPSChange(self, bad_revision, good_revision,
1896 good_svn_revision=None):
1897 """Checks to see if changes to DEPS file occurred, and that the revision
1898 range also includes the change to .DEPS.git. If it doesn't, attempts to
1899 expand the revision range to include it.
1901 Args:
1902 bad_revision: First known bad git revision.
1903 good_revision: Last known good git revision.
1904 good_svn_revision: Last known good svn revision.
1906 Returns:
1907 A tuple with the new bad and good revisions.
1909 # DONOT perform nudge because at revision 291563 .DEPS.git was removed
1910 # and source contain only DEPS file for dependency changes.
1911 if good_svn_revision >= 291563:
1912 return (bad_revision, good_revision)
1914 if self.opts.target_platform == 'chromium':
1915 changes_to_deps = source_control.QueryFileRevisionHistory(
1916 bisect_utils.FILE_DEPS, good_revision, bad_revision)
1918 if changes_to_deps:
1919 # DEPS file was changed, search from the oldest change to DEPS file to
1920 # bad_revision to see if there are matching .DEPS.git changes.
1921 oldest_deps_change = changes_to_deps[-1]
1922 changes_to_gitdeps = source_control.QueryFileRevisionHistory(
1923 bisect_utils.FILE_DEPS_GIT, oldest_deps_change, bad_revision)
1925 if len(changes_to_deps) != len(changes_to_gitdeps):
1926 # Grab the timestamp of the last DEPS change
1927 cmd = ['log', '--format=%ct', '-1', changes_to_deps[0]]
1928 output = bisect_utils.CheckRunGit(cmd)
1929 commit_time = int(output)
1931 # Try looking for a commit that touches the .DEPS.git file in the
1932 # next 15 minutes after the DEPS file change.
1933 cmd = ['log', '--format=%H', '-1',
1934 '--before=%d' % (commit_time + 900), '--after=%d' % commit_time,
1935 'origin/master', '--', bisect_utils.FILE_DEPS_GIT]
1936 output = bisect_utils.CheckRunGit(cmd)
1937 output = output.strip()
1938 if output:
1939 self.warnings.append('Detected change to DEPS and modified '
1940 'revision range to include change to .DEPS.git')
1941 return (output, good_revision)
1942 else:
1943 self.warnings.append('Detected change to DEPS but couldn\'t find '
1944 'matching change to .DEPS.git')
1945 return (bad_revision, good_revision)
1947 def CheckIfRevisionsInProperOrder(
1948 self, target_depot, good_revision, bad_revision):
1949 """Checks that |good_revision| is an earlier revision than |bad_revision|.
1951 Args:
1952 good_revision: Number/tag of the known good revision.
1953 bad_revision: Number/tag of the known bad revision.
1955 Returns:
1956 True if the revisions are in the proper order (good earlier than bad).
1958 cwd = self.depot_registry.GetDepotDir(target_depot)
1959 good_position = source_control.GetCommitPosition(good_revision, cwd)
1960 bad_position = source_control.GetCommitPosition(bad_revision, cwd)
1961 # Compare commit timestamp for repos that don't support commit position.
1962 if not (bad_position and good_position):
1963 good_position = source_control.GetCommitTime(good_revision, cwd=cwd)
1964 bad_position = source_control.GetCommitTime(bad_revision, cwd=cwd)
1966 return good_position <= bad_position
1968 def CanPerformBisect(self, good_revision, bad_revision):
1969 """Checks whether a given revision is bisectable.
1971 Checks for following:
1972 1. Non-bisectable revsions for android bots (refer to crbug.com/385324).
1973 2. Non-bisectable revsions for Windows bots (refer to crbug.com/405274).
1975 Args:
1976 good_revision: Known good revision.
1977 bad_revision: Known bad revision.
1979 Returns:
1980 A dictionary indicating the result. If revision is not bisectable,
1981 this will contain the field "error", otherwise None.
1983 if self.opts.target_platform == 'android':
1984 good_revision = source_control.GetCommitPosition(good_revision)
1985 if (bisect_utils.IsStringInt(good_revision)
1986 and good_revision < 265549):
1987 return {'error': (
1988 'Bisect cannot continue for the given revision range.\n'
1989 'It is impossible to bisect Android regressions '
1990 'prior to r265549, which allows the bisect bot to '
1991 'rely on Telemetry to do apk installation of the most recently '
1992 'built local ChromeShell(refer to crbug.com/385324).\n'
1993 'Please try bisecting revisions greater than or equal to r265549.')}
1995 if bisect_utils.IsWindowsHost():
1996 good_revision = source_control.GetCommitPosition(good_revision)
1997 bad_revision = source_control.GetCommitPosition(bad_revision)
1998 if (bisect_utils.IsStringInt(good_revision) and
1999 bisect_utils.IsStringInt(bad_revision)):
2000 if (289987 <= good_revision < 290716 or
2001 289987 <= bad_revision < 290716):
2002 return {'error': ('Oops! Revision between r289987 and r290716 are '
2003 'marked as dead zone for Windows due to '
2004 'crbug.com/405274. Please try another range.')}
2006 return None
2008 def _GatherResultsFromRevertedCulpritCL(
2009 self, results, target_depot, command_to_run, metric):
2010 """Gathers performance results with/without culprit CL.
2012 Attempts to revert the culprit CL against ToT and runs the
2013 performance tests again with and without the CL, adding the results to
2014 the over bisect results.
2016 Args:
2017 results: BisectResults from the bisect.
2018 target_depot: The target depot we're bisecting.
2019 command_to_run: Specify the command to execute the performance test.
2020 metric: The performance metric to monitor.
2022 run_results_tot, run_results_reverted = self._RevertCulpritCLAndRetest(
2023 results, target_depot, command_to_run, metric)
2025 results.AddRetestResults(run_results_tot, run_results_reverted)
2027 if len(results.culprit_revisions) != 1:
2028 return
2030 # Cleanup reverted files if anything is left.
2031 _, _, culprit_depot = results.culprit_revisions[0]
2032 bisect_utils.CheckRunGit(['reset', '--hard', 'HEAD'],
2033 cwd=self.depot_registry.GetDepotDir(culprit_depot))
2035 def _RevertCL(self, culprit_revision, culprit_depot):
2036 """Reverts the specified revision in the specified depot."""
2037 if self.opts.output_buildbot_annotations:
2038 bisect_utils.OutputAnnotationStepStart(
2039 'Reverting culprit CL: %s' % culprit_revision)
2040 _, return_code = bisect_utils.RunGit(
2041 ['revert', '--no-commit', culprit_revision],
2042 cwd=self.depot_registry.GetDepotDir(culprit_depot))
2043 if return_code:
2044 bisect_utils.OutputAnnotationStepWarning()
2045 bisect_utils.OutputAnnotationStepText('Failed to revert CL cleanly.')
2046 if self.opts.output_buildbot_annotations:
2047 bisect_utils.OutputAnnotationStepClosed()
2048 return not return_code
2050 def _RevertCulpritCLAndRetest(
2051 self, results, target_depot, command_to_run, metric):
2052 """Reverts the culprit CL against ToT and runs the performance test.
2054 Attempts to revert the culprit CL against ToT and runs the
2055 performance tests again with and without the CL.
2057 Args:
2058 results: BisectResults from the bisect.
2059 target_depot: The target depot we're bisecting.
2060 command_to_run: Specify the command to execute the performance test.
2061 metric: The performance metric to monitor.
2063 Returns:
2064 A tuple with the results of running the CL at ToT/reverted.
2066 # Might want to retest ToT with a revert of the CL to confirm that
2067 # performance returns.
2068 if results.confidence < bisect_utils.HIGH_CONFIDENCE:
2069 return (None, None)
2071 # If there were multiple culprit CLs, we won't try to revert.
2072 if len(results.culprit_revisions) != 1:
2073 return (None, None)
2075 culprit_revision, _, culprit_depot = results.culprit_revisions[0]
2077 if not self._SyncRevision(target_depot, None, 'gclient'):
2078 return (None, None)
2080 head_revision = bisect_utils.CheckRunGit(['log', '--format=%H', '-1'])
2081 head_revision = head_revision.strip()
2083 if not self._RevertCL(culprit_revision, culprit_depot):
2084 return (None, None)
2086 # If the culprit CL happened to be in a depot that gets pulled in, we
2087 # can't revert the change and issue a try job to build, since that would
2088 # require modifying both the DEPS file and files in another depot.
2089 # Instead, we build locally.
2090 force_build = (culprit_depot != target_depot)
2091 if force_build:
2092 results.warnings.append(
2093 'Culprit CL is in another depot, attempting to revert and build'
2094 ' locally to retest. This may not match the performance of official'
2095 ' builds.')
2097 run_results_reverted = self._RunTestWithAnnotations(
2098 'Re-Testing ToT with reverted culprit',
2099 'Failed to run reverted CL.',
2100 head_revision, target_depot, command_to_run, metric, force_build)
2102 # Clear the reverted file(s).
2103 bisect_utils.RunGit(['reset', '--hard', 'HEAD'],
2104 cwd=self.depot_registry.GetDepotDir(culprit_depot))
2106 # Retesting with the reverted CL failed, so bail out of retesting against
2107 # ToT.
2108 if run_results_reverted[1]:
2109 return (None, None)
2111 run_results_tot = self._RunTestWithAnnotations(
2112 'Re-Testing ToT',
2113 'Failed to run ToT.',
2114 head_revision, target_depot, command_to_run, metric, force_build)
2116 return (run_results_tot, run_results_reverted)
2118 def _RunTestWithAnnotations(self, step_text, error_text, head_revision,
2119 target_depot, command_to_run, metric, force_build):
2120 """Runs the performance test and outputs start/stop annotations.
2122 Args:
2123 results: BisectResults from the bisect.
2124 target_depot: The target depot we're bisecting.
2125 command_to_run: Specify the command to execute the performance test.
2126 metric: The performance metric to monitor.
2127 force_build: Whether to force a build locally.
2129 Returns:
2130 Results of the test.
2132 if self.opts.output_buildbot_annotations:
2133 bisect_utils.OutputAnnotationStepStart(step_text)
2135 # Build and run the test again with the reverted culprit CL against ToT.
2136 run_test_results = self.RunTest(
2137 head_revision, target_depot, command_to_run,
2138 metric, skippable=False, skip_sync=True, create_patch=True,
2139 force_build=force_build)
2141 if self.opts.output_buildbot_annotations:
2142 if run_test_results[1]:
2143 bisect_utils.OutputAnnotationStepWarning()
2144 bisect_utils.OutputAnnotationStepText(error_text)
2145 bisect_utils.OutputAnnotationStepClosed()
2147 return run_test_results
2149 def Run(self, command_to_run, bad_revision_in, good_revision_in, metric):
2150 """Given known good and bad revisions, run a binary search on all
2151 intermediate revisions to determine the CL where the performance regression
2152 occurred.
2154 Args:
2155 command_to_run: Specify the command to execute the performance test.
2156 good_revision: Number/tag of the known good revision.
2157 bad_revision: Number/tag of the known bad revision.
2158 metric: The performance metric to monitor.
2160 Returns:
2161 A BisectResults object.
2163 # Choose depot to bisect first
2164 target_depot = 'chromium'
2165 if self.opts.target_platform == 'android-chrome':
2166 target_depot = 'android-chrome'
2168 cwd = os.getcwd()
2169 self.depot_registry.ChangeToDepotDir(target_depot)
2171 # If they passed SVN revisions, we can try match them to git SHA1 hashes.
2172 bad_revision = source_control.ResolveToRevision(
2173 bad_revision_in, target_depot, bisect_utils.DEPOT_DEPS_NAME, 100)
2174 good_revision = source_control.ResolveToRevision(
2175 good_revision_in, target_depot, bisect_utils.DEPOT_DEPS_NAME, -100)
2177 os.chdir(cwd)
2178 if bad_revision is None:
2179 return BisectResults(
2180 error='Couldn\'t resolve [%s] to SHA1.' % bad_revision_in)
2182 if good_revision is None:
2183 return BisectResults(
2184 error='Couldn\'t resolve [%s] to SHA1.' % good_revision_in)
2186 # Check that they didn't accidentally swap good and bad revisions.
2187 if not self.CheckIfRevisionsInProperOrder(
2188 target_depot, good_revision, bad_revision):
2189 return BisectResults(error='bad_revision < good_revision, did you swap '
2190 'these by mistake?')
2192 bad_revision, good_revision = self.NudgeRevisionsIfDEPSChange(
2193 bad_revision, good_revision, good_revision_in)
2194 if self.opts.output_buildbot_annotations:
2195 bisect_utils.OutputAnnotationStepStart('Gathering Revisions')
2197 cannot_bisect = self.CanPerformBisect(good_revision, bad_revision)
2198 if cannot_bisect:
2199 return BisectResults(error=cannot_bisect.get('error'))
2201 print 'Gathering revision range for bisection.'
2202 # Retrieve a list of revisions to do bisection on.
2203 revision_list = self.GetRevisionList(target_depot, bad_revision,
2204 good_revision)
2206 if self.opts.output_buildbot_annotations:
2207 bisect_utils.OutputAnnotationStepClosed()
2209 if revision_list:
2210 self.PrintRevisionsToBisectMessage(revision_list, target_depot)
2212 if self.opts.output_buildbot_annotations:
2213 bisect_utils.OutputAnnotationStepStart('Gathering Reference Values')
2215 print 'Gathering reference values for bisection.'
2217 # Perform the performance tests on the good and bad revisions, to get
2218 # reference values.
2219 bad_results, good_results = self.GatherReferenceValues(good_revision,
2220 bad_revision,
2221 command_to_run,
2222 metric,
2223 target_depot)
2225 if self.opts.output_buildbot_annotations:
2226 bisect_utils.OutputAnnotationStepClosed()
2228 if bad_results[1]:
2229 error = ('An error occurred while building and running the \'bad\' '
2230 'reference value. The bisect cannot continue without '
2231 'a working \'bad\' revision to start from.\n\nError: %s' %
2232 bad_results[0])
2233 return BisectResults(error=error)
2235 if good_results[1]:
2236 error = ('An error occurred while building and running the \'good\' '
2237 'reference value. The bisect cannot continue without '
2238 'a working \'good\' revision to start from.\n\nError: %s' %
2239 good_results[0])
2240 return BisectResults(error=error)
2242 # We need these reference values to determine if later runs should be
2243 # classified as pass or fail.
2244 known_bad_value = bad_results[0]
2245 known_good_value = good_results[0]
2247 # Check the direction of improvement only if the improvement_direction
2248 # option is set to a specific direction (1 for higher is better or -1 for
2249 # lower is better).
2250 improvement_dir = self.opts.improvement_direction
2251 if improvement_dir:
2252 higher_is_better = improvement_dir > 0
2253 if higher_is_better:
2254 message = "Expecting higher values to be better for this metric, "
2255 else:
2256 message = "Expecting lower values to be better for this metric, "
2257 metric_increased = known_bad_value['mean'] > known_good_value['mean']
2258 if metric_increased:
2259 message += "and the metric appears to have increased. "
2260 else:
2261 message += "and the metric appears to have decreased. "
2262 if ((higher_is_better and metric_increased) or
2263 (not higher_is_better and not metric_increased)):
2264 error = (message + 'Then, the test results for the ends of the given '
2265 '\'good\' - \'bad\' range of revisions represent an '
2266 'improvement (and not a regression).')
2267 return BisectResults(error=error)
2268 logging.info(message + "Therefore we continue to bisect.")
2270 bisect_state = BisectState(target_depot, revision_list)
2271 revision_states = bisect_state.GetRevisionStates()
2273 min_revision = 0
2274 max_revision = len(revision_states) - 1
2276 # Can just mark the good and bad revisions explicitly here since we
2277 # already know the results.
2278 bad_revision_state = revision_states[min_revision]
2279 bad_revision_state.external = bad_results[2]
2280 bad_revision_state.perf_time = bad_results[3]
2281 bad_revision_state.build_time = bad_results[4]
2282 bad_revision_state.passed = False
2283 bad_revision_state.value = known_bad_value
2285 good_revision_state = revision_states[max_revision]
2286 good_revision_state.external = good_results[2]
2287 good_revision_state.perf_time = good_results[3]
2288 good_revision_state.build_time = good_results[4]
2289 good_revision_state.passed = True
2290 good_revision_state.value = known_good_value
2292 # Check how likely it is that the good and bad results are different
2293 # beyond chance-induced variation.
2294 confidence_error = False
2295 if not self.opts.debug_ignore_regression_confidence:
2296 confidence_error = _CheckRegressionConfidenceError(good_revision,
2297 bad_revision,
2298 known_good_value,
2299 known_bad_value)
2300 if confidence_error:
2301 self.warnings.append(confidence_error)
2302 bad_revision_state.passed = True # Marking the 'bad' revision as good.
2303 return BisectResults(bisect_state, self.depot_registry, self.opts,
2304 self.warnings)
2306 while True:
2307 if not revision_states:
2308 break
2310 if max_revision - min_revision <= 1:
2311 min_revision_state = revision_states[min_revision]
2312 max_revision_state = revision_states[max_revision]
2313 current_depot = min_revision_state.depot
2314 # TODO(sergiyb): Under which conditions can first two branches be hit?
2315 if min_revision_state.passed == '?':
2316 next_revision_index = min_revision
2317 elif max_revision_state.passed == '?':
2318 next_revision_index = max_revision
2319 elif current_depot in ['android-chrome', 'chromium', 'v8']:
2320 previous_revision = revision_states[min_revision].revision
2321 # If there were changes to any of the external libraries we track,
2322 # should bisect the changes there as well.
2323 external_depot = self._FindNextDepotToBisect(
2324 current_depot, min_revision_state, max_revision_state)
2325 # If there was no change in any of the external depots, the search
2326 # is over.
2327 if not external_depot:
2328 if current_depot == 'v8':
2329 self.warnings.append('Unfortunately, V8 bisection couldn\'t '
2330 'continue any further. The script can only bisect into '
2331 'V8\'s bleeding_edge repository if both the current and '
2332 'previous revisions in trunk map directly to revisions in '
2333 'bleeding_edge.')
2334 break
2336 earliest_revision = max_revision_state.external[external_depot]
2337 latest_revision = min_revision_state.external[external_depot]
2339 new_revision_list = self.PrepareToBisectOnDepot(
2340 external_depot, earliest_revision, latest_revision,
2341 previous_revision)
2343 if not new_revision_list:
2344 error = ('An error occurred attempting to retrieve revision '
2345 'range: [%s..%s]' % (earliest_revision, latest_revision))
2346 return BisectResults(error=error)
2348 revision_states = bisect_state.CreateRevisionStatesAfter(
2349 external_depot, new_revision_list, current_depot,
2350 previous_revision)
2352 # Reset the bisection and perform it on the newly inserted states.
2353 min_revision = 0
2354 max_revision = len(revision_states) - 1
2356 print ('Regression in metric %s appears to be the result of '
2357 'changes in [%s].' % (metric, external_depot))
2359 revision_list = [state.revision for state in revision_states]
2360 self.PrintRevisionsToBisectMessage(revision_list, external_depot)
2362 continue
2363 else:
2364 break
2365 else:
2366 next_revision_index = (int((max_revision - min_revision) / 2) +
2367 min_revision)
2369 next_revision_state = revision_states[next_revision_index]
2370 next_revision = next_revision_state.revision
2371 next_depot = next_revision_state.depot
2373 self.depot_registry.ChangeToDepotDir(next_depot)
2375 message = 'Working on [%s:%s]' % (next_depot, next_revision)
2376 print message
2377 if self.opts.output_buildbot_annotations:
2378 bisect_utils.OutputAnnotationStepStart(message)
2380 run_results = self.RunTest(next_revision, next_depot, command_to_run,
2381 metric, skippable=True)
2383 # If the build is successful, check whether or not the metric
2384 # had regressed.
2385 if not run_results[1]:
2386 if len(run_results) > 2:
2387 next_revision_state.external = run_results[2]
2388 next_revision_state.perf_time = run_results[3]
2389 next_revision_state.build_time = run_results[4]
2391 passed_regression = self._CheckIfRunPassed(run_results[0],
2392 known_good_value,
2393 known_bad_value)
2395 next_revision_state.passed = passed_regression
2396 next_revision_state.value = run_results[0]
2398 if passed_regression:
2399 max_revision = next_revision_index
2400 else:
2401 min_revision = next_revision_index
2402 else:
2403 if run_results[1] == BUILD_RESULT_SKIPPED:
2404 next_revision_state.passed = 'Skipped'
2405 elif run_results[1] == BUILD_RESULT_FAIL:
2406 next_revision_state.passed = 'Build Failed'
2408 print run_results[0]
2410 # If the build is broken, remove it and redo search.
2411 revision_states.pop(next_revision_index)
2413 max_revision -= 1
2415 if self.opts.output_buildbot_annotations:
2416 self.printer.PrintPartialResults(bisect_state)
2417 bisect_utils.OutputAnnotationStepClosed()
2420 self._ConfidenceExtraTestRuns(min_revision_state, max_revision_state,
2421 command_to_run, metric)
2422 results = BisectResults(bisect_state, self.depot_registry, self.opts,
2423 self.warnings)
2425 self._GatherResultsFromRevertedCulpritCL(
2426 results, target_depot, command_to_run, metric)
2428 return results
2429 else:
2430 # Weren't able to sync and retrieve the revision range.
2431 error = ('An error occurred attempting to retrieve revision range: '
2432 '[%s..%s]' % (good_revision, bad_revision))
2433 return BisectResults(error=error)
2435 def _ConfidenceExtraTestRuns(self, good_state, bad_state, command_to_run,
2436 metric):
2437 if (bool(good_state.passed) != bool(bad_state.passed)
2438 and good_state.passed not in ('Skipped', 'Build Failed')
2439 and bad_state.passed not in ('Skipped', 'Build Failed')):
2440 for state in (good_state, bad_state):
2441 run_results = self.RunTest(
2442 state.revision,
2443 state.depot,
2444 command_to_run,
2445 metric,
2446 test_run_multiplier=BORDER_REVISIONS_EXTRA_RUNS)
2447 # Is extend the right thing to do here?
2448 if run_results[1] != BUILD_RESULT_FAIL:
2449 state.value['values'].extend(run_results[0]['values'])
2450 else:
2451 warning_text = 'Re-test of revision %s failed with error message: %s'
2452 warning_text %= (state.revision, run_results[0])
2453 if warning_text not in self.warnings:
2454 self.warnings.append(warning_text)
2457 def _IsPlatformSupported():
2458 """Checks that this platform and build system are supported.
2460 Args:
2461 opts: The options parsed from the command line.
2463 Returns:
2464 True if the platform and build system are supported.
2466 # Haven't tested the script out on any other platforms yet.
2467 supported = ['posix', 'nt']
2468 return os.name in supported
2471 def RemoveBuildFiles(build_type):
2472 """Removes build files from previous runs."""
2473 out_dir = os.path.join('out', build_type)
2474 build_dir = os.path.join('build', build_type)
2475 logging.info('Removing build files in "%s" and "%s".',
2476 os.path.abspath(out_dir), os.path.abspath(build_dir))
2477 try:
2478 RemakeDirectoryTree(out_dir)
2479 RemakeDirectoryTree(build_dir)
2480 except Exception as e:
2481 raise RuntimeError('Got error in RemoveBuildFiles: %s' % e)
2484 def RemakeDirectoryTree(path_to_dir):
2485 """Removes a directory tree and replaces it with an empty one.
2487 Returns True if successful, False otherwise.
2489 RemoveDirectoryTree(path_to_dir)
2490 MaybeMakeDirectory(path_to_dir)
2493 def RemoveDirectoryTree(path_to_dir):
2494 """Removes a directory tree. Returns True if successful or False otherwise."""
2495 if os.path.isfile(path_to_dir):
2496 logging.info('REMOVING FILE %s' % path_to_dir)
2497 os.remove(path_to_dir)
2498 try:
2499 if os.path.exists(path_to_dir):
2500 shutil.rmtree(path_to_dir)
2501 except OSError, e:
2502 if e.errno != errno.ENOENT:
2503 raise
2506 # This is copied from build/scripts/common/chromium_utils.py.
2507 def MaybeMakeDirectory(*path):
2508 """Creates an entire path, if it doesn't already exist."""
2509 file_path = os.path.join(*path)
2510 try:
2511 os.makedirs(file_path)
2512 except OSError as e:
2513 if e.errno != errno.EEXIST:
2514 raise
2517 class BisectOptions(object):
2518 """Options to be used when running bisection."""
2519 def __init__(self):
2520 super(BisectOptions, self).__init__()
2522 self.target_platform = 'chromium'
2523 self.build_preference = None
2524 self.good_revision = None
2525 self.bad_revision = None
2526 self.use_goma = None
2527 self.goma_dir = None
2528 self.goma_threads = 64
2529 self.repeat_test_count = 20
2530 self.truncate_percent = 25
2531 self.max_time_minutes = 20
2532 self.metric = None
2533 self.command = None
2534 self.output_buildbot_annotations = None
2535 self.no_custom_deps = False
2536 self.working_directory = None
2537 self.extra_src = None
2538 self.debug_ignore_build = None
2539 self.debug_ignore_sync = None
2540 self.debug_ignore_perf_test = None
2541 self.debug_ignore_regression_confidence = None
2542 self.debug_fake_first_test_mean = 0
2543 self.target_arch = 'ia32'
2544 self.target_build_type = 'Release'
2545 self.builder_type = 'perf'
2546 self.bisect_mode = bisect_utils.BISECT_MODE_MEAN
2547 self.improvement_direction = 0
2548 self.bug_id = ''
2550 @staticmethod
2551 def _AddBisectOptionsGroup(parser):
2552 group = parser.add_argument_group('Bisect options')
2553 group.add_argument('-c', '--command', required=True,
2554 help='A command to execute your performance test at '
2555 'each point in the bisection.')
2556 group.add_argument('-b', '--bad_revision', required=True,
2557 help='A bad revision to start bisection. Must be later '
2558 'than good revision. May be either a git or svn '
2559 'revision.')
2560 group.add_argument('-g', '--good_revision', required=True,
2561 help='A revision to start bisection where performance '
2562 'test is known to pass. Must be earlier than the '
2563 'bad revision. May be either a git or a svn '
2564 'revision.')
2565 group.add_argument('-m', '--metric',
2566 help='The desired metric to bisect on. For example '
2567 '"vm_rss_final_b/vm_rss_f_b"')
2568 group.add_argument('-d', '--improvement_direction', type=int, default=0,
2569 help='An integer number representing the direction of '
2570 'improvement. 1 for higher is better, -1 for lower '
2571 'is better, 0 for ignore (default).')
2572 group.add_argument('-r', '--repeat_test_count', type=int, default=20,
2573 choices=range(1, 101),
2574 help='The number of times to repeat the performance '
2575 'test. Values will be clamped to range [1, 100]. '
2576 'Default value is 20.')
2577 group.add_argument('--max_time_minutes', type=int, default=20,
2578 choices=range(1, 61),
2579 help='The maximum time (in minutes) to take running the '
2580 'performance tests. The script will run the '
2581 'performance tests according to '
2582 '--repeat_test_count, so long as it doesn\'t exceed'
2583 ' --max_time_minutes. Values will be clamped to '
2584 'range [1, 60]. Default value is 20.')
2585 group.add_argument('-t', '--truncate_percent', type=int, default=25,
2586 help='The highest/lowest percent are discarded to form '
2587 'a truncated mean. Values will be clamped to range '
2588 '[0, 25]. Default value is 25 percent.')
2589 group.add_argument('--bisect_mode', default=bisect_utils.BISECT_MODE_MEAN,
2590 choices=[bisect_utils.BISECT_MODE_MEAN,
2591 bisect_utils.BISECT_MODE_STD_DEV,
2592 bisect_utils.BISECT_MODE_RETURN_CODE],
2593 help='The bisect mode. Choices are to bisect on the '
2594 'difference in mean, std_dev, or return_code.')
2595 group.add_argument('--bug_id', default='',
2596 help='The id for the bug associated with this bisect. ' +
2597 'If this number is given, bisect will attempt to ' +
2598 'verify that the bug is not closed before '
2599 'starting.')
2601 @staticmethod
2602 def _AddBuildOptionsGroup(parser):
2603 group = parser.add_argument_group('Build options')
2604 group.add_argument('-w', '--working_directory',
2605 help='Path to the working directory where the script '
2606 'will do an initial checkout of the chromium depot. The '
2607 'files will be placed in a subdirectory "bisect" under '
2608 'working_directory and that will be used to perform the '
2609 'bisection. This parameter is optional, if it is not '
2610 'supplied, the script will work from the current depot.')
2611 group.add_argument('--build_preference',
2612 choices=['msvs', 'ninja', 'make'],
2613 help='The preferred build system to use. On linux/mac '
2614 'the options are make/ninja. On Windows, the '
2615 'options are msvs/ninja.')
2616 group.add_argument('--target_platform', default='chromium',
2617 choices=['chromium', 'android', 'android-chrome'],
2618 help='The target platform. Choices are "chromium" '
2619 '(current platform), or "android". If you specify '
2620 'something other than "chromium", you must be '
2621 'properly set up to build that platform.')
2622 group.add_argument('--no_custom_deps', dest='no_custom_deps',
2623 action='store_true', default=False,
2624 help='Run the script with custom_deps or not.')
2625 group.add_argument('--extra_src',
2626 help='Path to a script which can be used to modify the '
2627 'bisect script\'s behavior.')
2628 group.add_argument('--use_goma', action='store_true',
2629 help='Add a bunch of extra threads for goma, and enable '
2630 'goma')
2631 group.add_argument('--goma_dir',
2632 help='Path to goma tools (or system default if not '
2633 'specified).')
2634 group.add_argument('--goma_threads', type=int, default='64',
2635 help='Number of threads for goma, only if using goma.')
2636 group.add_argument('--output_buildbot_annotations', action='store_true',
2637 help='Add extra annotation output for buildbot.')
2638 group.add_argument('--target_arch', default='ia32',
2639 dest='target_arch', choices=['ia32', 'x64', 'arm'],
2640 help='The target build architecture. Choices are "ia32" '
2641 '(default), "x64" or "arm".')
2642 group.add_argument('--target_build_type', default='Release',
2643 choices=['Release', 'Debug', 'Release_x64'],
2644 help='The target build type. Choices are "Release" '
2645 '(default), Release_x64 or "Debug".')
2646 group.add_argument('--builder_type', default=fetch_build.PERF_BUILDER,
2647 choices=[fetch_build.PERF_BUILDER,
2648 fetch_build.FULL_BUILDER,
2649 fetch_build.ANDROID_CHROME_PERF_BUILDER, ''],
2650 help='Type of builder to get build from. This '
2651 'determines both the bot that builds and the '
2652 'place where archived builds are downloaded from. '
2653 'For local builds, an empty string can be passed.')
2655 @staticmethod
2656 def _AddDebugOptionsGroup(parser):
2657 group = parser.add_argument_group('Debug options')
2658 group.add_argument('--debug_ignore_build', action='store_true',
2659 help='DEBUG: Don\'t perform builds.')
2660 group.add_argument('--debug_ignore_sync', action='store_true',
2661 help='DEBUG: Don\'t perform syncs.')
2662 group.add_argument('--debug_ignore_perf_test', action='store_true',
2663 help='DEBUG: Don\'t perform performance tests.')
2664 group.add_argument('--debug_ignore_regression_confidence',
2665 action='store_true',
2666 help='DEBUG: Don\'t score the confidence of the initial '
2667 'good and bad revisions\' test results.')
2668 group.add_argument('--debug_fake_first_test_mean', type=int, default='0',
2669 help='DEBUG: When faking performance tests, return this '
2670 'value as the mean of the first performance test, '
2671 'and return a mean of 0.0 for further tests.')
2672 return group
2674 @classmethod
2675 def _CreateCommandLineParser(cls):
2676 """Creates a parser with bisect options.
2678 Returns:
2679 An instance of argparse.ArgumentParser.
2681 usage = ('%(prog)s [options] [-- chromium-options]\n'
2682 'Perform binary search on revision history to find a minimal '
2683 'range of revisions where a performance metric regressed.\n')
2685 parser = argparse.ArgumentParser(usage=usage)
2686 cls._AddBisectOptionsGroup(parser)
2687 cls._AddBuildOptionsGroup(parser)
2688 cls._AddDebugOptionsGroup(parser)
2689 return parser
2691 def ParseCommandLine(self):
2692 """Parses the command line for bisect options."""
2693 parser = self._CreateCommandLineParser()
2694 opts = parser.parse_args()
2696 try:
2697 if (not opts.metric and
2698 opts.bisect_mode != bisect_utils.BISECT_MODE_RETURN_CODE):
2699 raise RuntimeError('missing required parameter: --metric')
2701 if opts.bisect_mode != bisect_utils.BISECT_MODE_RETURN_CODE:
2702 metric_values = opts.metric.split('/')
2703 if len(metric_values) != 2:
2704 raise RuntimeError('Invalid metric specified: [%s]' % opts.metric)
2705 opts.metric = metric_values
2707 opts.truncate_percent = min(max(opts.truncate_percent, 0), 25) / 100.0
2709 for k, v in opts.__dict__.iteritems():
2710 assert hasattr(self, k), 'Invalid %s attribute in BisectOptions.' % k
2711 setattr(self, k, v)
2712 except RuntimeError, e:
2713 output_string = StringIO.StringIO()
2714 parser.print_help(file=output_string)
2715 error_message = '%s\n\n%s' % (e.message, output_string.getvalue())
2716 output_string.close()
2717 raise RuntimeError(error_message)
2719 @staticmethod
2720 def FromDict(values):
2721 """Creates an instance of BisectOptions from a dictionary.
2723 Args:
2724 values: a dict containing options to set.
2726 Returns:
2727 An instance of BisectOptions.
2729 opts = BisectOptions()
2730 for k, v in values.iteritems():
2731 assert hasattr(opts, k), 'Invalid %s attribute in BisectOptions.' % k
2732 setattr(opts, k, v)
2734 if opts.metric and opts.bisect_mode != bisect_utils.BISECT_MODE_RETURN_CODE:
2735 metric_values = opts.metric.split('/')
2736 if len(metric_values) != 2:
2737 raise RuntimeError('Invalid metric specified: [%s]' % opts.metric)
2738 opts.metric = metric_values
2740 if opts.target_arch == 'x64' and opts.target_build_type == 'Release':
2741 opts.target_build_type = 'Release_x64'
2742 opts.repeat_test_count = min(max(opts.repeat_test_count, 1), 100)
2743 opts.max_time_minutes = min(max(opts.max_time_minutes, 1), 60)
2744 opts.truncate_percent = min(max(opts.truncate_percent, 0), 25)
2745 opts.truncate_percent = opts.truncate_percent / 100.0
2747 return opts
2750 def _ConfigureLogging():
2751 """Trivial logging config.
2753 Configures logging to output any messages at or above INFO to standard out,
2754 without any additional formatting.
2756 logging_format = '%(message)s'
2757 logging.basicConfig(
2758 stream=logging.sys.stdout, level=logging.INFO, format=logging_format)
2761 def main():
2762 _ConfigureLogging()
2763 try:
2764 opts = BisectOptions()
2765 opts.ParseCommandLine()
2767 if opts.bug_id:
2768 if opts.output_buildbot_annotations:
2769 bisect_utils.OutputAnnotationStepStart('Checking Issue Tracker')
2770 issue_closed = query_crbug.CheckIssueClosed(opts.bug_id)
2771 if issue_closed:
2772 print 'Aborting bisect because bug is closed'
2773 else:
2774 print 'Could not confirm bug is closed, proceeding.'
2775 if opts.output_buildbot_annotations:
2776 bisect_utils.OutputAnnotationStepClosed()
2777 if issue_closed:
2778 results = BisectResults(abort_reason='the bug is closed.')
2779 bisect_test = BisectPerformanceMetrics(opts, os.getcwd())
2780 bisect_test.printer.FormatAndPrintResults(results)
2781 return 0
2784 if opts.extra_src:
2785 extra_src = bisect_utils.LoadExtraSrc(opts.extra_src)
2786 if not extra_src:
2787 raise RuntimeError('Invalid or missing --extra_src.')
2788 bisect_utils.AddAdditionalDepotInfo(extra_src.GetAdditionalDepotInfo())
2790 if opts.working_directory:
2791 custom_deps = bisect_utils.DEFAULT_GCLIENT_CUSTOM_DEPS
2792 if opts.no_custom_deps:
2793 custom_deps = None
2794 bisect_utils.CreateBisectDirectoryAndSetupDepot(opts, custom_deps)
2796 os.chdir(os.path.join(os.getcwd(), 'src'))
2797 RemoveBuildFiles(opts.target_build_type)
2799 if not _IsPlatformSupported():
2800 raise RuntimeError('Sorry, this platform isn\'t supported yet.')
2802 if not source_control.IsInGitRepository():
2803 raise RuntimeError(
2804 'Sorry, only the git workflow is supported at the moment.')
2806 # gClient sync seems to fail if you're not in master branch.
2807 if (not source_control.IsInProperBranch() and
2808 not opts.debug_ignore_sync and
2809 not opts.working_directory):
2810 raise RuntimeError('You must switch to master branch to run bisection.')
2811 bisect_test = BisectPerformanceMetrics(opts, os.getcwd())
2812 try:
2813 results = bisect_test.Run(opts.command, opts.bad_revision,
2814 opts.good_revision, opts.metric)
2815 if results.error:
2816 raise RuntimeError(results.error)
2817 bisect_test.printer.FormatAndPrintResults(results)
2818 return 0
2819 finally:
2820 bisect_test.PerformCleanup()
2821 except RuntimeError as e:
2822 if opts.output_buildbot_annotations:
2823 # The perf dashboard scrapes the "results" step in order to comment on
2824 # bugs. If you change this, please update the perf dashboard as well.
2825 bisect_utils.OutputAnnotationStepStart('Results')
2826 print 'Runtime Error: %s' % e
2827 if opts.output_buildbot_annotations:
2828 bisect_utils.OutputAnnotationStepClosed()
2829 return 1
2832 if __name__ == '__main__':
2833 sys.exit(main())