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
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\
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.
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
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
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
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
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}
108 \tStandard error: {good_std_err}
109 \tSample size: {good_sample_size}
111 "Bad" revision: {bad_rev}
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'
128 ANDROID_CHROME_SVN_REPO_URL
= ('svn://svn.chromium.org/chrome-try-internal/'
132 class RunGitError(Exception):
135 return '%s\nError executing git command.' % self
.args
[0]
138 def GetSHA1HexDigest(contents
):
139 """Returns SHA1 hex digest of the given string."""
140 return hashlib
.sha1(contents
).hexdigest()
143 def WriteStringToFile(text
, file_name
):
144 """Writes text to a file, raising an RuntimeError on failure."""
146 with
open(file_name
, 'wb') as f
:
149 raise RuntimeError('Error writing to file [%s]' % file_name
)
152 def ReadStringFromFile(file_name
):
153 """Writes text to a file, raising an RuntimeError on failure."""
155 with
open(file_name
) as f
:
158 raise RuntimeError('Error reading file [%s]' % file_name
)
161 def ChangeBackslashToSlashInPatch(diff_text
):
162 """Formats file paths in the given patch text to Unix-style paths."""
165 diff_lines
= diff_text
.split('\n')
166 for i
in range(len(diff_lines
)):
168 if line
.startswith('--- ') or line
.startswith('+++ '):
169 diff_lines
[i
] = line
.replace('\\', '/')
170 return '\n'.join(diff_lines
)
173 def _ParseRevisionsFromDEPSFileManually(deps_file_contents
):
174 """Parses the vars section of the DEPS file using regular expressions.
177 deps_file_contents: The DEPS file contents as a string.
180 A dictionary in the format {depot: revision} if successful, otherwise None.
182 # We'll parse the "vars" section of the DEPS file.
183 rxp
= re
.compile('vars = {(?P<vars_body>[^}]+)', re
.MULTILINE
)
184 re_results
= rxp
.search(deps_file_contents
)
189 # We should be left with a series of entries in the vars component of
190 # the DEPS file with the following format:
191 # 'depot_name': 'revision',
192 vars_body
= re_results
.group('vars_body')
193 rxp
= re
.compile(r
"'(?P<depot_body>[\w_-]+)':[\s]+'(?P<rev_body>[\w@]+)'",
195 re_results
= rxp
.findall(vars_body
)
197 return dict(re_results
)
200 def _WaitUntilBuildIsReady(fetch_build_func
, builder_name
, build_request_id
,
201 max_timeout
, buildbot_server_url
):
202 """Waits until build is produced by bisect builder on try server.
205 fetch_build_func: Function to check and download build from cloud storage.
206 builder_name: Builder bot name on try server.
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.
209 buildbot_server_url: Buildbot url to check build status.
212 Downloaded archive file path if exists, otherwise None.
214 # Build number on the try server.
216 # Interval to check build on cloud storage.
218 # Interval to check build status on try server in seconds.
219 status_check_interval
= 600
220 last_status_check
= time
.time()
221 start_time
= time
.time()
224 # Checks for build on gs://chrome-perf and download if exists.
225 res
= fetch_build_func()
227 return (res
, 'Build successfully found')
228 elapsed_status_check
= time
.time() - last_status_check
229 # To avoid overloading try server with status check requests, we check
230 # build status for every 10 minutes.
231 if elapsed_status_check
> status_check_interval
:
232 last_status_check
= time
.time()
234 # Get the build number on try server for the current build.
235 build_num
= request_build
.GetBuildNumFromBuilder(
236 build_request_id
, builder_name
, buildbot_server_url
)
237 # Check the status of build using the build number.
238 # Note: Build is treated as PENDING if build number is not found
239 # on the the try server.
240 build_status
, status_link
= request_build
.GetBuildStatus(
241 build_num
, builder_name
, buildbot_server_url
)
242 if build_status
== request_build
.FAILED
:
243 return (None, 'Failed to produce build, log: %s' % status_link
)
244 elapsed_time
= time
.time() - start_time
245 if elapsed_time
> max_timeout
:
246 return (None, 'Timed out: %ss without build' % max_timeout
)
248 logging
.info('Time elapsed: %ss without build.', elapsed_time
)
249 time
.sleep(poll_interval
)
250 # For some reason, mac bisect bots were not flushing stdout periodically.
251 # As a result buildbot command is timed-out. Flush stdout on all platforms
252 # while waiting for build.
256 def _UpdateV8Branch(deps_content
):
257 """Updates V8 branch in DEPS file to process v8_bleeding_edge.
259 Check for "v8_branch" in DEPS file if exists update its value
260 with v8_bleeding_edge branch. Note: "v8_branch" is added to DEPS
261 variable from DEPS revision 254916, therefore check for "src/v8":
262 <v8 source path> in DEPS in order to support prior DEPS revisions
266 deps_content: DEPS file contents to be modified.
269 Modified DEPS file contents as a string.
271 new_branch
= r
'branches/bleeding_edge'
272 v8_branch_pattern
= re
.compile(r
'(?<="v8_branch": ")(.*)(?=")')
273 if re
.search(v8_branch_pattern
, deps_content
):
274 deps_content
= re
.sub(v8_branch_pattern
, new_branch
, deps_content
)
276 # Replaces the branch assigned to "src/v8" key in DEPS file.
277 # Format of "src/v8" in DEPS:
279 # (Var("googlecode_url") % "v8") + "/trunk@" + Var("v8_revision"),
280 # So, "/trunk@" is replace with "/branches/bleeding_edge@"
281 v8_src_pattern
= re
.compile(
282 r
'(?<="v8"\) \+ "/)(.*)(?=@" \+ Var\("v8_revision"\))', re
.MULTILINE
)
283 if re
.search(v8_src_pattern
, deps_content
):
284 deps_content
= re
.sub(v8_src_pattern
, new_branch
, deps_content
)
288 def _UpdateDEPSForAngle(revision
, depot
, deps_file
):
289 """Updates DEPS file with new revision for Angle repository.
291 This is a hack for Angle depot case because, in DEPS file "vars" dictionary
292 variable contains "angle_revision" key that holds git hash instead of
295 And sometimes "angle_revision" key is not specified in "vars" variable,
296 in such cases check "deps" dictionary variable that matches
297 angle.git@[a-fA-F0-9]{40}$ and replace git hash.
299 deps_var
= bisect_utils
.DEPOT_DEPS_NAME
[depot
]['deps_var']
301 deps_contents
= ReadStringFromFile(deps_file
)
302 # Check whether the depot and revision pattern in DEPS file vars variable
303 # e.g. "angle_revision": "fa63e947cb3eccf463648d21a05d5002c9b8adfa".
304 angle_rev_pattern
= re
.compile(r
'(?<="%s": ")([a-fA-F0-9]{40})(?=")' %
305 deps_var
, re
.MULTILINE
)
306 match
= re
.search(angle_rev_pattern
, deps_contents
)
308 # Update the revision information for the given depot
309 new_data
= re
.sub(angle_rev_pattern
, revision
, deps_contents
)
311 # Check whether the depot and revision pattern in DEPS file deps
313 # "src/third_party/angle": Var("chromium_git") +
314 # "/angle/angle.git@fa63e947cb3eccf463648d21a05d5002c9b8adfa",.
315 angle_rev_pattern
= re
.compile(
316 r
'(?<=angle\.git@)([a-fA-F0-9]{40})(?=")', re
.MULTILINE
)
317 match
= re
.search(angle_rev_pattern
, deps_contents
)
319 logging
.info('Could not find angle revision information in DEPS file.')
321 new_data
= re
.sub(angle_rev_pattern
, revision
, deps_contents
)
322 # Write changes to DEPS file
323 WriteStringToFile(new_data
, deps_file
)
326 logging
.warn('Something went wrong while updating DEPS file, %s', e
)
330 def _TryParseHistogramValuesFromOutput(metric
, text
):
331 """Attempts to parse a metric in the format HISTOGRAM <graph: <trace>.
334 metric: The metric as a list of [<trace>, <value>] strings.
335 text: The text to parse the metric values from.
338 A list of floating point numbers found, [] if none were found.
340 metric_formatted
= 'HISTOGRAM %s: %s= ' % (metric
[0], metric
[1])
342 text_lines
= text
.split('\n')
345 for current_line
in text_lines
:
346 if metric_formatted
in current_line
:
347 current_line
= current_line
[len(metric_formatted
):]
350 histogram_values
= eval(current_line
)
352 for b
in histogram_values
['buckets']:
353 average_for_bucket
= float(b
['high'] + b
['low']) * 0.5
354 # Extends the list with N-elements with the average for that bucket.
355 values_list
.extend([average_for_bucket
] * b
['count'])
362 def _TryParseResultValuesFromOutput(metric
, text
):
363 """Attempts to parse a metric in the format RESULT <graph>: <trace>= ...
366 metric: The metric as a list of [<trace>, <value>] string pairs.
367 text: The text to parse the metric values from.
370 A list of floating point numbers found.
372 # Format is: RESULT <graph>: <trace>= <value> <units>
373 metric_re
= re
.escape('RESULT %s: %s=' % (metric
[0], metric
[1]))
375 # The log will be parsed looking for format:
376 # <*>RESULT <graph_name>: <trace_name>= <value>
377 single_result_re
= re
.compile(
378 metric_re
+ r
'\s*(?P<VALUE>[-]?\d*(\.\d*)?)')
380 # The log will be parsed looking for format:
381 # <*>RESULT <graph_name>: <trace_name>= [<value>,value,value,...]
382 multi_results_re
= re
.compile(
383 metric_re
+ r
'\s*\[\s*(?P<VALUES>[-]?[\d\., ]+)\s*\]')
385 # The log will be parsed looking for format:
386 # <*>RESULT <graph_name>: <trace_name>= {<mean>, <std deviation>}
387 mean_stddev_re
= re
.compile(
389 r
'\s*\{\s*(?P<MEAN>[-]?\d*(\.\d*)?),\s*(?P<STDDEV>\d+(\.\d*)?)\s*\}')
391 text_lines
= text
.split('\n')
393 for current_line
in text_lines
:
394 # Parse the output from the performance test for the metric we're
396 single_result_match
= single_result_re
.search(current_line
)
397 multi_results_match
= multi_results_re
.search(current_line
)
398 mean_stddev_match
= mean_stddev_re
.search(current_line
)
399 if (not single_result_match
is None and
400 single_result_match
.group('VALUE')):
401 values_list
+= [single_result_match
.group('VALUE')]
402 elif (not multi_results_match
is None and
403 multi_results_match
.group('VALUES')):
404 metric_values
= multi_results_match
.group('VALUES')
405 values_list
+= metric_values
.split(',')
406 elif (not mean_stddev_match
is None and
407 mean_stddev_match
.group('MEAN')):
408 values_list
+= [mean_stddev_match
.group('MEAN')]
410 values_list
= [float(v
) for v
in values_list
411 if bisect_utils
.IsStringFloat(v
)]
416 def _ParseMetricValuesFromOutput(metric
, text
):
417 """Parses output from performance_ui_tests and retrieves the results for
421 metric: The metric as a list of [<trace>, <value>] strings.
422 text: The text to parse the metric values from.
425 A list of floating point numbers found.
427 metric_values
= _TryParseResultValuesFromOutput(metric
, text
)
429 if not metric_values
:
430 metric_values
= _TryParseHistogramValuesFromOutput(metric
, text
)
435 def _GenerateProfileIfNecessary(command_args
):
436 """Checks the command line of the performance test for dependencies on
437 profile generation, and runs tools/perf/generate_profile as necessary.
440 command_args: Command line being passed to performance test, as a list.
443 False if profile generation was necessary and failed, otherwise True.
445 if '--profile-dir' in ' '.join(command_args
):
446 # If we were using python 2.7+, we could just use the argparse
447 # module's parse_known_args to grab --profile-dir. Since some of the
448 # bots still run 2.6, have to grab the arguments manually.
450 args_to_parse
= ['--profile-dir', '--browser']
452 for arg_to_parse
in args_to_parse
:
453 for i
, current_arg
in enumerate(command_args
):
454 if arg_to_parse
in current_arg
:
455 current_arg_split
= current_arg
.split('=')
457 # Check 2 cases, --arg=<val> and --arg <val>
458 if len(current_arg_split
) == 2:
459 arg_dict
[arg_to_parse
] = current_arg_split
[1]
460 elif i
+ 1 < len(command_args
):
461 arg_dict
[arg_to_parse
] = command_args
[i
+1]
463 path_to_generate
= os
.path
.join('tools', 'perf', 'generate_profile')
465 if '--profile-dir' in arg_dict
and '--browser' in arg_dict
:
466 profile_path
, profile_type
= os
.path
.split(arg_dict
['--profile-dir'])
467 return not bisect_utils
.RunProcess(
469 'python', path_to_generate
,
470 '--profile-type-to-generate', profile_type
,
471 '--browser', arg_dict
['--browser'],
472 '--output-dir', profile_path
478 def _CheckRegressionConfidenceError(
483 """Checks whether we can be confident beyond a certain degree that the given
484 metrics represent a regression.
487 good_revision: string representing the commit considered 'good'
488 bad_revision: Same as above for 'bad'.
489 known_good_value: A dict with at least: 'values', 'mean' and 'std_err'
490 known_bad_value: Same as above.
493 False if there is no error (i.e. we can be confident there's a regression),
494 a string containing the details of the lack of confidence otherwise.
497 # Adding good and bad values to a parameter list.
498 confidence_params
= []
499 for l
in [known_bad_value
['values'], known_good_value
['values']]:
500 # Flatten if needed, by averaging the values in each nested list
501 if isinstance(l
, list) and all([isinstance(x
, list) for x
in l
]):
502 averages
= map(math_utils
.Mean
, l
)
503 confidence_params
.append(averages
)
505 confidence_params
.append(l
)
506 regression_confidence
= BisectResults
.ConfidenceScore(*confidence_params
)
507 if regression_confidence
< REGRESSION_CONFIDENCE
:
508 error
= REGRESSION_CONFIDENCE_ERROR_TEMPLATE
.format(
509 good_rev
=good_revision
,
510 good_mean
=known_good_value
['mean'],
511 good_std_err
=known_good_value
['std_err'],
512 good_sample_size
=len(known_good_value
['values']),
513 bad_rev
=bad_revision
,
514 bad_mean
=known_bad_value
['mean'],
515 bad_std_err
=known_bad_value
['std_err'],
516 bad_sample_size
=len(known_bad_value
['values']))
520 class DepotDirectoryRegistry(object):
522 def __init__(self
, src_cwd
):
524 for depot
in bisect_utils
.DEPOT_NAMES
:
525 # The working directory of each depot is just the path to the depot, but
526 # since we're already in 'src', we can skip that part.
527 path_in_src
= bisect_utils
.DEPOT_DEPS_NAME
[depot
]['src'][4:]
528 self
.SetDepotDir(depot
, os
.path
.join(src_cwd
, path_in_src
))
530 self
.SetDepotDir('chromium', src_cwd
)
532 def SetDepotDir(self
, depot_name
, depot_dir
):
533 self
.depot_cwd
[depot_name
] = depot_dir
535 def GetDepotDir(self
, depot_name
):
536 if depot_name
in self
.depot_cwd
:
537 return self
.depot_cwd
[depot_name
]
539 assert False, ('Unknown depot [ %s ] encountered. Possibly a new one '
540 'was added without proper support?' % depot_name
)
542 def ChangeToDepotDir(self
, depot_name
):
543 """Given a depot, changes to the appropriate working directory.
546 depot_name: The name of the depot (see DEPOT_NAMES).
548 os
.chdir(self
.GetDepotDir(depot_name
))
551 def _PrepareBisectBranch(parent_branch
, new_branch
):
552 """Creates a new branch to submit bisect try job.
555 parent_branch: Parent branch to be used to create new branch.
556 new_branch: New branch name.
558 current_branch
, returncode
= bisect_utils
.RunGit(
559 ['rev-parse', '--abbrev-ref', 'HEAD'])
561 raise RunGitError('Must be in a git repository to send changes to trybots.')
563 current_branch
= current_branch
.strip()
564 # Make sure current branch is master.
565 if current_branch
!= parent_branch
:
566 output
, returncode
= bisect_utils
.RunGit(['checkout', '-f', parent_branch
])
568 raise RunGitError('Failed to checkout branch: %s.' % output
)
570 # Delete new branch if exists.
571 output
, returncode
= bisect_utils
.RunGit(['branch', '--list'])
572 if new_branch
in output
:
573 output
, returncode
= bisect_utils
.RunGit(['branch', '-D', new_branch
])
575 raise RunGitError('Deleting branch failed, %s', output
)
577 # Check if the tree is dirty: make sure the index is up to date and then
579 bisect_utils
.RunGit(['update-index', '--refresh', '-q'])
580 output
, returncode
= bisect_utils
.RunGit(['diff-index', 'HEAD'])
582 raise RunGitError('Cannot send a try job with a dirty tree.')
584 # Create and check out the telemetry-tryjob branch, and edit the configs
585 # for the try job there.
586 output
, returncode
= bisect_utils
.RunGit(['checkout', '-b', new_branch
])
588 raise RunGitError('Failed to checkout branch: %s.' % output
)
590 output
, returncode
= bisect_utils
.RunGit(
591 ['branch', '--set-upstream-to', parent_branch
])
593 raise RunGitError('Error in git branch --set-upstream-to')
596 def _StartBuilderTryJob(
597 builder_type
, git_revision
, builder_name
, job_name
, patch
=None):
598 """Attempts to run a try job from the current directory.
601 builder_type: One of the builder types in fetch_build, e.g. "perf".
602 git_revision: A git commit hash.
603 builder_name: Name of the bisect bot to be used for try job.
604 bisect_job_name: Try job name, used to identify which bisect
605 job was responsible for requesting a build.
606 patch: A DEPS patch (used while bisecting dependency repositories),
607 or None if we're bisecting the top-level repository.
609 # TODO(prasadv, qyearsley): Make this a method of BuildArchive
610 # (which may be renamed to BuilderTryBot or Builder).
612 # Temporary branch for running a try job.
613 _PrepareBisectBranch(BISECT_MASTER_BRANCH
, BISECT_TRYJOB_BRANCH
)
614 patch_content
= '/dev/null'
615 # Create a temporary patch file.
617 WriteStringToFile(patch
, BISECT_PATCH_FILE
)
618 patch_content
= BISECT_PATCH_FILE
622 '--bot=%s' % builder_name
,
623 '--revision=%s' % git_revision
,
624 '--name=%s' % job_name
,
625 '--svn_repo=%s' % _TryJobSvnRepo(builder_type
),
626 '--diff=%s' % patch_content
,
628 # Execute try job to build revision.
630 output
, return_code
= bisect_utils
.RunGit(try_command
)
632 command_string
= ' '.join(['git'] + try_command
)
634 raise RunGitError('Could not execute try job: %s.\n'
635 'Error: %s' % (command_string
, output
))
636 logging
.info('Try job successfully submitted.\n TryJob Details: %s\n%s',
637 command_string
, output
)
639 # Delete patch file if exists.
641 os
.remove(BISECT_PATCH_FILE
)
643 if e
.errno
!= errno
.ENOENT
:
645 # Checkout master branch and delete bisect-tryjob branch.
646 bisect_utils
.RunGit(['checkout', '-f', BISECT_MASTER_BRANCH
])
647 bisect_utils
.RunGit(['branch', '-D', BISECT_TRYJOB_BRANCH
])
650 def _TryJobSvnRepo(builder_type
):
651 """Returns an SVN repo to use for try jobs based on the builder type."""
652 if builder_type
== fetch_build
.PERF_BUILDER
:
653 return PERF_SVN_REPO_URL
654 if builder_type
== fetch_build
.FULL_BUILDER
:
655 return FULL_SVN_REPO_URL
656 if builder_type
== fetch_build
.ANDROID_CHROME_PERF_BUILDER
:
657 return ANDROID_CHROME_SVN_REPO_URL
658 raise NotImplementedError('Unknown builder type "%s".' % builder_type
)
661 class BisectPerformanceMetrics(object):
662 """This class contains functionality to perform a bisection of a range of
663 revisions to narrow down where performance regressions may have occurred.
665 The main entry-point is the Run method.
668 def __init__(self
, opts
, src_cwd
):
669 """Constructs a BisectPerformancesMetrics object.
672 opts: BisectOptions object containing parsed options.
673 src_cwd: Root src/ directory of the test repository (inside bisect/ dir).
675 super(BisectPerformanceMetrics
, self
).__init
__()
678 self
.src_cwd
= src_cwd
679 self
.depot_registry
= DepotDirectoryRegistry(self
.src_cwd
)
680 self
.printer
= BisectPrinter(self
.opts
, self
.depot_registry
)
681 self
.cleanup_commands
= []
683 self
.builder
= builder
.Builder
.FromOpts(opts
)
685 def PerformCleanup(self
):
686 """Performs cleanup when script is finished."""
687 os
.chdir(self
.src_cwd
)
688 for c
in self
.cleanup_commands
:
690 shutil
.move(c
[1], c
[2])
692 assert False, 'Invalid cleanup command.'
694 def GetRevisionList(self
, depot
, bad_revision
, good_revision
):
695 """Retrieves a list of all the commits between the bad revision and
696 last known good revision."""
698 cwd
= self
.depot_registry
.GetDepotDir(depot
)
699 return source_control
.GetRevisionList(bad_revision
, good_revision
, cwd
=cwd
)
701 def _ParseRevisionsFromDEPSFile(self
, depot
):
702 """Parses the local DEPS file to determine blink/skia/v8 revisions which may
703 be needed if the bisect recurses into those depots later.
706 depot: Name of depot being bisected.
709 A dict in the format {depot:revision} if successful, otherwise None.
713 'Var': lambda _
: deps_data
["vars"][_
],
714 'From': lambda *args
: None,
717 deps_file
= bisect_utils
.FILE_DEPS_GIT
718 if not os
.path
.exists(deps_file
):
719 deps_file
= bisect_utils
.FILE_DEPS
720 execfile(deps_file
, {}, deps_data
)
721 deps_data
= deps_data
['deps']
723 rxp
= re
.compile(".git@(?P<revision>[a-fA-F0-9]+)")
725 for depot_name
, depot_data
in bisect_utils
.DEPOT_DEPS_NAME
.iteritems():
726 if (depot_data
.get('platform') and
727 depot_data
.get('platform') != os
.name
):
730 if depot_data
.get('recurse') and depot
in depot_data
.get('from'):
731 depot_data_src
= depot_data
.get('src') or depot_data
.get('src_old')
732 src_dir
= deps_data
.get(depot_data_src
)
734 self
.depot_registry
.SetDepotDir(depot_name
, os
.path
.join(
735 self
.src_cwd
, depot_data_src
[4:]))
736 re_results
= rxp
.search(src_dir
)
738 results
[depot_name
] = re_results
.group('revision')
740 warning_text
= ('Could not parse revision for %s while bisecting '
741 '%s' % (depot_name
, depot
))
742 if not warning_text
in self
.warnings
:
743 self
.warnings
.append(warning_text
)
745 results
[depot_name
] = None
748 deps_file_contents
= ReadStringFromFile(deps_file
)
749 parse_results
= _ParseRevisionsFromDEPSFileManually(deps_file_contents
)
751 for depot_name
, depot_revision
in parse_results
.iteritems():
752 depot_revision
= depot_revision
.strip('@')
753 logging
.warn(depot_name
, depot_revision
)
754 for cur_name
, cur_data
in bisect_utils
.DEPOT_DEPS_NAME
.iteritems():
755 if cur_data
.get('deps_var') == depot_name
:
757 results
[src_name
] = depot_revision
761 def _Get3rdPartyRevisions(self
, depot
):
762 """Parses the DEPS file to determine WebKit/v8/etc... versions.
765 depot: A depot name. Should be in the DEPOT_NAMES list.
768 A dict in the format {depot: revision} if successful, otherwise None.
771 self
.depot_registry
.ChangeToDepotDir(depot
)
775 if depot
== 'chromium' or depot
== 'android-chrome':
776 results
= self
._ParseRevisionsFromDEPSFile
(depot
)
780 # We can't try to map the trunk revision to bleeding edge yet, because
781 # we don't know which direction to try to search in. Have to wait until
782 # the bisect has narrowed the results down to 2 v8 rolls.
783 results
['v8_bleeding_edge'] = None
787 def BackupOrRestoreOutputDirectory(self
, restore
=False, build_type
='Release'):
788 """Backs up or restores build output directory based on restore argument.
791 restore: Indicates whether to restore or backup. Default is False(Backup)
792 build_type: Target build type ('Release', 'Debug', 'Release_x64' etc.)
795 Path to backup or restored location as string. otherwise None if it fails.
797 build_dir
= os
.path
.abspath(
798 builder
.GetBuildOutputDirectory(self
.opts
, self
.src_cwd
))
799 source_dir
= os
.path
.join(build_dir
, build_type
)
800 destination_dir
= os
.path
.join(build_dir
, '%s.bak' % build_type
)
802 source_dir
, destination_dir
= destination_dir
, source_dir
803 if os
.path
.exists(source_dir
):
804 RemoveDirectoryTree(destination_dir
)
805 shutil
.move(source_dir
, destination_dir
)
806 return destination_dir
809 def _DownloadAndUnzipBuild(self
, revision
, depot
, build_type
='Release',
811 """Downloads the build archive for the given revision.
814 revision: The git revision to download.
815 depot: The name of a dependency repository. Should be in DEPOT_NAMES.
816 build_type: Target build type, e.g. Release', 'Debug', 'Release_x64' etc.
817 create_patch: Create a patch with any locally modified files.
820 True if download succeeds, otherwise False.
824 if depot
not in ('chromium', 'android-chrome'):
825 # Create a DEPS patch with new revision for dependency repository.
826 self
._CreateDEPSPatch
(depot
, revision
)
830 revision
, patch
= self
._CreatePatch
(revision
)
833 # Get the SHA of the DEPS changes patch.
834 patch_sha
= GetSHA1HexDigest(patch
)
836 # Update the DEPS changes patch with a patch to create a new file named
837 # 'DEPS.sha' and add patch_sha evaluated above to it.
838 patch
= '%s\n%s' % (patch
, DEPS_SHA_PATCH
% {'deps_sha': patch_sha
})
840 build_dir
= builder
.GetBuildOutputDirectory(self
.opts
, self
.src_cwd
)
841 downloaded_file
= self
._WaitForBuildDownload
(
842 revision
, build_dir
, deps_patch
=patch
, deps_patch_sha
=patch_sha
)
843 if not downloaded_file
:
845 return self
._UnzipAndMoveBuildProducts
(downloaded_file
, build_dir
,
846 build_type
=build_type
)
848 def _WaitForBuildDownload(self
, revision
, build_dir
, deps_patch
=None,
849 deps_patch_sha
=None):
850 """Tries to download a zip archive for a build.
852 This involves seeing whether the archive is already available, and if not,
853 then requesting a build and waiting before downloading.
856 revision: A git commit hash.
857 build_dir: The directory to download the build into.
858 deps_patch: A patch which changes a dependency repository revision in
859 the DEPS, if applicable.
860 deps_patch_sha: The SHA1 hex digest of the above patch.
863 File path of the downloaded file if successful, otherwise None.
865 bucket_name
, remote_path
= fetch_build
.GetBucketAndRemotePath(
866 revision
, builder_type
=self
.opts
.builder_type
,
867 target_arch
=self
.opts
.target_arch
,
868 target_platform
=self
.opts
.target_platform
,
869 deps_patch_sha
=deps_patch_sha
,
870 extra_src
=self
.opts
.extra_src
)
871 output_dir
= os
.path
.abspath(build_dir
)
872 fetch_build_func
= lambda: fetch_build
.FetchFromCloudStorage(
873 bucket_name
, remote_path
, output_dir
)
875 is_available
= fetch_build
.BuildIsAvailable(bucket_name
, remote_path
)
877 return fetch_build_func()
879 # When build archive doesn't exist, make a request and wait.
880 return self
._RequestBuildAndWait
(
881 revision
, fetch_build_func
, deps_patch
=deps_patch
)
883 def _RequestBuildAndWait(self
, git_revision
, fetch_build_func
,
885 """Triggers a try job for a build job.
887 This function prepares and starts a try job for a builder, and waits for
888 the archive to be produced and archived. Once the build is ready it is
891 For performance tests, builders on the tryserver.chromium.perf are used.
893 TODO(qyearsley): Make this function take "builder_type" as a parameter
894 and make requests to different bot names based on that parameter.
897 git_revision: A git commit hash.
898 fetch_build_func: Function to check and download build from cloud storage.
899 deps_patch: DEPS patch string, used when bisecting dependency repos.
902 Downloaded archive file path when requested build exists and download is
903 successful, otherwise None.
905 if not fetch_build_func
:
908 # Create a unique ID for each build request posted to try server builders.
909 # This ID is added to "Reason" property of the build.
910 build_request_id
= GetSHA1HexDigest(
911 '%s-%s-%s' % (git_revision
, deps_patch
, time
.time()))
913 # Revert any changes to DEPS file.
914 bisect_utils
.CheckRunGit(['reset', '--hard', 'HEAD'], cwd
=self
.src_cwd
)
916 builder_name
, build_timeout
= fetch_build
.GetBuilderNameAndBuildTime(
917 builder_type
=self
.opts
.builder_type
,
918 target_arch
=self
.opts
.target_arch
,
919 target_platform
=self
.opts
.target_platform
,
920 extra_src
=self
.opts
.extra_src
)
923 _StartBuilderTryJob(self
.opts
.builder_type
, git_revision
, builder_name
,
924 job_name
=build_request_id
, patch
=deps_patch
)
925 except RunGitError
as e
:
926 logging
.warn('Failed to post builder try job for revision: [%s].\n'
927 'Error: %s', git_revision
, e
)
930 # Get the buildbot master URL to monitor build status.
931 buildbot_server_url
= fetch_build
.GetBuildBotUrl(
932 builder_type
=self
.opts
.builder_type
,
933 target_arch
=self
.opts
.target_arch
,
934 target_platform
=self
.opts
.target_platform
,
935 extra_src
=self
.opts
.extra_src
)
937 archive_filename
, error_msg
= _WaitUntilBuildIsReady(
938 fetch_build_func
, builder_name
, build_request_id
, build_timeout
,
940 if not archive_filename
:
941 logging
.warn('%s [revision: %s]', error_msg
, git_revision
)
942 return archive_filename
944 def _UnzipAndMoveBuildProducts(self
, downloaded_file
, build_dir
,
945 build_type
='Release'):
946 """Unzips the build archive and moves it to the build output directory.
948 The build output directory is wherever the binaries are expected to
949 be in order to start Chrome and run tests.
951 TODO: Simplify and clarify this method if possible.
954 downloaded_file: File path of the downloaded zip file.
955 build_dir: Directory where the the zip file was downloaded to.
956 build_type: "Release" or "Debug".
959 True if successful, False otherwise.
961 abs_build_dir
= os
.path
.abspath(build_dir
)
962 output_dir
= os
.path
.join(abs_build_dir
, self
.GetZipFileBuildDirName())
963 logging
.info('EXPERIMENTAL RUN, _UnzipAndMoveBuildProducts locals %s',
967 RemoveDirectoryTree(output_dir
)
968 self
.BackupOrRestoreOutputDirectory(restore
=False)
969 # Build output directory based on target(e.g. out/Release, out/Debug).
970 target_build_output_dir
= os
.path
.join(abs_build_dir
, build_type
)
972 logging
.info('Extracting "%s" to "%s"', downloaded_file
, abs_build_dir
)
973 fetch_build
.Unzip(downloaded_file
, abs_build_dir
)
975 if not os
.path
.exists(output_dir
):
976 # Due to recipe changes, the builds extract folder contains
977 # out/Release instead of full-build-<platform>/Release.
978 if os
.path
.exists(os
.path
.join(abs_build_dir
, 'out', build_type
)):
979 output_dir
= os
.path
.join(abs_build_dir
, 'out', build_type
)
981 raise IOError('Missing extracted folder %s ' % output_dir
)
983 logging
.info('Moving build from %s to %s',
984 output_dir
, target_build_output_dir
)
985 shutil
.move(output_dir
, target_build_output_dir
)
987 except Exception as e
:
988 logging
.info('Something went wrong while extracting archive file: %s', e
)
989 self
.BackupOrRestoreOutputDirectory(restore
=True)
990 # Cleanup any leftovers from unzipping.
991 if os
.path
.exists(output_dir
):
992 RemoveDirectoryTree(output_dir
)
994 # Delete downloaded archive
995 if os
.path
.exists(downloaded_file
):
996 os
.remove(downloaded_file
)
1000 def GetZipFileBuildDirName():
1001 """Gets the base file name of the zip file.
1003 After extracting the zip file, this is the name of the directory where
1004 the build files are expected to be. Possibly.
1006 TODO: Make sure that this returns the actual directory name where the
1007 Release or Debug directory is inside of the zip files. This probably
1008 depends on the builder recipe, and may depend on whether the builder is
1009 a perf builder or full builder.
1012 The name of the directory inside a build archive which is expected to
1013 contain a Release or Debug directory.
1015 if bisect_utils
.IsWindowsHost():
1016 return 'full-build-win32'
1017 if bisect_utils
.IsLinuxHost():
1018 return 'full-build-linux'
1019 if bisect_utils
.IsMacHost():
1020 return 'full-build-mac'
1021 raise NotImplementedError('Unknown platform "%s".' % sys
.platform
)
1023 def IsDownloadable(self
, depot
):
1024 """Checks if build can be downloaded based on target platform and depot."""
1025 if (self
.opts
.target_platform
in ['chromium', 'android', 'android-chrome']
1026 and self
.opts
.builder_type
):
1027 # In case of android-chrome platform, download archives only for
1028 # android-chrome depot; for other depots such as chromium, v8, skia
1029 # etc., build the binary locally.
1030 if self
.opts
.target_platform
== 'android-chrome':
1031 return depot
== 'android-chrome'
1033 return (depot
== 'chromium' or
1034 'chromium' in bisect_utils
.DEPOT_DEPS_NAME
[depot
]['from'] or
1035 'v8' in bisect_utils
.DEPOT_DEPS_NAME
[depot
]['from'])
1038 def UpdateDepsContents(self
, deps_contents
, depot
, git_revision
, deps_key
):
1039 """Returns modified version of DEPS file contents.
1042 deps_contents: DEPS file content.
1043 depot: Current depot being bisected.
1044 git_revision: A git hash to be updated in DEPS.
1045 deps_key: Key in vars section of DEPS file to be searched.
1048 Updated DEPS content as string if deps key is found, otherwise None.
1050 # Check whether the depot and revision pattern in DEPS file vars
1051 # e.g. for webkit the format is "webkit_revision": "12345".
1052 deps_revision
= re
.compile(r
'(?<="%s": ")([0-9]+)(?=")' % deps_key
,
1055 if re
.search(deps_revision
, deps_contents
):
1056 commit_position
= source_control
.GetCommitPosition(
1057 git_revision
, self
.depot_registry
.GetDepotDir(depot
))
1058 if not commit_position
:
1059 logging
.warn('Could not determine commit position for %s', git_revision
)
1061 # Update the revision information for the given depot
1062 new_data
= re
.sub(deps_revision
, str(commit_position
), deps_contents
)
1064 # Check whether the depot and revision pattern in DEPS file vars
1065 # e.g. for webkit the format is "webkit_revision": "559a6d4ab7a84c539..".
1066 deps_revision
= re
.compile(
1067 r
'(?<=["\']%s["\']: ["\'])([a
-fA
-F0
-9]{40}
)(?
=["\'])' % deps_key,
1069 if re.search(deps_revision, deps_contents):
1070 new_data = re.sub(deps_revision, git_revision, deps_contents)
1072 # For v8_bleeding_edge revisions change V8 branch in order
1073 # to fetch bleeding edge revision.
1074 if depot == 'v8_bleeding_edge':
1075 new_data = _UpdateV8Branch(new_data)
1080 def UpdateDeps(self, revision, depot, deps_file):
1081 """Updates DEPS file with new revision of dependency repository.
1083 This method search DEPS for a particular pattern in which depot revision
1084 is specified (e.g "webkit_revision
": "123456"). If a match is found then
1085 it resolves the given git hash to SVN revision and replace it in DEPS file.
1088 revision: A git hash revision of the dependency repository.
1089 depot: Current depot being bisected.
1090 deps_file: Path to DEPS file.
1093 True if DEPS file is modified successfully, otherwise False.
1095 if not os.path.exists(deps_file):
1098 deps_var = bisect_utils.DEPOT_DEPS_NAME[depot]['deps_var']
1099 # Don't update DEPS file if deps_var is not set in DEPOT_DEPS_NAME.
1101 logging.warn('DEPS update not supported for Depot: %s', depot)
1104 # Hack for Angle repository. In the DEPS file, "vars" dictionary variable
1105 # contains "angle_revision
" key that holds git hash instead of SVN revision.
1106 # And sometime "angle_revision
" key is not specified in "vars" variable.
1107 # In such cases check, "deps
" dictionary variable that matches
1108 # angle.git@[a-fA-F0-9]{40}$ and replace git hash.
1109 if depot == 'angle':
1110 return _UpdateDEPSForAngle(revision, depot, deps_file)
1113 deps_contents = ReadStringFromFile(deps_file)
1114 updated_deps_content = self.UpdateDepsContents(
1115 deps_contents, depot, revision, deps_var)
1116 # Write changes to DEPS file
1117 if updated_deps_content:
1118 WriteStringToFile(updated_deps_content, deps_file)
1121 logging.warn('Something went wrong while updating DEPS file. [%s]', e)
1124 def _CreateDEPSPatch(self, depot, revision):
1125 """Checks out the DEPS file at the specified revision and modifies it.
1128 depot: Current depot being bisected.
1129 revision: A git hash revision of the dependency repository.
1131 deps_file_path = os.path.join(self.src_cwd, bisect_utils.FILE_DEPS)
1132 if not os.path.exists(deps_file_path):
1133 raise RuntimeError('DEPS file does not exists.[%s]' % deps_file_path)
1134 # Get current chromium revision (git hash).
1135 cmd = ['rev-parse', 'HEAD']
1136 chromium_sha = bisect_utils.CheckRunGit(cmd).strip()
1137 if not chromium_sha:
1138 raise RuntimeError('Failed to determine Chromium revision for %s' %
1140 if ('chromium' in bisect_utils.DEPOT_DEPS_NAME[depot]['from'] or
1141 'v8' in bisect_utils.DEPOT_DEPS_NAME[depot]['from']):
1142 # Checkout DEPS file for the current chromium revision.
1143 if not source_control.CheckoutFileAtRevision(
1144 bisect_utils.FILE_DEPS, chromium_sha, cwd=self.src_cwd):
1146 'DEPS checkout Failed for chromium revision : [%s]' % chromium_sha)
1148 if not self.UpdateDeps(revision, depot, deps_file_path):
1150 'Failed to update DEPS file for chromium: [%s]' % chromium_sha)
1152 def _CreatePatch(self, revision):
1153 """Creates a patch from currently modified files.
1156 depot: Current depot being bisected.
1157 revision: A git hash revision of the dependency repository.
1160 A tuple with git hash of chromium revision and DEPS patch text.
1162 # Get current chromium revision (git hash).
1163 chromium_sha = bisect_utils.CheckRunGit(['rev-parse', 'HEAD']).strip()
1164 if not chromium_sha:
1165 raise RuntimeError('Failed to determine Chromium revision for %s' %
1167 # Checkout DEPS file for the current chromium revision.
1175 diff_text = bisect_utils.CheckRunGit(diff_command)
1176 return (chromium_sha, ChangeBackslashToSlashInPatch(diff_text))
1179 self, depot, revision=None, create_patch=False):
1180 """Obtains a build by either downloading or building directly.
1183 depot: Dependency repository name.
1184 revision: A git commit hash. If None is given, the currently checked-out
1186 create_patch: Create a patch with any locally modified files.
1191 if self.opts.debug_ignore_build:
1194 build_success = False
1196 os.chdir(self.src_cwd)
1197 # Fetch build archive for the given revision from the cloud storage when
1198 # the storage bucket is passed.
1199 if self.IsDownloadable(depot) and revision:
1200 build_success = self._DownloadAndUnzipBuild(
1201 revision, depot, build_type='Release', create_patch=create_patch)
1203 # Print the current environment set on the machine.
1204 print 'Full Environment:'
1205 for key, value in sorted(os.environ.items()):
1206 print '%s: %s' % (key, value)
1207 # Print the environment before proceeding with compile.
1209 build_success = self.builder.Build(depot, self.opts)
1211 return build_success
1213 def RunGClientHooks(self):
1214 """Runs gclient with runhooks command.
1217 True if gclient reports no errors.
1219 if self.opts.debug_ignore_build:
1221 return not bisect_utils.RunGClient(['runhooks'], cwd=self.src_cwd)
1223 def _IsBisectModeUsingMetric(self):
1224 return self.opts.bisect_mode in [bisect_utils.BISECT_MODE_MEAN,
1225 bisect_utils.BISECT_MODE_STD_DEV]
1227 def _IsBisectModeReturnCode(self):
1228 return self.opts.bisect_mode in [bisect_utils.BISECT_MODE_RETURN_CODE]
1230 def _IsBisectModeStandardDeviation(self):
1231 return self.opts.bisect_mode in [bisect_utils.BISECT_MODE_STD_DEV]
1233 def GetCompatibleCommand(self, command_to_run, revision, depot):
1234 """Return a possibly modified test command depending on the revision.
1236 Prior to crrev.com/274857 *only* android-chromium-testshell
1237 Then until crrev.com/276628 *both* (android-chromium-testshell and
1238 android-chrome-shell) work. After that rev 276628 *only*
1239 android-chrome-shell works. The bisect_perf_regression.py script should
1240 handle these cases and set appropriate browser type based on revision.
1242 if self.opts.target_platform in ['android']:
1243 # When its a third_party depot, get the chromium revision.
1244 if depot != 'chromium':
1245 revision = bisect_utils.CheckRunGit(
1246 ['rev-parse', 'HEAD'], cwd=self.src_cwd).strip()
1247 commit_position = source_control.GetCommitPosition(revision,
1249 if not commit_position:
1250 return command_to_run
1251 cmd_re = re.compile(r'--browser=(?P<browser_type>\S+)')
1252 matches = cmd_re.search(command_to_run)
1253 if bisect_utils.IsStringInt(commit_position) and matches:
1254 cmd_browser = matches.group('browser_type')
1255 if commit_position <= 274857 and cmd_browser == 'android-chrome-shell':
1256 return command_to_run.replace(cmd_browser,
1257 'android-chromium-testshell')
1258 elif (commit_position >= 276628 and
1259 cmd_browser == 'android-chromium-testshell'):
1260 return command_to_run.replace(cmd_browser,
1261 'android-chrome-shell')
1262 return command_to_run
1264 def RunPerformanceTestAndParseResults(
1265 self, command_to_run, metric, reset_on_first_run=False,
1266 upload_on_last_run=False, results_label=None, test_run_multiplier=1,
1268 """Runs a performance test on the current revision and parses the results.
1271 command_to_run: The command to be run to execute the performance test.
1272 metric: The metric to parse out from the results of the performance test.
1273 This is the result chart name and trace name, separated by slash.
1274 May be None for perf try jobs.
1275 reset_on_first_run: If True, pass the flag --reset-results on first run.
1276 upload_on_last_run: If True, pass the flag --upload-results on last run.
1277 results_label: A value for the option flag --results-label.
1278 The arguments reset_on_first_run, upload_on_last_run and results_label
1279 are all ignored if the test is not a Telemetry test.
1280 test_run_multiplier: Factor by which to multiply the number of test runs
1281 and the timeout period specified in self.opts.
1282 allow_flakes: Report success even if some tests fail to run.
1285 (values dict, 0) if --debug_ignore_perf_test was passed.
1286 (values dict, 0, test output) if the test was run successfully.
1287 (error message, -1) if the test couldn't be run.
1288 (error message, -1, test output) if the test ran but there was an error.
1290 success_code, failure_code = 0, -1
1292 if self.opts.debug_ignore_perf_test:
1300 # When debug_fake_test_mean is set, its value is returned as the mean
1301 # and the flag is cleared so that further calls behave as if it wasn't
1302 # set (returning the fake_results dict as defined above).
1303 if self.opts.debug_fake_first_test_mean:
1304 fake_results['mean'] = float(self.opts.debug_fake_first_test_mean)
1305 self.opts.debug_fake_first_test_mean = 0
1307 return (fake_results, success_code)
1309 # For Windows platform set posix=False, to parse windows paths correctly.
1310 # On Windows, path separators '\' or '\\' are replace by '' when posix=True,
1311 # refer to http://bugs.python.org/issue1724822. By default posix=True.
1312 args = shlex.split(command_to_run, posix=not bisect_utils.IsWindowsHost())
1314 if not _GenerateProfileIfNecessary(args):
1315 err_text = 'Failed to generate profile for performance test.'
1316 return (err_text, failure_code)
1318 is_telemetry = bisect_utils.IsTelemetryCommand(command_to_run)
1320 start_time = time.time()
1323 output_of_all_runs = ''
1324 repeat_count = self.opts.repeat_test_count * test_run_multiplier
1326 for i in xrange(repeat_count):
1327 # Can ignore the return code since if the tests fail, it won't return 0.
1328 current_args = copy.copy(args)
1330 if i == 0 and reset_on_first_run:
1331 current_args.append('--reset-results')
1332 if i == self.opts.repeat_test_count - 1 and upload_on_last_run:
1333 current_args.append('--upload-results')
1335 current_args.append('--results-label=%s' % results_label)
1337 output, return_code = bisect_utils.RunProcessAndRetrieveOutput(
1338 current_args, cwd=self.src_cwd)
1339 return_codes.append(return_code)
1341 if e.errno == errno.ENOENT:
1342 err_text = ('Something went wrong running the performance test. '
1343 'Please review the command line:\n\n')
1344 if 'src/' in ' '.join(args):
1345 err_text += ('Check that you haven\'t accidentally specified a '
1346 'path with src/ in the command.\n\n')
1347 err_text += ' '.join(args)
1350 return (err_text, failure_code)
1353 output_of_all_runs += output
1354 if self.opts.output_buildbot_annotations:
1357 if metric and self._IsBisectModeUsingMetric():
1358 parsed_metric = _ParseMetricValuesFromOutput(metric, output)
1360 metric_values.append(math_utils.Mean(parsed_metric))
1361 # If we're bisecting on a metric (ie, changes in the mean or
1362 # standard deviation) and no metric values are produced, bail out.
1363 if not metric_values:
1365 elif self._IsBisectModeReturnCode():
1366 metric_values.append(return_code)
1368 elapsed_minutes = (time.time() - start_time) / 60.0
1369 time_limit = self.opts.max_time_minutes * test_run_multiplier
1370 if elapsed_minutes >= time_limit:
1373 if metric and len(metric_values) == 0:
1374 err_text = 'Metric %s was not found in the test output.' % metric
1375 # TODO(qyearsley): Consider also getting and displaying a list of metrics
1376 # that were found in the output here.
1377 return (err_text, failure_code, output_of_all_runs)
1379 # If we're bisecting on return codes, we're really just looking for zero vs
1382 if self._IsBisectModeReturnCode():
1383 # If any of the return codes is non-zero, output 1.
1384 overall_return_code = 0 if (
1385 all(current_value == 0 for current_value in metric_values)) else 1
1388 'mean': overall_return_code,
1391 'values': metric_values,
1394 print 'Results of performance test: Command returned with %d' % (
1395 overall_return_code)
1398 # Need to get the average value if there were multiple values.
1399 truncated_mean = math_utils.TruncatedMean(
1400 metric_values, self.opts.truncate_percent)
1401 standard_err = math_utils.StandardError(metric_values)
1402 standard_dev = math_utils.StandardDeviation(metric_values)
1404 if self._IsBisectModeStandardDeviation():
1405 metric_values = [standard_dev]
1408 'mean': truncated_mean,
1409 'std_err': standard_err,
1410 'std_dev': standard_dev,
1411 'values': metric_values,
1414 print 'Results of performance test: %12f %12f' % (
1415 truncated_mean, standard_err)
1418 overall_success = success_code
1419 if not allow_flakes and not self._IsBisectModeReturnCode():
1422 if (all(current_value == 0 for current_value in return_codes))
1425 return (values, overall_success, output_of_all_runs)
1427 def PerformPreBuildCleanup(self):
1428 """Performs cleanup between runs."""
1429 print 'Cleaning up between runs.'
1432 # Leaving these .pyc files around between runs may disrupt some perf tests.
1433 for (path, _, files) in os.walk(self.src_cwd):
1434 for cur_file in files:
1435 if cur_file.endswith('.pyc'):
1436 path_to_file = os.path.join(path, cur_file)
1437 os.remove(path_to_file)
1439 def _RunPostSync(self, _depot):
1440 """Performs any work after syncing.
1448 if 'android' in self.opts.target_platform:
1449 if not builder.SetupAndroidBuildEnvironment(
1450 self.opts, path_to_src=self.src_cwd):
1453 return self.RunGClientHooks()
1456 def ShouldSkipRevision(depot, revision):
1457 """Checks whether a particular revision can be safely skipped.
1459 Some commits can be safely skipped (such as a DEPS roll for the repos
1460 still using .DEPS.git), since the tool is git based those changes
1461 would have no effect.
1464 depot: The depot being bisected.
1465 revision: Current revision we're synced to.
1468 True if we should skip building/testing this revision.
1470 # Skips revisions with DEPS on android-chrome.
1471 if depot == 'android-chrome':
1472 cmd = ['diff-tree', '--no-commit-id', '--name-only', '-r', revision]
1473 output = bisect_utils.CheckRunGit(cmd)
1475 files = output.splitlines()
1477 if len(files) == 1 and files[0] == 'DEPS':
1482 def RunTest(self, revision, depot, command, metric, skippable=False,
1483 skip_sync=False, create_patch=False, force_build=False,
1484 test_run_multiplier=1):
1485 """Performs a full sync/build/run of the specified revision.
1488 revision: The revision to sync to.
1489 depot: The depot that's being used at the moment (src, webkit, etc.)
1490 command: The command to execute the performance test.
1491 metric: The performance metric being tested.
1492 skip_sync: Skip the sync step.
1493 create_patch: Create a patch with any locally modified files.
1494 force_build: Force a local build.
1495 test_run_multiplier: Factor by which to multiply the given number of runs
1496 and the set timeout period.
1499 On success, a tuple containing the results of the performance test.
1500 Otherwise, a tuple with the error message.
1502 logging.info('Running RunTest with rev "%s", command "%s"',
1504 # Decide which sync program to use.
1506 if depot == 'chromium' or depot == 'android-chrome':
1507 sync_client = 'gclient'
1509 # Do the syncing for all depots.
1510 if not (self.opts.debug_ignore_sync or skip_sync):
1511 if not self._SyncRevision(depot, revision, sync_client):
1512 return ('Failed to sync: [%s]' % str(revision), BUILD_RESULT_FAIL)
1514 # Try to do any post-sync steps. This may include "gclient runhooks
".
1515 if not self._RunPostSync(depot):
1516 return ('Failed to run [gclient runhooks].', BUILD_RESULT_FAIL)
1518 # Skip this revision if it can be skipped.
1519 if skippable and self.ShouldSkipRevision(depot, revision):
1520 return ('Skipped revision: [%s]' % str(revision),
1521 BUILD_RESULT_SKIPPED)
1523 # Obtain a build for this revision. This may be done by requesting a build
1524 # from another builder, waiting for it and downloading it.
1525 start_build_time = time.time()
1526 revision_to_build = revision if not force_build else None
1527 build_success = self.ObtainBuild(
1528 depot, revision=revision_to_build, create_patch=create_patch)
1529 if not build_success:
1530 return ('Failed to build revision: [%s]' % str(revision),
1532 after_build_time = time.time()
1534 # Possibly alter the command.
1535 command = self.GetCompatibleCommand(command, revision, depot)
1537 # Run the command and get the results.
1538 results = self.RunPerformanceTestAndParseResults(
1539 command, metric, test_run_multiplier=test_run_multiplier)
1541 # Restore build output directory once the tests are done, to avoid
1542 # any discrepancies.
1543 if self.IsDownloadable(depot) and revision:
1544 self.BackupOrRestoreOutputDirectory(restore=True)
1546 # A value other than 0 indicates that the test couldn't be run, and results
1547 # should also include an error message.
1551 external_revisions = self._Get3rdPartyRevisions(depot)
1553 if not external_revisions is None:
1554 return (results[0], results[1], external_revisions,
1555 time.time() - after_build_time, after_build_time -
1558 return ('Failed to parse DEPS file for external revisions.',
1561 def _SyncRevision(self, depot, revision, sync_client):
1562 """Syncs depot to particular revision.
1565 depot: The depot that's being used at the moment (src, webkit, etc.)
1566 revision: The revision to sync to.
1567 sync_client: Program used to sync, e.g. "gclient
". Can be None.
1570 True if successful, False otherwise.
1572 self.depot_registry.ChangeToDepotDir(depot)
1575 self.PerformPreBuildCleanup()
1577 # When using gclient to sync, you need to specify the depot you
1578 # want so that all the dependencies sync properly as well.
1579 # i.e. gclient sync src@<SHA1>
1580 if sync_client == 'gclient' and revision:
1581 revision = '%s@%s' % (bisect_utils.DEPOT_DEPS_NAME[depot]['src'],
1583 if depot == 'chromium' and self.opts.target_platform == 'android-chrome':
1584 return self._SyncRevisionsForAndroidChrome(revision)
1586 return source_control.SyncToRevision(revision, sync_client)
1588 def _SyncRevisionsForAndroidChrome(self, revision):
1589 """Syncs android-chrome and chromium repos to particular revision.
1591 This is a special case for android-chrome as the gclient sync for chromium
1592 overwrites the android-chrome revision to TOT. Therefore both the repos
1593 are synced to known revisions.
1596 revision: Git hash of the Chromium to sync.
1599 True if successful, False otherwise.
1601 revisions_list = [revision]
1602 current_android_rev = source_control.GetCurrentRevision(
1603 self.depot_registry.GetDepotDir('android-chrome'))
1604 revisions_list.append(
1605 '%s@%s' % (bisect_utils.DEPOT_DEPS_NAME['android-chrome']['src'],
1606 current_android_rev))
1607 return not bisect_utils.RunGClientAndSync(revisions_list)
1609 def _CheckIfRunPassed(self, current_value, known_good_value, known_bad_value):
1610 """Given known good and bad values, decide if the current_value passed
1614 current_value: The value of the metric being checked.
1615 known_bad_value: The reference value for a "failed
" run.
1616 known_good_value: The reference value for a "passed
" run.
1619 True if the current_value is closer to the known_good_value than the
1622 if self.opts.bisect_mode == bisect_utils.BISECT_MODE_STD_DEV:
1623 dist_to_good_value = abs(current_value['std_dev'] -
1624 known_good_value['std_dev'])
1625 dist_to_bad_value = abs(current_value['std_dev'] -
1626 known_bad_value['std_dev'])
1628 dist_to_good_value = abs(current_value['mean'] - known_good_value['mean'])
1629 dist_to_bad_value = abs(current_value['mean'] - known_bad_value['mean'])
1631 return dist_to_good_value < dist_to_bad_value
1633 def _GetV8BleedingEdgeFromV8TrunkIfMappable(
1634 self, revision, bleeding_edge_branch):
1635 """Gets v8 bleeding edge revision mapped to v8 revision in trunk.
1638 revision: A trunk V8 revision mapped to bleeding edge revision.
1639 bleeding_edge_branch: Branch used to perform lookup of bleeding edge
1642 A mapped bleeding edge revision if found, otherwise None.
1644 commit_position = source_control.GetCommitPosition(revision)
1646 if bisect_utils.IsStringInt(commit_position):
1647 # V8 is tricky to bisect, in that there are only a few instances when
1648 # we can dive into bleeding_edge and get back a meaningful result.
1649 # Try to detect a V8 "business
as usual
" case, which is when:
1650 # 1. trunk revision N has description "Version X
.Y
.Z
"
1651 # 2. bleeding_edge revision (N-1) has description "Prepare push to
1652 # trunk. Now working on X.Y.(Z+1)."
1654 # As of 01/24/2014, V8 trunk descriptions are formatted:
1655 # "Version 3.X.Y (based on bleeding_edge revision rZ)"
1656 # So we can just try parsing that out first and fall back to the old way.
1657 v8_dir
= self
.depot_registry
.GetDepotDir('v8')
1658 v8_bleeding_edge_dir
= self
.depot_registry
.GetDepotDir('v8_bleeding_edge')
1660 revision_info
= source_control
.QueryRevisionInfo(revision
, cwd
=v8_dir
)
1661 version_re
= re
.compile("Version (?P<values>[0-9,.]+)")
1662 regex_results
= version_re
.search(revision_info
['subject'])
1665 if 'based on bleeding_edge' in revision_info
['subject']:
1667 bleeding_edge_revision
= revision_info
['subject'].split(
1668 'bleeding_edge revision r')[1]
1669 bleeding_edge_revision
= int(bleeding_edge_revision
.split(')')[0])
1670 bleeding_edge_url
= ('https://v8.googlecode.com/svn/branches/'
1671 'bleeding_edge@%s' % bleeding_edge_revision
)
1677 bleeding_edge_branch
]
1678 output
= bisect_utils
.CheckRunGit(cmd
, cwd
=v8_dir
)
1680 git_revision
= output
.strip()
1682 except (IndexError, ValueError):
1685 # V8 rolls description changed after V8 git migration, new description
1686 # includes "Version 3.X.Y (based on <git hash>)"
1688 rxp
= re
.compile('based on (?P<git_revision>[a-fA-F0-9]+)')
1689 re_results
= rxp
.search(revision_info
['subject'])
1691 return re_results
.group('git_revision')
1692 except (IndexError, ValueError):
1694 if not git_revision
:
1695 # Wasn't successful, try the old way of looking for "Prepare push to"
1696 git_revision
= source_control
.ResolveToRevision(
1697 int(commit_position
) - 1, 'v8_bleeding_edge',
1698 bisect_utils
.DEPOT_DEPS_NAME
, -1, cwd
=v8_bleeding_edge_dir
)
1701 revision_info
= source_control
.QueryRevisionInfo(
1702 git_revision
, cwd
=v8_bleeding_edge_dir
)
1704 if 'Prepare push to trunk' in revision_info
['subject']:
1708 def _GetNearestV8BleedingEdgeFromTrunk(
1709 self
, revision
, v8_branch
, bleeding_edge_branch
, search_forward
=True):
1710 """Gets the nearest V8 roll and maps to bleeding edge revision.
1712 V8 is a bit tricky to bisect since it isn't just rolled out like blink.
1713 Each revision on trunk might just be whatever was in bleeding edge, rolled
1714 directly out. Or it could be some mixture of previous v8 trunk versions,
1715 with bits and pieces cherry picked out from bleeding edge. In order to
1716 bisect, we need both the before/after versions on trunk v8 to be just pushes
1717 from bleeding edge. With the V8 git migration, the branches got switched.
1718 a) master (external/v8) == candidates (v8/v8)
1719 b) bleeding_edge (external/v8) == master (v8/v8)
1722 revision: A V8 revision to get its nearest bleeding edge revision
1723 search_forward: Searches forward if True, otherwise search backward.
1726 A mapped bleeding edge revision if found, otherwise None.
1728 cwd
= self
.depot_registry
.GetDepotDir('v8')
1729 cmd
= ['log', '--format=%ct', '-1', revision
]
1730 output
= bisect_utils
.CheckRunGit(cmd
, cwd
=cwd
)
1731 commit_time
= int(output
)
1736 '--after=%d' % commit_time
,
1739 output
= bisect_utils
.CheckRunGit(cmd
, cwd
=cwd
)
1740 output
= output
.split()
1742 #Get 10 git hashes immediately after the given commit.
1743 commits
= commits
[:10]
1748 '--before=%d' % commit_time
,
1750 output
= bisect_utils
.CheckRunGit(cmd
, cwd
=cwd
)
1751 output
= output
.split()
1754 bleeding_edge_revision
= None
1757 bleeding_edge_revision
= self
._GetV
8BleedingEdgeFromV
8TrunkIfMappable
(
1758 c
, bleeding_edge_branch
)
1759 if bleeding_edge_revision
:
1762 return bleeding_edge_revision
1764 def _FillInV8BleedingEdgeInfo(self
, min_revision_state
, max_revision_state
):
1765 cwd
= self
.depot_registry
.GetDepotDir('v8')
1766 # when "remote.origin.url" is https://chromium.googlesource.com/v8/v8.git
1767 v8_branch
= 'origin/candidates'
1768 bleeding_edge_branch
= 'origin/master'
1770 # Support for the chromium revisions with external V8 repo.
1771 # ie https://chromium.googlesource.com/external/v8.git
1772 cmd
= ['config', '--get', 'remote.origin.url']
1773 v8_repo_url
= bisect_utils
.CheckRunGit(cmd
, cwd
=cwd
)
1775 if 'external/v8.git' in v8_repo_url
:
1776 v8_branch
= 'origin/master'
1777 bleeding_edge_branch
= 'origin/bleeding_edge'
1779 r1
= self
._GetNearestV
8BleedingEdgeFromTrunk
(
1780 min_revision_state
.revision
,
1782 bleeding_edge_branch
,
1783 search_forward
=True)
1784 r2
= self
._GetNearestV
8BleedingEdgeFromTrunk
(
1785 max_revision_state
.revision
,
1787 bleeding_edge_branch
,
1788 search_forward
=False)
1789 min_revision_state
.external
['v8_bleeding_edge'] = r1
1790 max_revision_state
.external
['v8_bleeding_edge'] = r2
1792 if (not self
._GetV
8BleedingEdgeFromV
8TrunkIfMappable
(
1793 min_revision_state
.revision
, bleeding_edge_branch
)
1794 or not self
._GetV
8BleedingEdgeFromV
8TrunkIfMappable
(
1795 max_revision_state
.revision
, bleeding_edge_branch
)):
1796 self
.warnings
.append(
1797 'Trunk revisions in V8 did not map directly to bleeding_edge. '
1798 'Attempted to expand the range to find V8 rolls which did map '
1799 'directly to bleeding_edge revisions, but results might not be '
1802 def _FindNextDepotToBisect(
1803 self
, current_depot
, min_revision_state
, max_revision_state
):
1804 """Decides which depot the script should dive into next (if any).
1807 current_depot: Current depot being bisected.
1808 min_revision_state: State of the earliest revision in the bisect range.
1809 max_revision_state: State of the latest revision in the bisect range.
1812 Name of the depot to bisect next, or None.
1814 external_depot
= None
1815 for next_depot
in bisect_utils
.DEPOT_NAMES
:
1816 if ('platform' in bisect_utils
.DEPOT_DEPS_NAME
[next_depot
] and
1817 bisect_utils
.DEPOT_DEPS_NAME
[next_depot
]['platform'] != os
.name
):
1820 if not (bisect_utils
.DEPOT_DEPS_NAME
[next_depot
]['recurse']
1821 and min_revision_state
.depot
1822 in bisect_utils
.DEPOT_DEPS_NAME
[next_depot
]['from']):
1825 if current_depot
== 'v8':
1826 # We grab the bleeding_edge info here rather than earlier because we
1827 # finally have the revision range. From that we can search forwards and
1828 # backwards to try to match trunk revisions to bleeding_edge.
1829 self
._FillInV
8BleedingEdgeInfo
(min_revision_state
, max_revision_state
)
1831 if (min_revision_state
.external
.get(next_depot
) ==
1832 max_revision_state
.external
.get(next_depot
)):
1835 if (min_revision_state
.external
.get(next_depot
) and
1836 max_revision_state
.external
.get(next_depot
)):
1837 external_depot
= next_depot
1840 return external_depot
1842 def PrepareToBisectOnDepot(
1843 self
, current_depot
, start_revision
, end_revision
, previous_revision
):
1844 """Changes to the appropriate directory and gathers a list of revisions
1845 to bisect between |start_revision| and |end_revision|.
1848 current_depot: The depot we want to bisect.
1849 start_revision: Start of the revision range.
1850 end_revision: End of the revision range.
1851 previous_revision: The last revision we synced to on |previous_depot|.
1854 A list containing the revisions between |start_revision| and
1855 |end_revision| inclusive.
1857 # Change into working directory of external library to run
1858 # subsequent commands.
1859 self
.depot_registry
.ChangeToDepotDir(current_depot
)
1861 # V8 (and possibly others) is merged in periodically. Bisecting
1862 # this directory directly won't give much good info.
1863 if 'custom_deps' in bisect_utils
.DEPOT_DEPS_NAME
[current_depot
]:
1864 config_path
= os
.path
.join(self
.src_cwd
, '..')
1865 if bisect_utils
.RunGClientAndCreateConfig(
1866 self
.opts
, bisect_utils
.DEPOT_DEPS_NAME
[current_depot
]['custom_deps'],
1869 if bisect_utils
.RunGClient(
1870 ['sync', '--revision', previous_revision
], cwd
=self
.src_cwd
):
1873 if current_depot
== 'v8_bleeding_edge':
1874 self
.depot_registry
.ChangeToDepotDir('chromium')
1876 shutil
.move('v8', 'v8.bak')
1877 shutil
.move('v8_bleeding_edge', 'v8')
1879 self
.cleanup_commands
.append(['mv', 'v8', 'v8_bleeding_edge'])
1880 self
.cleanup_commands
.append(['mv', 'v8.bak', 'v8'])
1882 self
.depot_registry
.SetDepotDir(
1883 'v8_bleeding_edge', os
.path
.join(self
.src_cwd
, 'v8'))
1884 self
.depot_registry
.SetDepotDir(
1885 'v8', os
.path
.join(self
.src_cwd
, 'v8.bak'))
1887 self
.depot_registry
.ChangeToDepotDir(current_depot
)
1889 depot_revision_list
= self
.GetRevisionList(current_depot
,
1893 self
.depot_registry
.ChangeToDepotDir('chromium')
1895 return depot_revision_list
1897 def GatherReferenceValues(self
, good_rev
, bad_rev
, cmd
, metric
, target_depot
):
1898 """Gathers reference values by running the performance tests on the
1899 known good and bad revisions.
1902 good_rev: The last known good revision where the performance regression
1903 has not occurred yet.
1904 bad_rev: A revision where the performance regression has already occurred.
1905 cmd: The command to execute the performance test.
1906 metric: The metric being tested for regression.
1909 A tuple with the results of building and running each revision.
1911 bad_run_results
= self
.RunTest(bad_rev
, target_depot
, cmd
, metric
)
1913 good_run_results
= None
1915 if not bad_run_results
[1]:
1916 good_run_results
= self
.RunTest(good_rev
, target_depot
, cmd
, metric
)
1918 return (bad_run_results
, good_run_results
)
1920 def PrintRevisionsToBisectMessage(self
, revision_list
, depot
):
1921 if self
.opts
.output_buildbot_annotations
:
1922 step_name
= 'Bisection Range: [%s:%s - %s]' % (depot
, revision_list
[-1],
1924 bisect_utils
.OutputAnnotationStepStart(step_name
)
1927 print 'Revisions to bisect on [%s]:' % depot
1928 for revision_id
in revision_list
:
1929 print ' -> %s' % (revision_id
, )
1932 if self
.opts
.output_buildbot_annotations
:
1933 bisect_utils
.OutputAnnotationStepClosed()
1935 def NudgeRevisionsIfDEPSChange(self
, bad_revision
, good_revision
,
1936 good_svn_revision
=None):
1937 """Checks to see if changes to DEPS file occurred, and that the revision
1938 range also includes the change to .DEPS.git. If it doesn't, attempts to
1939 expand the revision range to include it.
1942 bad_revision: First known bad git revision.
1943 good_revision: Last known good git revision.
1944 good_svn_revision: Last known good svn revision.
1947 A tuple with the new bad and good revisions.
1949 # DONOT perform nudge because at revision 291563 .DEPS.git was removed
1950 # and source contain only DEPS file for dependency changes.
1951 if good_svn_revision
>= 291563:
1952 return (bad_revision
, good_revision
)
1954 if self
.opts
.target_platform
== 'chromium':
1955 changes_to_deps
= source_control
.QueryFileRevisionHistory(
1956 bisect_utils
.FILE_DEPS
, good_revision
, bad_revision
)
1959 # DEPS file was changed, search from the oldest change to DEPS file to
1960 # bad_revision to see if there are matching .DEPS.git changes.
1961 oldest_deps_change
= changes_to_deps
[-1]
1962 changes_to_gitdeps
= source_control
.QueryFileRevisionHistory(
1963 bisect_utils
.FILE_DEPS_GIT
, oldest_deps_change
, bad_revision
)
1965 if len(changes_to_deps
) != len(changes_to_gitdeps
):
1966 # Grab the timestamp of the last DEPS change
1967 cmd
= ['log', '--format=%ct', '-1', changes_to_deps
[0]]
1968 output
= bisect_utils
.CheckRunGit(cmd
)
1969 commit_time
= int(output
)
1971 # Try looking for a commit that touches the .DEPS.git file in the
1972 # next 15 minutes after the DEPS file change.
1974 'log', '--format=%H', '-1',
1975 '--before=%d' % (commit_time
+ 900),
1976 '--after=%d' % commit_time
,
1977 'origin/master', '--', bisect_utils
.FILE_DEPS_GIT
1979 output
= bisect_utils
.CheckRunGit(cmd
)
1980 output
= output
.strip()
1982 self
.warnings
.append(
1983 'Detected change to DEPS and modified '
1984 'revision range to include change to .DEPS.git')
1985 return (output
, good_revision
)
1987 self
.warnings
.append(
1988 'Detected change to DEPS but couldn\'t find '
1989 'matching change to .DEPS.git')
1990 return (bad_revision
, good_revision
)
1992 def CheckIfRevisionsInProperOrder(
1993 self
, target_depot
, good_revision
, bad_revision
):
1994 """Checks that |good_revision| is an earlier revision than |bad_revision|.
1997 good_revision: Number/tag of the known good revision.
1998 bad_revision: Number/tag of the known bad revision.
2001 True if the revisions are in the proper order (good earlier than bad).
2003 cwd
= self
.depot_registry
.GetDepotDir(target_depot
)
2004 good_position
= source_control
.GetCommitPosition(good_revision
, cwd
)
2005 bad_position
= source_control
.GetCommitPosition(bad_revision
, cwd
)
2006 # Compare commit timestamp for repos that don't support commit position.
2007 if not (bad_position
and good_position
):
2008 logging
.info('Could not get commit positions for revisions %s and %s in '
2009 'depot %s', good_position
, bad_position
, target_depot
)
2010 good_position
= source_control
.GetCommitTime(good_revision
, cwd
=cwd
)
2011 bad_position
= source_control
.GetCommitTime(bad_revision
, cwd
=cwd
)
2013 return good_position
<= bad_position
2015 def CanPerformBisect(self
, good_revision
, bad_revision
):
2016 """Checks whether a given revision is bisectable.
2018 Checks for following:
2019 1. Non-bisectable revisions for android bots (refer to crbug.com/385324).
2020 2. Non-bisectable revisions for Windows bots (refer to crbug.com/405274).
2023 good_revision: Known good revision.
2024 bad_revision: Known bad revision.
2027 A dictionary indicating the result. If revision is not bisectable,
2028 this will contain the field "error", otherwise None.
2030 if self
.opts
.target_platform
== 'android':
2031 good_revision
= source_control
.GetCommitPosition(good_revision
)
2032 if (bisect_utils
.IsStringInt(good_revision
)
2033 and good_revision
< 265549):
2035 'Bisect cannot continue for the given revision range.\n'
2036 'It is impossible to bisect Android regressions '
2037 'prior to r265549, which allows the bisect bot to '
2038 'rely on Telemetry to do apk installation of the most recently '
2039 'built local ChromeShell(refer to crbug.com/385324).\n'
2040 'Please try bisecting revisions greater than or equal to r265549.')}
2042 if bisect_utils
.IsWindowsHost():
2043 good_revision
= source_control
.GetCommitPosition(good_revision
)
2044 bad_revision
= source_control
.GetCommitPosition(bad_revision
)
2045 if (bisect_utils
.IsStringInt(good_revision
) and
2046 bisect_utils
.IsStringInt(bad_revision
)):
2047 if (289987 <= good_revision
< 290716 or
2048 289987 <= bad_revision
< 290716):
2049 return {'error': ('Oops! Revision between r289987 and r290716 are '
2050 'marked as dead zone for Windows due to '
2051 'crbug.com/405274. Please try another range.')}
2055 def _GatherResultsFromRevertedCulpritCL(
2056 self
, results
, target_depot
, command_to_run
, metric
):
2057 """Gathers performance results with/without culprit CL.
2059 Attempts to revert the culprit CL against ToT and runs the
2060 performance tests again with and without the CL, adding the results to
2061 the over bisect results.
2064 results: BisectResults from the bisect.
2065 target_depot: The target depot we're bisecting.
2066 command_to_run: Specify the command to execute the performance test.
2067 metric: The performance metric to monitor.
2069 run_results_tot
, run_results_reverted
= self
._RevertCulpritCLAndRetest
(
2070 results
, target_depot
, command_to_run
, metric
)
2072 results
.AddRetestResults(run_results_tot
, run_results_reverted
)
2074 if len(results
.culprit_revisions
) != 1:
2077 # Cleanup reverted files if anything is left.
2078 _
, _
, culprit_depot
= results
.culprit_revisions
[0]
2079 bisect_utils
.CheckRunGit(
2080 ['reset', '--hard', 'HEAD'],
2081 cwd
=self
.depot_registry
.GetDepotDir(culprit_depot
))
2083 def _RevertCL(self
, culprit_revision
, culprit_depot
):
2084 """Reverts the specified revision in the specified depot."""
2085 if self
.opts
.output_buildbot_annotations
:
2086 bisect_utils
.OutputAnnotationStepStart(
2087 'Reverting culprit CL: %s' % culprit_revision
)
2088 _
, return_code
= bisect_utils
.RunGit(
2089 ['revert', '--no-commit', culprit_revision
],
2090 cwd
=self
.depot_registry
.GetDepotDir(culprit_depot
))
2092 bisect_utils
.OutputAnnotationStepWarning()
2093 bisect_utils
.OutputAnnotationStepText('Failed to revert CL cleanly.')
2094 if self
.opts
.output_buildbot_annotations
:
2095 bisect_utils
.OutputAnnotationStepClosed()
2096 return not return_code
2098 def _RevertCulpritCLAndRetest(
2099 self
, results
, target_depot
, command_to_run
, metric
):
2100 """Reverts the culprit CL against ToT and runs the performance test.
2102 Attempts to revert the culprit CL against ToT and runs the
2103 performance tests again with and without the CL.
2106 results: BisectResults from the bisect.
2107 target_depot: The target depot we're bisecting.
2108 command_to_run: Specify the command to execute the performance test.
2109 metric: The performance metric to monitor.
2112 A tuple with the results of running the CL at ToT/reverted.
2114 # Might want to retest ToT with a revert of the CL to confirm that
2115 # performance returns.
2116 if results
.confidence
< bisect_utils
.HIGH_CONFIDENCE
:
2119 # If there were multiple culprit CLs, we won't try to revert.
2120 if len(results
.culprit_revisions
) != 1:
2123 culprit_revision
, _
, culprit_depot
= results
.culprit_revisions
[0]
2125 if not self
._SyncRevision
(target_depot
, None, 'gclient'):
2128 head_revision
= bisect_utils
.CheckRunGit(['log', '--format=%H', '-1'])
2129 head_revision
= head_revision
.strip()
2131 if not self
._RevertCL
(culprit_revision
, culprit_depot
):
2134 # If the culprit CL happened to be in a depot that gets pulled in, we
2135 # can't revert the change and issue a try job to build, since that would
2136 # require modifying both the DEPS file and files in another depot.
2137 # Instead, we build locally.
2138 force_build
= (culprit_depot
!= target_depot
)
2140 results
.warnings
.append(
2141 'Culprit CL is in another depot, attempting to revert and build'
2142 ' locally to retest. This may not match the performance of official'
2145 run_results_reverted
= self
._RunTestWithAnnotations
(
2146 'Re-Testing ToT with reverted culprit',
2147 'Failed to run reverted CL.',
2148 head_revision
, target_depot
, command_to_run
, metric
, force_build
)
2150 # Clear the reverted file(s).
2151 bisect_utils
.RunGit(
2152 ['reset', '--hard', 'HEAD'],
2153 cwd
=self
.depot_registry
.GetDepotDir(culprit_depot
))
2155 # Retesting with the reverted CL failed, so bail out of retesting against
2157 if run_results_reverted
[1]:
2160 run_results_tot
= self
._RunTestWithAnnotations
(
2162 'Failed to run ToT.',
2163 head_revision
, target_depot
, command_to_run
, metric
, force_build
)
2165 return (run_results_tot
, run_results_reverted
)
2167 def _RunTestWithAnnotations(
2168 self
, step_text
, error_text
, head_revision
,
2169 target_depot
, command_to_run
, metric
, force_build
):
2170 """Runs the performance test and outputs start/stop annotations.
2173 results: BisectResults from the bisect.
2174 target_depot: The target depot we're bisecting.
2175 command_to_run: Specify the command to execute the performance test.
2176 metric: The performance metric to monitor.
2177 force_build: Whether to force a build locally.
2180 Results of the test.
2182 if self
.opts
.output_buildbot_annotations
:
2183 bisect_utils
.OutputAnnotationStepStart(step_text
)
2185 # Build and run the test again with the reverted culprit CL against ToT.
2186 run_test_results
= self
.RunTest(
2187 head_revision
, target_depot
, command_to_run
,
2188 metric
, skippable
=False, skip_sync
=True, create_patch
=True,
2189 force_build
=force_build
)
2191 if self
.opts
.output_buildbot_annotations
:
2192 if run_test_results
[1]:
2193 bisect_utils
.OutputAnnotationStepWarning()
2194 bisect_utils
.OutputAnnotationStepText(error_text
)
2195 bisect_utils
.OutputAnnotationStepClosed()
2197 return run_test_results
2199 def Run(self
, command_to_run
, bad_revision_in
, good_revision_in
, metric
):
2200 """Given known good and bad revisions, run a binary search on all
2201 intermediate revisions to determine the CL where the performance regression
2205 command_to_run: Specify the command to execute the performance test.
2206 good_revision: Number/tag of the known good revision.
2207 bad_revision: Number/tag of the known bad revision.
2208 metric: The performance metric to monitor.
2211 A BisectResults object.
2213 # Choose depot to bisect first
2214 target_depot
= 'chromium'
2215 if self
.opts
.target_platform
== 'android-chrome':
2216 target_depot
= 'android-chrome'
2219 self
.depot_registry
.ChangeToDepotDir(target_depot
)
2221 # If they passed SVN revisions, we can try match them to git SHA1 hashes.
2222 bad_revision
= source_control
.ResolveToRevision(
2223 bad_revision_in
, target_depot
, bisect_utils
.DEPOT_DEPS_NAME
, 100)
2224 good_revision
= source_control
.ResolveToRevision(
2225 good_revision_in
, target_depot
, bisect_utils
.DEPOT_DEPS_NAME
, -100)
2228 if bad_revision
is None:
2229 return BisectResults(
2230 error
='Couldn\'t resolve [%s] to SHA1.' % bad_revision_in
)
2232 if good_revision
is None:
2233 return BisectResults(
2234 error
='Couldn\'t resolve [%s] to SHA1.' % good_revision_in
)
2236 # Check that they didn't accidentally swap good and bad revisions.
2237 if not self
.CheckIfRevisionsInProperOrder(
2238 target_depot
, good_revision
, bad_revision
):
2239 return BisectResults(error
='Bad rev (%s) appears to be earlier than good '
2240 'rev (%s).' % (good_revision
, bad_revision
))
2242 bad_revision
, good_revision
= self
.NudgeRevisionsIfDEPSChange(
2243 bad_revision
, good_revision
, good_revision_in
)
2244 if self
.opts
.output_buildbot_annotations
:
2245 bisect_utils
.OutputAnnotationStepStart('Gathering Revisions')
2247 cannot_bisect
= self
.CanPerformBisect(good_revision
, bad_revision
)
2249 return BisectResults(error
=cannot_bisect
.get('error'))
2251 print 'Gathering revision range for bisection.'
2252 # Retrieve a list of revisions to do bisection on.
2253 revision_list
= self
.GetRevisionList(target_depot
, bad_revision
,
2256 if self
.opts
.output_buildbot_annotations
:
2257 bisect_utils
.OutputAnnotationStepClosed()
2260 self
.PrintRevisionsToBisectMessage(revision_list
, target_depot
)
2262 if self
.opts
.output_buildbot_annotations
:
2263 bisect_utils
.OutputAnnotationStepStart('Gathering Reference Values')
2265 print 'Gathering reference values for bisection.'
2267 # Perform the performance tests on the good and bad revisions, to get
2269 bad_results
, good_results
= self
.GatherReferenceValues(good_revision
,
2275 if self
.opts
.output_buildbot_annotations
:
2276 bisect_utils
.OutputAnnotationStepClosed()
2279 error
= ('An error occurred while building and running the \'bad\' '
2280 'reference value. The bisect cannot continue without '
2281 'a working \'bad\' revision to start from.\n\nError: %s' %
2283 return BisectResults(error
=error
)
2286 error
= ('An error occurred while building and running the \'good\' '
2287 'reference value. The bisect cannot continue without '
2288 'a working \'good\' revision to start from.\n\nError: %s' %
2290 return BisectResults(error
=error
)
2292 # We need these reference values to determine if later runs should be
2293 # classified as pass or fail.
2294 known_bad_value
= bad_results
[0]
2295 known_good_value
= good_results
[0]
2297 # Check the direction of improvement only if the improvement_direction
2298 # option is set to a specific direction (1 for higher is better or -1 for
2300 improvement_dir
= self
.opts
.improvement_direction
2302 higher_is_better
= improvement_dir
> 0
2303 if higher_is_better
:
2304 message
= "Expecting higher values to be better for this metric, "
2306 message
= "Expecting lower values to be better for this metric, "
2307 metric_increased
= known_bad_value
['mean'] > known_good_value
['mean']
2308 if metric_increased
:
2309 message
+= "and the metric appears to have increased. "
2311 message
+= "and the metric appears to have decreased. "
2312 if ((higher_is_better
and metric_increased
) or
2313 (not higher_is_better
and not metric_increased
)):
2314 error
= (message
+ 'Then, the test results for the ends of the given '
2315 '\'good\' - \'bad\' range of revisions represent an '
2316 'improvement (and not a regression).')
2317 return BisectResults(error
=error
)
2318 logging
.info(message
+ "Therefore we continue to bisect.")
2320 bisect_state
= BisectState(target_depot
, revision_list
)
2321 revision_states
= bisect_state
.GetRevisionStates()
2324 max_revision
= len(revision_states
) - 1
2326 # Can just mark the good and bad revisions explicitly here since we
2327 # already know the results.
2328 bad_revision_state
= revision_states
[min_revision
]
2329 bad_revision_state
.external
= bad_results
[2]
2330 bad_revision_state
.perf_time
= bad_results
[3]
2331 bad_revision_state
.build_time
= bad_results
[4]
2332 bad_revision_state
.passed
= False
2333 bad_revision_state
.value
= known_bad_value
2335 good_revision_state
= revision_states
[max_revision
]
2336 good_revision_state
.external
= good_results
[2]
2337 good_revision_state
.perf_time
= good_results
[3]
2338 good_revision_state
.build_time
= good_results
[4]
2339 good_revision_state
.passed
= True
2340 good_revision_state
.value
= known_good_value
2342 # Check how likely it is that the good and bad results are different
2343 # beyond chance-induced variation.
2344 confidence_error
= False
2345 if not self
.opts
.debug_ignore_regression_confidence
:
2346 confidence_error
= _CheckRegressionConfidenceError(good_revision
,
2350 if confidence_error
:
2351 # If there is no significant difference between "good" and "bad"
2352 # revision results, then the "bad revision" is considered "good".
2353 # TODO(qyearsley): Remove this if it is not necessary.
2354 bad_revision_state
.passed
= True
2355 self
.warnings
.append(confidence_error
)
2356 return BisectResults(bisect_state
, self
.depot_registry
, self
.opts
,
2360 if not revision_states
:
2363 if max_revision
- min_revision
<= 1:
2364 min_revision_state
= revision_states
[min_revision
]
2365 max_revision_state
= revision_states
[max_revision
]
2366 current_depot
= min_revision_state
.depot
2367 # TODO(sergiyb): Under which conditions can first two branches be hit?
2368 if min_revision_state
.passed
== '?':
2369 next_revision_index
= min_revision
2370 elif max_revision_state
.passed
== '?':
2371 next_revision_index
= max_revision
2372 elif current_depot
in ['android-chrome', 'chromium', 'v8']:
2373 previous_revision
= revision_states
[min_revision
].revision
2374 # If there were changes to any of the external libraries we track,
2375 # should bisect the changes there as well.
2376 external_depot
= self
._FindNextDepotToBisect
(
2377 current_depot
, min_revision_state
, max_revision_state
)
2378 # If there was no change in any of the external depots, the search
2380 if not external_depot
:
2381 if current_depot
== 'v8':
2382 self
.warnings
.append(
2383 'Unfortunately, V8 bisection couldn\'t '
2384 'continue any further. The script can only bisect into '
2385 'V8\'s bleeding_edge repository if both the current and '
2386 'previous revisions in trunk map directly to revisions in '
2390 earliest_revision
= max_revision_state
.external
[external_depot
]
2391 latest_revision
= min_revision_state
.external
[external_depot
]
2393 new_revision_list
= self
.PrepareToBisectOnDepot(
2394 external_depot
, earliest_revision
, latest_revision
,
2397 if not new_revision_list
:
2398 error
= ('An error occurred attempting to retrieve revision '
2399 'range: [%s..%s]' % (earliest_revision
, latest_revision
))
2400 return BisectResults(error
=error
)
2402 revision_states
= bisect_state
.CreateRevisionStatesAfter(
2403 external_depot
, new_revision_list
, current_depot
,
2406 # Reset the bisection and perform it on the newly inserted states.
2408 max_revision
= len(revision_states
) - 1
2410 print ('Regression in metric %s appears to be the result of '
2411 'changes in [%s].' % (metric
, external_depot
))
2413 revision_list
= [state
.revision
for state
in revision_states
]
2414 self
.PrintRevisionsToBisectMessage(revision_list
, external_depot
)
2420 next_revision_index
= (int((max_revision
- min_revision
) / 2) +
2423 next_revision_state
= revision_states
[next_revision_index
]
2424 next_revision
= next_revision_state
.revision
2425 next_depot
= next_revision_state
.depot
2427 self
.depot_registry
.ChangeToDepotDir(next_depot
)
2429 message
= 'Working on [%s:%s]' % (next_depot
, next_revision
)
2431 if self
.opts
.output_buildbot_annotations
:
2432 bisect_utils
.OutputAnnotationStepStart(message
)
2434 run_results
= self
.RunTest(next_revision
, next_depot
, command_to_run
,
2435 metric
, skippable
=True)
2437 # If the build is successful, check whether or not the metric
2439 if not run_results
[1]:
2440 if len(run_results
) > 2:
2441 next_revision_state
.external
= run_results
[2]
2442 next_revision_state
.perf_time
= run_results
[3]
2443 next_revision_state
.build_time
= run_results
[4]
2445 passed_regression
= self
._CheckIfRunPassed
(run_results
[0],
2449 next_revision_state
.passed
= passed_regression
2450 next_revision_state
.value
= run_results
[0]
2452 if passed_regression
:
2453 max_revision
= next_revision_index
2455 min_revision
= next_revision_index
2457 if run_results
[1] == BUILD_RESULT_SKIPPED
:
2458 next_revision_state
.passed
= 'Skipped'
2459 elif run_results
[1] == BUILD_RESULT_FAIL
:
2460 next_revision_state
.passed
= 'Build Failed'
2462 print run_results
[0]
2464 # If the build is broken, remove it and redo search.
2465 revision_states
.pop(next_revision_index
)
2469 if self
.opts
.output_buildbot_annotations
:
2470 self
.printer
.PrintPartialResults(bisect_state
)
2471 bisect_utils
.OutputAnnotationStepClosed()
2473 self
._ConfidenceExtraTestRuns
(min_revision_state
, max_revision_state
,
2474 command_to_run
, metric
)
2475 results
= BisectResults(bisect_state
, self
.depot_registry
, self
.opts
,
2478 self
._GatherResultsFromRevertedCulpritCL
(
2479 results
, target_depot
, command_to_run
, metric
)
2483 # Weren't able to sync and retrieve the revision range.
2484 error
= ('An error occurred attempting to retrieve revision range: '
2485 '[%s..%s]' % (good_revision
, bad_revision
))
2486 return BisectResults(error
=error
)
2488 def _ConfidenceExtraTestRuns(self
, good_state
, bad_state
, command_to_run
,
2490 if (bool(good_state
.passed
) != bool(bad_state
.passed
)
2491 and good_state
.passed
not in ('Skipped', 'Build Failed')
2492 and bad_state
.passed
not in ('Skipped', 'Build Failed')):
2493 for state
in (good_state
, bad_state
):
2494 run_results
= self
.RunTest(
2499 test_run_multiplier
=BORDER_REVISIONS_EXTRA_RUNS
)
2500 # Is extend the right thing to do here?
2501 if run_results
[1] != BUILD_RESULT_FAIL
:
2502 state
.value
['values'].extend(run_results
[0]['values'])
2504 warning_text
= 'Re-test of revision %s failed with error message: %s'
2505 warning_text
%= (state
.revision
, run_results
[0])
2506 if warning_text
not in self
.warnings
:
2507 self
.warnings
.append(warning_text
)
2510 def _IsPlatformSupported():
2511 """Checks that this platform and build system are supported.
2514 opts: The options parsed from the command line.
2517 True if the platform and build system are supported.
2519 # Haven't tested the script out on any other platforms yet.
2520 supported
= ['posix', 'nt']
2521 return os
.name
in supported
2524 def RemoveBuildFiles(build_type
):
2525 """Removes build files from previous runs."""
2526 out_dir
= os
.path
.join('out', build_type
)
2527 build_dir
= os
.path
.join('build', build_type
)
2528 logging
.info('Removing build files in "%s" and "%s".',
2529 os
.path
.abspath(out_dir
), os
.path
.abspath(build_dir
))
2531 RemakeDirectoryTree(out_dir
)
2532 RemakeDirectoryTree(build_dir
)
2533 except Exception as e
:
2534 raise RuntimeError('Got error in RemoveBuildFiles: %s' % e
)
2537 def RemakeDirectoryTree(path_to_dir
):
2538 """Removes a directory tree and replaces it with an empty one.
2540 Returns True if successful, False otherwise.
2542 RemoveDirectoryTree(path_to_dir
)
2543 MaybeMakeDirectory(path_to_dir
)
2546 def RemoveDirectoryTree(path_to_dir
):
2547 """Removes a directory tree. Returns True if successful or False otherwise."""
2548 if os
.path
.isfile(path_to_dir
):
2549 logging
.info('REMOVING FILE %s' % path_to_dir
)
2550 os
.remove(path_to_dir
)
2552 if os
.path
.exists(path_to_dir
):
2553 shutil
.rmtree(path_to_dir
)
2555 if e
.errno
!= errno
.ENOENT
:
2559 # This is copied from build/scripts/common/chromium_utils.py.
2560 def MaybeMakeDirectory(*path
):
2561 """Creates an entire path, if it doesn't already exist."""
2562 file_path
= os
.path
.join(*path
)
2564 os
.makedirs(file_path
)
2565 except OSError as e
:
2566 if e
.errno
!= errno
.EEXIST
:
2570 class BisectOptions(object):
2571 """Options to be used when running bisection."""
2573 super(BisectOptions
, self
).__init
__()
2575 self
.target_platform
= 'chromium'
2576 self
.build_preference
= None
2577 self
.good_revision
= None
2578 self
.bad_revision
= None
2579 self
.use_goma
= None
2580 self
.goma_dir
= None
2581 self
.goma_threads
= 64
2582 self
.repeat_test_count
= 20
2583 self
.truncate_percent
= 25
2584 self
.max_time_minutes
= 20
2587 self
.output_buildbot_annotations
= None
2588 self
.no_custom_deps
= False
2589 self
.working_directory
= None
2590 self
.extra_src
= None
2591 self
.debug_ignore_build
= None
2592 self
.debug_ignore_sync
= None
2593 self
.debug_ignore_perf_test
= None
2594 self
.debug_ignore_regression_confidence
= None
2595 self
.debug_fake_first_test_mean
= 0
2596 self
.target_arch
= 'ia32'
2597 self
.target_build_type
= 'Release'
2598 self
.builder_type
= 'perf'
2599 self
.bisect_mode
= bisect_utils
.BISECT_MODE_MEAN
2600 self
.improvement_direction
= 0
2604 def _AddBisectOptionsGroup(parser
):
2605 group
= parser
.add_argument_group('Bisect options')
2606 group
.add_argument('-c', '--command', required
=True,
2607 help='A command to execute your performance test at '
2608 'each point in the bisection.')
2609 group
.add_argument('-b', '--bad_revision', required
=True,
2610 help='A bad revision to start bisection. Must be later '
2611 'than good revision. May be either a git or svn '
2613 group
.add_argument('-g', '--good_revision', required
=True,
2614 help='A revision to start bisection where performance '
2615 'test is known to pass. Must be earlier than the '
2616 'bad revision. May be either a git or a svn '
2618 group
.add_argument('-m', '--metric',
2619 help='The desired metric to bisect on. For example '
2620 '"vm_rss_final_b/vm_rss_f_b"')
2621 group
.add_argument('-d', '--improvement_direction', type=int, default
=0,
2622 help='An integer number representing the direction of '
2623 'improvement. 1 for higher is better, -1 for lower '
2624 'is better, 0 for ignore (default).')
2625 group
.add_argument('-r', '--repeat_test_count', type=int, default
=20,
2626 choices
=range(1, 101),
2627 help='The number of times to repeat the performance '
2628 'test. Values will be clamped to range [1, 100]. '
2629 'Default value is 20.')
2630 group
.add_argument('--max_time_minutes', type=int, default
=20,
2631 choices
=range(1, 61),
2632 help='The maximum time (in minutes) to take running the '
2633 'performance tests. The script will run the '
2634 'performance tests according to '
2635 '--repeat_test_count, so long as it doesn\'t exceed'
2636 ' --max_time_minutes. Values will be clamped to '
2637 'range [1, 60]. Default value is 20.')
2638 group
.add_argument('-t', '--truncate_percent', type=int, default
=25,
2639 help='The highest/lowest percent are discarded to form '
2640 'a truncated mean. Values will be clamped to range '
2641 '[0, 25]. Default value is 25 percent.')
2642 group
.add_argument('--bisect_mode', default
=bisect_utils
.BISECT_MODE_MEAN
,
2643 choices
=[bisect_utils
.BISECT_MODE_MEAN
,
2644 bisect_utils
.BISECT_MODE_STD_DEV
,
2645 bisect_utils
.BISECT_MODE_RETURN_CODE
],
2646 help='The bisect mode. Choices are to bisect on the '
2647 'difference in mean, std_dev, or return_code.')
2648 group
.add_argument('--bug_id', default
='',
2649 help='The id for the bug associated with this bisect. ' +
2650 'If this number is given, bisect will attempt to ' +
2651 'verify that the bug is not closed before '
2655 def _AddBuildOptionsGroup(parser
):
2656 group
= parser
.add_argument_group('Build options')
2657 group
.add_argument('-w', '--working_directory',
2658 help='Path to the working directory where the script '
2659 'will do an initial checkout of the chromium depot. The '
2660 'files will be placed in a subdirectory "bisect" under '
2661 'working_directory and that will be used to perform the '
2662 'bisection. This parameter is optional, if it is not '
2663 'supplied, the script will work from the current depot.')
2664 group
.add_argument('--build_preference',
2665 choices
=['msvs', 'ninja', 'make'],
2666 help='The preferred build system to use. On linux/mac '
2667 'the options are make/ninja. On Windows, the '
2668 'options are msvs/ninja.')
2669 group
.add_argument('--target_platform', default
='chromium',
2670 choices
=['chromium', 'android', 'android-chrome'],
2671 help='The target platform. Choices are "chromium" '
2672 '(current platform), or "android". If you specify '
2673 'something other than "chromium", you must be '
2674 'properly set up to build that platform.')
2675 group
.add_argument('--no_custom_deps', dest
='no_custom_deps',
2676 action
='store_true', default
=False,
2677 help='Run the script with custom_deps or not.')
2678 group
.add_argument('--extra_src',
2679 help='Path to a script which can be used to modify the '
2680 'bisect script\'s behavior.')
2681 group
.add_argument('--use_goma', action
='store_true',
2682 help='Add a bunch of extra threads for goma, and enable '
2684 group
.add_argument('--goma_dir',
2685 help='Path to goma tools (or system default if not '
2687 group
.add_argument('--goma_threads', type=int, default
='64',
2688 help='Number of threads for goma, only if using goma.')
2689 group
.add_argument('--output_buildbot_annotations', action
='store_true',
2690 help='Add extra annotation output for buildbot.')
2691 group
.add_argument('--target_arch', default
='ia32',
2692 dest
='target_arch', choices
=['ia32', 'x64', 'arm'],
2693 help='The target build architecture. Choices are "ia32" '
2694 '(default), "x64" or "arm".')
2695 group
.add_argument('--target_build_type', default
='Release',
2696 choices
=['Release', 'Debug', 'Release_x64'],
2697 help='The target build type. Choices are "Release" '
2698 '(default), Release_x64 or "Debug".')
2699 group
.add_argument('--builder_type', default
=fetch_build
.PERF_BUILDER
,
2700 choices
=[fetch_build
.PERF_BUILDER
,
2701 fetch_build
.FULL_BUILDER
,
2702 fetch_build
.ANDROID_CHROME_PERF_BUILDER
, ''],
2703 help='Type of builder to get build from. This '
2704 'determines both the bot that builds and the '
2705 'place where archived builds are downloaded from. '
2706 'For local builds, an empty string can be passed.')
2709 def _AddDebugOptionsGroup(parser
):
2710 group
= parser
.add_argument_group('Debug options')
2711 group
.add_argument('--debug_ignore_build', action
='store_true',
2712 help='DEBUG: Don\'t perform builds.')
2713 group
.add_argument('--debug_ignore_sync', action
='store_true',
2714 help='DEBUG: Don\'t perform syncs.')
2715 group
.add_argument('--debug_ignore_perf_test', action
='store_true',
2716 help='DEBUG: Don\'t perform performance tests.')
2717 group
.add_argument('--debug_ignore_regression_confidence',
2718 action
='store_true',
2719 help='DEBUG: Don\'t score the confidence of the initial '
2720 'good and bad revisions\' test results.')
2721 group
.add_argument('--debug_fake_first_test_mean', type=int, default
='0',
2722 help='DEBUG: When faking performance tests, return this '
2723 'value as the mean of the first performance test, '
2724 'and return a mean of 0.0 for further tests.')
2728 def _CreateCommandLineParser(cls
):
2729 """Creates a parser with bisect options.
2732 An instance of argparse.ArgumentParser.
2734 usage
= ('%(prog)s [options] [-- chromium-options]\n'
2735 'Perform binary search on revision history to find a minimal '
2736 'range of revisions where a performance metric regressed.\n')
2738 parser
= argparse
.ArgumentParser(usage
=usage
)
2739 cls
._AddBisectOptionsGroup
(parser
)
2740 cls
._AddBuildOptionsGroup
(parser
)
2741 cls
._AddDebugOptionsGroup
(parser
)
2744 def ParseCommandLine(self
):
2745 """Parses the command line for bisect options."""
2746 parser
= self
._CreateCommandLineParser
()
2747 opts
= parser
.parse_args()
2750 if (not opts
.metric
and
2751 opts
.bisect_mode
!= bisect_utils
.BISECT_MODE_RETURN_CODE
):
2752 raise RuntimeError('missing required parameter: --metric')
2754 if opts
.bisect_mode
!= bisect_utils
.BISECT_MODE_RETURN_CODE
:
2755 metric_values
= opts
.metric
.split('/')
2756 if len(metric_values
) != 2:
2757 raise RuntimeError('Invalid metric specified: [%s]' % opts
.metric
)
2758 opts
.metric
= metric_values
2760 opts
.truncate_percent
= min(max(opts
.truncate_percent
, 0), 25) / 100.0
2762 for k
, v
in opts
.__dict
__.iteritems():
2763 assert hasattr(self
, k
), 'Invalid %s attribute in BisectOptions.' % k
2765 except RuntimeError, e
:
2766 output_string
= StringIO
.StringIO()
2767 parser
.print_help(file=output_string
)
2768 error_message
= '%s\n\n%s' % (e
.message
, output_string
.getvalue())
2769 output_string
.close()
2770 raise RuntimeError(error_message
)
2773 def FromDict(values
):
2774 """Creates an instance of BisectOptions from a dictionary.
2777 values: a dict containing options to set.
2780 An instance of BisectOptions.
2782 opts
= BisectOptions()
2783 for k
, v
in values
.iteritems():
2784 assert hasattr(opts
, k
), 'Invalid %s attribute in BisectOptions.' % k
2787 if opts
.metric
and opts
.bisect_mode
!= bisect_utils
.BISECT_MODE_RETURN_CODE
:
2788 metric_values
= opts
.metric
.split('/')
2789 if len(metric_values
) != 2:
2790 raise RuntimeError('Invalid metric specified: [%s]' % opts
.metric
)
2791 opts
.metric
= metric_values
2793 if opts
.target_arch
== 'x64' and opts
.target_build_type
== 'Release':
2794 opts
.target_build_type
= 'Release_x64'
2795 opts
.repeat_test_count
= min(max(opts
.repeat_test_count
, 1), 100)
2796 opts
.max_time_minutes
= min(max(opts
.max_time_minutes
, 1), 60)
2797 opts
.truncate_percent
= min(max(opts
.truncate_percent
, 0), 25)
2798 opts
.truncate_percent
= opts
.truncate_percent
/ 100.0
2803 def _ConfigureLogging():
2804 """Trivial logging config.
2806 Configures logging to output any messages at or above INFO to standard out,
2807 without any additional formatting.
2809 logging_format
= '%(message)s'
2810 logging
.basicConfig(
2811 stream
=logging
.sys
.stdout
, level
=logging
.INFO
, format
=logging_format
)
2817 opts
= BisectOptions()
2818 opts
.ParseCommandLine()
2821 if opts
.output_buildbot_annotations
:
2822 bisect_utils
.OutputAnnotationStepStart('Checking Issue Tracker')
2823 issue_closed
= query_crbug
.CheckIssueClosed(opts
.bug_id
)
2825 print 'Aborting bisect because bug is closed'
2827 print 'Could not confirm bug is closed, proceeding.'
2828 if opts
.output_buildbot_annotations
:
2829 bisect_utils
.OutputAnnotationStepClosed()
2831 results
= BisectResults(abort_reason
='the bug is closed.')
2832 bisect_test
= BisectPerformanceMetrics(opts
, os
.getcwd())
2833 bisect_test
.printer
.FormatAndPrintResults(results
)
2837 extra_src
= bisect_utils
.LoadExtraSrc(opts
.extra_src
)
2839 raise RuntimeError('Invalid or missing --extra_src.')
2840 bisect_utils
.AddAdditionalDepotInfo(extra_src
.GetAdditionalDepotInfo())
2842 if opts
.working_directory
:
2843 custom_deps
= bisect_utils
.DEFAULT_GCLIENT_CUSTOM_DEPS
2844 if opts
.no_custom_deps
:
2846 bisect_utils
.CreateBisectDirectoryAndSetupDepot(opts
, custom_deps
)
2848 os
.chdir(os
.path
.join(os
.getcwd(), 'src'))
2849 RemoveBuildFiles(opts
.target_build_type
)
2851 if not _IsPlatformSupported():
2852 raise RuntimeError('Sorry, this platform isn\'t supported yet.')
2854 if not source_control
.IsInGitRepository():
2856 'Sorry, only the git workflow is supported at the moment.')
2858 # gClient sync seems to fail if you're not in master branch.
2859 if (not source_control
.IsInProperBranch() and
2860 not opts
.debug_ignore_sync
and
2861 not opts
.working_directory
):
2862 raise RuntimeError('You must switch to master branch to run bisection.')
2863 bisect_test
= BisectPerformanceMetrics(opts
, os
.getcwd())
2865 results
= bisect_test
.Run(opts
.command
, opts
.bad_revision
,
2866 opts
.good_revision
, opts
.metric
)
2868 raise RuntimeError(results
.error
)
2869 bisect_test
.printer
.FormatAndPrintResults(results
)
2872 bisect_test
.PerformCleanup()
2873 except RuntimeError as e
:
2874 if opts
.output_buildbot_annotations
:
2875 # The perf dashboard scrapes the "results" step in order to comment on
2876 # bugs. If you change this, please update the perf dashboard as well.
2877 bisect_utils
.OutputAnnotationStepStart('Results')
2878 print 'Runtime Error: %s' % e
2879 if opts
.output_buildbot_annotations
:
2880 bisect_utils
.OutputAnnotationStepClosed()
2884 if __name__
== '__main__':