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