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)
1367 # If there's a failed test, we can bail out early.
1371 elapsed_minutes = (time.time() - start_time) / 60.0
1372 time_limit = self.opts.max_time_minutes * test_run_multiplier
1373 if elapsed_minutes >= time_limit:
1376 if metric and len(metric_values) == 0:
1377 err_text = 'Metric %s was not found in the test output.' % metric
1378 # TODO(qyearsley): Consider also getting and displaying a list of metrics
1379 # that were found in the output here.
1380 return (err_text, failure_code, output_of_all_runs)
1382 # If we're bisecting on return codes, we're really just looking for zero vs
1385 if self._IsBisectModeReturnCode():
1386 # If any of the return codes is non-zero, output 1.
1387 overall_return_code = 0 if (
1388 all(current_value == 0 for current_value in metric_values)) else 1
1391 'mean': overall_return_code,
1394 'values': metric_values,
1397 print 'Results of performance test: Command returned with %d' % (
1398 overall_return_code)
1401 # Need to get the average value if there were multiple values.
1402 truncated_mean = math_utils.TruncatedMean(
1403 metric_values, self.opts.truncate_percent)
1404 standard_err = math_utils.StandardError(metric_values)
1405 standard_dev = math_utils.StandardDeviation(metric_values)
1407 if self._IsBisectModeStandardDeviation():
1408 metric_values = [standard_dev]
1411 'mean': truncated_mean,
1412 'std_err': standard_err,
1413 'std_dev': standard_dev,
1414 'values': metric_values,
1417 print 'Results of performance test: %12f %12f' % (
1418 truncated_mean, standard_err)
1421 overall_success = success_code
1422 if not allow_flakes and not self._IsBisectModeReturnCode():
1425 if (all(current_value == 0 for current_value in return_codes))
1428 return (values, overall_success, output_of_all_runs)
1430 def PerformPreBuildCleanup(self):
1431 """Performs cleanup between runs."""
1432 print 'Cleaning up between runs.'
1435 # Leaving these .pyc files around between runs may disrupt some perf tests.
1436 for (path, _, files) in os.walk(self.src_cwd):
1437 for cur_file in files:
1438 if cur_file.endswith('.pyc'):
1439 path_to_file = os.path.join(path, cur_file)
1440 os.remove(path_to_file)
1442 def _RunPostSync(self, _depot):
1443 """Performs any work after syncing.
1451 if 'android' in self.opts.target_platform:
1452 if not builder.SetupAndroidBuildEnvironment(
1453 self.opts, path_to_src=self.src_cwd):
1456 return self.RunGClientHooks()
1459 def ShouldSkipRevision(depot, revision):
1460 """Checks whether a particular revision can be safely skipped.
1462 Some commits can be safely skipped (such as a DEPS roll for the repos
1463 still using .DEPS.git), since the tool is git based those changes
1464 would have no effect.
1467 depot: The depot being bisected.
1468 revision: Current revision we're synced to.
1471 True if we should skip building/testing this revision.
1473 # Skips revisions with DEPS on android-chrome.
1474 if depot == 'android-chrome':
1475 cmd = ['diff-tree', '--no-commit-id', '--name-only', '-r', revision]
1476 output = bisect_utils.CheckRunGit(cmd)
1478 files = output.splitlines()
1480 if len(files) == 1 and files[0] == 'DEPS':
1485 def RunTest(self, revision, depot, command, metric, skippable=False,
1486 skip_sync=False, create_patch=False, force_build=False,
1487 test_run_multiplier=1):
1488 """Performs a full sync/build/run of the specified revision.
1491 revision: The revision to sync to.
1492 depot: The depot that's being used at the moment (src, webkit, etc.)
1493 command: The command to execute the performance test.
1494 metric: The performance metric being tested.
1495 skip_sync: Skip the sync step.
1496 create_patch: Create a patch with any locally modified files.
1497 force_build: Force a local build.
1498 test_run_multiplier: Factor by which to multiply the given number of runs
1499 and the set timeout period.
1502 On success, a tuple containing the results of the performance test.
1503 Otherwise, a tuple with the error message.
1505 logging.info('Running RunTest with rev "%s", command "%s"',
1507 # Decide which sync program to use.
1509 if depot == 'chromium' or depot == 'android-chrome':
1510 sync_client = 'gclient'
1512 # Do the syncing for all depots.
1513 if not (self.opts.debug_ignore_sync or skip_sync):
1514 if not self._SyncRevision(depot, revision, sync_client):
1515 return ('Failed to sync: [%s]' % str(revision), BUILD_RESULT_FAIL)
1517 # Try to do any post-sync steps. This may include "gclient runhooks
".
1518 if not self._RunPostSync(depot):
1519 return ('Failed to run [gclient runhooks].', BUILD_RESULT_FAIL)
1521 # Skip this revision if it can be skipped.
1522 if skippable and self.ShouldSkipRevision(depot, revision):
1523 return ('Skipped revision: [%s]' % str(revision),
1524 BUILD_RESULT_SKIPPED)
1526 # Obtain a build for this revision. This may be done by requesting a build
1527 # from another builder, waiting for it and downloading it.
1528 start_build_time = time.time()
1529 revision_to_build = revision if not force_build else None
1530 build_success = self.ObtainBuild(
1531 depot, revision=revision_to_build, create_patch=create_patch)
1532 if not build_success:
1533 return ('Failed to build revision: [%s]' % str(revision),
1535 after_build_time = time.time()
1537 # Possibly alter the command.
1538 command = self.GetCompatibleCommand(command, revision, depot)
1540 # Run the command and get the results.
1541 results = self.RunPerformanceTestAndParseResults(
1542 command, metric, test_run_multiplier=test_run_multiplier)
1544 # Restore build output directory once the tests are done, to avoid
1545 # any discrepancies.
1546 if self.IsDownloadable(depot) and revision:
1547 self.BackupOrRestoreOutputDirectory(restore=True)
1549 # A value other than 0 indicates that the test couldn't be run, and results
1550 # should also include an error message.
1554 external_revisions = self._Get3rdPartyRevisions(depot)
1556 if not external_revisions is None:
1557 return (results[0], results[1], external_revisions,
1558 time.time() - after_build_time, after_build_time -
1561 return ('Failed to parse DEPS file for external revisions.',
1564 def _SyncRevision(self, depot, revision, sync_client):
1565 """Syncs depot to particular revision.
1568 depot: The depot that's being used at the moment (src, webkit, etc.)
1569 revision: The revision to sync to.
1570 sync_client: Program used to sync, e.g. "gclient
". Can be None.
1573 True if successful, False otherwise.
1575 self.depot_registry.ChangeToDepotDir(depot)
1578 self.PerformPreBuildCleanup()
1580 # When using gclient to sync, you need to specify the depot you
1581 # want so that all the dependencies sync properly as well.
1582 # i.e. gclient sync src@<SHA1>
1583 if sync_client == 'gclient' and revision:
1584 revision = '%s@%s' % (bisect_utils.DEPOT_DEPS_NAME[depot]['src'],
1586 if depot == 'chromium' and self.opts.target_platform == 'android-chrome':
1587 return self._SyncRevisionsForAndroidChrome(revision)
1589 return source_control.SyncToRevision(revision, sync_client)
1591 def _SyncRevisionsForAndroidChrome(self, revision):
1592 """Syncs android-chrome and chromium repos to particular revision.
1594 This is a special case for android-chrome as the gclient sync for chromium
1595 overwrites the android-chrome revision to TOT. Therefore both the repos
1596 are synced to known revisions.
1599 revision: Git hash of the Chromium to sync.
1602 True if successful, False otherwise.
1604 revisions_list = [revision]
1605 current_android_rev = source_control.GetCurrentRevision(
1606 self.depot_registry.GetDepotDir('android-chrome'))
1607 revisions_list.append(
1608 '%s@%s' % (bisect_utils.DEPOT_DEPS_NAME['android-chrome']['src'],
1609 current_android_rev))
1610 return not bisect_utils.RunGClientAndSync(revisions_list)
1612 def _CheckIfRunPassed(self, current_value, known_good_value, known_bad_value):
1613 """Given known good and bad values, decide if the current_value passed
1617 current_value: The value of the metric being checked.
1618 known_bad_value: The reference value for a "failed
" run.
1619 known_good_value: The reference value for a "passed
" run.
1622 True if the current_value is closer to the known_good_value than the
1625 if self.opts.bisect_mode == bisect_utils.BISECT_MODE_STD_DEV:
1626 dist_to_good_value = abs(current_value['std_dev'] -
1627 known_good_value['std_dev'])
1628 dist_to_bad_value = abs(current_value['std_dev'] -
1629 known_bad_value['std_dev'])
1631 dist_to_good_value = abs(current_value['mean'] - known_good_value['mean'])
1632 dist_to_bad_value = abs(current_value['mean'] - known_bad_value['mean'])
1634 return dist_to_good_value < dist_to_bad_value
1636 def _GetV8BleedingEdgeFromV8TrunkIfMappable(
1637 self, revision, bleeding_edge_branch):
1638 """Gets v8 bleeding edge revision mapped to v8 revision in trunk.
1641 revision: A trunk V8 revision mapped to bleeding edge revision.
1642 bleeding_edge_branch: Branch used to perform lookup of bleeding edge
1645 A mapped bleeding edge revision if found, otherwise None.
1647 commit_position = source_control.GetCommitPosition(revision)
1649 if bisect_utils.IsStringInt(commit_position):
1650 # V8 is tricky to bisect, in that there are only a few instances when
1651 # we can dive into bleeding_edge and get back a meaningful result.
1652 # Try to detect a V8 "business
as usual
" case, which is when:
1653 # 1. trunk revision N has description "Version X
.Y
.Z
"
1654 # 2. bleeding_edge revision (N-1) has description "Prepare push to
1655 # trunk. Now working on X.Y.(Z+1)."
1657 # As of 01/24/2014, V8 trunk descriptions are formatted:
1658 # "Version 3.X.Y (based on bleeding_edge revision rZ)"
1659 # So we can just try parsing that out first and fall back to the old way.
1660 v8_dir
= self
.depot_registry
.GetDepotDir('v8')
1661 v8_bleeding_edge_dir
= self
.depot_registry
.GetDepotDir('v8_bleeding_edge')
1663 revision_info
= source_control
.QueryRevisionInfo(revision
, cwd
=v8_dir
)
1664 version_re
= re
.compile("Version (?P<values>[0-9,.]+)")
1665 regex_results
= version_re
.search(revision_info
['subject'])
1668 if 'based on bleeding_edge' in revision_info
['subject']:
1670 bleeding_edge_revision
= revision_info
['subject'].split(
1671 'bleeding_edge revision r')[1]
1672 bleeding_edge_revision
= int(bleeding_edge_revision
.split(')')[0])
1673 bleeding_edge_url
= ('https://v8.googlecode.com/svn/branches/'
1674 'bleeding_edge@%s' % bleeding_edge_revision
)
1680 bleeding_edge_branch
]
1681 output
= bisect_utils
.CheckRunGit(cmd
, cwd
=v8_dir
)
1683 git_revision
= output
.strip()
1685 except (IndexError, ValueError):
1688 # V8 rolls description changed after V8 git migration, new description
1689 # includes "Version 3.X.Y (based on <git hash>)"
1691 rxp
= re
.compile('based on (?P<git_revision>[a-fA-F0-9]+)')
1692 re_results
= rxp
.search(revision_info
['subject'])
1694 return re_results
.group('git_revision')
1695 except (IndexError, ValueError):
1697 if not git_revision
:
1698 # Wasn't successful, try the old way of looking for "Prepare push to"
1699 git_revision
= source_control
.ResolveToRevision(
1700 int(commit_position
) - 1, 'v8_bleeding_edge',
1701 bisect_utils
.DEPOT_DEPS_NAME
, -1, cwd
=v8_bleeding_edge_dir
)
1704 revision_info
= source_control
.QueryRevisionInfo(
1705 git_revision
, cwd
=v8_bleeding_edge_dir
)
1707 if 'Prepare push to trunk' in revision_info
['subject']:
1711 def _GetNearestV8BleedingEdgeFromTrunk(
1712 self
, revision
, v8_branch
, bleeding_edge_branch
, search_forward
=True):
1713 """Gets the nearest V8 roll and maps to bleeding edge revision.
1715 V8 is a bit tricky to bisect since it isn't just rolled out like blink.
1716 Each revision on trunk might just be whatever was in bleeding edge, rolled
1717 directly out. Or it could be some mixture of previous v8 trunk versions,
1718 with bits and pieces cherry picked out from bleeding edge. In order to
1719 bisect, we need both the before/after versions on trunk v8 to be just pushes
1720 from bleeding edge. With the V8 git migration, the branches got switched.
1721 a) master (external/v8) == candidates (v8/v8)
1722 b) bleeding_edge (external/v8) == master (v8/v8)
1725 revision: A V8 revision to get its nearest bleeding edge revision
1726 search_forward: Searches forward if True, otherwise search backward.
1729 A mapped bleeding edge revision if found, otherwise None.
1731 cwd
= self
.depot_registry
.GetDepotDir('v8')
1732 cmd
= ['log', '--format=%ct', '-1', revision
]
1733 output
= bisect_utils
.CheckRunGit(cmd
, cwd
=cwd
)
1734 commit_time
= int(output
)
1739 '--after=%d' % commit_time
,
1742 output
= bisect_utils
.CheckRunGit(cmd
, cwd
=cwd
)
1743 output
= output
.split()
1745 #Get 10 git hashes immediately after the given commit.
1746 commits
= commits
[:10]
1751 '--before=%d' % commit_time
,
1753 output
= bisect_utils
.CheckRunGit(cmd
, cwd
=cwd
)
1754 output
= output
.split()
1757 bleeding_edge_revision
= None
1760 bleeding_edge_revision
= self
._GetV
8BleedingEdgeFromV
8TrunkIfMappable
(
1761 c
, bleeding_edge_branch
)
1762 if bleeding_edge_revision
:
1765 return bleeding_edge_revision
1767 def _FillInV8BleedingEdgeInfo(self
, min_revision_state
, max_revision_state
):
1768 cwd
= self
.depot_registry
.GetDepotDir('v8')
1769 # when "remote.origin.url" is https://chromium.googlesource.com/v8/v8.git
1770 v8_branch
= 'origin/candidates'
1771 bleeding_edge_branch
= 'origin/master'
1773 # Support for the chromium revisions with external V8 repo.
1774 # ie https://chromium.googlesource.com/external/v8.git
1775 cmd
= ['config', '--get', 'remote.origin.url']
1776 v8_repo_url
= bisect_utils
.CheckRunGit(cmd
, cwd
=cwd
)
1778 if 'external/v8.git' in v8_repo_url
:
1779 v8_branch
= 'origin/master'
1780 bleeding_edge_branch
= 'origin/bleeding_edge'
1782 r1
= self
._GetNearestV
8BleedingEdgeFromTrunk
(
1783 min_revision_state
.revision
,
1785 bleeding_edge_branch
,
1786 search_forward
=True)
1787 r2
= self
._GetNearestV
8BleedingEdgeFromTrunk
(
1788 max_revision_state
.revision
,
1790 bleeding_edge_branch
,
1791 search_forward
=False)
1792 min_revision_state
.external
['v8_bleeding_edge'] = r1
1793 max_revision_state
.external
['v8_bleeding_edge'] = r2
1795 if (not self
._GetV
8BleedingEdgeFromV
8TrunkIfMappable
(
1796 min_revision_state
.revision
, bleeding_edge_branch
)
1797 or not self
._GetV
8BleedingEdgeFromV
8TrunkIfMappable
(
1798 max_revision_state
.revision
, bleeding_edge_branch
)):
1799 self
.warnings
.append(
1800 'Trunk revisions in V8 did not map directly to bleeding_edge. '
1801 'Attempted to expand the range to find V8 rolls which did map '
1802 'directly to bleeding_edge revisions, but results might not be '
1805 def _FindNextDepotToBisect(
1806 self
, current_depot
, min_revision_state
, max_revision_state
):
1807 """Decides which depot the script should dive into next (if any).
1810 current_depot: Current depot being bisected.
1811 min_revision_state: State of the earliest revision in the bisect range.
1812 max_revision_state: State of the latest revision in the bisect range.
1815 Name of the depot to bisect next, or None.
1817 external_depot
= None
1818 for next_depot
in bisect_utils
.DEPOT_NAMES
:
1819 if ('platform' in bisect_utils
.DEPOT_DEPS_NAME
[next_depot
] and
1820 bisect_utils
.DEPOT_DEPS_NAME
[next_depot
]['platform'] != os
.name
):
1823 if not (bisect_utils
.DEPOT_DEPS_NAME
[next_depot
]['recurse']
1824 and min_revision_state
.depot
1825 in bisect_utils
.DEPOT_DEPS_NAME
[next_depot
]['from']):
1828 if current_depot
== 'v8':
1829 # We grab the bleeding_edge info here rather than earlier because we
1830 # finally have the revision range. From that we can search forwards and
1831 # backwards to try to match trunk revisions to bleeding_edge.
1832 self
._FillInV
8BleedingEdgeInfo
(min_revision_state
, max_revision_state
)
1834 if (min_revision_state
.external
.get(next_depot
) ==
1835 max_revision_state
.external
.get(next_depot
)):
1838 if (min_revision_state
.external
.get(next_depot
) and
1839 max_revision_state
.external
.get(next_depot
)):
1840 external_depot
= next_depot
1843 return external_depot
1845 def PrepareToBisectOnDepot(
1846 self
, current_depot
, start_revision
, end_revision
, previous_revision
):
1847 """Changes to the appropriate directory and gathers a list of revisions
1848 to bisect between |start_revision| and |end_revision|.
1851 current_depot: The depot we want to bisect.
1852 start_revision: Start of the revision range.
1853 end_revision: End of the revision range.
1854 previous_revision: The last revision we synced to on |previous_depot|.
1857 A list containing the revisions between |start_revision| and
1858 |end_revision| inclusive.
1860 # Change into working directory of external library to run
1861 # subsequent commands.
1862 self
.depot_registry
.ChangeToDepotDir(current_depot
)
1864 # V8 (and possibly others) is merged in periodically. Bisecting
1865 # this directory directly won't give much good info.
1866 if 'custom_deps' in bisect_utils
.DEPOT_DEPS_NAME
[current_depot
]:
1867 config_path
= os
.path
.join(self
.src_cwd
, '..')
1868 if bisect_utils
.RunGClientAndCreateConfig(
1869 self
.opts
, bisect_utils
.DEPOT_DEPS_NAME
[current_depot
]['custom_deps'],
1872 if bisect_utils
.RunGClient(
1873 ['sync', '--revision', previous_revision
], cwd
=self
.src_cwd
):
1876 if current_depot
== 'v8_bleeding_edge':
1877 self
.depot_registry
.ChangeToDepotDir('chromium')
1879 shutil
.move('v8', 'v8.bak')
1880 shutil
.move('v8_bleeding_edge', 'v8')
1882 self
.cleanup_commands
.append(['mv', 'v8', 'v8_bleeding_edge'])
1883 self
.cleanup_commands
.append(['mv', 'v8.bak', 'v8'])
1885 self
.depot_registry
.SetDepotDir(
1886 'v8_bleeding_edge', os
.path
.join(self
.src_cwd
, 'v8'))
1887 self
.depot_registry
.SetDepotDir(
1888 'v8', os
.path
.join(self
.src_cwd
, 'v8.bak'))
1890 self
.depot_registry
.ChangeToDepotDir(current_depot
)
1892 depot_revision_list
= self
.GetRevisionList(current_depot
,
1896 self
.depot_registry
.ChangeToDepotDir('chromium')
1898 return depot_revision_list
1900 def GatherReferenceValues(self
, good_rev
, bad_rev
, cmd
, metric
, target_depot
):
1901 """Gathers reference values by running the performance tests on the
1902 known good and bad revisions.
1905 good_rev: The last known good revision where the performance regression
1906 has not occurred yet.
1907 bad_rev: A revision where the performance regression has already occurred.
1908 cmd: The command to execute the performance test.
1909 metric: The metric being tested for regression.
1912 A tuple with the results of building and running each revision.
1914 bad_run_results
= self
.RunTest(bad_rev
, target_depot
, cmd
, metric
)
1916 good_run_results
= None
1918 if not bad_run_results
[1]:
1919 good_run_results
= self
.RunTest(good_rev
, target_depot
, cmd
, metric
)
1921 return (bad_run_results
, good_run_results
)
1923 def PrintRevisionsToBisectMessage(self
, revision_list
, depot
):
1924 if self
.opts
.output_buildbot_annotations
:
1925 step_name
= 'Bisection Range: [%s:%s - %s]' % (depot
, revision_list
[-1],
1927 bisect_utils
.OutputAnnotationStepStart(step_name
)
1930 print 'Revisions to bisect on [%s]:' % depot
1931 for revision_id
in revision_list
:
1932 print ' -> %s' % (revision_id
, )
1935 if self
.opts
.output_buildbot_annotations
:
1936 bisect_utils
.OutputAnnotationStepClosed()
1938 def NudgeRevisionsIfDEPSChange(self
, bad_revision
, good_revision
,
1939 good_svn_revision
=None):
1940 """Checks to see if changes to DEPS file occurred, and that the revision
1941 range also includes the change to .DEPS.git. If it doesn't, attempts to
1942 expand the revision range to include it.
1945 bad_revision: First known bad git revision.
1946 good_revision: Last known good git revision.
1947 good_svn_revision: Last known good svn revision.
1950 A tuple with the new bad and good revisions.
1952 # DONOT perform nudge because at revision 291563 .DEPS.git was removed
1953 # and source contain only DEPS file for dependency changes.
1954 if good_svn_revision
>= 291563:
1955 return (bad_revision
, good_revision
)
1957 if self
.opts
.target_platform
== 'chromium':
1958 changes_to_deps
= source_control
.QueryFileRevisionHistory(
1959 bisect_utils
.FILE_DEPS
, good_revision
, bad_revision
)
1962 # DEPS file was changed, search from the oldest change to DEPS file to
1963 # bad_revision to see if there are matching .DEPS.git changes.
1964 oldest_deps_change
= changes_to_deps
[-1]
1965 changes_to_gitdeps
= source_control
.QueryFileRevisionHistory(
1966 bisect_utils
.FILE_DEPS_GIT
, oldest_deps_change
, bad_revision
)
1968 if len(changes_to_deps
) != len(changes_to_gitdeps
):
1969 # Grab the timestamp of the last DEPS change
1970 cmd
= ['log', '--format=%ct', '-1', changes_to_deps
[0]]
1971 output
= bisect_utils
.CheckRunGit(cmd
)
1972 commit_time
= int(output
)
1974 # Try looking for a commit that touches the .DEPS.git file in the
1975 # next 15 minutes after the DEPS file change.
1977 'log', '--format=%H', '-1',
1978 '--before=%d' % (commit_time
+ 900),
1979 '--after=%d' % commit_time
,
1980 'origin/master', '--', bisect_utils
.FILE_DEPS_GIT
1982 output
= bisect_utils
.CheckRunGit(cmd
)
1983 output
= output
.strip()
1985 self
.warnings
.append(
1986 'Detected change to DEPS and modified '
1987 'revision range to include change to .DEPS.git')
1988 return (output
, good_revision
)
1990 self
.warnings
.append(
1991 'Detected change to DEPS but couldn\'t find '
1992 'matching change to .DEPS.git')
1993 return (bad_revision
, good_revision
)
1995 def CheckIfRevisionsInProperOrder(
1996 self
, target_depot
, good_revision
, bad_revision
):
1997 """Checks that |good_revision| is an earlier revision than |bad_revision|.
2000 good_revision: Number/tag of the known good revision.
2001 bad_revision: Number/tag of the known bad revision.
2004 True if the revisions are in the proper order (good earlier than bad).
2006 cwd
= self
.depot_registry
.GetDepotDir(target_depot
)
2007 good_position
= source_control
.GetCommitPosition(good_revision
, cwd
)
2008 bad_position
= source_control
.GetCommitPosition(bad_revision
, cwd
)
2009 # Compare commit timestamp for repos that don't support commit position.
2010 if not (bad_position
and good_position
):
2011 logging
.info('Could not get commit positions for revisions %s and %s in '
2012 'depot %s', good_position
, bad_position
, target_depot
)
2013 good_position
= source_control
.GetCommitTime(good_revision
, cwd
=cwd
)
2014 bad_position
= source_control
.GetCommitTime(bad_revision
, cwd
=cwd
)
2016 return good_position
<= bad_position
2018 def CanPerformBisect(self
, good_revision
, bad_revision
):
2019 """Checks whether a given revision is bisectable.
2021 Checks for following:
2022 1. Non-bisectable revisions for android bots (refer to crbug.com/385324).
2023 2. Non-bisectable revisions for Windows bots (refer to crbug.com/405274).
2026 good_revision: Known good revision.
2027 bad_revision: Known bad revision.
2030 A dictionary indicating the result. If revision is not bisectable,
2031 this will contain the field "error", otherwise None.
2033 if self
.opts
.target_platform
== 'android':
2034 good_revision
= source_control
.GetCommitPosition(good_revision
)
2035 if (bisect_utils
.IsStringInt(good_revision
)
2036 and good_revision
< 265549):
2038 'Bisect cannot continue for the given revision range.\n'
2039 'It is impossible to bisect Android regressions '
2040 'prior to r265549, which allows the bisect bot to '
2041 'rely on Telemetry to do apk installation of the most recently '
2042 'built local ChromeShell(refer to crbug.com/385324).\n'
2043 'Please try bisecting revisions greater than or equal to r265549.')}
2045 if bisect_utils
.IsWindowsHost():
2046 good_revision
= source_control
.GetCommitPosition(good_revision
)
2047 bad_revision
= source_control
.GetCommitPosition(bad_revision
)
2048 if (bisect_utils
.IsStringInt(good_revision
) and
2049 bisect_utils
.IsStringInt(bad_revision
)):
2050 if (289987 <= good_revision
< 290716 or
2051 289987 <= bad_revision
< 290716):
2052 return {'error': ('Oops! Revision between r289987 and r290716 are '
2053 'marked as dead zone for Windows due to '
2054 'crbug.com/405274. Please try another range.')}
2058 def _GatherResultsFromRevertedCulpritCL(
2059 self
, results
, target_depot
, command_to_run
, metric
):
2060 """Gathers performance results with/without culprit CL.
2062 Attempts to revert the culprit CL against ToT and runs the
2063 performance tests again with and without the CL, adding the results to
2064 the over bisect results.
2067 results: BisectResults from the bisect.
2068 target_depot: The target depot we're bisecting.
2069 command_to_run: Specify the command to execute the performance test.
2070 metric: The performance metric to monitor.
2072 run_results_tot
, run_results_reverted
= self
._RevertCulpritCLAndRetest
(
2073 results
, target_depot
, command_to_run
, metric
)
2075 results
.AddRetestResults(run_results_tot
, run_results_reverted
)
2077 if len(results
.culprit_revisions
) != 1:
2080 # Cleanup reverted files if anything is left.
2081 _
, _
, culprit_depot
= results
.culprit_revisions
[0]
2082 bisect_utils
.CheckRunGit(
2083 ['reset', '--hard', 'HEAD'],
2084 cwd
=self
.depot_registry
.GetDepotDir(culprit_depot
))
2086 def _RevertCL(self
, culprit_revision
, culprit_depot
):
2087 """Reverts the specified revision in the specified depot."""
2088 if self
.opts
.output_buildbot_annotations
:
2089 bisect_utils
.OutputAnnotationStepStart(
2090 'Reverting culprit CL: %s' % culprit_revision
)
2091 _
, return_code
= bisect_utils
.RunGit(
2092 ['revert', '--no-commit', culprit_revision
],
2093 cwd
=self
.depot_registry
.GetDepotDir(culprit_depot
))
2095 bisect_utils
.OutputAnnotationStepWarning()
2096 bisect_utils
.OutputAnnotationStepText('Failed to revert CL cleanly.')
2097 if self
.opts
.output_buildbot_annotations
:
2098 bisect_utils
.OutputAnnotationStepClosed()
2099 return not return_code
2101 def _RevertCulpritCLAndRetest(
2102 self
, results
, target_depot
, command_to_run
, metric
):
2103 """Reverts the culprit CL against ToT and runs the performance test.
2105 Attempts to revert the culprit CL against ToT and runs the
2106 performance tests again with and without the CL.
2109 results: BisectResults from the bisect.
2110 target_depot: The target depot we're bisecting.
2111 command_to_run: Specify the command to execute the performance test.
2112 metric: The performance metric to monitor.
2115 A tuple with the results of running the CL at ToT/reverted.
2117 # Might want to retest ToT with a revert of the CL to confirm that
2118 # performance returns.
2119 if results
.confidence
< bisect_utils
.HIGH_CONFIDENCE
:
2122 # If there were multiple culprit CLs, we won't try to revert.
2123 if len(results
.culprit_revisions
) != 1:
2126 culprit_revision
, _
, culprit_depot
= results
.culprit_revisions
[0]
2128 if not self
._SyncRevision
(target_depot
, None, 'gclient'):
2131 head_revision
= bisect_utils
.CheckRunGit(['log', '--format=%H', '-1'])
2132 head_revision
= head_revision
.strip()
2134 if not self
._RevertCL
(culprit_revision
, culprit_depot
):
2137 # If the culprit CL happened to be in a depot that gets pulled in, we
2138 # can't revert the change and issue a try job to build, since that would
2139 # require modifying both the DEPS file and files in another depot.
2140 # Instead, we build locally.
2141 force_build
= (culprit_depot
!= target_depot
)
2143 results
.warnings
.append(
2144 'Culprit CL is in another depot, attempting to revert and build'
2145 ' locally to retest. This may not match the performance of official'
2148 run_results_reverted
= self
._RunTestWithAnnotations
(
2149 'Re-Testing ToT with reverted culprit',
2150 'Failed to run reverted CL.',
2151 head_revision
, target_depot
, command_to_run
, metric
, force_build
)
2153 # Clear the reverted file(s).
2154 bisect_utils
.RunGit(
2155 ['reset', '--hard', 'HEAD'],
2156 cwd
=self
.depot_registry
.GetDepotDir(culprit_depot
))
2158 # Retesting with the reverted CL failed, so bail out of retesting against
2160 if run_results_reverted
[1]:
2163 run_results_tot
= self
._RunTestWithAnnotations
(
2165 'Failed to run ToT.',
2166 head_revision
, target_depot
, command_to_run
, metric
, force_build
)
2168 return (run_results_tot
, run_results_reverted
)
2170 def _RunTestWithAnnotations(
2171 self
, step_text
, error_text
, head_revision
,
2172 target_depot
, command_to_run
, metric
, force_build
):
2173 """Runs the performance test and outputs start/stop annotations.
2176 results: BisectResults from the bisect.
2177 target_depot: The target depot we're bisecting.
2178 command_to_run: Specify the command to execute the performance test.
2179 metric: The performance metric to monitor.
2180 force_build: Whether to force a build locally.
2183 Results of the test.
2185 if self
.opts
.output_buildbot_annotations
:
2186 bisect_utils
.OutputAnnotationStepStart(step_text
)
2188 # Build and run the test again with the reverted culprit CL against ToT.
2189 run_test_results
= self
.RunTest(
2190 head_revision
, target_depot
, command_to_run
,
2191 metric
, skippable
=False, skip_sync
=True, create_patch
=True,
2192 force_build
=force_build
)
2194 if self
.opts
.output_buildbot_annotations
:
2195 if run_test_results
[1]:
2196 bisect_utils
.OutputAnnotationStepWarning()
2197 bisect_utils
.OutputAnnotationStepText(error_text
)
2198 bisect_utils
.OutputAnnotationStepClosed()
2200 return run_test_results
2202 def Run(self
, command_to_run
, bad_revision_in
, good_revision_in
, metric
):
2203 """Given known good and bad revisions, run a binary search on all
2204 intermediate revisions to determine the CL where the performance regression
2208 command_to_run: Specify the command to execute the performance test.
2209 good_revision: Number/tag of the known good revision.
2210 bad_revision: Number/tag of the known bad revision.
2211 metric: The performance metric to monitor.
2214 A BisectResults object.
2216 # Choose depot to bisect first
2217 target_depot
= 'chromium'
2218 if self
.opts
.target_platform
== 'android-chrome':
2219 target_depot
= 'android-chrome'
2222 self
.depot_registry
.ChangeToDepotDir(target_depot
)
2224 # If they passed SVN revisions, we can try match them to git SHA1 hashes.
2225 bad_revision
= source_control
.ResolveToRevision(
2226 bad_revision_in
, target_depot
, bisect_utils
.DEPOT_DEPS_NAME
, 100)
2227 good_revision
= source_control
.ResolveToRevision(
2228 good_revision_in
, target_depot
, bisect_utils
.DEPOT_DEPS_NAME
, -100)
2231 if bad_revision
is None:
2232 return BisectResults(
2233 error
='Couldn\'t resolve [%s] to SHA1.' % bad_revision_in
)
2235 if good_revision
is None:
2236 return BisectResults(
2237 error
='Couldn\'t resolve [%s] to SHA1.' % good_revision_in
)
2239 # Check that they didn't accidentally swap good and bad revisions.
2240 if not self
.CheckIfRevisionsInProperOrder(
2241 target_depot
, good_revision
, bad_revision
):
2242 return BisectResults(error
='Bad rev (%s) appears to be earlier than good '
2243 'rev (%s).' % (good_revision
, bad_revision
))
2245 bad_revision
, good_revision
= self
.NudgeRevisionsIfDEPSChange(
2246 bad_revision
, good_revision
, good_revision_in
)
2247 if self
.opts
.output_buildbot_annotations
:
2248 bisect_utils
.OutputAnnotationStepStart('Gathering Revisions')
2250 cannot_bisect
= self
.CanPerformBisect(good_revision
, bad_revision
)
2252 return BisectResults(error
=cannot_bisect
.get('error'))
2254 print 'Gathering revision range for bisection.'
2255 # Retrieve a list of revisions to do bisection on.
2256 revision_list
= self
.GetRevisionList(target_depot
, bad_revision
,
2259 if self
.opts
.output_buildbot_annotations
:
2260 bisect_utils
.OutputAnnotationStepClosed()
2263 self
.PrintRevisionsToBisectMessage(revision_list
, target_depot
)
2265 if self
.opts
.output_buildbot_annotations
:
2266 bisect_utils
.OutputAnnotationStepStart('Gathering Reference Values')
2268 print 'Gathering reference values for bisection.'
2270 # Perform the performance tests on the good and bad revisions, to get
2272 bad_results
, good_results
= self
.GatherReferenceValues(good_revision
,
2278 if self
.opts
.output_buildbot_annotations
:
2279 bisect_utils
.OutputAnnotationStepClosed()
2282 error
= ('An error occurred while building and running the \'bad\' '
2283 'reference value. The bisect cannot continue without '
2284 'a working \'bad\' revision to start from.\n\nError: %s' %
2286 return BisectResults(error
=error
)
2289 error
= ('An error occurred while building and running the \'good\' '
2290 'reference value. The bisect cannot continue without '
2291 'a working \'good\' revision to start from.\n\nError: %s' %
2293 return BisectResults(error
=error
)
2295 # We need these reference values to determine if later runs should be
2296 # classified as pass or fail.
2297 known_bad_value
= bad_results
[0]
2298 known_good_value
= good_results
[0]
2300 # Check the direction of improvement only if the improvement_direction
2301 # option is set to a specific direction (1 for higher is better or -1 for
2303 improvement_dir
= self
.opts
.improvement_direction
2305 higher_is_better
= improvement_dir
> 0
2306 if higher_is_better
:
2307 message
= "Expecting higher values to be better for this metric, "
2309 message
= "Expecting lower values to be better for this metric, "
2310 metric_increased
= known_bad_value
['mean'] > known_good_value
['mean']
2311 if metric_increased
:
2312 message
+= "and the metric appears to have increased. "
2314 message
+= "and the metric appears to have decreased. "
2315 if ((higher_is_better
and metric_increased
) or
2316 (not higher_is_better
and not metric_increased
)):
2317 error
= (message
+ 'Then, the test results for the ends of the given '
2318 '\'good\' - \'bad\' range of revisions represent an '
2319 'improvement (and not a regression).')
2320 return BisectResults(error
=error
)
2321 logging
.info(message
+ "Therefore we continue to bisect.")
2323 bisect_state
= BisectState(target_depot
, revision_list
)
2324 revision_states
= bisect_state
.GetRevisionStates()
2327 max_revision
= len(revision_states
) - 1
2329 # Can just mark the good and bad revisions explicitly here since we
2330 # already know the results.
2331 bad_revision_state
= revision_states
[min_revision
]
2332 bad_revision_state
.external
= bad_results
[2]
2333 bad_revision_state
.perf_time
= bad_results
[3]
2334 bad_revision_state
.build_time
= bad_results
[4]
2335 bad_revision_state
.passed
= False
2336 bad_revision_state
.value
= known_bad_value
2338 good_revision_state
= revision_states
[max_revision
]
2339 good_revision_state
.external
= good_results
[2]
2340 good_revision_state
.perf_time
= good_results
[3]
2341 good_revision_state
.build_time
= good_results
[4]
2342 good_revision_state
.passed
= True
2343 good_revision_state
.value
= known_good_value
2345 # Check how likely it is that the good and bad results are different
2346 # beyond chance-induced variation.
2347 confidence_error
= False
2348 if not (self
.opts
.debug_ignore_regression_confidence
or
2349 self
._IsBisectModeReturnCode
()):
2350 confidence_error
= _CheckRegressionConfidenceError(good_revision
,
2354 if confidence_error
:
2355 # If there is no significant difference between "good" and "bad"
2356 # revision results, then the "bad revision" is considered "good".
2357 # TODO(qyearsley): Remove this if it is not necessary.
2358 bad_revision_state
.passed
= True
2359 self
.warnings
.append(confidence_error
)
2360 return BisectResults(bisect_state
, self
.depot_registry
, self
.opts
,
2364 if not revision_states
:
2367 if max_revision
- min_revision
<= 1:
2368 min_revision_state
= revision_states
[min_revision
]
2369 max_revision_state
= revision_states
[max_revision
]
2370 current_depot
= min_revision_state
.depot
2371 # TODO(sergiyb): Under which conditions can first two branches be hit?
2372 if min_revision_state
.passed
== '?':
2373 next_revision_index
= min_revision
2374 elif max_revision_state
.passed
== '?':
2375 next_revision_index
= max_revision
2376 elif current_depot
in ['android-chrome', 'chromium', 'v8']:
2377 previous_revision
= revision_states
[min_revision
].revision
2378 # If there were changes to any of the external libraries we track,
2379 # should bisect the changes there as well.
2380 external_depot
= self
._FindNextDepotToBisect
(
2381 current_depot
, min_revision_state
, max_revision_state
)
2382 # If there was no change in any of the external depots, the search
2384 if not external_depot
:
2385 if current_depot
== 'v8':
2386 self
.warnings
.append(
2387 'Unfortunately, V8 bisection couldn\'t '
2388 'continue any further. The script can only bisect into '
2389 'V8\'s bleeding_edge repository if both the current and '
2390 'previous revisions in trunk map directly to revisions in '
2394 earliest_revision
= max_revision_state
.external
[external_depot
]
2395 latest_revision
= min_revision_state
.external
[external_depot
]
2397 new_revision_list
= self
.PrepareToBisectOnDepot(
2398 external_depot
, earliest_revision
, latest_revision
,
2401 if not new_revision_list
:
2402 error
= ('An error occurred attempting to retrieve revision '
2403 'range: [%s..%s]' % (earliest_revision
, latest_revision
))
2404 return BisectResults(error
=error
)
2406 revision_states
= bisect_state
.CreateRevisionStatesAfter(
2407 external_depot
, new_revision_list
, current_depot
,
2410 # Reset the bisection and perform it on the newly inserted states.
2412 max_revision
= len(revision_states
) - 1
2414 print ('Regression in metric %s appears to be the result of '
2415 'changes in [%s].' % (metric
, external_depot
))
2417 revision_list
= [state
.revision
for state
in revision_states
]
2418 self
.PrintRevisionsToBisectMessage(revision_list
, external_depot
)
2424 next_revision_index
= (int((max_revision
- min_revision
) / 2) +
2427 next_revision_state
= revision_states
[next_revision_index
]
2428 next_revision
= next_revision_state
.revision
2429 next_depot
= next_revision_state
.depot
2431 self
.depot_registry
.ChangeToDepotDir(next_depot
)
2433 message
= 'Working on [%s:%s]' % (next_depot
, next_revision
)
2435 if self
.opts
.output_buildbot_annotations
:
2436 bisect_utils
.OutputAnnotationStepStart(message
)
2438 run_results
= self
.RunTest(next_revision
, next_depot
, command_to_run
,
2439 metric
, skippable
=True)
2441 # If the build is successful, check whether or not the metric
2443 if not run_results
[1]:
2444 if len(run_results
) > 2:
2445 next_revision_state
.external
= run_results
[2]
2446 next_revision_state
.perf_time
= run_results
[3]
2447 next_revision_state
.build_time
= run_results
[4]
2449 passed_regression
= self
._CheckIfRunPassed
(run_results
[0],
2453 next_revision_state
.passed
= passed_regression
2454 next_revision_state
.value
= run_results
[0]
2456 if passed_regression
:
2457 max_revision
= next_revision_index
2459 min_revision
= next_revision_index
2461 if run_results
[1] == BUILD_RESULT_SKIPPED
:
2462 next_revision_state
.passed
= 'Skipped'
2463 elif run_results
[1] == BUILD_RESULT_FAIL
:
2464 next_revision_state
.passed
= 'Build Failed'
2466 print run_results
[0]
2468 # If the build is broken, remove it and redo search.
2469 revision_states
.pop(next_revision_index
)
2473 if self
.opts
.output_buildbot_annotations
:
2474 self
.printer
.PrintPartialResults(bisect_state
)
2475 bisect_utils
.OutputAnnotationStepClosed()
2477 self
._ConfidenceExtraTestRuns
(min_revision_state
, max_revision_state
,
2478 command_to_run
, metric
)
2479 results
= BisectResults(bisect_state
, self
.depot_registry
, self
.opts
,
2482 self
._GatherResultsFromRevertedCulpritCL
(
2483 results
, target_depot
, command_to_run
, metric
)
2487 # Weren't able to sync and retrieve the revision range.
2488 error
= ('An error occurred attempting to retrieve revision range: '
2489 '[%s..%s]' % (good_revision
, bad_revision
))
2490 return BisectResults(error
=error
)
2492 def _ConfidenceExtraTestRuns(self
, good_state
, bad_state
, command_to_run
,
2494 if (bool(good_state
.passed
) != bool(bad_state
.passed
)
2495 and good_state
.passed
not in ('Skipped', 'Build Failed')
2496 and bad_state
.passed
not in ('Skipped', 'Build Failed')):
2497 for state
in (good_state
, bad_state
):
2498 run_results
= self
.RunTest(
2503 test_run_multiplier
=BORDER_REVISIONS_EXTRA_RUNS
)
2504 # Is extend the right thing to do here?
2505 if run_results
[1] != BUILD_RESULT_FAIL
:
2506 state
.value
['values'].extend(run_results
[0]['values'])
2508 warning_text
= 'Re-test of revision %s failed with error message: %s'
2509 warning_text
%= (state
.revision
, run_results
[0])
2510 if warning_text
not in self
.warnings
:
2511 self
.warnings
.append(warning_text
)
2514 def _IsPlatformSupported():
2515 """Checks that this platform and build system are supported.
2518 opts: The options parsed from the command line.
2521 True if the platform and build system are supported.
2523 # Haven't tested the script out on any other platforms yet.
2524 supported
= ['posix', 'nt']
2525 return os
.name
in supported
2528 def RemoveBuildFiles(build_type
):
2529 """Removes build files from previous runs."""
2530 out_dir
= os
.path
.join('out', build_type
)
2531 build_dir
= os
.path
.join('build', build_type
)
2532 logging
.info('Removing build files in "%s" and "%s".',
2533 os
.path
.abspath(out_dir
), os
.path
.abspath(build_dir
))
2535 RemakeDirectoryTree(out_dir
)
2536 RemakeDirectoryTree(build_dir
)
2537 except Exception as e
:
2538 raise RuntimeError('Got error in RemoveBuildFiles: %s' % e
)
2541 def RemakeDirectoryTree(path_to_dir
):
2542 """Removes a directory tree and replaces it with an empty one.
2544 Returns True if successful, False otherwise.
2546 RemoveDirectoryTree(path_to_dir
)
2547 MaybeMakeDirectory(path_to_dir
)
2550 def RemoveDirectoryTree(path_to_dir
):
2551 """Removes a directory tree. Returns True if successful or False otherwise."""
2552 if os
.path
.isfile(path_to_dir
):
2553 logging
.info('REMOVING FILE %s' % path_to_dir
)
2554 os
.remove(path_to_dir
)
2556 if os
.path
.exists(path_to_dir
):
2557 shutil
.rmtree(path_to_dir
)
2559 if e
.errno
!= errno
.ENOENT
:
2563 # This is copied from build/scripts/common/chromium_utils.py.
2564 def MaybeMakeDirectory(*path
):
2565 """Creates an entire path, if it doesn't already exist."""
2566 file_path
= os
.path
.join(*path
)
2568 os
.makedirs(file_path
)
2569 except OSError as e
:
2570 if e
.errno
!= errno
.EEXIST
:
2574 class BisectOptions(object):
2575 """Options to be used when running bisection."""
2577 super(BisectOptions
, self
).__init
__()
2579 self
.target_platform
= 'chromium'
2580 self
.build_preference
= None
2581 self
.good_revision
= None
2582 self
.bad_revision
= None
2583 self
.use_goma
= None
2584 self
.goma_dir
= None
2585 self
.goma_threads
= 64
2586 self
.repeat_test_count
= 20
2587 self
.truncate_percent
= 25
2588 self
.max_time_minutes
= 20
2591 self
.output_buildbot_annotations
= None
2592 self
.no_custom_deps
= False
2593 self
.working_directory
= None
2594 self
.extra_src
= None
2595 self
.debug_ignore_build
= None
2596 self
.debug_ignore_sync
= None
2597 self
.debug_ignore_perf_test
= None
2598 self
.debug_ignore_regression_confidence
= None
2599 self
.debug_fake_first_test_mean
= 0
2600 self
.target_arch
= 'ia32'
2601 self
.target_build_type
= 'Release'
2602 self
.builder_type
= 'perf'
2603 self
.bisect_mode
= bisect_utils
.BISECT_MODE_MEAN
2604 self
.improvement_direction
= 0
2608 def _AddBisectOptionsGroup(parser
):
2609 group
= parser
.add_argument_group('Bisect options')
2610 group
.add_argument('-c', '--command', required
=True,
2611 help='A command to execute your performance test at '
2612 'each point in the bisection.')
2613 group
.add_argument('-b', '--bad_revision', required
=True,
2614 help='A bad revision to start bisection. Must be later '
2615 'than good revision. May be either a git or svn '
2617 group
.add_argument('-g', '--good_revision', required
=True,
2618 help='A revision to start bisection where performance '
2619 'test is known to pass. Must be earlier than the '
2620 'bad revision. May be either a git or a svn '
2622 group
.add_argument('-m', '--metric',
2623 help='The desired metric to bisect on. For example '
2624 '"vm_rss_final_b/vm_rss_f_b"')
2625 group
.add_argument('-d', '--improvement_direction', type=int, default
=0,
2626 help='An integer number representing the direction of '
2627 'improvement. 1 for higher is better, -1 for lower '
2628 'is better, 0 for ignore (default).')
2629 group
.add_argument('-r', '--repeat_test_count', type=int, default
=20,
2630 choices
=range(1, 101),
2631 help='The number of times to repeat the performance '
2632 'test. Values will be clamped to range [1, 100]. '
2633 'Default value is 20.')
2634 group
.add_argument('--max_time_minutes', type=int, default
=20,
2635 choices
=range(1, 61),
2636 help='The maximum time (in minutes) to take running the '
2637 'performance tests. The script will run the '
2638 'performance tests according to '
2639 '--repeat_test_count, so long as it doesn\'t exceed'
2640 ' --max_time_minutes. Values will be clamped to '
2641 'range [1, 60]. Default value is 20.')
2642 group
.add_argument('-t', '--truncate_percent', type=int, default
=25,
2643 help='The highest/lowest percent are discarded to form '
2644 'a truncated mean. Values will be clamped to range '
2645 '[0, 25]. Default value is 25 percent.')
2646 group
.add_argument('--bisect_mode', default
=bisect_utils
.BISECT_MODE_MEAN
,
2647 choices
=[bisect_utils
.BISECT_MODE_MEAN
,
2648 bisect_utils
.BISECT_MODE_STD_DEV
,
2649 bisect_utils
.BISECT_MODE_RETURN_CODE
],
2650 help='The bisect mode. Choices are to bisect on the '
2651 'difference in mean, std_dev, or return_code.')
2652 group
.add_argument('--bug_id', default
='',
2653 help='The id for the bug associated with this bisect. ' +
2654 'If this number is given, bisect will attempt to ' +
2655 'verify that the bug is not closed before '
2659 def _AddBuildOptionsGroup(parser
):
2660 group
= parser
.add_argument_group('Build options')
2661 group
.add_argument('-w', '--working_directory',
2662 help='Path to the working directory where the script '
2663 'will do an initial checkout of the chromium depot. The '
2664 'files will be placed in a subdirectory "bisect" under '
2665 'working_directory and that will be used to perform the '
2666 'bisection. This parameter is optional, if it is not '
2667 'supplied, the script will work from the current depot.')
2668 group
.add_argument('--build_preference',
2669 choices
=['msvs', 'ninja', 'make'],
2670 help='The preferred build system to use. On linux/mac '
2671 'the options are make/ninja. On Windows, the '
2672 'options are msvs/ninja.')
2673 group
.add_argument('--target_platform', default
='chromium',
2674 choices
=['chromium', 'android', 'android-chrome'],
2675 help='The target platform. Choices are "chromium" '
2676 '(current platform), or "android". If you specify '
2677 'something other than "chromium", you must be '
2678 'properly set up to build that platform.')
2679 group
.add_argument('--no_custom_deps', dest
='no_custom_deps',
2680 action
='store_true', default
=False,
2681 help='Run the script with custom_deps or not.')
2682 group
.add_argument('--extra_src',
2683 help='Path to a script which can be used to modify the '
2684 'bisect script\'s behavior.')
2685 group
.add_argument('--use_goma', action
='store_true',
2686 help='Add a bunch of extra threads for goma, and enable '
2688 group
.add_argument('--goma_dir',
2689 help='Path to goma tools (or system default if not '
2691 group
.add_argument('--goma_threads', type=int, default
='64',
2692 help='Number of threads for goma, only if using goma.')
2693 group
.add_argument('--output_buildbot_annotations', action
='store_true',
2694 help='Add extra annotation output for buildbot.')
2695 group
.add_argument('--target_arch', default
='ia32',
2696 dest
='target_arch', choices
=['ia32', 'x64', 'arm'],
2697 help='The target build architecture. Choices are "ia32" '
2698 '(default), "x64" or "arm".')
2699 group
.add_argument('--target_build_type', default
='Release',
2700 choices
=['Release', 'Debug', 'Release_x64'],
2701 help='The target build type. Choices are "Release" '
2702 '(default), Release_x64 or "Debug".')
2703 group
.add_argument('--builder_type', default
=fetch_build
.PERF_BUILDER
,
2704 choices
=[fetch_build
.PERF_BUILDER
,
2705 fetch_build
.FULL_BUILDER
,
2706 fetch_build
.ANDROID_CHROME_PERF_BUILDER
, ''],
2707 help='Type of builder to get build from. This '
2708 'determines both the bot that builds and the '
2709 'place where archived builds are downloaded from. '
2710 'For local builds, an empty string can be passed.')
2713 def _AddDebugOptionsGroup(parser
):
2714 group
= parser
.add_argument_group('Debug options')
2715 group
.add_argument('--debug_ignore_build', action
='store_true',
2716 help='DEBUG: Don\'t perform builds.')
2717 group
.add_argument('--debug_ignore_sync', action
='store_true',
2718 help='DEBUG: Don\'t perform syncs.')
2719 group
.add_argument('--debug_ignore_perf_test', action
='store_true',
2720 help='DEBUG: Don\'t perform performance tests.')
2721 group
.add_argument('--debug_ignore_regression_confidence',
2722 action
='store_true',
2723 help='DEBUG: Don\'t score the confidence of the initial '
2724 'good and bad revisions\' test results.')
2725 group
.add_argument('--debug_fake_first_test_mean', type=int, default
='0',
2726 help='DEBUG: When faking performance tests, return this '
2727 'value as the mean of the first performance test, '
2728 'and return a mean of 0.0 for further tests.')
2732 def _CreateCommandLineParser(cls
):
2733 """Creates a parser with bisect options.
2736 An instance of argparse.ArgumentParser.
2738 usage
= ('%(prog)s [options] [-- chromium-options]\n'
2739 'Perform binary search on revision history to find a minimal '
2740 'range of revisions where a performance metric regressed.\n')
2742 parser
= argparse
.ArgumentParser(usage
=usage
)
2743 cls
._AddBisectOptionsGroup
(parser
)
2744 cls
._AddBuildOptionsGroup
(parser
)
2745 cls
._AddDebugOptionsGroup
(parser
)
2748 def ParseCommandLine(self
):
2749 """Parses the command line for bisect options."""
2750 parser
= self
._CreateCommandLineParser
()
2751 opts
= parser
.parse_args()
2754 if (not opts
.metric
and
2755 opts
.bisect_mode
!= bisect_utils
.BISECT_MODE_RETURN_CODE
):
2756 raise RuntimeError('missing required parameter: --metric')
2758 if opts
.bisect_mode
!= bisect_utils
.BISECT_MODE_RETURN_CODE
:
2759 metric_values
= opts
.metric
.split('/')
2760 if len(metric_values
) != 2:
2761 raise RuntimeError('Invalid metric specified: [%s]' % opts
.metric
)
2762 opts
.metric
= metric_values
2764 opts
.truncate_percent
= min(max(opts
.truncate_percent
, 0), 25) / 100.0
2766 for k
, v
in opts
.__dict
__.iteritems():
2767 assert hasattr(self
, k
), 'Invalid %s attribute in BisectOptions.' % k
2769 except RuntimeError, e
:
2770 output_string
= StringIO
.StringIO()
2771 parser
.print_help(file=output_string
)
2772 error_message
= '%s\n\n%s' % (e
.message
, output_string
.getvalue())
2773 output_string
.close()
2774 raise RuntimeError(error_message
)
2777 def FromDict(values
):
2778 """Creates an instance of BisectOptions from a dictionary.
2781 values: a dict containing options to set.
2784 An instance of BisectOptions.
2786 opts
= BisectOptions()
2787 for k
, v
in values
.iteritems():
2788 assert hasattr(opts
, k
), 'Invalid %s attribute in BisectOptions.' % k
2791 if opts
.metric
and opts
.bisect_mode
!= bisect_utils
.BISECT_MODE_RETURN_CODE
:
2792 metric_values
= opts
.metric
.split('/')
2793 if len(metric_values
) != 2:
2794 raise RuntimeError('Invalid metric specified: [%s]' % opts
.metric
)
2795 opts
.metric
= metric_values
2797 if opts
.target_arch
== 'x64' and opts
.target_build_type
== 'Release':
2798 opts
.target_build_type
= 'Release_x64'
2799 opts
.repeat_test_count
= min(max(opts
.repeat_test_count
, 1), 100)
2800 opts
.max_time_minutes
= min(max(opts
.max_time_minutes
, 1), 60)
2801 opts
.truncate_percent
= min(max(opts
.truncate_percent
, 0), 25)
2802 opts
.truncate_percent
= opts
.truncate_percent
/ 100.0
2807 def _ConfigureLogging():
2808 """Trivial logging config.
2810 Configures logging to output any messages at or above INFO to standard out,
2811 without any additional formatting.
2813 logging_format
= '%(message)s'
2814 logging
.basicConfig(
2815 stream
=logging
.sys
.stdout
, level
=logging
.INFO
, format
=logging_format
)
2821 opts
= BisectOptions()
2822 opts
.ParseCommandLine()
2825 if opts
.output_buildbot_annotations
:
2826 bisect_utils
.OutputAnnotationStepStart('Checking Issue Tracker')
2827 issue_closed
= query_crbug
.CheckIssueClosed(opts
.bug_id
)
2829 print 'Aborting bisect because bug is closed'
2831 print 'Could not confirm bug is closed, proceeding.'
2832 if opts
.output_buildbot_annotations
:
2833 bisect_utils
.OutputAnnotationStepClosed()
2835 results
= BisectResults(abort_reason
='the bug is closed.')
2836 bisect_test
= BisectPerformanceMetrics(opts
, os
.getcwd())
2837 bisect_test
.printer
.FormatAndPrintResults(results
)
2841 extra_src
= bisect_utils
.LoadExtraSrc(opts
.extra_src
)
2843 raise RuntimeError('Invalid or missing --extra_src.')
2844 bisect_utils
.AddAdditionalDepotInfo(extra_src
.GetAdditionalDepotInfo())
2846 if opts
.working_directory
:
2847 custom_deps
= bisect_utils
.DEFAULT_GCLIENT_CUSTOM_DEPS
2848 if opts
.no_custom_deps
:
2850 bisect_utils
.CreateBisectDirectoryAndSetupDepot(opts
, custom_deps
)
2852 os
.chdir(os
.path
.join(os
.getcwd(), 'src'))
2853 RemoveBuildFiles(opts
.target_build_type
)
2855 if not _IsPlatformSupported():
2856 raise RuntimeError('Sorry, this platform isn\'t supported yet.')
2858 if not source_control
.IsInGitRepository():
2860 'Sorry, only the git workflow is supported at the moment.')
2862 # gClient sync seems to fail if you're not in master branch.
2863 if (not source_control
.IsInProperBranch() and
2864 not opts
.debug_ignore_sync
and
2865 not opts
.working_directory
):
2866 raise RuntimeError('You must switch to master branch to run bisection.')
2867 bisect_test
= BisectPerformanceMetrics(opts
, os
.getcwd())
2869 results
= bisect_test
.Run(opts
.command
, opts
.bad_revision
,
2870 opts
.good_revision
, opts
.metric
)
2872 raise RuntimeError(results
.error
)
2873 bisect_test
.printer
.FormatAndPrintResults(results
)
2876 bisect_test
.PerformCleanup()
2877 except RuntimeError as e
:
2878 if opts
.output_buildbot_annotations
:
2879 # The perf dashboard scrapes the "results" step in order to comment on
2880 # bugs. If you change this, please update the perf dashboard as well.
2881 bisect_utils
.OutputAnnotationStepStart('Results')
2882 print 'Runtime Error: %s' % e
2883 if opts
.output_buildbot_annotations
:
2884 bisect_utils
.OutputAnnotationStepClosed()
2888 if __name__
== '__main__':