Explicitly add python-numpy dependency to install-build-deps.
[chromium-blink-merge.git] / tools / auto_bisect / bisect_perf_regression.py
blob2c081ac06d74904a766ba54350ed89a710f1d7ad
1 #!/usr/bin/env python
2 # Copyright (c) 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 """Performance Test Bisect Tool
8 This script bisects a series of changelists using binary search. It starts at
9 a bad revision where a performance metric has regressed, and asks for a last
10 known-good revision. It will then binary search across this revision range by
11 syncing, building, and running a performance test. If the change is
12 suspected to occur as a result of WebKit/V8 changes, the script will
13 further bisect changes to those depots and attempt to narrow down the revision
14 range.
16 Example usage using SVN revisions:
18 ./tools/bisect_perf_regression.py -c\
19 "out/Release/performance_ui_tests --gtest_filter=ShutdownTest.SimpleUserQuit"\
20 -g 168222 -b 168232 -m shutdown/simple-user-quit
22 Be aware that if you're using the git workflow and specify an SVN revision,
23 the script will attempt to find the git SHA1 where SVN changes up to that
24 revision were merged in.
26 Example usage using git hashes:
28 ./tools/bisect_perf_regression.py -c\
29 "out/Release/performance_ui_tests --gtest_filter=ShutdownTest.SimpleUserQuit"\
30 -g 1f6e67861535121c5c819c16a666f2436c207e7b\
31 -b b732f23b4f81c382db0b23b9035f3dadc7d925bb\
32 -m shutdown/simple-user-quit
33 """
35 import copy
36 import errno
37 import hashlib
38 import logging
39 import argparse
40 import os
41 import re
42 import shlex
43 import shutil
44 import StringIO
45 import sys
46 import time
47 import zipfile
49 sys.path.append(os.path.join(
50 os.path.dirname(__file__), os.path.pardir, 'telemetry'))
52 from bisect_printer import BisectPrinter
53 from bisect_results import BisectResults
54 from bisect_state import BisectState
55 import bisect_utils
56 import builder
57 import query_crbug
58 import math_utils
59 import request_build
60 import source_control
61 from telemetry.util import cloud_storage
63 # The script is in chromium/src/tools/auto_bisect. Throughout this script,
64 # we use paths to other things in the chromium/src repository.
66 # Possible return values from BisectPerformanceMetrics.RunTest.
67 BUILD_RESULT_SUCCEED = 0
68 BUILD_RESULT_FAIL = 1
69 BUILD_RESULT_SKIPPED = 2
71 # Maximum time in seconds to wait after posting build request to the try server.
72 # TODO: Change these values based on the actual time taken by buildbots on
73 # the try server.
74 MAX_MAC_BUILD_TIME = 14400
75 MAX_WIN_BUILD_TIME = 14400
76 MAX_LINUX_BUILD_TIME = 14400
78 # The confidence percentage we require to consider the initial range a
79 # regression based on the test results of the inital good and bad revisions.
80 REGRESSION_CONFIDENCE = 95
82 # Patch template to add a new file, DEPS.sha under src folder.
83 # This file contains SHA1 value of the DEPS changes made while bisecting
84 # dependency repositories. This patch send along with DEPS patch to try server.
85 # When a build requested is posted with a patch, bisect builders on try server,
86 # once build is produced, it reads SHA value from this file and appends it
87 # to build archive filename.
88 DEPS_SHA_PATCH = """diff --git DEPS.sha DEPS.sha
89 new file mode 100644
90 --- /dev/null
91 +++ DEPS.sha
92 @@ -0,0 +1 @@
93 +%(deps_sha)s
94 """
96 REGRESSION_CONFIDENCE_ERROR_TEMPLATE = """
97 We could not reproduce the regression with this test/metric/platform combination
98 with enough confidence.
100 Here are the results for the given "good" and "bad" revisions:
101 "Good" revision: {good_rev}
102 \tMean: {good_mean}
103 \tStandard error: {good_std_err}
104 \tSample size: {good_sample_size}
106 "Bad" revision: {bad_rev}
107 \tMean: {bad_mean}
108 \tStandard error: {bad_std_err}
109 \tSample size: {bad_sample_size}
111 NOTE: There's still a chance that this is actually a regression, but you may
112 need to bisect a different platform."""
114 # Git branch name used to run bisect try jobs.
115 BISECT_TRYJOB_BRANCH = 'bisect-tryjob'
116 # Git master branch name.
117 BISECT_MASTER_BRANCH = 'master'
118 # File to store 'git diff' content.
119 BISECT_PATCH_FILE = 'deps_patch.txt'
120 # SVN repo where the bisect try jobs are submitted.
121 SVN_REPO_URL = 'svn://svn.chromium.org/chrome-try/try-perf'
123 class RunGitError(Exception):
125 def __str__(self):
126 return '%s\nError executing git command.' % self.args[0]
129 def GetSHA1HexDigest(contents):
130 """Returns SHA1 hex digest of the given string."""
131 return hashlib.sha1(contents).hexdigest()
134 def GetZipFileName(build_revision=None, target_arch='ia32', patch_sha=None):
135 """Gets the archive file name for the given revision."""
136 def PlatformName():
137 """Return a string to be used in paths for the platform."""
138 if bisect_utils.IsWindowsHost():
139 # Build archive for x64 is still stored with the "win32" suffix.
140 # See chromium_utils.PlatformName().
141 if bisect_utils.Is64BitWindows() and target_arch == 'x64':
142 return 'win32'
143 return 'win32'
144 if bisect_utils.IsLinuxHost():
145 # Android builds are also archived with the "full-build-linux prefix.
146 return 'linux'
147 if bisect_utils.IsMacHost():
148 return 'mac'
149 raise NotImplementedError('Unknown platform "%s".' % sys.platform)
151 base_name = 'full-build-%s' % PlatformName()
152 if not build_revision:
153 return base_name
154 if patch_sha:
155 build_revision = '%s_%s' % (build_revision, patch_sha)
156 return '%s_%s.zip' % (base_name, build_revision)
159 def GetRemoteBuildPath(build_revision, target_platform='chromium',
160 target_arch='ia32', patch_sha=None):
161 """Returns the URL to download the build from."""
162 def GetGSRootFolderName(target_platform):
163 """Returns the Google Cloud Storage root folder name."""
164 if bisect_utils.IsWindowsHost():
165 if bisect_utils.Is64BitWindows() and target_arch == 'x64':
166 return 'Win x64 Builder'
167 return 'Win Builder'
168 if bisect_utils.IsLinuxHost():
169 if target_platform == 'android':
170 return 'android_perf_rel'
171 return 'Linux Builder'
172 if bisect_utils.IsMacHost():
173 return 'Mac Builder'
174 raise NotImplementedError('Unsupported Platform "%s".' % sys.platform)
176 base_filename = GetZipFileName(
177 build_revision, target_arch, patch_sha)
178 builder_folder = GetGSRootFolderName(target_platform)
179 return '%s/%s' % (builder_folder, base_filename)
182 def FetchFromCloudStorage(bucket_name, source_path, destination_path):
183 """Fetches file(s) from the Google Cloud Storage.
185 Args:
186 bucket_name: Google Storage bucket name.
187 source_path: Source file path.
188 destination_path: Destination file path.
190 Returns:
191 Downloaded file path if exists, otherwise None.
193 target_file = os.path.join(destination_path, os.path.basename(source_path))
194 try:
195 if cloud_storage.Exists(bucket_name, source_path):
196 logging.info('Fetching file from gs//%s/%s ...',
197 bucket_name, source_path)
198 cloud_storage.Get(bucket_name, source_path, destination_path)
199 if os.path.exists(target_file):
200 return target_file
201 else:
202 logging.info('File gs://%s/%s not found in cloud storage.',
203 bucket_name, source_path)
204 except Exception as e:
205 logging.warn('Something went wrong while fetching file from cloud: %s', e)
206 if os.path.exists(target_file):
207 os.remove(target_file)
208 return None
211 # This was copied from build/scripts/common/chromium_utils.py.
212 def ExtractZip(filename, output_dir, verbose=True):
213 """ Extract the zip archive in the output directory."""
214 MaybeMakeDirectory(output_dir)
216 # On Linux and Mac, we use the unzip command as it will
217 # handle links and file bits (executable), which is much
218 # easier then trying to do that with ZipInfo options.
220 # The Mac Version of unzip unfortunately does not support Zip64, whereas
221 # the python module does, so we have to fall back to the python zip module
222 # on Mac if the file size is greater than 4GB.
224 # On Windows, try to use 7z if it is installed, otherwise fall back to python
225 # zip module and pray we don't have files larger than 512MB to unzip.
226 unzip_cmd = None
227 if ((bisect_utils.IsMacHost()
228 and os.path.getsize(filename) < 4 * 1024 * 1024 * 1024)
229 or bisect_utils.IsLinuxHost()):
230 unzip_cmd = ['unzip', '-o']
231 elif (bisect_utils.IsWindowsHost()
232 and os.path.exists('C:\\Program Files\\7-Zip\\7z.exe')):
233 unzip_cmd = ['C:\\Program Files\\7-Zip\\7z.exe', 'x', '-y']
235 if unzip_cmd:
236 # Make sure path is absolute before changing directories.
237 filepath = os.path.abspath(filename)
238 saved_dir = os.getcwd()
239 os.chdir(output_dir)
240 command = unzip_cmd + [filepath]
241 result = bisect_utils.RunProcess(command)
242 os.chdir(saved_dir)
243 if result:
244 raise IOError('unzip failed: %s => %s' % (str(command), result))
245 else:
246 assert bisect_utils.IsWindowsHost() or bisect_utils.IsMacHost()
247 zf = zipfile.ZipFile(filename)
248 for name in zf.namelist():
249 if verbose:
250 logging.info('Extracting %s', name)
251 zf.extract(name, output_dir)
252 if bisect_utils.IsMacHost():
253 # Restore permission bits.
254 os.chmod(os.path.join(output_dir, name),
255 zf.getinfo(name).external_attr >> 16L)
258 def WriteStringToFile(text, file_name):
259 """Writes text to a file, raising an RuntimeError on failure."""
260 try:
261 with open(file_name, 'wb') as f:
262 f.write(text)
263 except IOError:
264 raise RuntimeError('Error writing to file [%s]' % file_name)
267 def ReadStringFromFile(file_name):
268 """Writes text to a file, raising an RuntimeError on failure."""
269 try:
270 with open(file_name) as f:
271 return f.read()
272 except IOError:
273 raise RuntimeError('Error reading file [%s]' % file_name)
276 def ChangeBackslashToSlashInPatch(diff_text):
277 """Formats file paths in the given patch text to Unix-style paths."""
278 if not diff_text:
279 return None
280 diff_lines = diff_text.split('\n')
281 for i in range(len(diff_lines)):
282 line = diff_lines[i]
283 if line.startswith('--- ') or line.startswith('+++ '):
284 diff_lines[i] = line.replace('\\', '/')
285 return '\n'.join(diff_lines)
288 def _ParseRevisionsFromDEPSFileManually(deps_file_contents):
289 """Parses the vars section of the DEPS file using regular expressions.
291 Args:
292 deps_file_contents: The DEPS file contents as a string.
294 Returns:
295 A dictionary in the format {depot: revision} if successful, otherwise None.
297 # We'll parse the "vars" section of the DEPS file.
298 rxp = re.compile('vars = {(?P<vars_body>[^}]+)', re.MULTILINE)
299 re_results = rxp.search(deps_file_contents)
301 if not re_results:
302 return None
304 # We should be left with a series of entries in the vars component of
305 # the DEPS file with the following format:
306 # 'depot_name': 'revision',
307 vars_body = re_results.group('vars_body')
308 rxp = re.compile(r"'(?P<depot_body>[\w_-]+)':[\s]+'(?P<rev_body>[\w@]+)'",
309 re.MULTILINE)
310 re_results = rxp.findall(vars_body)
312 return dict(re_results)
315 def _WaitUntilBuildIsReady(
316 fetch_build, bot_name, builder_host, builder_port, build_request_id,
317 max_timeout):
318 """Waits until build is produced by bisect builder on try server.
320 Args:
321 fetch_build: Function to check and download build from cloud storage.
322 bot_name: Builder bot name on try server.
323 builder_host Try server host name.
324 builder_port: Try server port.
325 build_request_id: A unique ID of the build request posted to try server.
326 max_timeout: Maximum time to wait for the build.
328 Returns:
329 Downloaded archive file path if exists, otherwise None.
331 # Build number on the try server.
332 build_num = None
333 # Interval to check build on cloud storage.
334 poll_interval = 60
335 # Interval to check build status on try server in seconds.
336 status_check_interval = 600
337 last_status_check = time.time()
338 start_time = time.time()
339 while True:
340 # Checks for build on gs://chrome-perf and download if exists.
341 res = fetch_build()
342 if res:
343 return (res, 'Build successfully found')
344 elapsed_status_check = time.time() - last_status_check
345 # To avoid overloading try server with status check requests, we check
346 # build status for every 10 minutes.
347 if elapsed_status_check > status_check_interval:
348 last_status_check = time.time()
349 if not build_num:
350 # Get the build number on try server for the current build.
351 build_num = request_build.GetBuildNumFromBuilder(
352 build_request_id, bot_name, builder_host, builder_port)
353 # Check the status of build using the build number.
354 # Note: Build is treated as PENDING if build number is not found
355 # on the the try server.
356 build_status, status_link = request_build.GetBuildStatus(
357 build_num, bot_name, builder_host, builder_port)
358 if build_status == request_build.FAILED:
359 return (None, 'Failed to produce build, log: %s' % status_link)
360 elapsed_time = time.time() - start_time
361 if elapsed_time > max_timeout:
362 return (None, 'Timed out: %ss without build' % max_timeout)
364 logging.info('Time elapsed: %ss without build.', elapsed_time)
365 time.sleep(poll_interval)
366 # For some reason, mac bisect bots were not flushing stdout periodically.
367 # As a result buildbot command is timed-out. Flush stdout on all platforms
368 # while waiting for build.
369 sys.stdout.flush()
372 def _UpdateV8Branch(deps_content):
373 """Updates V8 branch in DEPS file to process v8_bleeding_edge.
375 Check for "v8_branch" in DEPS file if exists update its value
376 with v8_bleeding_edge branch. Note: "v8_branch" is added to DEPS
377 variable from DEPS revision 254916, therefore check for "src/v8":
378 <v8 source path> in DEPS in order to support prior DEPS revisions
379 and update it.
381 Args:
382 deps_content: DEPS file contents to be modified.
384 Returns:
385 Modified DEPS file contents as a string.
387 new_branch = r'branches/bleeding_edge'
388 v8_branch_pattern = re.compile(r'(?<="v8_branch": ")(.*)(?=")')
389 if re.search(v8_branch_pattern, deps_content):
390 deps_content = re.sub(v8_branch_pattern, new_branch, deps_content)
391 else:
392 # Replaces the branch assigned to "src/v8" key in DEPS file.
393 # Format of "src/v8" in DEPS:
394 # "src/v8":
395 # (Var("googlecode_url") % "v8") + "/trunk@" + Var("v8_revision"),
396 # So, "/trunk@" is replace with "/branches/bleeding_edge@"
397 v8_src_pattern = re.compile(
398 r'(?<="v8"\) \+ "/)(.*)(?=@" \+ Var\("v8_revision"\))', re.MULTILINE)
399 if re.search(v8_src_pattern, deps_content):
400 deps_content = re.sub(v8_src_pattern, new_branch, deps_content)
401 return deps_content
404 def _UpdateDEPSForAngle(revision, depot, deps_file):
405 """Updates DEPS file with new revision for Angle repository.
407 This is a hack for Angle depot case because, in DEPS file "vars" dictionary
408 variable contains "angle_revision" key that holds git hash instead of
409 SVN revision.
411 And sometimes "angle_revision" key is not specified in "vars" variable,
412 in such cases check "deps" dictionary variable that matches
413 angle.git@[a-fA-F0-9]{40}$ and replace git hash.
415 deps_var = bisect_utils.DEPOT_DEPS_NAME[depot]['deps_var']
416 try:
417 deps_contents = ReadStringFromFile(deps_file)
418 # Check whether the depot and revision pattern in DEPS file vars variable
419 # e.g. "angle_revision": "fa63e947cb3eccf463648d21a05d5002c9b8adfa".
420 angle_rev_pattern = re.compile(r'(?<="%s": ")([a-fA-F0-9]{40})(?=")' %
421 deps_var, re.MULTILINE)
422 match = re.search(angle_rev_pattern, deps_contents)
423 if match:
424 # Update the revision information for the given depot
425 new_data = re.sub(angle_rev_pattern, revision, deps_contents)
426 else:
427 # Check whether the depot and revision pattern in DEPS file deps
428 # variable. e.g.,
429 # "src/third_party/angle": Var("chromium_git") +
430 # "/angle/angle.git@fa63e947cb3eccf463648d21a05d5002c9b8adfa",.
431 angle_rev_pattern = re.compile(
432 r'(?<=angle\.git@)([a-fA-F0-9]{40})(?=")', re.MULTILINE)
433 match = re.search(angle_rev_pattern, deps_contents)
434 if not match:
435 logging.info('Could not find angle revision information in DEPS file.')
436 return False
437 new_data = re.sub(angle_rev_pattern, revision, deps_contents)
438 # Write changes to DEPS file
439 WriteStringToFile(new_data, deps_file)
440 return True
441 except IOError, e:
442 logging.warn('Something went wrong while updating DEPS file, %s', e)
443 return False
446 def _TryParseHistogramValuesFromOutput(metric, text):
447 """Attempts to parse a metric in the format HISTOGRAM <graph: <trace>.
449 Args:
450 metric: The metric as a list of [<trace>, <value>] strings.
451 text: The text to parse the metric values from.
453 Returns:
454 A list of floating point numbers found, [] if none were found.
456 metric_formatted = 'HISTOGRAM %s: %s= ' % (metric[0], metric[1])
458 text_lines = text.split('\n')
459 values_list = []
461 for current_line in text_lines:
462 if metric_formatted in current_line:
463 current_line = current_line[len(metric_formatted):]
465 try:
466 histogram_values = eval(current_line)
468 for b in histogram_values['buckets']:
469 average_for_bucket = float(b['high'] + b['low']) * 0.5
470 # Extends the list with N-elements with the average for that bucket.
471 values_list.extend([average_for_bucket] * b['count'])
472 except Exception:
473 pass
475 return values_list
478 def _TryParseResultValuesFromOutput(metric, text):
479 """Attempts to parse a metric in the format RESULT <graph>: <trace>= ...
481 Args:
482 metric: The metric as a list of [<trace>, <value>] string pairs.
483 text: The text to parse the metric values from.
485 Returns:
486 A list of floating point numbers found.
488 # Format is: RESULT <graph>: <trace>= <value> <units>
489 metric_re = re.escape('RESULT %s: %s=' % (metric[0], metric[1]))
491 # The log will be parsed looking for format:
492 # <*>RESULT <graph_name>: <trace_name>= <value>
493 single_result_re = re.compile(
494 metric_re + r'\s*(?P<VALUE>[-]?\d*(\.\d*)?)')
496 # The log will be parsed looking for format:
497 # <*>RESULT <graph_name>: <trace_name>= [<value>,value,value,...]
498 multi_results_re = re.compile(
499 metric_re + r'\s*\[\s*(?P<VALUES>[-]?[\d\., ]+)\s*\]')
501 # The log will be parsed looking for format:
502 # <*>RESULT <graph_name>: <trace_name>= {<mean>, <std deviation>}
503 mean_stddev_re = re.compile(
504 metric_re +
505 r'\s*\{\s*(?P<MEAN>[-]?\d*(\.\d*)?),\s*(?P<STDDEV>\d+(\.\d*)?)\s*\}')
507 text_lines = text.split('\n')
508 values_list = []
509 for current_line in text_lines:
510 # Parse the output from the performance test for the metric we're
511 # interested in.
512 single_result_match = single_result_re.search(current_line)
513 multi_results_match = multi_results_re.search(current_line)
514 mean_stddev_match = mean_stddev_re.search(current_line)
515 if (not single_result_match is None and
516 single_result_match.group('VALUE')):
517 values_list += [single_result_match.group('VALUE')]
518 elif (not multi_results_match is None and
519 multi_results_match.group('VALUES')):
520 metric_values = multi_results_match.group('VALUES')
521 values_list += metric_values.split(',')
522 elif (not mean_stddev_match is None and
523 mean_stddev_match.group('MEAN')):
524 values_list += [mean_stddev_match.group('MEAN')]
526 values_list = [float(v) for v in values_list
527 if bisect_utils.IsStringFloat(v)]
529 # If the metric is times/t, we need to sum the timings in order to get
530 # similar regression results as the try-bots.
531 metrics_to_sum = [
532 ['times', 't'],
533 ['times', 'page_load_time'],
534 ['cold_times', 'page_load_time'],
535 ['warm_times', 'page_load_time'],
538 if metric in metrics_to_sum:
539 if values_list:
540 values_list = [reduce(lambda x, y: float(x) + float(y), values_list)]
542 return values_list
545 def _ParseMetricValuesFromOutput(metric, text):
546 """Parses output from performance_ui_tests and retrieves the results for
547 a given metric.
549 Args:
550 metric: The metric as a list of [<trace>, <value>] strings.
551 text: The text to parse the metric values from.
553 Returns:
554 A list of floating point numbers found.
556 metric_values = _TryParseResultValuesFromOutput(metric, text)
558 if not metric_values:
559 metric_values = _TryParseHistogramValuesFromOutput(metric, text)
561 return metric_values
564 def _GenerateProfileIfNecessary(command_args):
565 """Checks the command line of the performance test for dependencies on
566 profile generation, and runs tools/perf/generate_profile as necessary.
568 Args:
569 command_args: Command line being passed to performance test, as a list.
571 Returns:
572 False if profile generation was necessary and failed, otherwise True.
574 if '--profile-dir' in ' '.join(command_args):
575 # If we were using python 2.7+, we could just use the argparse
576 # module's parse_known_args to grab --profile-dir. Since some of the
577 # bots still run 2.6, have to grab the arguments manually.
578 arg_dict = {}
579 args_to_parse = ['--profile-dir', '--browser']
581 for arg_to_parse in args_to_parse:
582 for i, current_arg in enumerate(command_args):
583 if arg_to_parse in current_arg:
584 current_arg_split = current_arg.split('=')
586 # Check 2 cases, --arg=<val> and --arg <val>
587 if len(current_arg_split) == 2:
588 arg_dict[arg_to_parse] = current_arg_split[1]
589 elif i + 1 < len(command_args):
590 arg_dict[arg_to_parse] = command_args[i+1]
592 path_to_generate = os.path.join('tools', 'perf', 'generate_profile')
594 if arg_dict.has_key('--profile-dir') and arg_dict.has_key('--browser'):
595 profile_path, profile_type = os.path.split(arg_dict['--profile-dir'])
596 return not bisect_utils.RunProcess(['python', path_to_generate,
597 '--profile-type-to-generate', profile_type,
598 '--browser', arg_dict['--browser'], '--output-dir', profile_path])
599 return False
600 return True
603 def _CheckRegressionConfidenceError(
604 good_revision,
605 bad_revision,
606 known_good_value,
607 known_bad_value):
608 """Checks whether we can be confident beyond a certain degree that the given
609 metrics represent a regression.
611 Args:
612 good_revision: string representing the commit considered 'good'
613 bad_revision: Same as above for 'bad'.
614 known_good_value: A dict with at least: 'values', 'mean' and 'std_err'
615 known_bad_value: Same as above.
617 Returns:
618 False if there is no error (i.e. we can be confident there's a regressioni),
619 a string containing the details of the lack of confidence otherwise.
621 error = False
622 # Adding good and bad values to a parameter list.
623 confidenceParams = []
624 for l in [known_bad_value['values'], known_good_value['values']]:
625 # Flatten if needed
626 if isinstance(l, list) and all([isinstance(x, list) for x in l]):
627 confidenceParams.append(sum(l, []))
628 else:
629 confidenceParams.append(l)
630 regression_confidence = BisectResults.ConfidenceScore(*confidenceParams)
631 if regression_confidence < REGRESSION_CONFIDENCE:
632 error = REGRESSION_CONFIDENCE_ERROR_TEMPLATE.format(
633 good_rev=good_revision,
634 good_mean=known_good_value['mean'],
635 good_std_err=known_good_value['std_err'],
636 good_sample_size=len(known_good_value['values']),
637 bad_rev=bad_revision,
638 bad_mean=known_bad_value['mean'],
639 bad_std_err=known_bad_value['std_err'],
640 bad_sample_size=len(known_bad_value['values']))
641 return error
643 class DepotDirectoryRegistry(object):
645 def __init__(self, src_cwd):
646 self.depot_cwd = {}
647 for depot in bisect_utils.DEPOT_NAMES:
648 # The working directory of each depot is just the path to the depot, but
649 # since we're already in 'src', we can skip that part.
650 path_in_src = bisect_utils.DEPOT_DEPS_NAME[depot]['src'][4:]
651 self.SetDepotDir(depot, os.path.join(src_cwd, path_in_src))
653 self.SetDepotDir('chromium', src_cwd)
655 def SetDepotDir(self, depot_name, depot_dir):
656 self.depot_cwd[depot_name] = depot_dir
658 def GetDepotDir(self, depot_name):
659 if depot_name in self.depot_cwd:
660 return self.depot_cwd[depot_name]
661 else:
662 assert False, ('Unknown depot [ %s ] encountered. Possibly a new one '
663 'was added without proper support?' % depot_name)
665 def ChangeToDepotDir(self, depot_name):
666 """Given a depot, changes to the appropriate working directory.
668 Args:
669 depot_name: The name of the depot (see DEPOT_NAMES).
671 os.chdir(self.GetDepotDir(depot_name))
673 def _PrepareBisectBranch(parent_branch, new_branch):
674 """Creates a new branch to submit bisect try job.
676 Args:
677 parent_branch: Parent branch to be used to create new branch.
678 new_branch: New branch name.
680 current_branch, returncode = bisect_utils.RunGit(
681 ['rev-parse', '--abbrev-ref', 'HEAD'])
682 if returncode:
683 raise RunGitError('Must be in a git repository to send changes to trybots.')
685 current_branch = current_branch.strip()
686 # Make sure current branch is master.
687 if current_branch != parent_branch:
688 output, returncode = bisect_utils.RunGit(['checkout', '-f', parent_branch])
689 if returncode:
690 raise RunGitError('Failed to checkout branch: %s.' % output)
692 # Delete new branch if exists.
693 output, returncode = bisect_utils.RunGit(['branch', '--list'])
694 if new_branch in output:
695 output, returncode = bisect_utils.RunGit(['branch', '-D', new_branch])
696 if returncode:
697 raise RunGitError('Deleting branch failed, %s', output)
699 # Check if the tree is dirty: make sure the index is up to date and then
700 # run diff-index.
701 bisect_utils.RunGit(['update-index', '--refresh', '-q'])
702 output, returncode = bisect_utils.RunGit(['diff-index', 'HEAD'])
703 if output:
704 raise RunGitError('Cannot send a try job with a dirty tree.')
706 # Create/check out the telemetry-tryjob branch, and edit the configs
707 # for the tryjob there.
708 output, returncode = bisect_utils.RunGit(['checkout', '-b', new_branch])
709 if returncode:
710 raise RunGitError('Failed to checkout branch: %s.' % output)
712 output, returncode = bisect_utils.RunGit(
713 ['branch', '--set-upstream-to', parent_branch])
714 if returncode:
715 raise RunGitError('Error in git branch --set-upstream-to')
718 def _BuilderTryjob(git_revision, bot_name, bisect_job_name, patch=None):
719 """Attempts to run a tryjob from the current directory.
721 Args:
722 git_revision: A Git hash revision.
723 bot_name: Name of the bisect bot to be used for try job.
724 bisect_job_name: Bisect try job name.
725 patch: A DEPS patch (used while bisecting 3rd party repositories).
727 try:
728 # Temporary branch for running tryjob.
729 _PrepareBisectBranch(BISECT_MASTER_BRANCH, BISECT_TRYJOB_BRANCH)
730 patch_content = '/dev/null'
731 # Create a temporary patch file, if it fails raise an exception.
732 if patch:
733 WriteStringToFile(patch, BISECT_PATCH_FILE)
734 patch_content = BISECT_PATCH_FILE
736 try_cmd = ['try',
737 '-b', bot_name,
738 '-r', git_revision,
739 '-n', bisect_job_name,
740 '--svn_repo=%s' % SVN_REPO_URL,
741 '--diff=%s' % patch_content
743 # Execute try job to build revision.
744 output, returncode = bisect_utils.RunGit(try_cmd)
746 if returncode:
747 raise RunGitError('Could not execute tryjob: %s.\n Error: %s' % (
748 'git %s' % ' '.join(try_cmd), output))
749 logging.info('Try job successfully submitted.\n TryJob Details: %s\n%s',
750 'git %s' % ' '.join(try_cmd), output)
751 finally:
752 # Delete patch file if exists
753 try:
754 os.remove(BISECT_PATCH_FILE)
755 except OSError as e:
756 if e.errno != errno.ENOENT:
757 raise
758 # Checkout master branch and delete bisect-tryjob branch.
759 bisect_utils.RunGit(['checkout', '-f', BISECT_MASTER_BRANCH])
760 bisect_utils.RunGit(['branch', '-D', BISECT_TRYJOB_BRANCH])
763 class BisectPerformanceMetrics(object):
764 """This class contains functionality to perform a bisection of a range of
765 revisions to narrow down where performance regressions may have occurred.
767 The main entry-point is the Run method.
770 def __init__(self, opts, src_cwd):
771 """Constructs a BisectPerformancesMetrics object.
773 Args:
774 opts: BisectOptions object containing parsed options.
775 src_cwd: Root src/ directory of the test repository (inside bisect/ dir).
777 super(BisectPerformanceMetrics, self).__init__()
779 self.opts = opts
780 self.src_cwd = src_cwd
781 self.depot_registry = DepotDirectoryRegistry(self.src_cwd)
782 self.printer = BisectPrinter(self.opts, self.depot_registry)
783 self.cleanup_commands = []
784 self.warnings = []
785 self.builder = builder.Builder.FromOpts(opts)
787 def PerformCleanup(self):
788 """Performs cleanup when script is finished."""
789 os.chdir(self.src_cwd)
790 for c in self.cleanup_commands:
791 if c[0] == 'mv':
792 shutil.move(c[1], c[2])
793 else:
794 assert False, 'Invalid cleanup command.'
796 def GetRevisionList(self, depot, bad_revision, good_revision):
797 """Retrieves a list of all the commits between the bad revision and
798 last known good revision."""
800 cwd = self.depot_registry.GetDepotDir(depot)
801 return source_control.GetRevisionList(bad_revision, good_revision, cwd=cwd)
803 def _ParseRevisionsFromDEPSFile(self, depot):
804 """Parses the local DEPS file to determine blink/skia/v8 revisions which may
805 be needed if the bisect recurses into those depots later.
807 Args:
808 depot: Name of depot being bisected.
810 Returns:
811 A dict in the format {depot:revision} if successful, otherwise None.
813 try:
814 deps_data = {
815 'Var': lambda _: deps_data["vars"][_],
816 'From': lambda *args: None,
819 deps_file = bisect_utils.FILE_DEPS_GIT
820 if not os.path.exists(deps_file):
821 deps_file = bisect_utils.FILE_DEPS
822 execfile(deps_file, {}, deps_data)
823 deps_data = deps_data['deps']
825 rxp = re.compile(".git@(?P<revision>[a-fA-F0-9]+)")
826 results = {}
827 for depot_name, depot_data in bisect_utils.DEPOT_DEPS_NAME.iteritems():
828 if (depot_data.get('platform') and
829 depot_data.get('platform') != os.name):
830 continue
832 if depot_data.get('recurse') and depot in depot_data.get('from'):
833 depot_data_src = depot_data.get('src') or depot_data.get('src_old')
834 src_dir = deps_data.get(depot_data_src)
835 if src_dir:
836 self.depot_registry.SetDepotDir(depot_name, os.path.join(
837 self.src_cwd, depot_data_src[4:]))
838 re_results = rxp.search(src_dir)
839 if re_results:
840 results[depot_name] = re_results.group('revision')
841 else:
842 warning_text = ('Could not parse revision for %s while bisecting '
843 '%s' % (depot_name, depot))
844 if not warning_text in self.warnings:
845 self.warnings.append(warning_text)
846 else:
847 results[depot_name] = None
848 return results
849 except ImportError:
850 deps_file_contents = ReadStringFromFile(deps_file)
851 parse_results = _ParseRevisionsFromDEPSFileManually(deps_file_contents)
852 results = {}
853 for depot_name, depot_revision in parse_results.iteritems():
854 depot_revision = depot_revision.strip('@')
855 logging.warn(depot_name, depot_revision)
856 for cur_name, cur_data in bisect_utils.DEPOT_DEPS_NAME.iteritems():
857 if (cur_data.has_key('deps_var') and
858 cur_data['deps_var'] == depot_name):
859 src_name = cur_name
860 results[src_name] = depot_revision
861 break
862 return results
864 def _Get3rdPartyRevisions(self, depot):
865 """Parses the DEPS file to determine WebKit/v8/etc... versions.
867 Args:
868 depot: A depot name. Should be in the DEPOT_NAMES list.
870 Returns:
871 A dict in the format {depot: revision} if successful, otherwise None.
873 cwd = os.getcwd()
874 self.depot_registry.ChangeToDepotDir(depot)
876 results = {}
878 if depot == 'chromium' or depot == 'android-chrome':
879 results = self._ParseRevisionsFromDEPSFile(depot)
880 os.chdir(cwd)
882 if depot == 'v8':
883 # We can't try to map the trunk revision to bleeding edge yet, because
884 # we don't know which direction to try to search in. Have to wait until
885 # the bisect has narrowed the results down to 2 v8 rolls.
886 results['v8_bleeding_edge'] = None
888 return results
890 def BackupOrRestoreOutputDirectory(self, restore=False, build_type='Release'):
891 """Backs up or restores build output directory based on restore argument.
893 Args:
894 restore: Indicates whether to restore or backup. Default is False(Backup)
895 build_type: Target build type ('Release', 'Debug', 'Release_x64' etc.)
897 Returns:
898 Path to backup or restored location as string. otherwise None if it fails.
900 build_dir = os.path.abspath(
901 builder.GetBuildOutputDirectory(self.opts, self.src_cwd))
902 source_dir = os.path.join(build_dir, build_type)
903 destination_dir = os.path.join(build_dir, '%s.bak' % build_type)
904 if restore:
905 source_dir, destination_dir = destination_dir, source_dir
906 if os.path.exists(source_dir):
907 RemoveDirectoryTree(destination_dir)
908 shutil.move(source_dir, destination_dir)
909 return destination_dir
910 return None
912 def _GetBuildArchiveForRevision(self, revision, gs_bucket, target_arch,
913 patch_sha, out_dir):
914 """Checks and downloads build archive for a given revision.
916 Checks for build archive with Git hash or SVN revision. If either of the
917 file exists, then downloads the archive file.
919 Args:
920 revision: A git commit hash.
921 gs_bucket: Cloud storage bucket name.
922 target_arch: Architecture name string, e.g. "ia32" or "x64".
923 patch_sha: A SHA1 hex digest of a DEPS file patch, used while
924 bisecting 3rd party repositories.
925 out_dir: Build output directory where downloaded file is stored.
927 Returns:
928 Downloaded archive file path if exists, otherwise None.
930 # Source archive file path on cloud storage using Git revision.
931 source_file = GetRemoteBuildPath(
932 revision, self.opts.target_platform, target_arch, patch_sha)
933 downloaded_archive = FetchFromCloudStorage(gs_bucket, source_file, out_dir)
934 if not downloaded_archive:
935 # Get commit position for the given SHA.
936 commit_position = source_control.GetCommitPosition(revision)
937 if commit_position:
938 # Source archive file path on cloud storage using SVN revision.
939 source_file = GetRemoteBuildPath(
940 commit_position, self.opts.target_platform, target_arch, patch_sha)
941 return FetchFromCloudStorage(gs_bucket, source_file, out_dir)
942 return downloaded_archive
944 def _DownloadAndUnzipBuild(self, revision, depot, build_type='Release'):
945 """Downloads the build archive for the given revision.
947 Args:
948 revision: The git revision to download.
949 depot: The name of a dependency repository. Should be in DEPOT_NAMES.
950 build_type: Target build type, e.g. Release', 'Debug', 'Release_x64' etc.
952 Returns:
953 True if download succeeds, otherwise False.
955 patch = None
956 patch_sha = None
957 if depot != 'chromium':
958 # Create a DEPS patch with new revision for dependency repository.
959 revision, patch = self.CreateDEPSPatch(depot, revision)
961 if patch:
962 # Get the SHA of the DEPS changes patch.
963 patch_sha = GetSHA1HexDigest(patch)
965 # Update the DEPS changes patch with a patch to create a new file named
966 # 'DEPS.sha' and add patch_sha evaluated above to it.
967 patch = '%s\n%s' % (patch, DEPS_SHA_PATCH % {'deps_sha': patch_sha})
969 build_dir = builder.GetBuildOutputDirectory(self.opts, self.src_cwd)
970 downloaded_file = self._WaitForBuildDownload(
971 revision, build_dir, deps_patch=patch, deps_patch_sha=patch_sha)
972 if not downloaded_file:
973 return False
974 return self._UnzipAndMoveBuildProducts(downloaded_file, build_dir,
975 build_type=build_type)
977 def _WaitForBuildDownload(self, revision, build_dir, deps_patch=None,
978 deps_patch_sha=None):
979 """Tries to download a zip archive for a build.
981 This involves seeing whether the archive is already available, and if not,
982 then requesting a build and waiting before downloading.
984 Args:
985 revision: A git commit hash.
986 build_dir: The directory to download the build into.
987 deps_patch: A patch which changes a dependency repository revision in
988 the DEPS, if applicable.
989 deps_patch_sha: The SHA1 hex digest of the above patch.
991 Returns:
992 File path of the downloaded file if successful, otherwise None.
994 abs_build_dir = os.path.abspath(build_dir)
995 fetch_build_func = lambda: self._GetBuildArchiveForRevision(
996 revision, self.opts.gs_bucket, self.opts.target_arch,
997 deps_patch_sha, abs_build_dir)
999 # Downloaded archive file path, downloads build archive for given revision.
1000 # This will be False if the build isn't yet available.
1001 downloaded_file = fetch_build_func()
1003 # When build archive doesn't exist, post a build request to try server
1004 # and wait for the build to be produced.
1005 if not downloaded_file:
1006 downloaded_file = self._RequestBuildAndWait(
1007 revision, fetch_build=fetch_build_func, patch=deps_patch)
1008 if not downloaded_file:
1009 return None
1011 return downloaded_file
1013 def _RequestBuildAndWait(self, git_revision, fetch_build, patch=None):
1014 """Triggers a try job for a build job.
1016 This function prepares and starts a try job on the tryserver.chromium.perf
1017 master, and waits for the binaries to be produced and archived in cloud
1018 storage. Once the build is ready it's downloaded.
1020 Args:
1021 git_revision: A Git hash revision.
1022 fetch_build: Function to check and download build from cloud storage.
1023 patch: A DEPS patch (used while bisecting 3rd party repositories).
1025 Returns:
1026 Downloaded archive file path when requested build exists and download is
1027 successful, otherwise None.
1029 if not fetch_build:
1030 return False
1032 # Create a unique ID for each build request posted to try server builders.
1033 # This ID is added to "Reason" property of the build.
1034 build_request_id = GetSHA1HexDigest(
1035 '%s-%s-%s' % (git_revision, patch, time.time()))
1037 # Reverts any changes to DEPS file.
1038 source_control.CheckoutFileAtRevision(
1039 bisect_utils.FILE_DEPS, git_revision, cwd=self.src_cwd)
1041 bot_name = self._GetBuilderName(self.opts.target_platform)
1042 build_timeout = self._GetBuilderBuildTime()
1043 target_file = None
1044 try:
1045 # Execute try job request to build revision with patch.
1046 _BuilderTryjob(git_revision, bot_name, build_request_id, patch)
1047 target_file, error_msg = _WaitUntilBuildIsReady(
1048 fetch_build, bot_name, self.opts.builder_host,
1049 self.opts.builder_port, build_request_id, build_timeout)
1050 if not target_file:
1051 logging.warn('%s [revision: %s]', error_msg, git_revision)
1052 except RunGitError as e:
1053 logging.warn('Failed to post builder try job for revision: [%s].\n'
1054 'Error: %s', git_revision, e)
1056 return target_file
1058 @staticmethod
1059 def _GetBuilderName(target_platform):
1060 """Gets builder bot name and build time in seconds based on platform."""
1061 if bisect_utils.IsWindowsHost():
1062 return 'win_perf_bisect_builder'
1063 if bisect_utils.IsLinuxHost():
1064 if target_platform == 'android':
1065 return 'android_perf_bisect_builder'
1066 return 'linux_perf_bisect_builder'
1067 if bisect_utils.IsMacHost():
1068 return 'mac_perf_bisect_builder'
1069 raise NotImplementedError('Unsupported Platform "%s".' % sys.platform)
1071 @staticmethod
1072 def _GetBuilderBuildTime():
1073 """Returns the time to wait for a build after requesting one."""
1074 if bisect_utils.IsWindowsHost():
1075 return MAX_WIN_BUILD_TIME
1076 if bisect_utils.IsLinuxHost():
1077 return MAX_LINUX_BUILD_TIME
1078 if bisect_utils.IsMacHost():
1079 return MAX_MAC_BUILD_TIME
1080 raise NotImplementedError('Unsupported Platform "%s".' % sys.platform)
1082 def _UnzipAndMoveBuildProducts(self, downloaded_file, build_dir,
1083 build_type='Release'):
1084 """Unzips the build archive and moves it to the build output directory.
1086 The build output directory is whereever the binaries are expected to
1087 be in order to start Chrome and run tests.
1089 Args:
1090 downloaded_file: File path of the downloaded zip file.
1091 build_dir: Directory where the the zip file was downloaded to.
1092 build_type: "Release" or "Debug".
1094 Returns:
1095 True if successful, False otherwise.
1097 abs_build_dir = os.path.abspath(build_dir)
1098 output_dir = os.path.join(
1099 abs_build_dir, GetZipFileName(target_arch=self.opts.target_arch))
1101 try:
1102 RemoveDirectoryTree(output_dir)
1103 self.BackupOrRestoreOutputDirectory(restore=False)
1104 # Build output directory based on target(e.g. out/Release, out/Debug).
1105 target_build_output_dir = os.path.join(abs_build_dir, build_type)
1106 ExtractZip(downloaded_file, abs_build_dir)
1107 if not os.path.exists(output_dir):
1108 # Due to recipe changes, the builds extract folder contains
1109 # out/Release instead of full-build-<platform>/Release.
1110 if os.path.exists(os.path.join(abs_build_dir, 'out', build_type)):
1111 output_dir = os.path.join(abs_build_dir, 'out', build_type)
1112 else:
1113 raise IOError('Missing extracted folder %s ' % output_dir)
1115 logging.info('Moving build from %s to %s',
1116 output_dir, target_build_output_dir)
1117 shutil.move(output_dir, target_build_output_dir)
1118 return True
1119 except Exception as e:
1120 logging.info('Something went wrong while extracting archive file: %s', e)
1121 self.BackupOrRestoreOutputDirectory(restore=True)
1122 # Cleanup any leftovers from unzipping.
1123 if os.path.exists(output_dir):
1124 RemoveDirectoryTree(output_dir)
1125 finally:
1126 # Delete downloaded archive
1127 if os.path.exists(downloaded_file):
1128 os.remove(downloaded_file)
1129 return False
1131 def IsDownloadable(self, depot):
1132 """Checks if build can be downloaded based on target platform and depot."""
1133 if (self.opts.target_platform in ['chromium', 'android'] and
1134 self.opts.gs_bucket):
1135 return (depot == 'chromium' or
1136 'chromium' in bisect_utils.DEPOT_DEPS_NAME[depot]['from'] or
1137 'v8' in bisect_utils.DEPOT_DEPS_NAME[depot]['from'])
1138 return False
1140 def UpdateDepsContents(self, deps_contents, depot, git_revision, deps_key):
1141 """Returns modified version of DEPS file contents.
1143 Args:
1144 deps_contents: DEPS file content.
1145 depot: Current depot being bisected.
1146 git_revision: A git hash to be updated in DEPS.
1147 deps_key: Key in vars section of DEPS file to be searched.
1149 Returns:
1150 Updated DEPS content as string if deps key is found, otherwise None.
1152 # Check whether the depot and revision pattern in DEPS file vars
1153 # e.g. for webkit the format is "webkit_revision": "12345".
1154 deps_revision = re.compile(r'(?<="%s": ")([0-9]+)(?=")' % deps_key,
1155 re.MULTILINE)
1156 new_data = None
1157 if re.search(deps_revision, deps_contents):
1158 commit_position = source_control.GetCommitPosition(
1159 git_revision, self.depot_registry.GetDepotDir(depot))
1160 if not commit_position:
1161 logging.warn('Could not determine commit position for %s', git_revision)
1162 return None
1163 # Update the revision information for the given depot
1164 new_data = re.sub(deps_revision, str(commit_position), deps_contents)
1165 else:
1166 # Check whether the depot and revision pattern in DEPS file vars
1167 # e.g. for webkit the format is "webkit_revision": "559a6d4ab7a84c539..".
1168 deps_revision = re.compile(
1169 r'(?<=["\']%s["\']: ["\'])([a-fA-F0-9]{40})(?=["\'])' % deps_key,
1170 re.MULTILINE)
1171 if re.search(deps_revision, deps_contents):
1172 new_data = re.sub(deps_revision, git_revision, deps_contents)
1173 if new_data:
1174 # For v8_bleeding_edge revisions change V8 branch in order
1175 # to fetch bleeding edge revision.
1176 if depot == 'v8_bleeding_edge':
1177 new_data = _UpdateV8Branch(new_data)
1178 if not new_data:
1179 return None
1180 return new_data
1182 def UpdateDeps(self, revision, depot, deps_file):
1183 """Updates DEPS file with new revision of dependency repository.
1185 This method search DEPS for a particular pattern in which depot revision
1186 is specified (e.g "webkit_revision": "123456"). If a match is found then
1187 it resolves the given git hash to SVN revision and replace it in DEPS file.
1189 Args:
1190 revision: A git hash revision of the dependency repository.
1191 depot: Current depot being bisected.
1192 deps_file: Path to DEPS file.
1194 Returns:
1195 True if DEPS file is modified successfully, otherwise False.
1197 if not os.path.exists(deps_file):
1198 return False
1200 deps_var = bisect_utils.DEPOT_DEPS_NAME[depot]['deps_var']
1201 # Don't update DEPS file if deps_var is not set in DEPOT_DEPS_NAME.
1202 if not deps_var:
1203 logging.warn('DEPS update not supported for Depot: %s', depot)
1204 return False
1206 # Hack for Angle repository. In the DEPS file, "vars" dictionary variable
1207 # contains "angle_revision" key that holds git hash instead of SVN revision.
1208 # And sometime "angle_revision" key is not specified in "vars" variable.
1209 # In such cases check, "deps" dictionary variable that matches
1210 # angle.git@[a-fA-F0-9]{40}$ and replace git hash.
1211 if depot == 'angle':
1212 return _UpdateDEPSForAngle(revision, depot, deps_file)
1214 try:
1215 deps_contents = ReadStringFromFile(deps_file)
1216 updated_deps_content = self.UpdateDepsContents(
1217 deps_contents, depot, revision, deps_var)
1218 # Write changes to DEPS file
1219 if updated_deps_content:
1220 WriteStringToFile(updated_deps_content, deps_file)
1221 return True
1222 except IOError, e:
1223 logging.warn('Something went wrong while updating DEPS file. [%s]', e)
1224 return False
1226 def CreateDEPSPatch(self, depot, revision):
1227 """Modifies DEPS and returns diff as text.
1229 Args:
1230 depot: Current depot being bisected.
1231 revision: A git hash revision of the dependency repository.
1233 Returns:
1234 A tuple with git hash of chromium revision and DEPS patch text.
1236 deps_file_path = os.path.join(self.src_cwd, bisect_utils.FILE_DEPS)
1237 if not os.path.exists(deps_file_path):
1238 raise RuntimeError('DEPS file does not exists.[%s]' % deps_file_path)
1239 # Get current chromium revision (git hash).
1240 cmd = ['rev-parse', 'HEAD']
1241 chromium_sha = bisect_utils.CheckRunGit(cmd).strip()
1242 if not chromium_sha:
1243 raise RuntimeError('Failed to determine Chromium revision for %s' %
1244 revision)
1245 if ('chromium' in bisect_utils.DEPOT_DEPS_NAME[depot]['from'] or
1246 'v8' in bisect_utils.DEPOT_DEPS_NAME[depot]['from']):
1247 # Checkout DEPS file for the current chromium revision.
1248 if source_control.CheckoutFileAtRevision(
1249 bisect_utils.FILE_DEPS, chromium_sha, cwd=self.src_cwd):
1250 if self.UpdateDeps(revision, depot, deps_file_path):
1251 diff_command = [
1252 'diff',
1253 '--src-prefix=',
1254 '--dst-prefix=',
1255 '--no-ext-diff',
1256 bisect_utils.FILE_DEPS,
1258 diff_text = bisect_utils.CheckRunGit(diff_command, cwd=self.src_cwd)
1259 return (chromium_sha, ChangeBackslashToSlashInPatch(diff_text))
1260 else:
1261 raise RuntimeError(
1262 'Failed to update DEPS file for chromium: [%s]' % chromium_sha)
1263 else:
1264 raise RuntimeError(
1265 'DEPS checkout Failed for chromium revision : [%s]' % chromium_sha)
1266 return (None, None)
1268 def ObtainBuild(self, depot, revision=None):
1269 """Obtains a build by either downloading or building directly.
1271 Args:
1272 depot: Dependency repository name.
1273 revision: A git commit hash. If None is given, the currently checked-out
1274 revision is built.
1276 Returns:
1277 True for success.
1279 if self.opts.debug_ignore_build:
1280 return True
1282 build_success = False
1283 cwd = os.getcwd()
1284 os.chdir(self.src_cwd)
1285 # Fetch build archive for the given revision from the cloud storage when
1286 # the storage bucket is passed.
1287 if self.IsDownloadable(depot) and revision:
1288 build_success = self._DownloadAndUnzipBuild(revision, depot)
1289 else:
1290 # These codes are executed when bisect bots builds binaries locally.
1291 build_success = self.builder.Build(depot, self.opts)
1292 os.chdir(cwd)
1293 return build_success
1295 def RunGClientHooks(self):
1296 """Runs gclient with runhooks command.
1298 Returns:
1299 True if gclient reports no errors.
1301 if self.opts.debug_ignore_build:
1302 return True
1303 return not bisect_utils.RunGClient(['runhooks'], cwd=self.src_cwd)
1305 def _IsBisectModeUsingMetric(self):
1306 return self.opts.bisect_mode in [bisect_utils.BISECT_MODE_MEAN,
1307 bisect_utils.BISECT_MODE_STD_DEV]
1309 def _IsBisectModeReturnCode(self):
1310 return self.opts.bisect_mode in [bisect_utils.BISECT_MODE_RETURN_CODE]
1312 def _IsBisectModeStandardDeviation(self):
1313 return self.opts.bisect_mode in [bisect_utils.BISECT_MODE_STD_DEV]
1315 def GetCompatibleCommand(self, command_to_run, revision, depot):
1316 """Return a possibly modified test command depending on the revision.
1318 Prior to crrev.com/274857 *only* android-chromium-testshell
1319 Then until crrev.com/276628 *both* (android-chromium-testshell and
1320 android-chrome-shell) work. After that rev 276628 *only*
1321 android-chrome-shell works. The bisect_perf_regression.py script should
1322 handle these cases and set appropriate browser type based on revision.
1324 if self.opts.target_platform in ['android']:
1325 # When its a third_party depot, get the chromium revision.
1326 if depot != 'chromium':
1327 revision = bisect_utils.CheckRunGit(
1328 ['rev-parse', 'HEAD'], cwd=self.src_cwd).strip()
1329 commit_position = source_control.GetCommitPosition(revision,
1330 cwd=self.src_cwd)
1331 if not commit_position:
1332 return command_to_run
1333 cmd_re = re.compile(r'--browser=(?P<browser_type>\S+)')
1334 matches = cmd_re.search(command_to_run)
1335 if bisect_utils.IsStringInt(commit_position) and matches:
1336 cmd_browser = matches.group('browser_type')
1337 if commit_position <= 274857 and cmd_browser == 'android-chrome-shell':
1338 return command_to_run.replace(cmd_browser,
1339 'android-chromium-testshell')
1340 elif (commit_position >= 276628 and
1341 cmd_browser == 'android-chromium-testshell'):
1342 return command_to_run.replace(cmd_browser,
1343 'android-chrome-shell')
1344 return command_to_run
1346 def RunPerformanceTestAndParseResults(
1347 self, command_to_run, metric, reset_on_first_run=False,
1348 upload_on_last_run=False, results_label=None):
1349 """Runs a performance test on the current revision and parses the results.
1351 Args:
1352 command_to_run: The command to be run to execute the performance test.
1353 metric: The metric to parse out from the results of the performance test.
1354 This is the result chart name and trace name, separated by slash.
1355 May be None for perf try jobs.
1356 reset_on_first_run: If True, pass the flag --reset-results on first run.
1357 upload_on_last_run: If True, pass the flag --upload-results on last run.
1358 results_label: A value for the option flag --results-label.
1359 The arguments reset_on_first_run, upload_on_last_run and results_label
1360 are all ignored if the test is not a Telemetry test.
1362 Returns:
1363 (values dict, 0) if --debug_ignore_perf_test was passed.
1364 (values dict, 0, test output) if the test was run successfully.
1365 (error message, -1) if the test couldn't be run.
1366 (error message, -1, test output) if the test ran but there was an error.
1368 success_code, failure_code = 0, -1
1370 if self.opts.debug_ignore_perf_test:
1371 fake_results = {
1372 'mean': 0.0,
1373 'std_err': 0.0,
1374 'std_dev': 0.0,
1375 'values': [0.0]
1378 # When debug_fake_test_mean is set, its value is returned as the mean
1379 # and the flag is cleared so that further calls behave as if it wasn't
1380 # set (returning the fake_results dict as defined above).
1381 if self.opts.debug_fake_first_test_mean:
1382 fake_results['mean'] = float(self.opts.debug_fake_first_test_mean)
1383 self.opts.debug_fake_first_test_mean = 0
1385 return (fake_results, success_code)
1387 # For Windows platform set posix=False, to parse windows paths correctly.
1388 # On Windows, path separators '\' or '\\' are replace by '' when posix=True,
1389 # refer to http://bugs.python.org/issue1724822. By default posix=True.
1390 args = shlex.split(command_to_run, posix=not bisect_utils.IsWindowsHost())
1392 if not _GenerateProfileIfNecessary(args):
1393 err_text = 'Failed to generate profile for performance test.'
1394 return (err_text, failure_code)
1396 is_telemetry = bisect_utils.IsTelemetryCommand(command_to_run)
1398 start_time = time.time()
1400 metric_values = []
1401 output_of_all_runs = ''
1402 for i in xrange(self.opts.repeat_test_count):
1403 # Can ignore the return code since if the tests fail, it won't return 0.
1404 current_args = copy.copy(args)
1405 if is_telemetry:
1406 if i == 0 and reset_on_first_run:
1407 current_args.append('--reset-results')
1408 if i == self.opts.repeat_test_count - 1 and upload_on_last_run:
1409 current_args.append('--upload-results')
1410 if results_label:
1411 current_args.append('--results-label=%s' % results_label)
1412 try:
1413 output, return_code = bisect_utils.RunProcessAndRetrieveOutput(
1414 current_args, cwd=self.src_cwd)
1415 except OSError, e:
1416 if e.errno == errno.ENOENT:
1417 err_text = ('Something went wrong running the performance test. '
1418 'Please review the command line:\n\n')
1419 if 'src/' in ' '.join(args):
1420 err_text += ('Check that you haven\'t accidentally specified a '
1421 'path with src/ in the command.\n\n')
1422 err_text += ' '.join(args)
1423 err_text += '\n'
1425 return (err_text, failure_code)
1426 raise
1428 output_of_all_runs += output
1429 if self.opts.output_buildbot_annotations:
1430 print output
1432 if metric and self._IsBisectModeUsingMetric():
1433 metric_values += _ParseMetricValuesFromOutput(metric, output)
1434 # If we're bisecting on a metric (ie, changes in the mean or
1435 # standard deviation) and no metric values are produced, bail out.
1436 if not metric_values:
1437 break
1438 elif self._IsBisectModeReturnCode():
1439 metric_values.append(return_code)
1441 elapsed_minutes = (time.time() - start_time) / 60.0
1442 if elapsed_minutes >= self.opts.max_time_minutes:
1443 break
1445 if metric and len(metric_values) == 0:
1446 err_text = 'Metric %s was not found in the test output.' % metric
1447 # TODO(qyearsley): Consider also getting and displaying a list of metrics
1448 # that were found in the output here.
1449 return (err_text, failure_code, output_of_all_runs)
1451 # If we're bisecting on return codes, we're really just looking for zero vs
1452 # non-zero.
1453 values = {}
1454 if self._IsBisectModeReturnCode():
1455 # If any of the return codes is non-zero, output 1.
1456 overall_return_code = 0 if (
1457 all(current_value == 0 for current_value in metric_values)) else 1
1459 values = {
1460 'mean': overall_return_code,
1461 'std_err': 0.0,
1462 'std_dev': 0.0,
1463 'values': metric_values,
1466 print 'Results of performance test: Command returned with %d' % (
1467 overall_return_code)
1468 print
1469 elif metric:
1470 # Need to get the average value if there were multiple values.
1471 truncated_mean = math_utils.TruncatedMean(
1472 metric_values, self.opts.truncate_percent)
1473 standard_err = math_utils.StandardError(metric_values)
1474 standard_dev = math_utils.StandardDeviation(metric_values)
1476 if self._IsBisectModeStandardDeviation():
1477 metric_values = [standard_dev]
1479 values = {
1480 'mean': truncated_mean,
1481 'std_err': standard_err,
1482 'std_dev': standard_dev,
1483 'values': metric_values,
1486 print 'Results of performance test: %12f %12f' % (
1487 truncated_mean, standard_err)
1488 print
1489 return (values, success_code, output_of_all_runs)
1491 def PerformPreBuildCleanup(self):
1492 """Performs cleanup between runs."""
1493 print 'Cleaning up between runs.'
1494 print
1496 # Leaving these .pyc files around between runs may disrupt some perf tests.
1497 for (path, _, files) in os.walk(self.src_cwd):
1498 for cur_file in files:
1499 if cur_file.endswith('.pyc'):
1500 path_to_file = os.path.join(path, cur_file)
1501 os.remove(path_to_file)
1503 def _RunPostSync(self, _depot):
1504 """Performs any work after syncing.
1506 Args:
1507 depot: Depot name.
1509 Returns:
1510 True if successful.
1512 if self.opts.target_platform == 'android':
1513 if not builder.SetupAndroidBuildEnvironment(self.opts,
1514 path_to_src=self.src_cwd):
1515 return False
1517 return self.RunGClientHooks()
1519 @staticmethod
1520 def ShouldSkipRevision(depot, revision):
1521 """Checks whether a particular revision can be safely skipped.
1523 Some commits can be safely skipped (such as a DEPS roll), since the tool
1524 is git based those changes would have no effect.
1526 Args:
1527 depot: The depot being bisected.
1528 revision: Current revision we're synced to.
1530 Returns:
1531 True if we should skip building/testing this revision.
1533 if depot == 'chromium':
1534 cmd = ['diff-tree', '--no-commit-id', '--name-only', '-r', revision]
1535 output = bisect_utils.CheckRunGit(cmd)
1537 files = output.splitlines()
1539 if len(files) == 1 and files[0] == 'DEPS':
1540 return True
1542 return False
1544 def RunTest(self, revision, depot, command, metric, skippable=False):
1545 """Performs a full sync/build/run of the specified revision.
1547 Args:
1548 revision: The revision to sync to.
1549 depot: The depot that's being used at the moment (src, webkit, etc.)
1550 command: The command to execute the performance test.
1551 metric: The performance metric being tested.
1553 Returns:
1554 On success, a tuple containing the results of the performance test.
1555 Otherwise, a tuple with the error message.
1557 logging.info('Running RunTest with rev "%s", command "%s"',
1558 revision, command)
1559 # Decide which sync program to use.
1560 sync_client = None
1561 if depot == 'chromium' or depot == 'android-chrome':
1562 sync_client = 'gclient'
1564 # Do the syncing for all depots.
1565 if not self.opts.debug_ignore_sync:
1566 if not self._SyncRevision(depot, revision, sync_client):
1567 return ('Failed to sync: [%s]' % str(revision), BUILD_RESULT_FAIL)
1569 # Try to do any post-sync steps. This may include "gclient runhooks".
1570 if not self._RunPostSync(depot):
1571 return ('Failed to run [gclient runhooks].', BUILD_RESULT_FAIL)
1573 # Skip this revision if it can be skipped.
1574 if skippable and self.ShouldSkipRevision(depot, revision):
1575 return ('Skipped revision: [%s]' % str(revision),
1576 BUILD_RESULT_SKIPPED)
1578 # Obtain a build for this revision. This may be done by requesting a build
1579 # from another builder, waiting for it and downloading it.
1580 start_build_time = time.time()
1581 build_success = self.ObtainBuild(depot, revision)
1582 if not build_success:
1583 return ('Failed to build revision: [%s]' % str(revision),
1584 BUILD_RESULT_FAIL)
1585 after_build_time = time.time()
1587 # Possibly alter the command.
1588 command = self.GetCompatibleCommand(command, revision, depot)
1590 # Run the command and get the results.
1591 results = self.RunPerformanceTestAndParseResults(command, metric)
1593 # Restore build output directory once the tests are done, to avoid
1594 # any discrepancies.
1595 if self.IsDownloadable(depot) and revision:
1596 self.BackupOrRestoreOutputDirectory(restore=True)
1598 # A value other than 0 indicates that the test couldn't be run, and results
1599 # should also include an error message.
1600 if results[1] != 0:
1601 return results
1603 external_revisions = self._Get3rdPartyRevisions(depot)
1605 if not external_revisions is None:
1606 return (results[0], results[1], external_revisions,
1607 time.time() - after_build_time, after_build_time -
1608 start_build_time)
1609 else:
1610 return ('Failed to parse DEPS file for external revisions.',
1611 BUILD_RESULT_FAIL)
1613 def _SyncRevision(self, depot, revision, sync_client):
1614 """Syncs depot to particular revision.
1616 Args:
1617 depot: The depot that's being used at the moment (src, webkit, etc.)
1618 revision: The revision to sync to.
1619 sync_client: Program used to sync, e.g. "gclient". Can be None.
1621 Returns:
1622 True if successful, False otherwise.
1624 self.depot_registry.ChangeToDepotDir(depot)
1626 if sync_client:
1627 self.PerformPreBuildCleanup()
1629 # When using gclient to sync, you need to specify the depot you
1630 # want so that all the dependencies sync properly as well.
1631 # i.e. gclient sync src@<SHA1>
1632 if sync_client == 'gclient':
1633 revision = '%s@%s' % (bisect_utils.DEPOT_DEPS_NAME[depot]['src'],
1634 revision)
1636 return source_control.SyncToRevision(revision, sync_client)
1638 def _CheckIfRunPassed(self, current_value, known_good_value, known_bad_value):
1639 """Given known good and bad values, decide if the current_value passed
1640 or failed.
1642 Args:
1643 current_value: The value of the metric being checked.
1644 known_bad_value: The reference value for a "failed" run.
1645 known_good_value: The reference value for a "passed" run.
1647 Returns:
1648 True if the current_value is closer to the known_good_value than the
1649 known_bad_value.
1651 if self.opts.bisect_mode == bisect_utils.BISECT_MODE_STD_DEV:
1652 dist_to_good_value = abs(current_value['std_dev'] -
1653 known_good_value['std_dev'])
1654 dist_to_bad_value = abs(current_value['std_dev'] -
1655 known_bad_value['std_dev'])
1656 else:
1657 dist_to_good_value = abs(current_value['mean'] - known_good_value['mean'])
1658 dist_to_bad_value = abs(current_value['mean'] - known_bad_value['mean'])
1660 return dist_to_good_value < dist_to_bad_value
1662 def _GetV8BleedingEdgeFromV8TrunkIfMappable(
1663 self, revision, bleeding_edge_branch):
1664 """Gets v8 bleeding edge revision mapped to v8 revision in trunk.
1666 Args:
1667 revision: A trunk V8 revision mapped to bleeding edge revision.
1668 bleeding_edge_branch: Branch used to perform lookup of bleeding edge
1669 revision.
1670 Return:
1671 A mapped bleeding edge revision if found, otherwise None.
1673 commit_position = source_control.GetCommitPosition(revision)
1675 if bisect_utils.IsStringInt(commit_position):
1676 # V8 is tricky to bisect, in that there are only a few instances when
1677 # we can dive into bleeding_edge and get back a meaningful result.
1678 # Try to detect a V8 "business as usual" case, which is when:
1679 # 1. trunk revision N has description "Version X.Y.Z"
1680 # 2. bleeding_edge revision (N-1) has description "Prepare push to
1681 # trunk. Now working on X.Y.(Z+1)."
1683 # As of 01/24/2014, V8 trunk descriptions are formatted:
1684 # "Version 3.X.Y (based on bleeding_edge revision rZ)"
1685 # So we can just try parsing that out first and fall back to the old way.
1686 v8_dir = self.depot_registry.GetDepotDir('v8')
1687 v8_bleeding_edge_dir = self.depot_registry.GetDepotDir('v8_bleeding_edge')
1689 revision_info = source_control.QueryRevisionInfo(revision, cwd=v8_dir)
1690 version_re = re.compile("Version (?P<values>[0-9,.]+)")
1691 regex_results = version_re.search(revision_info['subject'])
1692 if regex_results:
1693 git_revision = None
1694 if 'based on bleeding_edge' in revision_info['subject']:
1695 try:
1696 bleeding_edge_revision = revision_info['subject'].split(
1697 'bleeding_edge revision r')[1]
1698 bleeding_edge_revision = int(bleeding_edge_revision.split(')')[0])
1699 bleeding_edge_url = ('https://v8.googlecode.com/svn/branches/'
1700 'bleeding_edge@%s' % bleeding_edge_revision)
1701 cmd = ['log',
1702 '--format=%H',
1703 '--grep',
1704 bleeding_edge_url,
1705 '-1',
1706 bleeding_edge_branch]
1707 output = bisect_utils.CheckRunGit(cmd, cwd=v8_dir)
1708 if output:
1709 git_revision = output.strip()
1710 return git_revision
1711 except (IndexError, ValueError):
1712 pass
1713 else:
1714 # V8 rolls description changed after V8 git migration, new description
1715 # includes "Version 3.X.Y (based on <git hash>)"
1716 try:
1717 rxp = re.compile('based on (?P<git_revision>[a-fA-F0-9]+)')
1718 re_results = rxp.search(revision_info['subject'])
1719 if re_results:
1720 return re_results.group('git_revision')
1721 except (IndexError, ValueError):
1722 pass
1723 if not git_revision:
1724 # Wasn't successful, try the old way of looking for "Prepare push to"
1725 git_revision = source_control.ResolveToRevision(
1726 int(commit_position) - 1, 'v8_bleeding_edge',
1727 bisect_utils.DEPOT_DEPS_NAME, -1, cwd=v8_bleeding_edge_dir)
1729 if git_revision:
1730 revision_info = source_control.QueryRevisionInfo(git_revision,
1731 cwd=v8_bleeding_edge_dir)
1733 if 'Prepare push to trunk' in revision_info['subject']:
1734 return git_revision
1735 return None
1737 def _GetNearestV8BleedingEdgeFromTrunk(
1738 self, revision, v8_branch, bleeding_edge_branch, search_forward=True):
1739 """Gets the nearest V8 roll and maps to bleeding edge revision.
1741 V8 is a bit tricky to bisect since it isn't just rolled out like blink.
1742 Each revision on trunk might just be whatever was in bleeding edge, rolled
1743 directly out. Or it could be some mixture of previous v8 trunk versions,
1744 with bits and pieces cherry picked out from bleeding edge. In order to
1745 bisect, we need both the before/after versions on trunk v8 to be just pushes
1746 from bleeding edge. With the V8 git migration, the branches got switched.
1747 a) master (external/v8) == candidates (v8/v8)
1748 b) bleeding_edge (external/v8) == master (v8/v8)
1750 Args:
1751 revision: A V8 revision to get its nearest bleeding edge revision
1752 search_forward: Searches forward if True, otherwise search backward.
1754 Return:
1755 A mapped bleeding edge revision if found, otherwise None.
1757 cwd = self.depot_registry.GetDepotDir('v8')
1758 cmd = ['log', '--format=%ct', '-1', revision]
1759 output = bisect_utils.CheckRunGit(cmd, cwd=cwd)
1760 commit_time = int(output)
1761 commits = []
1762 if search_forward:
1763 cmd = ['log',
1764 '--format=%H',
1765 '--after=%d' % commit_time,
1766 v8_branch,
1767 '--reverse']
1768 output = bisect_utils.CheckRunGit(cmd, cwd=cwd)
1769 output = output.split()
1770 commits = output
1771 #Get 10 git hashes immediately after the given commit.
1772 commits = commits[:10]
1773 else:
1774 cmd = ['log',
1775 '--format=%H',
1776 '-10',
1777 '--before=%d' % commit_time,
1778 v8_branch]
1779 output = bisect_utils.CheckRunGit(cmd, cwd=cwd)
1780 output = output.split()
1781 commits = output
1783 bleeding_edge_revision = None
1785 for c in commits:
1786 bleeding_edge_revision = self._GetV8BleedingEdgeFromV8TrunkIfMappable(
1787 c, bleeding_edge_branch)
1788 if bleeding_edge_revision:
1789 break
1791 return bleeding_edge_revision
1793 def _FillInV8BleedingEdgeInfo(self, min_revision_state, max_revision_state):
1794 cwd = self.depot_registry.GetDepotDir('v8')
1795 # when "remote.origin.url" is https://chromium.googlesource.com/v8/v8.git
1796 v8_branch = 'origin/candidates'
1797 bleeding_edge_branch = 'origin/master'
1799 # Support for the chromium revisions with external V8 repo.
1800 # ie https://chromium.googlesource.com/external/v8.git
1801 cmd = ['config', '--get', 'remote.origin.url']
1802 v8_repo_url = bisect_utils.CheckRunGit(cmd, cwd=cwd)
1804 if 'external/v8.git' in v8_repo_url:
1805 v8_branch = 'origin/master'
1806 bleeding_edge_branch = 'origin/bleeding_edge'
1808 r1 = self._GetNearestV8BleedingEdgeFromTrunk(min_revision_state.revision,
1809 v8_branch, bleeding_edge_branch, search_forward=True)
1810 r2 = self._GetNearestV8BleedingEdgeFromTrunk(max_revision_state.revision,
1811 v8_branch, bleeding_edge_branch, search_forward=False)
1812 min_revision_state.external['v8_bleeding_edge'] = r1
1813 max_revision_state.external['v8_bleeding_edge'] = r2
1815 if (not self._GetV8BleedingEdgeFromV8TrunkIfMappable(
1816 min_revision_state.revision, bleeding_edge_branch)
1817 or not self._GetV8BleedingEdgeFromV8TrunkIfMappable(
1818 max_revision_state.revision, bleeding_edge_branch)):
1819 self.warnings.append(
1820 'Trunk revisions in V8 did not map directly to bleeding_edge. '
1821 'Attempted to expand the range to find V8 rolls which did map '
1822 'directly to bleeding_edge revisions, but results might not be '
1823 'valid.')
1825 def _FindNextDepotToBisect(
1826 self, current_depot, min_revision_state, max_revision_state):
1827 """Decides which depot the script should dive into next (if any).
1829 Args:
1830 current_depot: Current depot being bisected.
1831 min_revision_state: State of the earliest revision in the bisect range.
1832 max_revision_state: State of the latest revision in the bisect range.
1834 Returns:
1835 Name of the depot to bisect next, or None.
1837 external_depot = None
1838 for next_depot in bisect_utils.DEPOT_NAMES:
1839 if bisect_utils.DEPOT_DEPS_NAME[next_depot].has_key('platform'):
1840 if bisect_utils.DEPOT_DEPS_NAME[next_depot]['platform'] != os.name:
1841 continue
1843 if not (bisect_utils.DEPOT_DEPS_NAME[next_depot]['recurse']
1844 and min_revision_state.depot
1845 in bisect_utils.DEPOT_DEPS_NAME[next_depot]['from']):
1846 continue
1848 if current_depot == 'v8':
1849 # We grab the bleeding_edge info here rather than earlier because we
1850 # finally have the revision range. From that we can search forwards and
1851 # backwards to try to match trunk revisions to bleeding_edge.
1852 self._FillInV8BleedingEdgeInfo(min_revision_state, max_revision_state)
1854 if (min_revision_state.external.get(next_depot) ==
1855 max_revision_state.external.get(next_depot)):
1856 continue
1858 if (min_revision_state.external.get(next_depot) and
1859 max_revision_state.external.get(next_depot)):
1860 external_depot = next_depot
1861 break
1863 return external_depot
1865 def PrepareToBisectOnDepot(
1866 self, current_depot, start_revision, end_revision, previous_revision):
1867 """Changes to the appropriate directory and gathers a list of revisions
1868 to bisect between |start_revision| and |end_revision|.
1870 Args:
1871 current_depot: The depot we want to bisect.
1872 start_revision: Start of the revision range.
1873 end_revision: End of the revision range.
1874 previous_revision: The last revision we synced to on |previous_depot|.
1876 Returns:
1877 A list containing the revisions between |start_revision| and
1878 |end_revision| inclusive.
1880 # Change into working directory of external library to run
1881 # subsequent commands.
1882 self.depot_registry.ChangeToDepotDir(current_depot)
1884 # V8 (and possibly others) is merged in periodically. Bisecting
1885 # this directory directly won't give much good info.
1886 if bisect_utils.DEPOT_DEPS_NAME[current_depot].has_key('custom_deps'):
1887 config_path = os.path.join(self.src_cwd, '..')
1888 if bisect_utils.RunGClientAndCreateConfig(
1889 self.opts, bisect_utils.DEPOT_DEPS_NAME[current_depot]['custom_deps'],
1890 cwd=config_path):
1891 return []
1892 if bisect_utils.RunGClient(
1893 ['sync', '--revision', previous_revision], cwd=self.src_cwd):
1894 return []
1896 if current_depot == 'v8_bleeding_edge':
1897 self.depot_registry.ChangeToDepotDir('chromium')
1899 shutil.move('v8', 'v8.bak')
1900 shutil.move('v8_bleeding_edge', 'v8')
1902 self.cleanup_commands.append(['mv', 'v8', 'v8_bleeding_edge'])
1903 self.cleanup_commands.append(['mv', 'v8.bak', 'v8'])
1905 self.depot_registry.SetDepotDir('v8_bleeding_edge',
1906 os.path.join(self.src_cwd, 'v8'))
1907 self.depot_registry.SetDepotDir('v8', os.path.join(self.src_cwd,
1908 'v8.bak'))
1910 self.depot_registry.ChangeToDepotDir(current_depot)
1912 depot_revision_list = self.GetRevisionList(current_depot,
1913 end_revision,
1914 start_revision)
1916 self.depot_registry.ChangeToDepotDir('chromium')
1918 return depot_revision_list
1920 def GatherReferenceValues(self, good_rev, bad_rev, cmd, metric, target_depot):
1921 """Gathers reference values by running the performance tests on the
1922 known good and bad revisions.
1924 Args:
1925 good_rev: The last known good revision where the performance regression
1926 has not occurred yet.
1927 bad_rev: A revision where the performance regression has already occurred.
1928 cmd: The command to execute the performance test.
1929 metric: The metric being tested for regression.
1931 Returns:
1932 A tuple with the results of building and running each revision.
1934 bad_run_results = self.RunTest(bad_rev, target_depot, cmd, metric)
1936 good_run_results = None
1938 if not bad_run_results[1]:
1939 good_run_results = self.RunTest(good_rev, target_depot, cmd, metric)
1941 return (bad_run_results, good_run_results)
1943 def PrintRevisionsToBisectMessage(self, revision_list, depot):
1944 if self.opts.output_buildbot_annotations:
1945 step_name = 'Bisection Range: [%s:%s - %s]' % (depot, revision_list[-1],
1946 revision_list[0])
1947 bisect_utils.OutputAnnotationStepStart(step_name)
1949 print
1950 print 'Revisions to bisect on [%s]:' % depot
1951 for revision_id in revision_list:
1952 print ' -> %s' % (revision_id, )
1953 print
1955 if self.opts.output_buildbot_annotations:
1956 bisect_utils.OutputAnnotationStepClosed()
1958 def NudgeRevisionsIfDEPSChange(self, bad_revision, good_revision,
1959 good_svn_revision=None):
1960 """Checks to see if changes to DEPS file occurred, and that the revision
1961 range also includes the change to .DEPS.git. If it doesn't, attempts to
1962 expand the revision range to include it.
1964 Args:
1965 bad_revision: First known bad git revision.
1966 good_revision: Last known good git revision.
1967 good_svn_revision: Last known good svn revision.
1969 Returns:
1970 A tuple with the new bad and good revisions.
1972 # DONOT perform nudge because at revision 291563 .DEPS.git was removed
1973 # and source contain only DEPS file for dependency changes.
1974 if good_svn_revision >= 291563:
1975 return (bad_revision, good_revision)
1977 if self.opts.target_platform == 'chromium':
1978 changes_to_deps = source_control.QueryFileRevisionHistory(
1979 bisect_utils.FILE_DEPS, good_revision, bad_revision)
1981 if changes_to_deps:
1982 # DEPS file was changed, search from the oldest change to DEPS file to
1983 # bad_revision to see if there are matching .DEPS.git changes.
1984 oldest_deps_change = changes_to_deps[-1]
1985 changes_to_gitdeps = source_control.QueryFileRevisionHistory(
1986 bisect_utils.FILE_DEPS_GIT, oldest_deps_change, bad_revision)
1988 if len(changes_to_deps) != len(changes_to_gitdeps):
1989 # Grab the timestamp of the last DEPS change
1990 cmd = ['log', '--format=%ct', '-1', changes_to_deps[0]]
1991 output = bisect_utils.CheckRunGit(cmd)
1992 commit_time = int(output)
1994 # Try looking for a commit that touches the .DEPS.git file in the
1995 # next 15 minutes after the DEPS file change.
1996 cmd = ['log', '--format=%H', '-1',
1997 '--before=%d' % (commit_time + 900), '--after=%d' % commit_time,
1998 'origin/master', '--', bisect_utils.FILE_DEPS_GIT]
1999 output = bisect_utils.CheckRunGit(cmd)
2000 output = output.strip()
2001 if output:
2002 self.warnings.append('Detected change to DEPS and modified '
2003 'revision range to include change to .DEPS.git')
2004 return (output, good_revision)
2005 else:
2006 self.warnings.append('Detected change to DEPS but couldn\'t find '
2007 'matching change to .DEPS.git')
2008 return (bad_revision, good_revision)
2010 def CheckIfRevisionsInProperOrder(
2011 self, target_depot, good_revision, bad_revision):
2012 """Checks that |good_revision| is an earlier revision than |bad_revision|.
2014 Args:
2015 good_revision: Number/tag of the known good revision.
2016 bad_revision: Number/tag of the known bad revision.
2018 Returns:
2019 True if the revisions are in the proper order (good earlier than bad).
2021 cwd = self.depot_registry.GetDepotDir(target_depot)
2022 good_position = source_control.GetCommitPosition(good_revision, cwd)
2023 bad_position = source_control.GetCommitPosition(bad_revision, cwd)
2025 return good_position <= bad_position
2027 def CanPerformBisect(self, good_revision, bad_revision):
2028 """Checks whether a given revision is bisectable.
2030 Checks for following:
2031 1. Non-bisectable revsions for android bots (refer to crbug.com/385324).
2032 2. Non-bisectable revsions for Windows bots (refer to crbug.com/405274).
2034 Args:
2035 good_revision: Known good revision.
2036 bad_revision: Known bad revision.
2038 Returns:
2039 A dictionary indicating the result. If revision is not bisectable,
2040 this will contain the field "error", otherwise None.
2042 if self.opts.target_platform == 'android':
2043 good_revision = source_control.GetCommitPosition(good_revision)
2044 if (bisect_utils.IsStringInt(good_revision)
2045 and good_revision < 265549):
2046 return {'error': (
2047 'Bisect cannot continue for the given revision range.\n'
2048 'It is impossible to bisect Android regressions '
2049 'prior to r265549, which allows the bisect bot to '
2050 'rely on Telemetry to do apk installation of the most recently '
2051 'built local ChromeShell(refer to crbug.com/385324).\n'
2052 'Please try bisecting revisions greater than or equal to r265549.')}
2054 if bisect_utils.IsWindowsHost():
2055 good_revision = source_control.GetCommitPosition(good_revision)
2056 bad_revision = source_control.GetCommitPosition(bad_revision)
2057 if (bisect_utils.IsStringInt(good_revision) and
2058 bisect_utils.IsStringInt(bad_revision)):
2059 if (289987 <= good_revision < 290716 or
2060 289987 <= bad_revision < 290716):
2061 return {'error': ('Oops! Revision between r289987 and r290716 are '
2062 'marked as dead zone for Windows due to '
2063 'crbug.com/405274. Please try another range.')}
2065 return None
2067 def Run(self, command_to_run, bad_revision_in, good_revision_in, metric):
2068 """Given known good and bad revisions, run a binary search on all
2069 intermediate revisions to determine the CL where the performance regression
2070 occurred.
2072 Args:
2073 command_to_run: Specify the command to execute the performance test.
2074 good_revision: Number/tag of the known good revision.
2075 bad_revision: Number/tag of the known bad revision.
2076 metric: The performance metric to monitor.
2078 Returns:
2079 A BisectResults object.
2082 # Choose depot to bisect first
2083 target_depot = 'chromium'
2084 if self.opts.target_platform == 'android-chrome':
2085 target_depot = 'android-chrome'
2087 cwd = os.getcwd()
2088 self.depot_registry.ChangeToDepotDir(target_depot)
2090 # If they passed SVN revisions, we can try match them to git SHA1 hashes.
2091 bad_revision = source_control.ResolveToRevision(
2092 bad_revision_in, target_depot, bisect_utils.DEPOT_DEPS_NAME, 100)
2093 good_revision = source_control.ResolveToRevision(
2094 good_revision_in, target_depot, bisect_utils.DEPOT_DEPS_NAME, -100)
2096 os.chdir(cwd)
2097 if bad_revision is None:
2098 return BisectResults(
2099 error='Couldn\'t resolve [%s] to SHA1.' % bad_revision_in)
2101 if good_revision is None:
2102 return BisectResults(
2103 error='Couldn\'t resolve [%s] to SHA1.' % good_revision_in)
2105 # Check that they didn't accidentally swap good and bad revisions.
2106 if not self.CheckIfRevisionsInProperOrder(
2107 target_depot, good_revision, bad_revision):
2108 return BisectResults(error='bad_revision < good_revision, did you swap '
2109 'these by mistake?')
2111 bad_revision, good_revision = self.NudgeRevisionsIfDEPSChange(
2112 bad_revision, good_revision, good_revision_in)
2113 if self.opts.output_buildbot_annotations:
2114 bisect_utils.OutputAnnotationStepStart('Gathering Revisions')
2116 cannot_bisect = self.CanPerformBisect(good_revision, bad_revision)
2117 if cannot_bisect:
2118 return BisectResults(error=cannot_bisect.get('error'))
2120 print 'Gathering revision range for bisection.'
2121 # Retrieve a list of revisions to do bisection on.
2122 revision_list = self.GetRevisionList(target_depot, bad_revision,
2123 good_revision)
2125 if self.opts.output_buildbot_annotations:
2126 bisect_utils.OutputAnnotationStepClosed()
2128 if revision_list:
2129 self.PrintRevisionsToBisectMessage(revision_list, target_depot)
2131 if self.opts.output_buildbot_annotations:
2132 bisect_utils.OutputAnnotationStepStart('Gathering Reference Values')
2134 print 'Gathering reference values for bisection.'
2136 # Perform the performance tests on the good and bad revisions, to get
2137 # reference values.
2138 bad_results, good_results = self.GatherReferenceValues(good_revision,
2139 bad_revision,
2140 command_to_run,
2141 metric,
2142 target_depot)
2144 if self.opts.output_buildbot_annotations:
2145 bisect_utils.OutputAnnotationStepClosed()
2147 if bad_results[1]:
2148 error = ('An error occurred while building and running the \'bad\' '
2149 'reference value. The bisect cannot continue without '
2150 'a working \'bad\' revision to start from.\n\nError: %s' %
2151 bad_results[0])
2152 return BisectResults(error=error)
2154 if good_results[1]:
2155 error = ('An error occurred while building and running the \'good\' '
2156 'reference value. The bisect cannot continue without '
2157 'a working \'good\' revision to start from.\n\nError: %s' %
2158 good_results[0])
2159 return BisectResults(error=error)
2161 # We need these reference values to determine if later runs should be
2162 # classified as pass or fail.
2163 known_bad_value = bad_results[0]
2164 known_good_value = good_results[0]
2166 # Check the direction of improvement only if the improvement_direction
2167 # option is set to a specific direction (1 for higher is better or -1 for
2168 # lower is better).
2169 improvement_dir = self.opts.improvement_direction
2170 if improvement_dir:
2171 higher_is_better = improvement_dir > 0
2172 if higher_is_better:
2173 message = "Expecting higher values to be better for this metric, "
2174 else:
2175 message = "Expecting lower values to be better for this metric, "
2176 metric_increased = known_bad_value['mean'] > known_good_value['mean']
2177 if metric_increased:
2178 message += "and the metric appears to have increased. "
2179 else:
2180 message += "and the metric appears to have decreased. "
2181 if ((higher_is_better and metric_increased) or
2182 (not higher_is_better and not metric_increased)):
2183 error = (message + 'Then, the test results for the ends of the given '
2184 '\'good\' - \'bad\' range of revisions represent an '
2185 'improvement (and not a regression).')
2186 return BisectResults(error=error)
2187 logging.info(message + "Therefore we continue to bisect.")
2189 bisect_state = BisectState(target_depot, revision_list)
2190 revision_states = bisect_state.GetRevisionStates()
2192 min_revision = 0
2193 max_revision = len(revision_states) - 1
2195 # Can just mark the good and bad revisions explicitly here since we
2196 # already know the results.
2197 bad_revision_state = revision_states[min_revision]
2198 bad_revision_state.external = bad_results[2]
2199 bad_revision_state.perf_time = bad_results[3]
2200 bad_revision_state.build_time = bad_results[4]
2201 bad_revision_state.passed = False
2202 bad_revision_state.value = known_bad_value
2204 good_revision_state = revision_states[max_revision]
2205 good_revision_state.external = good_results[2]
2206 good_revision_state.perf_time = good_results[3]
2207 good_revision_state.build_time = good_results[4]
2208 good_revision_state.passed = True
2209 good_revision_state.value = known_good_value
2211 # Check how likely it is that the good and bad results are different
2212 # beyond chance-induced variation.
2213 confidence_error = False
2214 if not self.opts.debug_ignore_regression_confidence:
2215 confidence_error = _CheckRegressionConfidenceError(good_revision,
2216 bad_revision,
2217 known_good_value,
2218 known_bad_value)
2219 if confidence_error:
2220 self.warnings.append(confidence_error)
2221 bad_revision_state.passed = True # Marking the 'bad' revision as good.
2222 return BisectResults(bisect_state, self.depot_registry, self.opts,
2223 self.warnings)
2225 while True:
2226 if not revision_states:
2227 break
2229 if max_revision - min_revision <= 1:
2230 min_revision_state = revision_states[min_revision]
2231 max_revision_state = revision_states[max_revision]
2232 current_depot = min_revision_state.depot
2233 # TODO(sergiyb): Under which conditions can first two branches be hit?
2234 if min_revision_state.passed == '?':
2235 next_revision_index = min_revision
2236 elif max_revision_state.passed == '?':
2237 next_revision_index = max_revision
2238 elif current_depot in ['android-chrome', 'chromium', 'v8']:
2239 previous_revision = revision_states[min_revision].revision
2240 # If there were changes to any of the external libraries we track,
2241 # should bisect the changes there as well.
2242 external_depot = self._FindNextDepotToBisect(
2243 current_depot, min_revision_state, max_revision_state)
2244 # If there was no change in any of the external depots, the search
2245 # is over.
2246 if not external_depot:
2247 if current_depot == 'v8':
2248 self.warnings.append('Unfortunately, V8 bisection couldn\'t '
2249 'continue any further. The script can only bisect into '
2250 'V8\'s bleeding_edge repository if both the current and '
2251 'previous revisions in trunk map directly to revisions in '
2252 'bleeding_edge.')
2253 break
2255 earliest_revision = max_revision_state.external[external_depot]
2256 latest_revision = min_revision_state.external[external_depot]
2258 new_revision_list = self.PrepareToBisectOnDepot(
2259 external_depot, earliest_revision, latest_revision,
2260 previous_revision)
2262 if not new_revision_list:
2263 error = ('An error occurred attempting to retrieve revision '
2264 'range: [%s..%s]' % (earliest_revision, latest_revision))
2265 return BisectResults(error=error)
2267 revision_states = bisect_state.CreateRevisionStatesAfter(
2268 external_depot, new_revision_list, current_depot,
2269 previous_revision)
2271 # Reset the bisection and perform it on the newly inserted states.
2272 min_revision = 0
2273 max_revision = len(revision_states) - 1
2275 print ('Regression in metric %s appears to be the result of '
2276 'changes in [%s].' % (metric, external_depot))
2278 revision_list = [state.revision for state in revision_states]
2279 self.PrintRevisionsToBisectMessage(revision_list, external_depot)
2281 continue
2282 else:
2283 break
2284 else:
2285 next_revision_index = (int((max_revision - min_revision) / 2) +
2286 min_revision)
2288 next_revision_state = revision_states[next_revision_index]
2289 next_revision = next_revision_state.revision
2290 next_depot = next_revision_state.depot
2292 self.depot_registry.ChangeToDepotDir(next_depot)
2294 message = 'Working on [%s:%s]' % (next_depot, next_revision)
2295 print message
2296 if self.opts.output_buildbot_annotations:
2297 bisect_utils.OutputAnnotationStepStart(message)
2299 run_results = self.RunTest(next_revision, next_depot, command_to_run,
2300 metric, skippable=True)
2302 # If the build is successful, check whether or not the metric
2303 # had regressed.
2304 if not run_results[1]:
2305 if len(run_results) > 2:
2306 next_revision_state.external = run_results[2]
2307 next_revision_state.perf_time = run_results[3]
2308 next_revision_state.build_time = run_results[4]
2310 passed_regression = self._CheckIfRunPassed(run_results[0],
2311 known_good_value,
2312 known_bad_value)
2314 next_revision_state.passed = passed_regression
2315 next_revision_state.value = run_results[0]
2317 if passed_regression:
2318 max_revision = next_revision_index
2319 else:
2320 min_revision = next_revision_index
2321 else:
2322 if run_results[1] == BUILD_RESULT_SKIPPED:
2323 next_revision_state.passed = 'Skipped'
2324 elif run_results[1] == BUILD_RESULT_FAIL:
2325 next_revision_state.passed = 'Build Failed'
2327 print run_results[0]
2329 # If the build is broken, remove it and redo search.
2330 revision_states.pop(next_revision_index)
2332 max_revision -= 1
2334 if self.opts.output_buildbot_annotations:
2335 self.printer.PrintPartialResults(bisect_state)
2336 bisect_utils.OutputAnnotationStepClosed()
2338 return BisectResults(bisect_state, self.depot_registry, self.opts,
2339 self.warnings)
2340 else:
2341 # Weren't able to sync and retrieve the revision range.
2342 error = ('An error occurred attempting to retrieve revision range: '
2343 '[%s..%s]' % (good_revision, bad_revision))
2344 return BisectResults(error=error)
2347 def _IsPlatformSupported():
2348 """Checks that this platform and build system are supported.
2350 Args:
2351 opts: The options parsed from the command line.
2353 Returns:
2354 True if the platform and build system are supported.
2356 # Haven't tested the script out on any other platforms yet.
2357 supported = ['posix', 'nt']
2358 return os.name in supported
2361 def RemoveBuildFiles(build_type):
2362 """Removes build files from previous runs."""
2363 out_dir = os.path.join('out', build_type)
2364 build_dir = os.path.join('build', build_type)
2365 logging.info('Removing build files in "%s" and "%s".',
2366 os.path.abspath(out_dir), os.path.abspath(build_dir))
2367 try:
2368 RemakeDirectoryTree(out_dir)
2369 RemakeDirectoryTree(build_dir)
2370 except Exception as e:
2371 raise RuntimeError('Got error in RemoveBuildFiles: %s' % e)
2374 def RemakeDirectoryTree(path_to_dir):
2375 """Removes a directory tree and replaces it with an empty one.
2377 Returns True if successful, False otherwise.
2379 RemoveDirectoryTree(path_to_dir)
2380 MaybeMakeDirectory(path_to_dir)
2383 def RemoveDirectoryTree(path_to_dir):
2384 """Removes a directory tree. Returns True if successful or False otherwise."""
2385 try:
2386 if os.path.exists(path_to_dir):
2387 shutil.rmtree(path_to_dir)
2388 except OSError, e:
2389 if e.errno != errno.ENOENT:
2390 raise
2393 # This is copied from build/scripts/common/chromium_utils.py.
2394 def MaybeMakeDirectory(*path):
2395 """Creates an entire path, if it doesn't already exist."""
2396 file_path = os.path.join(*path)
2397 try:
2398 os.makedirs(file_path)
2399 except OSError as e:
2400 if e.errno != errno.EEXIST:
2401 raise
2404 class BisectOptions(object):
2405 """Options to be used when running bisection."""
2406 def __init__(self):
2407 super(BisectOptions, self).__init__()
2409 self.target_platform = 'chromium'
2410 self.build_preference = None
2411 self.good_revision = None
2412 self.bad_revision = None
2413 self.use_goma = None
2414 self.goma_dir = None
2415 self.goma_threads = 64
2416 self.repeat_test_count = 20
2417 self.truncate_percent = 25
2418 self.max_time_minutes = 20
2419 self.metric = None
2420 self.command = None
2421 self.output_buildbot_annotations = None
2422 self.no_custom_deps = False
2423 self.working_directory = None
2424 self.extra_src = None
2425 self.debug_ignore_build = None
2426 self.debug_ignore_sync = None
2427 self.debug_ignore_perf_test = None
2428 self.debug_ignore_regression_confidence = None
2429 self.debug_fake_first_test_mean = 0
2430 self.gs_bucket = None
2431 self.target_arch = 'ia32'
2432 self.target_build_type = 'Release'
2433 self.builder_host = None
2434 self.builder_port = None
2435 self.bisect_mode = bisect_utils.BISECT_MODE_MEAN
2436 self.improvement_direction = 0
2437 self.bug_id = ''
2439 @staticmethod
2440 def _AddBisectOptionsGroup(parser):
2441 group = parser.add_argument_group('Bisect options')
2442 group.add_argument('-c', '--command', required=True,
2443 help='A command to execute your performance test at '
2444 'each point in the bisection.')
2445 group.add_argument('-b', '--bad_revision', required=True,
2446 help='A bad revision to start bisection. Must be later '
2447 'than good revision. May be either a git or svn '
2448 'revision.')
2449 group.add_argument('-g', '--good_revision', required=True,
2450 help='A revision to start bisection where performance '
2451 'test is known to pass. Must be earlier than the '
2452 'bad revision. May be either a git or a svn '
2453 'revision.')
2454 group.add_argument('-m', '--metric',
2455 help='The desired metric to bisect on. For example '
2456 '"vm_rss_final_b/vm_rss_f_b"')
2457 group.add_argument('-d', '--improvement_direction', type=int, default=0,
2458 help='An integer number representing the direction of '
2459 'improvement. 1 for higher is better, -1 for lower '
2460 'is better, 0 for ignore (default).')
2461 group.add_argument('-r', '--repeat_test_count', type=int, default=20,
2462 choices=range(1, 101),
2463 help='The number of times to repeat the performance '
2464 'test. Values will be clamped to range [1, 100]. '
2465 'Default value is 20.')
2466 group.add_argument('--max_time_minutes', type=int, default=20,
2467 choices=range(1, 61),
2468 help='The maximum time (in minutes) to take running the '
2469 'performance tests. The script will run the '
2470 'performance tests according to '
2471 '--repeat_test_count, so long as it doesn\'t exceed'
2472 ' --max_time_minutes. Values will be clamped to '
2473 'range [1, 60]. Default value is 20.')
2474 group.add_argument('-t', '--truncate_percent', type=int, default=25,
2475 help='The highest/lowest % are discarded to form a '
2476 'truncated mean. Values will be clamped to range '
2477 '[0, 25]. Default value is 25 (highest/lowest 25% '
2478 'will be discarded).')
2479 group.add_argument('--bisect_mode', default=bisect_utils.BISECT_MODE_MEAN,
2480 choices=[bisect_utils.BISECT_MODE_MEAN,
2481 bisect_utils.BISECT_MODE_STD_DEV,
2482 bisect_utils.BISECT_MODE_RETURN_CODE],
2483 help='The bisect mode. Choices are to bisect on the '
2484 'difference in mean, std_dev, or return_code.')
2485 group.add_argument('--bug_id', default='',
2486 help='The id for the bug associated with this bisect. ' +
2487 'If this number is given, bisect will attempt to ' +
2488 'verify that the bug is not closed before '
2489 'starting.')
2491 @staticmethod
2492 def _AddBuildOptionsGroup(parser):
2493 group = parser.add_argument_group('Build options')
2494 group.add_argument('-w', '--working_directory',
2495 help='Path to the working directory where the script '
2496 'will do an initial checkout of the chromium depot. The '
2497 'files will be placed in a subdirectory "bisect" under '
2498 'working_directory and that will be used to perform the '
2499 'bisection. This parameter is optional, if it is not '
2500 'supplied, the script will work from the current depot.')
2501 group.add_argument('--build_preference',
2502 choices=['msvs', 'ninja', 'make'],
2503 help='The preferred build system to use. On linux/mac '
2504 'the options are make/ninja. On Windows, the '
2505 'options are msvs/ninja.')
2506 group.add_argument('--target_platform', default='chromium',
2507 choices=['chromium', 'android', 'android-chrome'],
2508 help='The target platform. Choices are "chromium" '
2509 '(current platform), or "android". If you specify '
2510 'something other than "chromium", you must be '
2511 'properly set up to build that platform.')
2512 group.add_argument('--no_custom_deps', dest='no_custom_deps',
2513 action='store_true', default=False,
2514 help='Run the script with custom_deps or not.')
2515 group.add_argument('--extra_src',
2516 help='Path to a script which can be used to modify the '
2517 'bisect script\'s behavior.')
2518 group.add_argument('--use_goma', action='store_true',
2519 help='Add a bunch of extra threads for goma, and enable '
2520 'goma')
2521 group.add_argument('--goma_dir',
2522 help='Path to goma tools (or system default if not '
2523 'specified).')
2524 group.add_argument('--goma_threads', type=int, default='64',
2525 help='Number of threads for goma, only if using goma.')
2526 group.add_argument('--output_buildbot_annotations', action='store_true',
2527 help='Add extra annotation output for buildbot.')
2528 group.add_argument('--gs_bucket', default='', dest='gs_bucket',
2529 help='Name of Google Storage bucket to upload or '
2530 'download build. e.g., chrome-perf')
2531 group.add_argument('--target_arch', default='ia32',
2532 dest='target_arch', choices=['ia32', 'x64', 'arm'],
2533 help='The target build architecture. Choices are "ia32" '
2534 '(default), "x64" or "arm".')
2535 group.add_argument('--target_build_type', default='Release',
2536 choices=['Release', 'Debug'],
2537 help='The target build type. Choices are "Release" '
2538 '(default), or "Debug".')
2539 group.add_argument('--builder_host', dest='builder_host',
2540 help='Host address of server to produce build by '
2541 'posting try job request.')
2542 group.add_argument('--builder_port', dest='builder_port', type=int,
2543 help='HTTP port of the server to produce build by '
2544 'posting try job request.')
2546 @staticmethod
2547 def _AddDebugOptionsGroup(parser):
2548 group = parser.add_argument_group('Debug options')
2549 group.add_argument('--debug_ignore_build', action='store_true',
2550 help='DEBUG: Don\'t perform builds.')
2551 group.add_argument('--debug_ignore_sync', action='store_true',
2552 help='DEBUG: Don\'t perform syncs.')
2553 group.add_argument('--debug_ignore_perf_test', action='store_true',
2554 help='DEBUG: Don\'t perform performance tests.')
2555 group.add_argument('--debug_ignore_regression_confidence',
2556 action='store_true',
2557 help='DEBUG: Don\'t score the confidence of the initial '
2558 'good and bad revisions\' test results.')
2559 group.add_argument('--debug_fake_first_test_mean', type=int, default='0',
2560 help='DEBUG: When faking performance tests, return this '
2561 'value as the mean of the first performance test, '
2562 'and return a mean of 0.0 for further tests.')
2563 return group
2565 @classmethod
2566 def _CreateCommandLineParser(cls):
2567 """Creates a parser with bisect options.
2569 Returns:
2570 An instance of optparse.OptionParser.
2572 usage = ('%prog [options] [-- chromium-options]\n'
2573 'Perform binary search on revision history to find a minimal '
2574 'range of revisions where a performance metric regressed.\n')
2576 parser = argparse.ArgumentParser(usage=usage)
2577 cls._AddBisectOptionsGroup(parser)
2578 cls._AddBuildOptionsGroup(parser)
2579 cls._AddDebugOptionsGroup(parser)
2580 return parser
2582 def ParseCommandLine(self):
2583 """Parses the command line for bisect options."""
2584 parser = self._CreateCommandLineParser()
2585 opts = parser.parse_args()
2587 try:
2588 if (not opts.metric and
2589 opts.bisect_mode != bisect_utils.BISECT_MODE_RETURN_CODE):
2590 raise RuntimeError('missing required parameter: --metric')
2592 if opts.gs_bucket:
2593 if not cloud_storage.List(opts.gs_bucket):
2594 raise RuntimeError('Invalid Google Storage: gs://%s' % opts.gs_bucket)
2595 if not opts.builder_host:
2596 raise RuntimeError('Must specify try server host name using '
2597 '--builder_host when gs_bucket is used.')
2598 if not opts.builder_port:
2599 raise RuntimeError('Must specify try server port number using '
2600 '--builder_port when gs_bucket is used.')
2602 if opts.bisect_mode != bisect_utils.BISECT_MODE_RETURN_CODE:
2603 metric_values = opts.metric.split('/')
2604 if len(metric_values) != 2:
2605 raise RuntimeError('Invalid metric specified: [%s]' % opts.metric)
2606 opts.metric = metric_values
2608 opts.truncate_percent = min(max(opts.truncate_percent, 0), 25) / 100.0
2610 for k, v in opts.__dict__.iteritems():
2611 assert hasattr(self, k), 'Invalid %s attribute in BisectOptions.' % k
2612 setattr(self, k, v)
2613 except RuntimeError, e:
2614 output_string = StringIO.StringIO()
2615 parser.print_help(file=output_string)
2616 error_message = '%s\n\n%s' % (e.message, output_string.getvalue())
2617 output_string.close()
2618 raise RuntimeError(error_message)
2620 @staticmethod
2621 def FromDict(values):
2622 """Creates an instance of BisectOptions from a dictionary.
2624 Args:
2625 values: a dict containing options to set.
2627 Returns:
2628 An instance of BisectOptions.
2630 opts = BisectOptions()
2631 for k, v in values.iteritems():
2632 assert hasattr(opts, k), 'Invalid %s attribute in BisectOptions.' % k
2633 setattr(opts, k, v)
2635 if opts.metric and opts.bisect_mode != bisect_utils.BISECT_MODE_RETURN_CODE:
2636 metric_values = opts.metric.split('/')
2637 if len(metric_values) != 2:
2638 raise RuntimeError('Invalid metric specified: [%s]' % opts.metric)
2639 opts.metric = metric_values
2641 opts.repeat_test_count = min(max(opts.repeat_test_count, 1), 100)
2642 opts.max_time_minutes = min(max(opts.max_time_minutes, 1), 60)
2643 opts.truncate_percent = min(max(opts.truncate_percent, 0), 25)
2644 opts.truncate_percent = opts.truncate_percent / 100.0
2646 return opts
2649 def _ConfigureLogging():
2650 """Trivial logging config.
2652 Configures logging to output any messages at or above INFO to standard out,
2653 without any additional formatting.
2655 logging_format = '%(message)s'
2656 logging.basicConfig(
2657 stream=logging.sys.stdout, level=logging.INFO, format=logging_format)
2660 def main():
2661 _ConfigureLogging()
2662 try:
2663 opts = BisectOptions()
2664 opts.ParseCommandLine()
2666 if opts.bug_id:
2667 if opts.output_buildbot_annotations:
2668 bisect_utils.OutputAnnotationStepStart('Checking Issue Tracker')
2669 issue_closed = query_crbug.CheckIssueClosed(opts.bug_id)
2670 if issue_closed:
2671 print 'Aborting bisect because bug is closed'
2672 else:
2673 print 'Could not confirm bug is closed, proceeding.'
2674 if opts.output_buildbot_annotations:
2675 bisect_utils.OutputAnnotationStepClosed()
2676 if issue_closed:
2677 results = BisectResults(abort_reason='the bug is closed.')
2678 bisect_test = BisectPerformanceMetrics(opts, os.getcwd())
2679 bisect_test.printer.FormatAndPrintResults(results)
2680 return 0
2683 if opts.extra_src:
2684 extra_src = bisect_utils.LoadExtraSrc(opts.extra_src)
2685 if not extra_src:
2686 raise RuntimeError('Invalid or missing --extra_src.')
2687 bisect_utils.AddAdditionalDepotInfo(extra_src.GetAdditionalDepotInfo())
2689 if opts.working_directory:
2690 custom_deps = bisect_utils.DEFAULT_GCLIENT_CUSTOM_DEPS
2691 if opts.no_custom_deps:
2692 custom_deps = None
2693 bisect_utils.CreateBisectDirectoryAndSetupDepot(opts, custom_deps)
2695 os.chdir(os.path.join(os.getcwd(), 'src'))
2696 RemoveBuildFiles(opts.target_build_type)
2698 if not _IsPlatformSupported():
2699 raise RuntimeError('Sorry, this platform isn\'t supported yet.')
2701 if not source_control.IsInGitRepository():
2702 raise RuntimeError(
2703 'Sorry, only the git workflow is supported at the moment.')
2705 # gClient sync seems to fail if you're not in master branch.
2706 if (not source_control.IsInProperBranch() and
2707 not opts.debug_ignore_sync and
2708 not opts.working_directory):
2709 raise RuntimeError('You must switch to master branch to run bisection.')
2710 bisect_test = BisectPerformanceMetrics(opts, os.getcwd())
2711 try:
2712 results = bisect_test.Run(opts.command, opts.bad_revision,
2713 opts.good_revision, opts.metric)
2714 if results.error:
2715 raise RuntimeError(results.error)
2716 bisect_test.printer.FormatAndPrintResults(results)
2717 return 0
2718 finally:
2719 bisect_test.PerformCleanup()
2720 except RuntimeError as e:
2721 if opts.output_buildbot_annotations:
2722 # The perf dashboard scrapes the "results" step in order to comment on
2723 # bugs. If you change this, please update the perf dashboard as well.
2724 bisect_utils.OutputAnnotationStepStart('Results')
2725 print 'Runtime Error: %s' % e
2726 if opts.output_buildbot_annotations:
2727 bisect_utils.OutputAnnotationStepClosed()
2728 return 1
2731 if __name__ == '__main__':
2732 sys.exit(main())