Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(17)

Side by Side Diff: tools/bisect-perf-regression.py

Issue 564663002: Move bisect-perf-regression.py into auto_bisect directory. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Upload a config for doing a test run. Created 6 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
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.
5
6 """Performance Test Bisect Tool
7
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.
15
16 Example usage using SVN revisions:
17
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
21
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.
25
26 Example usage using git hashes:
27
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 """
34
35 import copy
36 import datetime
37 import errno
38 import hashlib
39 import math
40 import optparse
41 import os
42 import re
43 import shlex
44 import shutil
45 import StringIO
46 import sys
47 import time
48 import zipfile
49
50 sys.path.append(os.path.join(os.path.dirname(__file__), 'telemetry'))
51
52 from auto_bisect import bisect_utils
53 from auto_bisect import builder
54 from auto_bisect import math_utils
55 from auto_bisect import request_build
56 from auto_bisect import source_control as source_control_module
57 from auto_bisect import ttest
58 from telemetry.util import cloud_storage
59
60 # Below is the map of "depot" names to information about each depot. Each depot
61 # is a repository, and in the process of bisecting, revision ranges in these
62 # repositories may also be bisected.
63 #
64 # Each depot information dictionary may contain:
65 # src: Path to the working directory.
66 # recurse: True if this repository will get bisected.
67 # depends: A list of other repositories that are actually part of the same
68 # repository in svn. If the repository has any dependent repositories
69 # (e.g. skia/src needs skia/include and skia/gyp to be updated), then
70 # they are specified here.
71 # svn: URL of SVN repository. Needed for git workflow to resolve hashes to
72 # SVN revisions.
73 # from: Parent depot that must be bisected before this is bisected.
74 # deps_var: Key name in vars variable in DEPS file that has revision
75 # information.
76 DEPOT_DEPS_NAME = {
77 'chromium': {
78 'src': 'src',
79 'recurse': True,
80 'depends': None,
81 'from': ['cros', 'android-chrome'],
82 'viewvc':
83 'http://src.chromium.org/viewvc/chrome?view=revision&revision=',
84 'deps_var': 'chromium_rev'
85 },
86 'webkit': {
87 'src': 'src/third_party/WebKit',
88 'recurse': True,
89 'depends': None,
90 'from': ['chromium'],
91 'viewvc':
92 'http://src.chromium.org/viewvc/blink?view=revision&revision=',
93 'deps_var': 'webkit_revision'
94 },
95 'angle': {
96 'src': 'src/third_party/angle',
97 'src_old': 'src/third_party/angle_dx11',
98 'recurse': True,
99 'depends': None,
100 'from': ['chromium'],
101 'platform': 'nt',
102 'deps_var': 'angle_revision'
103 },
104 'v8': {
105 'src': 'src/v8',
106 'recurse': True,
107 'depends': None,
108 'from': ['chromium'],
109 'custom_deps': bisect_utils.GCLIENT_CUSTOM_DEPS_V8,
110 'viewvc': 'https://code.google.com/p/v8/source/detail?r=',
111 'deps_var': 'v8_revision'
112 },
113 'v8_bleeding_edge': {
114 'src': 'src/v8_bleeding_edge',
115 'recurse': True,
116 'depends': None,
117 'svn': 'https://v8.googlecode.com/svn/branches/bleeding_edge',
118 'from': ['v8'],
119 'viewvc': 'https://code.google.com/p/v8/source/detail?r=',
120 'deps_var': 'v8_revision'
121 },
122 'skia/src': {
123 'src': 'src/third_party/skia/src',
124 'recurse': True,
125 'svn': 'http://skia.googlecode.com/svn/trunk/src',
126 'depends': ['skia/include', 'skia/gyp'],
127 'from': ['chromium'],
128 'viewvc': 'https://code.google.com/p/skia/source/detail?r=',
129 'deps_var': 'skia_revision'
130 },
131 'skia/include': {
132 'src': 'src/third_party/skia/include',
133 'recurse': False,
134 'svn': 'http://skia.googlecode.com/svn/trunk/include',
135 'depends': None,
136 'from': ['chromium'],
137 'viewvc': 'https://code.google.com/p/skia/source/detail?r=',
138 'deps_var': 'None'
139 },
140 'skia/gyp': {
141 'src': 'src/third_party/skia/gyp',
142 'recurse': False,
143 'svn': 'http://skia.googlecode.com/svn/trunk/gyp',
144 'depends': None,
145 'from': ['chromium'],
146 'viewvc': 'https://code.google.com/p/skia/source/detail?r=',
147 'deps_var': 'None'
148 }
149 }
150
151 DEPOT_NAMES = DEPOT_DEPS_NAME.keys()
152
153 CROS_CHROMEOS_PATTERN = 'chromeos-base/chromeos-chrome'
154
155 # Possible return values from BisectPerformanceMetrics.RunTest.
156 BUILD_RESULT_SUCCEED = 0
157 BUILD_RESULT_FAIL = 1
158 BUILD_RESULT_SKIPPED = 2
159
160 # Maximum time in seconds to wait after posting build request to the try server.
161 # TODO: Change these values based on the actual time taken by buildbots on
162 # the try server.
163 MAX_MAC_BUILD_TIME = 14400
164 MAX_WIN_BUILD_TIME = 14400
165 MAX_LINUX_BUILD_TIME = 14400
166
167 # The confidence percentage at which confidence can be consider "high".
168 HIGH_CONFIDENCE = 95
169
170 # Patch template to add a new file, DEPS.sha under src folder.
171 # This file contains SHA1 value of the DEPS changes made while bisecting
172 # dependency repositories. This patch send along with DEPS patch to try server.
173 # When a build requested is posted with a patch, bisect builders on try server,
174 # once build is produced, it reads SHA value from this file and appends it
175 # to build archive filename.
176 DEPS_SHA_PATCH = """diff --git src/DEPS.sha src/DEPS.sha
177 new file mode 100644
178 --- /dev/null
179 +++ src/DEPS.sha
180 @@ -0,0 +1 @@
181 +%(deps_sha)s
182 """
183
184 # The possible values of the --bisect_mode flag, which determines what to
185 # use when classifying a revision as "good" or "bad".
186 BISECT_MODE_MEAN = 'mean'
187 BISECT_MODE_STD_DEV = 'std_dev'
188 BISECT_MODE_RETURN_CODE = 'return_code'
189
190 # The perf dashboard looks for a string like "Estimated Confidence: 95%"
191 # to decide whether or not to cc the author(s). If you change this, please
192 # update the perf dashboard as well.
193 RESULTS_BANNER = """
194 ===== BISECT JOB RESULTS =====
195 Status: %(status)s
196
197 Test Command: %(command)s
198 Test Metric: %(metrics)s
199 Relative Change: %(change)s
200 Estimated Confidence: %(confidence).02f%%"""
201
202 # The perf dashboard specifically looks for the string
203 # "Author : " to parse out who to cc on a bug. If you change the
204 # formatting here, please update the perf dashboard as well.
205 RESULTS_REVISION_INFO = """
206 ===== SUSPECTED CL(s) =====
207 Subject : %(subject)s
208 Author : %(author)s%(email_info)s%(commit_info)s
209 Commit : %(cl)s
210 Date : %(cl_date)s"""
211
212 REPRO_STEPS_LOCAL = """
213 ==== INSTRUCTIONS TO REPRODUCE ====
214 To run locally:
215 $%(command)s"""
216
217 REPRO_STEPS_TRYJOB = """
218 To reproduce on a performance try bot:
219 1. Create new git branch or check out existing branch.
220 2. Edit tools/run-perf-test.cfg (instructions in file) or \
221 third_party/WebKit/Tools/run-perf-test.cfg.
222 a) Take care to strip any src/ directories from the head of \
223 relative path names.
224 b) On desktop, only --browser=release is supported, on android \
225 --browser=android-chromium-testshell.
226 c) Test command to use: %(command)s
227 3. Upload your patch. --bypass-hooks is necessary to upload the changes you \
228 committed locally to run-perf-test.cfg.
229 Note: *DO NOT* commit run-perf-test.cfg changes to the project repository.
230 $ git cl upload --bypass-hooks
231 4. Send your try job to the try server. \
232 [Please make sure to use appropriate bot to reproduce]
233 $ git cl try -m tryserver.chromium.perf -b <bot>
234
235 For more details please visit
236 https://sites.google.com/a/chromium.org/dev/developers/performance-try-bots"""
237
238 REPRO_STEPS_TRYJOB_TELEMETRY = """
239 To reproduce on a performance try bot:
240 %(command)s
241 (Where <bot-name> comes from tools/perf/run_benchmark --browser=list)
242
243 For more details please visit
244 https://sites.google.com/a/chromium.org/dev/developers/performance-try-bots
245 """
246
247 RESULTS_THANKYOU = """
248 ===== THANK YOU FOR CHOOSING BISECT AIRLINES =====
249 Visit http://www.chromium.org/developers/core-principles for Chrome's policy
250 on perf regressions.
251 Contact chrome-perf-dashboard-team with any questions or suggestions about
252 bisecting.
253 . .------.
254 . .---. \ \==)
255 . |PERF\ \ \\
256 . | ---------'-------'-----------.
257 . . 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 `-.
258 . \______________.-------._______________)
259 . / /
260 . / /
261 . / /==)
262 . ._______."""
263
264
265 def _AddAdditionalDepotInfo(depot_info):
266 """Adds additional depot info to the global depot variables."""
267 global DEPOT_DEPS_NAME
268 global DEPOT_NAMES
269 DEPOT_DEPS_NAME = dict(DEPOT_DEPS_NAME.items() + depot_info.items())
270 DEPOT_NAMES = DEPOT_DEPS_NAME.keys()
271
272
273 def ConfidenceScore(good_results_lists, bad_results_lists):
274 """Calculates a confidence score.
275
276 This score is a percentage which represents our degree of confidence in the
277 proposition that the good results and bad results are distinct groups, and
278 their differences aren't due to chance alone.
279
280
281 Args:
282 good_results_lists: A list of lists of "good" result numbers.
283 bad_results_lists: A list of lists of "bad" result numbers.
284
285 Returns:
286 A number in the range [0, 100].
287 """
288 # If there's only one item in either list, this means only one revision was
289 # classified good or bad; this isn't good enough evidence to make a decision.
290 # If an empty list was passed, that also implies zero confidence.
291 if len(good_results_lists) <= 1 or len(bad_results_lists) <= 1:
292 return 0.0
293
294 # Flatten the lists of results lists.
295 sample1 = sum(good_results_lists, [])
296 sample2 = sum(bad_results_lists, [])
297
298 # If there were only empty lists in either of the lists (this is unexpected
299 # and normally shouldn't happen), then we also want to return 0.
300 if not sample1 or not sample2:
301 return 0.0
302
303 # The p-value is approximately the probability of obtaining the given set
304 # of good and bad values just by chance.
305 _, _, p_value = ttest.WelchsTTest(sample1, sample2)
306 return 100.0 * (1.0 - p_value)
307
308
309 def GetSHA1HexDigest(contents):
310 """Returns SHA1 hex digest of the given string."""
311 return hashlib.sha1(contents).hexdigest()
312
313
314 def GetZipFileName(build_revision=None, target_arch='ia32', patch_sha=None):
315 """Gets the archive file name for the given revision."""
316 def PlatformName():
317 """Return a string to be used in paths for the platform."""
318 if bisect_utils.IsWindowsHost():
319 # Build archive for x64 is still stored with the "win32" suffix.
320 # See chromium_utils.PlatformName().
321 if bisect_utils.Is64BitWindows() and target_arch == 'x64':
322 return 'win32'
323 return 'win32'
324 if bisect_utils.IsLinuxHost():
325 # Android builds are also archived with the "full-build-linux prefix.
326 return 'linux'
327 if bisect_utils.IsMacHost():
328 return 'mac'
329 raise NotImplementedError('Unknown platform "%s".' % sys.platform)
330
331 base_name = 'full-build-%s' % PlatformName()
332 if not build_revision:
333 return base_name
334 if patch_sha:
335 build_revision = '%s_%s' % (build_revision , patch_sha)
336 return '%s_%s.zip' % (base_name, build_revision)
337
338
339 def GetRemoteBuildPath(build_revision, target_platform='chromium',
340 target_arch='ia32', patch_sha=None):
341 """Returns the URL to download the build from."""
342 def GetGSRootFolderName(target_platform):
343 """Returns the Google Cloud Storage root folder name."""
344 if bisect_utils.IsWindowsHost():
345 if bisect_utils.Is64BitWindows() and target_arch == 'x64':
346 return 'Win x64 Builder'
347 return 'Win Builder'
348 if bisect_utils.IsLinuxHost():
349 if target_platform == 'android':
350 return 'android_perf_rel'
351 return 'Linux Builder'
352 if bisect_utils.IsMacHost():
353 return 'Mac Builder'
354 raise NotImplementedError('Unsupported Platform "%s".' % sys.platform)
355
356 base_filename = GetZipFileName(
357 build_revision, target_arch, patch_sha)
358 builder_folder = GetGSRootFolderName(target_platform)
359 return '%s/%s' % (builder_folder, base_filename)
360
361
362 def FetchFromCloudStorage(bucket_name, source_path, destination_path):
363 """Fetches file(s) from the Google Cloud Storage.
364
365 Args:
366 bucket_name: Google Storage bucket name.
367 source_path: Source file path.
368 destination_path: Destination file path.
369
370 Returns:
371 Downloaded file path if exists, otherwise None.
372 """
373 target_file = os.path.join(destination_path, os.path.basename(source_path))
374 try:
375 if cloud_storage.Exists(bucket_name, source_path):
376 print 'Fetching file from gs//%s/%s ...' % (bucket_name, source_path)
377 cloud_storage.Get(bucket_name, source_path, destination_path)
378 if os.path.exists(target_file):
379 return target_file
380 else:
381 print ('File gs://%s/%s not found in cloud storage.' % (
382 bucket_name, source_path))
383 except Exception as e:
384 print 'Something went wrong while fetching file from cloud: %s' % e
385 if os.path.exists(target_file):
386 os.remove(target_file)
387 return None
388
389
390 # This is copied from build/scripts/common/chromium_utils.py.
391 def MaybeMakeDirectory(*path):
392 """Creates an entire path, if it doesn't already exist."""
393 file_path = os.path.join(*path)
394 try:
395 os.makedirs(file_path)
396 except OSError as e:
397 if e.errno != errno.EEXIST:
398 return False
399 return True
400
401
402 # This was copied from build/scripts/common/chromium_utils.py.
403 def ExtractZip(filename, output_dir, verbose=True):
404 """ Extract the zip archive in the output directory."""
405 MaybeMakeDirectory(output_dir)
406
407 # On Linux and Mac, we use the unzip command as it will
408 # handle links and file bits (executable), which is much
409 # easier then trying to do that with ZipInfo options.
410 #
411 # The Mac Version of unzip unfortunately does not support Zip64, whereas
412 # the python module does, so we have to fall back to the python zip module
413 # on Mac if the file size is greater than 4GB.
414 #
415 # On Windows, try to use 7z if it is installed, otherwise fall back to python
416 # zip module and pray we don't have files larger than 512MB to unzip.
417 unzip_cmd = None
418 if ((bisect_utils.IsMacHost()
419 and os.path.getsize(filename) < 4 * 1024 * 1024 * 1024)
420 or bisect_utils.IsLinuxHost()):
421 unzip_cmd = ['unzip', '-o']
422 elif (bisect_utils.IsWindowsHost()
423 and os.path.exists('C:\\Program Files\\7-Zip\\7z.exe')):
424 unzip_cmd = ['C:\\Program Files\\7-Zip\\7z.exe', 'x', '-y']
425
426 if unzip_cmd:
427 # Make sure path is absolute before changing directories.
428 filepath = os.path.abspath(filename)
429 saved_dir = os.getcwd()
430 os.chdir(output_dir)
431 command = unzip_cmd + [filepath]
432 result = bisect_utils.RunProcess(command)
433 os.chdir(saved_dir)
434 if result:
435 raise IOError('unzip failed: %s => %s' % (str(command), result))
436 else:
437 assert bisect_utils.IsWindowsHost() or bisect_utils.IsMacHost()
438 zf = zipfile.ZipFile(filename)
439 for name in zf.namelist():
440 if verbose:
441 print 'Extracting %s' % name
442 zf.extract(name, output_dir)
443 if bisect_utils.IsMacHost():
444 # Restore permission bits.
445 os.chmod(os.path.join(output_dir, name),
446 zf.getinfo(name).external_attr >> 16L)
447
448
449 def WriteStringToFile(text, file_name):
450 """Writes text to a file, raising an RuntimeError on failure."""
451 try:
452 with open(file_name, 'wb') as f:
453 f.write(text)
454 except IOError:
455 raise RuntimeError('Error writing to file [%s]' % file_name )
456
457
458 def ReadStringFromFile(file_name):
459 """Writes text to a file, raising an RuntimeError on failure."""
460 try:
461 with open(file_name) as f:
462 return f.read()
463 except IOError:
464 raise RuntimeError('Error reading file [%s]' % file_name )
465
466
467 def ChangeBackslashToSlashInPatch(diff_text):
468 """Formats file paths in the given patch text to Unix-style paths."""
469 if not diff_text:
470 return None
471 diff_lines = diff_text.split('\n')
472 for i in range(len(diff_lines)):
473 line = diff_lines[i]
474 if line.startswith('--- ') or line.startswith('+++ '):
475 diff_lines[i] = line.replace('\\', '/')
476 return '\n'.join(diff_lines)
477
478
479 def _ParseRevisionsFromDEPSFileManually(deps_file_contents):
480 """Parses the vars section of the DEPS file using regular expressions.
481
482 Args:
483 deps_file_contents: The DEPS file contents as a string.
484
485 Returns:
486 A dictionary in the format {depot: revision} if successful, otherwise None.
487 """
488 # We'll parse the "vars" section of the DEPS file.
489 rxp = re.compile('vars = {(?P<vars_body>[^}]+)', re.MULTILINE)
490 re_results = rxp.search(deps_file_contents)
491
492 if not re_results:
493 return None
494
495 # We should be left with a series of entries in the vars component of
496 # the DEPS file with the following format:
497 # 'depot_name': 'revision',
498 vars_body = re_results.group('vars_body')
499 rxp = re.compile("'(?P<depot_body>[\w_-]+)':[\s]+'(?P<rev_body>[\w@]+)'",
500 re.MULTILINE)
501 re_results = rxp.findall(vars_body)
502
503 return dict(re_results)
504
505
506 def _WaitUntilBuildIsReady(
507 fetch_build, bot_name, builder_host, builder_port, build_request_id,
508 max_timeout):
509 """Waits until build is produced by bisect builder on try server.
510
511 Args:
512 fetch_build: Function to check and download build from cloud storage.
513 bot_name: Builder bot name on try server.
514 builder_host Try server host name.
515 builder_port: Try server port.
516 build_request_id: A unique ID of the build request posted to try server.
517 max_timeout: Maximum time to wait for the build.
518
519 Returns:
520 Downloaded archive file path if exists, otherwise None.
521 """
522 # Build number on the try server.
523 build_num = None
524 # Interval to check build on cloud storage.
525 poll_interval = 60
526 # Interval to check build status on try server in seconds.
527 status_check_interval = 600
528 last_status_check = time.time()
529 start_time = time.time()
530 while True:
531 # Checks for build on gs://chrome-perf and download if exists.
532 res = fetch_build()
533 if res:
534 return (res, 'Build successfully found')
535 elapsed_status_check = time.time() - last_status_check
536 # To avoid overloading try server with status check requests, we check
537 # build status for every 10 minutes.
538 if elapsed_status_check > status_check_interval:
539 last_status_check = time.time()
540 if not build_num:
541 # Get the build number on try server for the current build.
542 build_num = request_build.GetBuildNumFromBuilder(
543 build_request_id, bot_name, builder_host, builder_port)
544 # Check the status of build using the build number.
545 # Note: Build is treated as PENDING if build number is not found
546 # on the the try server.
547 build_status, status_link = request_build.GetBuildStatus(
548 build_num, bot_name, builder_host, builder_port)
549 if build_status == request_build.FAILED:
550 return (None, 'Failed to produce build, log: %s' % status_link)
551 elapsed_time = time.time() - start_time
552 if elapsed_time > max_timeout:
553 return (None, 'Timed out: %ss without build' % max_timeout)
554
555 print 'Time elapsed: %ss without build.' % elapsed_time
556 time.sleep(poll_interval)
557 # For some reason, mac bisect bots were not flushing stdout periodically.
558 # As a result buildbot command is timed-out. Flush stdout on all platforms
559 # while waiting for build.
560 sys.stdout.flush()
561
562
563 def _UpdateV8Branch(deps_content):
564 """Updates V8 branch in DEPS file to process v8_bleeding_edge.
565
566 Check for "v8_branch" in DEPS file if exists update its value
567 with v8_bleeding_edge branch. Note: "v8_branch" is added to DEPS
568 variable from DEPS revision 254916, therefore check for "src/v8":
569 <v8 source path> in DEPS in order to support prior DEPS revisions
570 and update it.
571
572 Args:
573 deps_content: DEPS file contents to be modified.
574
575 Returns:
576 Modified DEPS file contents as a string.
577 """
578 new_branch = r'branches/bleeding_edge'
579 v8_branch_pattern = re.compile(r'(?<="v8_branch": ")(.*)(?=")')
580 if re.search(v8_branch_pattern, deps_content):
581 deps_content = re.sub(v8_branch_pattern, new_branch, deps_content)
582 else:
583 # Replaces the branch assigned to "src/v8" key in DEPS file.
584 # Format of "src/v8" in DEPS:
585 # "src/v8":
586 # (Var("googlecode_url") % "v8") + "/trunk@" + Var("v8_revision"),
587 # So, "/trunk@" is replace with "/branches/bleeding_edge@"
588 v8_src_pattern = re.compile(
589 r'(?<="v8"\) \+ "/)(.*)(?=@" \+ Var\("v8_revision"\))', re.MULTILINE)
590 if re.search(v8_src_pattern, deps_content):
591 deps_content = re.sub(v8_src_pattern, new_branch, deps_content)
592 return deps_content
593
594
595 def _UpdateDEPSForAngle(revision, depot, deps_file):
596 """Updates DEPS file with new revision for Angle repository.
597
598 This is a hack for Angle depot case because, in DEPS file "vars" dictionary
599 variable contains "angle_revision" key that holds git hash instead of
600 SVN revision.
601
602 And sometimes "angle_revision" key is not specified in "vars" variable,
603 in such cases check "deps" dictionary variable that matches
604 angle.git@[a-fA-F0-9]{40}$ and replace git hash.
605 """
606 deps_var = DEPOT_DEPS_NAME[depot]['deps_var']
607 try:
608 deps_contents = ReadStringFromFile(deps_file)
609 # Check whether the depot and revision pattern in DEPS file vars variable
610 # e.g. "angle_revision": "fa63e947cb3eccf463648d21a05d5002c9b8adfa".
611 angle_rev_pattern = re.compile(r'(?<="%s": ")([a-fA-F0-9]{40})(?=")' %
612 deps_var, re.MULTILINE)
613 match = re.search(angle_rev_pattern % deps_var, deps_contents)
614 if match:
615 # Update the revision information for the given depot
616 new_data = re.sub(angle_rev_pattern, revision, deps_contents)
617 else:
618 # Check whether the depot and revision pattern in DEPS file deps
619 # variable. e.g.,
620 # "src/third_party/angle": Var("chromium_git") +
621 # "/angle/angle.git@fa63e947cb3eccf463648d21a05d5002c9b8adfa",.
622 angle_rev_pattern = re.compile(
623 r'(?<=angle\.git@)([a-fA-F0-9]{40})(?=")', re.MULTILINE)
624 match = re.search(angle_rev_pattern, deps_contents)
625 if not match:
626 print 'Could not find angle revision information in DEPS file.'
627 return False
628 new_data = re.sub(angle_rev_pattern, revision, deps_contents)
629 # Write changes to DEPS file
630 WriteStringToFile(new_data, deps_file)
631 return True
632 except IOError, e:
633 print 'Something went wrong while updating DEPS file, %s' % e
634 return False
635
636
637 def _TryParseHistogramValuesFromOutput(metric, text):
638 """Attempts to parse a metric in the format HISTOGRAM <graph: <trace>.
639
640 Args:
641 metric: The metric as a list of [<trace>, <value>] strings.
642 text: The text to parse the metric values from.
643
644 Returns:
645 A list of floating point numbers found, [] if none were found.
646 """
647 metric_formatted = 'HISTOGRAM %s: %s= ' % (metric[0], metric[1])
648
649 text_lines = text.split('\n')
650 values_list = []
651
652 for current_line in text_lines:
653 if metric_formatted in current_line:
654 current_line = current_line[len(metric_formatted):]
655
656 try:
657 histogram_values = eval(current_line)
658
659 for b in histogram_values['buckets']:
660 average_for_bucket = float(b['high'] + b['low']) * 0.5
661 # Extends the list with N-elements with the average for that bucket.
662 values_list.extend([average_for_bucket] * b['count'])
663 except Exception:
664 pass
665
666 return values_list
667
668
669 def _TryParseResultValuesFromOutput(metric, text):
670 """Attempts to parse a metric in the format RESULT <graph>: <trace>= ...
671
672 Args:
673 metric: The metric as a list of [<trace>, <value>] string pairs.
674 text: The text to parse the metric values from.
675
676 Returns:
677 A list of floating point numbers found.
678 """
679 # Format is: RESULT <graph>: <trace>= <value> <units>
680 metric_re = re.escape('RESULT %s: %s=' % (metric[0], metric[1]))
681
682 # The log will be parsed looking for format:
683 # <*>RESULT <graph_name>: <trace_name>= <value>
684 single_result_re = re.compile(
685 metric_re + '\s*(?P<VALUE>[-]?\d*(\.\d*)?)')
686
687 # The log will be parsed looking for format:
688 # <*>RESULT <graph_name>: <trace_name>= [<value>,value,value,...]
689 multi_results_re = re.compile(
690 metric_re + '\s*\[\s*(?P<VALUES>[-]?[\d\., ]+)\s*\]')
691
692 # The log will be parsed looking for format:
693 # <*>RESULT <graph_name>: <trace_name>= {<mean>, <std deviation>}
694 mean_stddev_re = re.compile(
695 metric_re +
696 '\s*\{\s*(?P<MEAN>[-]?\d*(\.\d*)?),\s*(?P<STDDEV>\d+(\.\d*)?)\s*\}')
697
698 text_lines = text.split('\n')
699 values_list = []
700 for current_line in text_lines:
701 # Parse the output from the performance test for the metric we're
702 # interested in.
703 single_result_match = single_result_re.search(current_line)
704 multi_results_match = multi_results_re.search(current_line)
705 mean_stddev_match = mean_stddev_re.search(current_line)
706 if (not single_result_match is None and
707 single_result_match.group('VALUE')):
708 values_list += [single_result_match.group('VALUE')]
709 elif (not multi_results_match is None and
710 multi_results_match.group('VALUES')):
711 metric_values = multi_results_match.group('VALUES')
712 values_list += metric_values.split(',')
713 elif (not mean_stddev_match is None and
714 mean_stddev_match.group('MEAN')):
715 values_list += [mean_stddev_match.group('MEAN')]
716
717 values_list = [float(v) for v in values_list
718 if bisect_utils.IsStringFloat(v)]
719
720 # If the metric is times/t, we need to sum the timings in order to get
721 # similar regression results as the try-bots.
722 metrics_to_sum = [
723 ['times', 't'],
724 ['times', 'page_load_time'],
725 ['cold_times', 'page_load_time'],
726 ['warm_times', 'page_load_time'],
727 ]
728
729 if metric in metrics_to_sum:
730 if values_list:
731 values_list = [reduce(lambda x, y: float(x) + float(y), values_list)]
732
733 return values_list
734
735
736 def _ParseMetricValuesFromOutput(metric, text):
737 """Parses output from performance_ui_tests and retrieves the results for
738 a given metric.
739
740 Args:
741 metric: The metric as a list of [<trace>, <value>] strings.
742 text: The text to parse the metric values from.
743
744 Returns:
745 A list of floating point numbers found.
746 """
747 metric_values = _TryParseResultValuesFromOutput(metric, text)
748
749 if not metric_values:
750 metric_values = _TryParseHistogramValuesFromOutput(metric, text)
751
752 return metric_values
753
754
755 def _GenerateProfileIfNecessary(command_args):
756 """Checks the command line of the performance test for dependencies on
757 profile generation, and runs tools/perf/generate_profile as necessary.
758
759 Args:
760 command_args: Command line being passed to performance test, as a list.
761
762 Returns:
763 False if profile generation was necessary and failed, otherwise True.
764 """
765 if '--profile-dir' in ' '.join(command_args):
766 # If we were using python 2.7+, we could just use the argparse
767 # module's parse_known_args to grab --profile-dir. Since some of the
768 # bots still run 2.6, have to grab the arguments manually.
769 arg_dict = {}
770 args_to_parse = ['--profile-dir', '--browser']
771
772 for arg_to_parse in args_to_parse:
773 for i, current_arg in enumerate(command_args):
774 if arg_to_parse in current_arg:
775 current_arg_split = current_arg.split('=')
776
777 # Check 2 cases, --arg=<val> and --arg <val>
778 if len(current_arg_split) == 2:
779 arg_dict[arg_to_parse] = current_arg_split[1]
780 elif i + 1 < len(command_args):
781 arg_dict[arg_to_parse] = command_args[i+1]
782
783 path_to_generate = os.path.join('tools', 'perf', 'generate_profile')
784
785 if arg_dict.has_key('--profile-dir') and arg_dict.has_key('--browser'):
786 profile_path, profile_type = os.path.split(arg_dict['--profile-dir'])
787 return not bisect_utils.RunProcess(['python', path_to_generate,
788 '--profile-type-to-generate', profile_type,
789 '--browser', arg_dict['--browser'], '--output-dir', profile_path])
790 return False
791 return True
792
793
794 def _AddRevisionsIntoRevisionData(revisions, depot, sort, revision_data):
795 """Adds new revisions to the revision_data dictionary and initializes them.
796
797 Args:
798 revisions: List of revisions to add.
799 depot: Depot that's currently in use (src, webkit, etc...)
800 sort: Sorting key for displaying revisions.
801 revision_data: A dictionary to add the new revisions into.
802 Existing revisions will have their sort keys adjusted.
803 """
804 num_depot_revisions = len(revisions)
805
806 for _, v in revision_data.iteritems():
807 if v['sort'] > sort:
808 v['sort'] += num_depot_revisions
809
810 for i in xrange(num_depot_revisions):
811 r = revisions[i]
812 revision_data[r] = {
813 'revision' : r,
814 'depot' : depot,
815 'value' : None,
816 'perf_time' : 0,
817 'build_time' : 0,
818 'passed' : '?',
819 'sort' : i + sort + 1,
820 }
821
822
823 def _PrintThankYou():
824 print RESULTS_THANKYOU
825
826
827 def _PrintTableRow(column_widths, row_data):
828 """Prints out a row in a formatted table that has columns aligned.
829
830 Args:
831 column_widths: A list of column width numbers.
832 row_data: A list of items for each column in this row.
833 """
834 assert len(column_widths) == len(row_data)
835 text = ''
836 for i in xrange(len(column_widths)):
837 current_row_data = row_data[i].center(column_widths[i], ' ')
838 text += ('%%%ds' % column_widths[i]) % current_row_data
839 print text
840
841
842 def _PrintStepTime(revision_data_sorted):
843 """Prints information about how long various steps took.
844
845 Args:
846 revision_data_sorted: The sorted list of revision data dictionaries."""
847 step_perf_time_avg = 0.0
848 step_build_time_avg = 0.0
849 step_count = 0.0
850 for _, current_data in revision_data_sorted:
851 if current_data['value']:
852 step_perf_time_avg += current_data['perf_time']
853 step_build_time_avg += current_data['build_time']
854 step_count += 1
855 if step_count:
856 step_perf_time_avg = step_perf_time_avg / step_count
857 step_build_time_avg = step_build_time_avg / step_count
858 print
859 print 'Average build time : %s' % datetime.timedelta(
860 seconds=int(step_build_time_avg))
861 print 'Average test time : %s' % datetime.timedelta(
862 seconds=int(step_perf_time_avg))
863
864
865 def _FindOtherRegressions(revision_data_sorted, bad_greater_than_good):
866 """Compiles a list of other possible regressions from the revision data.
867
868 Args:
869 revision_data_sorted: Sorted list of (revision, revision data) pairs.
870 bad_greater_than_good: Whether the result value at the "bad" revision is
871 numerically greater than the result value at the "good" revision.
872
873 Returns:
874 A list of [current_rev, previous_rev, confidence] for other places where
875 there may have been a regression.
876 """
877 other_regressions = []
878 previous_values = []
879 previous_id = None
880 for current_id, current_data in revision_data_sorted:
881 current_values = current_data['value']
882 if current_values:
883 current_values = current_values['values']
884 if previous_values:
885 confidence = ConfidenceScore(previous_values, [current_values])
886 mean_of_prev_runs = math_utils.Mean(sum(previous_values, []))
887 mean_of_current_runs = math_utils.Mean(current_values)
888
889 # Check that the potential regression is in the same direction as
890 # the overall regression. If the mean of the previous runs < the
891 # mean of the current runs, this local regression is in same
892 # direction.
893 prev_less_than_current = mean_of_prev_runs < mean_of_current_runs
894 is_same_direction = (prev_less_than_current if
895 bad_greater_than_good else not prev_less_than_current)
896
897 # Only report potential regressions with high confidence.
898 if is_same_direction and confidence > 50:
899 other_regressions.append([current_id, previous_id, confidence])
900 previous_values.append(current_values)
901 previous_id = current_id
902 return other_regressions
903
904
905 class BisectPerformanceMetrics(object):
906 """This class contains functionality to perform a bisection of a range of
907 revisions to narrow down where performance regressions may have occurred.
908
909 The main entry-point is the Run method.
910 """
911
912 def __init__(self, source_control, opts):
913 super(BisectPerformanceMetrics, self).__init__()
914
915 self.opts = opts
916 self.source_control = source_control
917 self.src_cwd = os.getcwd()
918 self.cros_cwd = os.path.join(os.getcwd(), '..', 'cros')
919 self.depot_cwd = {}
920 self.cleanup_commands = []
921 self.warnings = []
922 self.builder = builder.Builder.FromOpts(opts)
923
924 for d in DEPOT_NAMES:
925 # The working directory of each depot is just the path to the depot, but
926 # since we're already in 'src', we can skip that part.
927
928 self.depot_cwd[d] = os.path.join(
929 self.src_cwd, DEPOT_DEPS_NAME[d]['src'][4:])
930
931 def PerformCleanup(self):
932 """Performs cleanup when script is finished."""
933 os.chdir(self.src_cwd)
934 for c in self.cleanup_commands:
935 if c[0] == 'mv':
936 shutil.move(c[1], c[2])
937 else:
938 assert False, 'Invalid cleanup command.'
939
940 def GetRevisionList(self, depot, bad_revision, good_revision):
941 """Retrieves a list of all the commits between the bad revision and
942 last known good revision."""
943
944 revision_work_list = []
945
946 if depot == 'cros':
947 revision_range_start = good_revision
948 revision_range_end = bad_revision
949
950 cwd = os.getcwd()
951 self.ChangeToDepotWorkingDirectory('cros')
952
953 # Print the commit timestamps for every commit in the revision time
954 # range. We'll sort them and bisect by that. There is a remote chance that
955 # 2 (or more) commits will share the exact same timestamp, but it's
956 # probably safe to ignore that case.
957 cmd = ['repo', 'forall', '-c',
958 'git log --format=%%ct --before=%d --after=%d' % (
959 revision_range_end, revision_range_start)]
960 output, return_code = bisect_utils.RunProcessAndRetrieveOutput(cmd)
961
962 assert not return_code, ('An error occurred while running '
963 '"%s"' % ' '.join(cmd))
964
965 os.chdir(cwd)
966
967 revision_work_list = list(set(
968 [int(o) for o in output.split('\n') if bisect_utils.IsStringInt(o)]))
969 revision_work_list = sorted(revision_work_list, reverse=True)
970 else:
971 cwd = self._GetDepotDirectory(depot)
972 revision_work_list = self.source_control.GetRevisionList(bad_revision,
973 good_revision, cwd=cwd)
974
975 return revision_work_list
976
977 def _GetV8BleedingEdgeFromV8TrunkIfMappable(self, revision):
978 commit_position = self.source_control.GetCommitPosition(revision)
979
980 if bisect_utils.IsStringInt(commit_position):
981 # V8 is tricky to bisect, in that there are only a few instances when
982 # we can dive into bleeding_edge and get back a meaningful result.
983 # Try to detect a V8 "business as usual" case, which is when:
984 # 1. trunk revision N has description "Version X.Y.Z"
985 # 2. bleeding_edge revision (N-1) has description "Prepare push to
986 # trunk. Now working on X.Y.(Z+1)."
987 #
988 # As of 01/24/2014, V8 trunk descriptions are formatted:
989 # "Version 3.X.Y (based on bleeding_edge revision rZ)"
990 # So we can just try parsing that out first and fall back to the old way.
991 v8_dir = self._GetDepotDirectory('v8')
992 v8_bleeding_edge_dir = self._GetDepotDirectory('v8_bleeding_edge')
993
994 revision_info = self.source_control.QueryRevisionInfo(revision,
995 cwd=v8_dir)
996
997 version_re = re.compile("Version (?P<values>[0-9,.]+)")
998
999 regex_results = version_re.search(revision_info['subject'])
1000
1001 if regex_results:
1002 git_revision = None
1003
1004 # Look for "based on bleeding_edge" and parse out revision
1005 if 'based on bleeding_edge' in revision_info['subject']:
1006 try:
1007 bleeding_edge_revision = revision_info['subject'].split(
1008 'bleeding_edge revision r')[1]
1009 bleeding_edge_revision = int(bleeding_edge_revision.split(')')[0])
1010 git_revision = self.source_control.ResolveToRevision(
1011 bleeding_edge_revision, 'v8_bleeding_edge', DEPOT_DEPS_NAME, 1,
1012 cwd=v8_bleeding_edge_dir)
1013 return git_revision
1014 except (IndexError, ValueError):
1015 pass
1016
1017 if not git_revision:
1018 # Wasn't successful, try the old way of looking for "Prepare push to"
1019 git_revision = self.source_control.ResolveToRevision(
1020 int(commit_position) - 1, 'v8_bleeding_edge', DEPOT_DEPS_NAME, -1,
1021 cwd=v8_bleeding_edge_dir)
1022
1023 if git_revision:
1024 revision_info = self.source_control.QueryRevisionInfo(git_revision,
1025 cwd=v8_bleeding_edge_dir)
1026
1027 if 'Prepare push to trunk' in revision_info['subject']:
1028 return git_revision
1029 return None
1030
1031 def _GetNearestV8BleedingEdgeFromTrunk(self, revision, search_forward=True):
1032 cwd = self._GetDepotDirectory('v8')
1033 cmd = ['log', '--format=%ct', '-1', revision]
1034 output = bisect_utils.CheckRunGit(cmd, cwd=cwd)
1035 commit_time = int(output)
1036 commits = []
1037
1038 if search_forward:
1039 cmd = ['log', '--format=%H', '-10', '--after=%d' % commit_time,
1040 'origin/master']
1041 output = bisect_utils.CheckRunGit(cmd, cwd=cwd)
1042 output = output.split()
1043 commits = output
1044 commits = reversed(commits)
1045 else:
1046 cmd = ['log', '--format=%H', '-10', '--before=%d' % commit_time,
1047 'origin/master']
1048 output = bisect_utils.CheckRunGit(cmd, cwd=cwd)
1049 output = output.split()
1050 commits = output
1051
1052 bleeding_edge_revision = None
1053
1054 for c in commits:
1055 bleeding_edge_revision = self._GetV8BleedingEdgeFromV8TrunkIfMappable(c)
1056 if bleeding_edge_revision:
1057 break
1058
1059 return bleeding_edge_revision
1060
1061 def _ParseRevisionsFromDEPSFile(self, depot):
1062 """Parses the local DEPS file to determine blink/skia/v8 revisions which may
1063 be needed if the bisect recurses into those depots later.
1064
1065 Args:
1066 depot: Name of depot being bisected.
1067
1068 Returns:
1069 A dict in the format {depot:revision} if successful, otherwise None.
1070 """
1071 try:
1072 deps_data = {
1073 'Var': lambda _: deps_data["vars"][_],
1074 'From': lambda *args: None,
1075 }
1076
1077 deps_file = bisect_utils.FILE_DEPS_GIT
1078 if not os.path.exists(deps_file):
1079 deps_file = bisect_utils.FILE_DEPS
1080 execfile(deps_file, {}, deps_data)
1081 deps_data = deps_data['deps']
1082
1083 rxp = re.compile(".git@(?P<revision>[a-fA-F0-9]+)")
1084 results = {}
1085 for depot_name, depot_data in DEPOT_DEPS_NAME.iteritems():
1086 if (depot_data.get('platform') and
1087 depot_data.get('platform') != os.name):
1088 continue
1089
1090 if (depot_data.get('recurse') and depot in depot_data.get('from')):
1091 depot_data_src = depot_data.get('src') or depot_data.get('src_old')
1092 src_dir = deps_data.get(depot_data_src)
1093 if src_dir:
1094 self.depot_cwd[depot_name] = os.path.join(self.src_cwd,
1095 depot_data_src[4:])
1096 re_results = rxp.search(src_dir)
1097 if re_results:
1098 results[depot_name] = re_results.group('revision')
1099 else:
1100 warning_text = ('Could not parse revision for %s while bisecting '
1101 '%s' % (depot_name, depot))
1102 if not warning_text in self.warnings:
1103 self.warnings.append(warning_text)
1104 else:
1105 results[depot_name] = None
1106 return results
1107 except ImportError:
1108 deps_file_contents = ReadStringFromFile(deps_file)
1109 parse_results = _ParseRevisionsFromDEPSFileManually(deps_file_contents)
1110 results = {}
1111 for depot_name, depot_revision in parse_results.iteritems():
1112 depot_revision = depot_revision.strip('@')
1113 print depot_name, depot_revision
1114 for current_name, current_data in DEPOT_DEPS_NAME.iteritems():
1115 if (current_data.has_key('deps_var') and
1116 current_data['deps_var'] == depot_name):
1117 src_name = current_name
1118 results[src_name] = depot_revision
1119 break
1120 return results
1121
1122 def _Get3rdPartyRevisions(self, depot):
1123 """Parses the DEPS file to determine WebKit/v8/etc... versions.
1124
1125 Args:
1126 depot: A depot name. Should be in the DEPOT_NAMES list.
1127
1128 Returns:
1129 A dict in the format {depot: revision} if successful, otherwise None.
1130 """
1131 cwd = os.getcwd()
1132 self.ChangeToDepotWorkingDirectory(depot)
1133
1134 results = {}
1135
1136 if depot == 'chromium' or depot == 'android-chrome':
1137 results = self._ParseRevisionsFromDEPSFile(depot)
1138 os.chdir(cwd)
1139
1140 if depot == 'cros':
1141 cmd = [
1142 bisect_utils.CROS_SDK_PATH,
1143 '--',
1144 'portageq-%s' % self.opts.cros_board,
1145 'best_visible',
1146 '/build/%s' % self.opts.cros_board,
1147 'ebuild',
1148 CROS_CHROMEOS_PATTERN
1149 ]
1150 output, return_code = bisect_utils.RunProcessAndRetrieveOutput(cmd)
1151
1152 assert not return_code, ('An error occurred while running '
1153 '"%s"' % ' '.join(cmd))
1154
1155 if len(output) > CROS_CHROMEOS_PATTERN:
1156 output = output[len(CROS_CHROMEOS_PATTERN):]
1157
1158 if len(output) > 1:
1159 output = output.split('_')[0]
1160
1161 if len(output) > 3:
1162 contents = output.split('.')
1163
1164 version = contents[2]
1165
1166 if contents[3] != '0':
1167 warningText = ('Chrome version: %s.%s but using %s.0 to bisect.' %
1168 (version, contents[3], version))
1169 if not warningText in self.warnings:
1170 self.warnings.append(warningText)
1171
1172 cwd = os.getcwd()
1173 self.ChangeToDepotWorkingDirectory('chromium')
1174 cmd = ['log', '-1', '--format=%H',
1175 '--author=chrome-release@google.com',
1176 '--grep=to %s' % version, 'origin/master']
1177 return_code = bisect_utils.CheckRunGit(cmd)
1178 os.chdir(cwd)
1179
1180 results['chromium'] = output.strip()
1181
1182 if depot == 'v8':
1183 # We can't try to map the trunk revision to bleeding edge yet, because
1184 # we don't know which direction to try to search in. Have to wait until
1185 # the bisect has narrowed the results down to 2 v8 rolls.
1186 results['v8_bleeding_edge'] = None
1187
1188 return results
1189
1190 def BackupOrRestoreOutputDirectory(self, restore=False, build_type='Release'):
1191 """Backs up or restores build output directory based on restore argument.
1192
1193 Args:
1194 restore: Indicates whether to restore or backup. Default is False(Backup)
1195 build_type: Target build type ('Release', 'Debug', 'Release_x64' etc.)
1196
1197 Returns:
1198 Path to backup or restored location as string. otherwise None if it fails.
1199 """
1200 build_dir = os.path.abspath(
1201 builder.GetBuildOutputDirectory(self.opts, self.src_cwd))
1202 source_dir = os.path.join(build_dir, build_type)
1203 destination_dir = os.path.join(build_dir, '%s.bak' % build_type)
1204 if restore:
1205 source_dir, destination_dir = destination_dir, source_dir
1206 if os.path.exists(source_dir):
1207 RmTreeAndMkDir(destination_dir, skip_makedir=True)
1208 shutil.move(source_dir, destination_dir)
1209 return destination_dir
1210 return None
1211
1212 def GetBuildArchiveForRevision(self, revision, gs_bucket, target_arch,
1213 patch_sha, out_dir):
1214 """Checks and downloads build archive for a given revision.
1215
1216 Checks for build archive with Git hash or SVN revision. If either of the
1217 file exists, then downloads the archive file.
1218
1219 Args:
1220 revision: A Git hash revision.
1221 gs_bucket: Cloud storage bucket name
1222 target_arch: 32 or 64 bit build target
1223 patch: A DEPS patch (used while bisecting 3rd party repositories).
1224 out_dir: Build output directory where downloaded file is stored.
1225
1226 Returns:
1227 Downloaded archive file path if exists, otherwise None.
1228 """
1229 # Source archive file path on cloud storage using Git revision.
1230 source_file = GetRemoteBuildPath(
1231 revision, self.opts.target_platform, target_arch, patch_sha)
1232 downloaded_archive = FetchFromCloudStorage(gs_bucket, source_file, out_dir)
1233 if not downloaded_archive:
1234 # Get commit position for the given SHA.
1235 commit_position = self.source_control.GetCommitPosition(revision)
1236 if commit_position:
1237 # Source archive file path on cloud storage using SVN revision.
1238 source_file = GetRemoteBuildPath(
1239 commit_position, self.opts.target_platform, target_arch, patch_sha)
1240 return FetchFromCloudStorage(gs_bucket, source_file, out_dir)
1241 return downloaded_archive
1242
1243 def DownloadCurrentBuild(self, revision, build_type='Release', patch=None):
1244 """Downloads the build archive for the given revision.
1245
1246 Args:
1247 revision: The Git revision to download or build.
1248 build_type: Target build type ('Release', 'Debug', 'Release_x64' etc.)
1249 patch: A DEPS patch (used while bisecting 3rd party repositories).
1250
1251 Returns:
1252 True if download succeeds, otherwise False.
1253 """
1254 patch_sha = None
1255 if patch:
1256 # Get the SHA of the DEPS changes patch.
1257 patch_sha = GetSHA1HexDigest(patch)
1258
1259 # Update the DEPS changes patch with a patch to create a new file named
1260 # 'DEPS.sha' and add patch_sha evaluated above to it.
1261 patch = '%s\n%s' % (patch, DEPS_SHA_PATCH % {'deps_sha': patch_sha})
1262
1263 # Get Build output directory
1264 abs_build_dir = os.path.abspath(
1265 builder.GetBuildOutputDirectory(self.opts, self.src_cwd))
1266
1267 fetch_build_func = lambda: self.GetBuildArchiveForRevision(
1268 revision, self.opts.gs_bucket, self.opts.target_arch,
1269 patch_sha, abs_build_dir)
1270
1271 # Downloaded archive file path, downloads build archive for given revision.
1272 downloaded_file = fetch_build_func()
1273
1274 # When build archive doesn't exists, post a build request to tryserver
1275 # and wait for the build to be produced.
1276 if not downloaded_file:
1277 downloaded_file = self.PostBuildRequestAndWait(
1278 revision, fetch_build=fetch_build_func, patch=patch)
1279 if not downloaded_file:
1280 return False
1281
1282 # Generic name for the archive, created when archive file is extracted.
1283 output_dir = os.path.join(
1284 abs_build_dir, GetZipFileName(target_arch=self.opts.target_arch))
1285 # Unzip build archive directory.
1286 try:
1287 RmTreeAndMkDir(output_dir, skip_makedir=True)
1288 self.BackupOrRestoreOutputDirectory(restore=False)
1289 # Build output directory based on target(e.g. out/Release, out/Debug).
1290 target_build_output_dir = os.path.join(abs_build_dir, build_type)
1291 ExtractZip(downloaded_file, abs_build_dir)
1292 if not os.path.exists(output_dir):
1293 # Due to recipe changes, the builds extract folder contains
1294 # out/Release instead of full-build-<platform>/Release.
1295 if os.path.exists(os.path.join(abs_build_dir, 'out', build_type)):
1296 output_dir = os.path.join(abs_build_dir, 'out', build_type)
1297 else:
1298 raise IOError('Missing extracted folder %s ' % output_dir)
1299
1300 print 'Moving build from %s to %s' % (
1301 output_dir, target_build_output_dir)
1302 shutil.move(output_dir, target_build_output_dir)
1303 return True
1304 except Exception as e:
1305 print 'Something went wrong while extracting archive file: %s' % e
1306 self.BackupOrRestoreOutputDirectory(restore=True)
1307 # Cleanup any leftovers from unzipping.
1308 if os.path.exists(output_dir):
1309 RmTreeAndMkDir(output_dir, skip_makedir=True)
1310 finally:
1311 # Delete downloaded archive
1312 if os.path.exists(downloaded_file):
1313 os.remove(downloaded_file)
1314 return False
1315
1316 def PostBuildRequestAndWait(self, git_revision, fetch_build, patch=None):
1317 """POSTs the build request job to the try server instance.
1318
1319 A try job build request is posted to tryserver.chromium.perf master,
1320 and waits for the binaries to be produced and archived on cloud storage.
1321 Once the build is ready and stored onto cloud, build archive is downloaded
1322 into the output folder.
1323
1324 Args:
1325 git_revision: A Git hash revision.
1326 fetch_build: Function to check and download build from cloud storage.
1327 patch: A DEPS patch (used while bisecting 3rd party repositories).
1328
1329 Returns:
1330 Downloaded archive file path when requested build exists and download is
1331 successful, otherwise None.
1332 """
1333 def GetBuilderNameAndBuildTime(target_platform, target_arch='ia32'):
1334 """Gets builder bot name and build time in seconds based on platform."""
1335 # Bot names should match the one listed in tryserver.chromium's
1336 # master.cfg which produces builds for bisect.
1337 if bisect_utils.IsWindowsHost():
1338 if bisect_utils.Is64BitWindows() and target_arch == 'x64':
1339 return ('win_perf_bisect_builder', MAX_WIN_BUILD_TIME)
1340 return ('win_perf_bisect_builder', MAX_WIN_BUILD_TIME)
1341 if bisect_utils.IsLinuxHost():
1342 if target_platform == 'android':
1343 return ('android_perf_bisect_builder', MAX_LINUX_BUILD_TIME)
1344 return ('linux_perf_bisect_builder', MAX_LINUX_BUILD_TIME)
1345 if bisect_utils.IsMacHost():
1346 return ('mac_perf_bisect_builder', MAX_MAC_BUILD_TIME)
1347 raise NotImplementedError('Unsupported Platform "%s".' % sys.platform)
1348 if not fetch_build:
1349 return False
1350
1351 bot_name, build_timeout = GetBuilderNameAndBuildTime(
1352 self.opts.target_platform, self.opts.target_arch)
1353 builder_host = self.opts.builder_host
1354 builder_port = self.opts.builder_port
1355 # Create a unique ID for each build request posted to try server builders.
1356 # This ID is added to "Reason" property of the build.
1357 build_request_id = GetSHA1HexDigest(
1358 '%s-%s-%s' % (git_revision, patch, time.time()))
1359
1360 # Creates a try job description.
1361 # Always use Git hash to post build request since Commit positions are
1362 # not supported by builders to build.
1363 job_args = {
1364 'revision': 'src@%s' % git_revision,
1365 'bot': bot_name,
1366 'name': build_request_id,
1367 }
1368 # Update patch information if supplied.
1369 if patch:
1370 job_args['patch'] = patch
1371 # Posts job to build the revision on the server.
1372 if request_build.PostTryJob(builder_host, builder_port, job_args):
1373 target_file, error_msg = _WaitUntilBuildIsReady(
1374 fetch_build, bot_name, builder_host, builder_port, build_request_id,
1375 build_timeout)
1376 if not target_file:
1377 print '%s [revision: %s]' % (error_msg, git_revision)
1378 return None
1379 return target_file
1380 print 'Failed to post build request for revision: [%s]' % git_revision
1381 return None
1382
1383 def IsDownloadable(self, depot):
1384 """Checks if build can be downloaded based on target platform and depot."""
1385 if (self.opts.target_platform in ['chromium', 'android'] and
1386 self.opts.gs_bucket):
1387 return (depot == 'chromium' or
1388 'chromium' in DEPOT_DEPS_NAME[depot]['from'] or
1389 'v8' in DEPOT_DEPS_NAME[depot]['from'])
1390 return False
1391
1392 def UpdateDepsContents(self, deps_contents, depot, git_revision, deps_key):
1393 """Returns modified version of DEPS file contents.
1394
1395 Args:
1396 deps_contents: DEPS file content.
1397 depot: Current depot being bisected.
1398 git_revision: A git hash to be updated in DEPS.
1399 deps_key: Key in vars section of DEPS file to be searched.
1400
1401 Returns:
1402 Updated DEPS content as string if deps key is found, otherwise None.
1403 """
1404 # Check whether the depot and revision pattern in DEPS file vars
1405 # e.g. for webkit the format is "webkit_revision": "12345".
1406 deps_revision = re.compile(r'(?<="%s": ")([0-9]+)(?=")' % deps_key,
1407 re.MULTILINE)
1408 new_data = None
1409 if re.search(deps_revision, deps_contents):
1410 commit_position = self.source_control.GetCommitPosition(
1411 git_revision, self._GetDepotDirectory(depot))
1412 if not commit_position:
1413 print 'Could not determine commit position for %s' % git_revision
1414 return None
1415 # Update the revision information for the given depot
1416 new_data = re.sub(deps_revision, str(commit_position), deps_contents)
1417 else:
1418 # Check whether the depot and revision pattern in DEPS file vars
1419 # e.g. for webkit the format is "webkit_revision": "559a6d4ab7a84c539..".
1420 deps_revision = re.compile(
1421 r'(?<=["\']%s["\']: ["\'])([a-fA-F0-9]{40})(?=["\'])' % deps_key,
1422 re.MULTILINE)
1423 if re.search(deps_revision, deps_contents):
1424 new_data = re.sub(deps_revision, git_revision, deps_contents)
1425 if new_data:
1426 # For v8_bleeding_edge revisions change V8 branch in order
1427 # to fetch bleeding edge revision.
1428 if depot == 'v8_bleeding_edge':
1429 new_data = _UpdateV8Branch(new_data)
1430 if not new_data:
1431 return None
1432 return new_data
1433
1434 def UpdateDeps(self, revision, depot, deps_file):
1435 """Updates DEPS file with new revision of dependency repository.
1436
1437 This method search DEPS for a particular pattern in which depot revision
1438 is specified (e.g "webkit_revision": "123456"). If a match is found then
1439 it resolves the given git hash to SVN revision and replace it in DEPS file.
1440
1441 Args:
1442 revision: A git hash revision of the dependency repository.
1443 depot: Current depot being bisected.
1444 deps_file: Path to DEPS file.
1445
1446 Returns:
1447 True if DEPS file is modified successfully, otherwise False.
1448 """
1449 if not os.path.exists(deps_file):
1450 return False
1451
1452 deps_var = DEPOT_DEPS_NAME[depot]['deps_var']
1453 # Don't update DEPS file if deps_var is not set in DEPOT_DEPS_NAME.
1454 if not deps_var:
1455 print 'DEPS update not supported for Depot: %s', depot
1456 return False
1457
1458 # Hack for Angle repository. In the DEPS file, "vars" dictionary variable
1459 # contains "angle_revision" key that holds git hash instead of SVN revision.
1460 # And sometime "angle_revision" key is not specified in "vars" variable.
1461 # In such cases check, "deps" dictionary variable that matches
1462 # angle.git@[a-fA-F0-9]{40}$ and replace git hash.
1463 if depot == 'angle':
1464 return _UpdateDEPSForAngle(revision, depot, deps_file)
1465
1466 try:
1467 deps_contents = ReadStringFromFile(deps_file)
1468 updated_deps_content = self.UpdateDepsContents(
1469 deps_contents, depot, revision, deps_var)
1470 # Write changes to DEPS file
1471 if updated_deps_content:
1472 WriteStringToFile(updated_deps_content, deps_file)
1473 return True
1474 except IOError, e:
1475 print 'Something went wrong while updating DEPS file. [%s]' % e
1476 return False
1477
1478 def CreateDEPSPatch(self, depot, revision):
1479 """Modifies DEPS and returns diff as text.
1480
1481 Args:
1482 depot: Current depot being bisected.
1483 revision: A git hash revision of the dependency repository.
1484
1485 Returns:
1486 A tuple with git hash of chromium revision and DEPS patch text.
1487 """
1488 deps_file_path = os.path.join(self.src_cwd, bisect_utils.FILE_DEPS)
1489 if not os.path.exists(deps_file_path):
1490 raise RuntimeError('DEPS file does not exists.[%s]' % deps_file_path)
1491 # Get current chromium revision (git hash).
1492 cmd = ['rev-parse', 'HEAD']
1493 chromium_sha = bisect_utils.CheckRunGit(cmd).strip()
1494 if not chromium_sha:
1495 raise RuntimeError('Failed to determine Chromium revision for %s' %
1496 revision)
1497 if ('chromium' in DEPOT_DEPS_NAME[depot]['from'] or
1498 'v8' in DEPOT_DEPS_NAME[depot]['from']):
1499 # Checkout DEPS file for the current chromium revision.
1500 if self.source_control.CheckoutFileAtRevision(
1501 bisect_utils.FILE_DEPS, chromium_sha, cwd=self.src_cwd):
1502 if self.UpdateDeps(revision, depot, deps_file_path):
1503 diff_command = [
1504 'diff',
1505 '--src-prefix=src/',
1506 '--dst-prefix=src/',
1507 '--no-ext-diff',
1508 bisect_utils.FILE_DEPS,
1509 ]
1510 diff_text = bisect_utils.CheckRunGit(diff_command, cwd=self.src_cwd)
1511 return (chromium_sha, ChangeBackslashToSlashInPatch(diff_text))
1512 else:
1513 raise RuntimeError(
1514 'Failed to update DEPS file for chromium: [%s]' % chromium_sha)
1515 else:
1516 raise RuntimeError(
1517 'DEPS checkout Failed for chromium revision : [%s]' % chromium_sha)
1518 return (None, None)
1519
1520 def BuildCurrentRevision(self, depot, revision=None):
1521 """Builds chrome and performance_ui_tests on the current revision.
1522
1523 Returns:
1524 True if the build was successful.
1525 """
1526 if self.opts.debug_ignore_build:
1527 return True
1528
1529 build_success = False
1530 cwd = os.getcwd()
1531 os.chdir(self.src_cwd)
1532 # Fetch build archive for the given revision from the cloud storage when
1533 # the storage bucket is passed.
1534 if self.IsDownloadable(depot) and revision:
1535 deps_patch = None
1536 if depot != 'chromium':
1537 # Create a DEPS patch with new revision for dependency repository.
1538 revision, deps_patch = self.CreateDEPSPatch(depot, revision)
1539 if self.DownloadCurrentBuild(revision, patch=deps_patch):
1540 if deps_patch:
1541 # Reverts the changes to DEPS file.
1542 self.source_control.CheckoutFileAtRevision(
1543 bisect_utils.FILE_DEPS, revision, cwd=self.src_cwd)
1544 build_success = True
1545 else:
1546 # These codes are executed when bisect bots builds binaries locally.
1547 build_success = self.builder.Build(depot, self.opts)
1548 os.chdir(cwd)
1549 return build_success
1550
1551 def RunGClientHooks(self):
1552 """Runs gclient with runhooks command.
1553
1554 Returns:
1555 True if gclient reports no errors.
1556 """
1557 if self.opts.debug_ignore_build:
1558 return True
1559 return not bisect_utils.RunGClient(['runhooks'], cwd=self.src_cwd)
1560
1561 def _IsBisectModeUsingMetric(self):
1562 return self.opts.bisect_mode in [BISECT_MODE_MEAN, BISECT_MODE_STD_DEV]
1563
1564 def _IsBisectModeReturnCode(self):
1565 return self.opts.bisect_mode in [BISECT_MODE_RETURN_CODE]
1566
1567 def _IsBisectModeStandardDeviation(self):
1568 return self.opts.bisect_mode in [BISECT_MODE_STD_DEV]
1569
1570 def GetCompatibleCommand(self, command_to_run, revision, depot):
1571 # Prior to crrev.com/274857 *only* android-chromium-testshell
1572 # Then until crrev.com/276628 *both* (android-chromium-testshell and
1573 # android-chrome-shell) work. After that rev 276628 *only*
1574 # android-chrome-shell works. bisect-perf-regression.py script should
1575 # handle these cases and set appropriate browser type based on revision.
1576 if self.opts.target_platform in ['android']:
1577 # When its a third_party depot, get the chromium revision.
1578 if depot != 'chromium':
1579 revision = bisect_utils.CheckRunGit(
1580 ['rev-parse', 'HEAD'], cwd=self.src_cwd).strip()
1581 commit_position = self.source_control.GetCommitPosition(revision,
1582 cwd=self.src_cwd)
1583 if not commit_position:
1584 return command_to_run
1585 cmd_re = re.compile('--browser=(?P<browser_type>\S+)')
1586 matches = cmd_re.search(command_to_run)
1587 if bisect_utils.IsStringInt(commit_position) and matches:
1588 cmd_browser = matches.group('browser_type')
1589 if commit_position <= 274857 and cmd_browser == 'android-chrome-shell':
1590 return command_to_run.replace(cmd_browser,
1591 'android-chromium-testshell')
1592 elif (commit_position >= 276628 and
1593 cmd_browser == 'android-chromium-testshell'):
1594 return command_to_run.replace(cmd_browser,
1595 'android-chrome-shell')
1596 return command_to_run
1597
1598 def RunPerformanceTestAndParseResults(
1599 self, command_to_run, metric, reset_on_first_run=False,
1600 upload_on_last_run=False, results_label=None):
1601 """Runs a performance test on the current revision and parses the results.
1602
1603 Args:
1604 command_to_run: The command to be run to execute the performance test.
1605 metric: The metric to parse out from the results of the performance test.
1606 This is the result chart name and trace name, separated by slash.
1607 May be None for perf try jobs.
1608 reset_on_first_run: If True, pass the flag --reset-results on first run.
1609 upload_on_last_run: If True, pass the flag --upload-results on last run.
1610 results_label: A value for the option flag --results-label.
1611 The arguments reset_on_first_run, upload_on_last_run and results_label
1612 are all ignored if the test is not a Telemetry test.
1613
1614 Returns:
1615 (values dict, 0) if --debug_ignore_perf_test was passed.
1616 (values dict, 0, test output) if the test was run successfully.
1617 (error message, -1) if the test couldn't be run.
1618 (error message, -1, test output) if the test ran but there was an error.
1619 """
1620 success_code, failure_code = 0, -1
1621
1622 if self.opts.debug_ignore_perf_test:
1623 fake_results = {
1624 'mean': 0.0,
1625 'std_err': 0.0,
1626 'std_dev': 0.0,
1627 'values': [0.0]
1628 }
1629 return (fake_results, success_code)
1630
1631 # For Windows platform set posix=False, to parse windows paths correctly.
1632 # On Windows, path separators '\' or '\\' are replace by '' when posix=True,
1633 # refer to http://bugs.python.org/issue1724822. By default posix=True.
1634 args = shlex.split(command_to_run, posix=not bisect_utils.IsWindowsHost())
1635
1636 if not _GenerateProfileIfNecessary(args):
1637 err_text = 'Failed to generate profile for performance test.'
1638 return (err_text, failure_code)
1639
1640 # If running a Telemetry test for Chrome OS, insert the remote IP and
1641 # identity parameters.
1642 is_telemetry = bisect_utils.IsTelemetryCommand(command_to_run)
1643 if self.opts.target_platform == 'cros' and is_telemetry:
1644 args.append('--remote=%s' % self.opts.cros_remote_ip)
1645 args.append('--identity=%s' % bisect_utils.CROS_TEST_KEY_PATH)
1646
1647 start_time = time.time()
1648
1649 metric_values = []
1650 output_of_all_runs = ''
1651 for i in xrange(self.opts.repeat_test_count):
1652 # Can ignore the return code since if the tests fail, it won't return 0.
1653 current_args = copy.copy(args)
1654 if is_telemetry:
1655 if i == 0 and reset_on_first_run:
1656 current_args.append('--reset-results')
1657 elif i == self.opts.repeat_test_count - 1 and upload_on_last_run:
1658 current_args.append('--upload-results')
1659 if results_label:
1660 current_args.append('--results-label=%s' % results_label)
1661 try:
1662 output, return_code = bisect_utils.RunProcessAndRetrieveOutput(
1663 current_args, cwd=self.src_cwd)
1664 except OSError, e:
1665 if e.errno == errno.ENOENT:
1666 err_text = ('Something went wrong running the performance test. '
1667 'Please review the command line:\n\n')
1668 if 'src/' in ' '.join(args):
1669 err_text += ('Check that you haven\'t accidentally specified a '
1670 'path with src/ in the command.\n\n')
1671 err_text += ' '.join(args)
1672 err_text += '\n'
1673
1674 return (err_text, failure_code)
1675 raise
1676
1677 output_of_all_runs += output
1678 if self.opts.output_buildbot_annotations:
1679 print output
1680
1681 if metric and self._IsBisectModeUsingMetric():
1682 metric_values += _ParseMetricValuesFromOutput(metric, output)
1683 # If we're bisecting on a metric (ie, changes in the mean or
1684 # standard deviation) and no metric values are produced, bail out.
1685 if not metric_values:
1686 break
1687 elif self._IsBisectModeReturnCode():
1688 metric_values.append(return_code)
1689
1690 elapsed_minutes = (time.time() - start_time) / 60.0
1691 if elapsed_minutes >= self.opts.max_time_minutes:
1692 break
1693
1694 if metric and len(metric_values) == 0:
1695 err_text = 'Metric %s was not found in the test output.' % metric
1696 # TODO(qyearsley): Consider also getting and displaying a list of metrics
1697 # that were found in the output here.
1698 return (err_text, failure_code, output_of_all_runs)
1699
1700 # If we're bisecting on return codes, we're really just looking for zero vs
1701 # non-zero.
1702 values = {}
1703 if self._IsBisectModeReturnCode():
1704 # If any of the return codes is non-zero, output 1.
1705 overall_return_code = 0 if (
1706 all(current_value == 0 for current_value in metric_values)) else 1
1707
1708 values = {
1709 'mean': overall_return_code,
1710 'std_err': 0.0,
1711 'std_dev': 0.0,
1712 'values': metric_values,
1713 }
1714
1715 print 'Results of performance test: Command returned with %d' % (
1716 overall_return_code)
1717 print
1718 elif metric:
1719 # Need to get the average value if there were multiple values.
1720 truncated_mean = math_utils.TruncatedMean(
1721 metric_values, self.opts.truncate_percent)
1722 standard_err = math_utils.StandardError(metric_values)
1723 standard_dev = math_utils.StandardDeviation(metric_values)
1724
1725 if self._IsBisectModeStandardDeviation():
1726 metric_values = [standard_dev]
1727
1728 values = {
1729 'mean': truncated_mean,
1730 'std_err': standard_err,
1731 'std_dev': standard_dev,
1732 'values': metric_values,
1733 }
1734
1735 print 'Results of performance test: %12f %12f' % (
1736 truncated_mean, standard_err)
1737 print
1738 return (values, success_code, output_of_all_runs)
1739
1740 def _FindAllRevisionsToSync(self, revision, depot):
1741 """Finds all dependent revisions and depots that need to be synced.
1742
1743 For example skia is broken up into 3 git mirrors over skia/src,
1744 skia/gyp, and skia/include. To sync skia/src properly, one has to find
1745 the proper revisions in skia/gyp and skia/include.
1746
1747 This is only useful in the git workflow, as an SVN depot may be split into
1748 multiple mirrors.
1749
1750 Args:
1751 revision: The revision to sync to.
1752 depot: The depot in use at the moment (probably skia).
1753
1754 Returns:
1755 A list of [depot, revision] pairs that need to be synced.
1756 """
1757 revisions_to_sync = [[depot, revision]]
1758
1759 is_base = ((depot == 'chromium') or (depot == 'cros') or
1760 (depot == 'android-chrome'))
1761
1762 # Some SVN depots were split into multiple git depots, so we need to
1763 # figure out for each mirror which git revision to grab. There's no
1764 # guarantee that the SVN revision will exist for each of the dependent
1765 # depots, so we have to grep the git logs and grab the next earlier one.
1766 if (not is_base
1767 and DEPOT_DEPS_NAME[depot]['depends']
1768 and self.source_control.IsGit()):
1769 commit_position = self.source_control.GetCommitPosition(revision)
1770
1771 for d in DEPOT_DEPS_NAME[depot]['depends']:
1772 self.ChangeToDepotWorkingDirectory(d)
1773
1774 dependant_rev = self.source_control.ResolveToRevision(
1775 commit_position, d, DEPOT_DEPS_NAME, -1000)
1776
1777 if dependant_rev:
1778 revisions_to_sync.append([d, dependant_rev])
1779
1780 num_resolved = len(revisions_to_sync)
1781 num_needed = len(DEPOT_DEPS_NAME[depot]['depends'])
1782
1783 self.ChangeToDepotWorkingDirectory(depot)
1784
1785 if not ((num_resolved - 1) == num_needed):
1786 return None
1787
1788 return revisions_to_sync
1789
1790 def PerformPreBuildCleanup(self):
1791 """Performs cleanup between runs."""
1792 print 'Cleaning up between runs.'
1793 print
1794
1795 # Leaving these .pyc files around between runs may disrupt some perf tests.
1796 for (path, _, files) in os.walk(self.src_cwd):
1797 for cur_file in files:
1798 if cur_file.endswith('.pyc'):
1799 path_to_file = os.path.join(path, cur_file)
1800 os.remove(path_to_file)
1801
1802 def PerformCrosChrootCleanup(self):
1803 """Deletes the chroot.
1804
1805 Returns:
1806 True if successful.
1807 """
1808 cwd = os.getcwd()
1809 self.ChangeToDepotWorkingDirectory('cros')
1810 cmd = [bisect_utils.CROS_SDK_PATH, '--delete']
1811 return_code = bisect_utils.RunProcess(cmd)
1812 os.chdir(cwd)
1813 return not return_code
1814
1815 def CreateCrosChroot(self):
1816 """Creates a new chroot.
1817
1818 Returns:
1819 True if successful.
1820 """
1821 cwd = os.getcwd()
1822 self.ChangeToDepotWorkingDirectory('cros')
1823 cmd = [bisect_utils.CROS_SDK_PATH, '--create']
1824 return_code = bisect_utils.RunProcess(cmd)
1825 os.chdir(cwd)
1826 return not return_code
1827
1828 def _PerformPreSyncCleanup(self, depot):
1829 """Performs any necessary cleanup before syncing.
1830
1831 Args:
1832 depot: Depot name.
1833
1834 Returns:
1835 True if successful.
1836 """
1837 if depot == 'chromium' or depot == 'android-chrome':
1838 # Removes third_party/libjingle. At some point, libjingle was causing
1839 # issues syncing when using the git workflow (crbug.com/266324).
1840 os.chdir(self.src_cwd)
1841 if not bisect_utils.RemoveThirdPartyDirectory('libjingle'):
1842 return False
1843 # Removes third_party/skia. At some point, skia was causing
1844 # issues syncing when using the git workflow (crbug.com/377951).
1845 if not bisect_utils.RemoveThirdPartyDirectory('skia'):
1846 return False
1847 elif depot == 'cros':
1848 return self.PerformCrosChrootCleanup()
1849 return True
1850
1851 def _RunPostSync(self, depot):
1852 """Performs any work after syncing.
1853
1854 Args:
1855 depot: Depot name.
1856
1857 Returns:
1858 True if successful.
1859 """
1860 if self.opts.target_platform == 'android':
1861 if not builder.SetupAndroidBuildEnvironment(self.opts,
1862 path_to_src=self.src_cwd):
1863 return False
1864
1865 if depot == 'cros':
1866 return self.CreateCrosChroot()
1867 else:
1868 return self.RunGClientHooks()
1869 return True
1870
1871 def ShouldSkipRevision(self, depot, revision):
1872 """Checks whether a particular revision can be safely skipped.
1873
1874 Some commits can be safely skipped (such as a DEPS roll), since the tool
1875 is git based those changes would have no effect.
1876
1877 Args:
1878 depot: The depot being bisected.
1879 revision: Current revision we're synced to.
1880
1881 Returns:
1882 True if we should skip building/testing this revision.
1883 """
1884 if depot == 'chromium':
1885 if self.source_control.IsGit():
1886 cmd = ['diff-tree', '--no-commit-id', '--name-only', '-r', revision]
1887 output = bisect_utils.CheckRunGit(cmd)
1888
1889 files = output.splitlines()
1890
1891 if len(files) == 1 and files[0] == 'DEPS':
1892 return True
1893
1894 return False
1895
1896 def RunTest(self, revision, depot, command, metric, skippable=False):
1897 """Performs a full sync/build/run of the specified revision.
1898
1899 Args:
1900 revision: The revision to sync to.
1901 depot: The depot that's being used at the moment (src, webkit, etc.)
1902 command: The command to execute the performance test.
1903 metric: The performance metric being tested.
1904
1905 Returns:
1906 On success, a tuple containing the results of the performance test.
1907 Otherwise, a tuple with the error message.
1908 """
1909 # Decide which sync program to use.
1910 sync_client = None
1911 if depot == 'chromium' or depot == 'android-chrome':
1912 sync_client = 'gclient'
1913 elif depot == 'cros':
1914 sync_client = 'repo'
1915
1916 # Decide what depots will need to be synced to what revisions.
1917 revisions_to_sync = self._FindAllRevisionsToSync(revision, depot)
1918 if not revisions_to_sync:
1919 return ('Failed to resolve dependent depots.', BUILD_RESULT_FAIL)
1920
1921 if not self._PerformPreSyncCleanup(depot):
1922 return ('Failed to perform pre-sync cleanup.', BUILD_RESULT_FAIL)
1923
1924 # Do the syncing for all depots.
1925 if not self.opts.debug_ignore_sync:
1926 if not self._SyncAllRevisions(revisions_to_sync, sync_client):
1927 return ('Failed to sync: [%s]' % str(revision), BUILD_RESULT_FAIL)
1928
1929 # Try to do any post-sync steps. This may include "gclient runhooks".
1930 if not self._RunPostSync(depot):
1931 return ('Failed to run [gclient runhooks].', BUILD_RESULT_FAIL)
1932
1933 # Skip this revision if it can be skipped.
1934 if skippable and self.ShouldSkipRevision(depot, revision):
1935 return ('Skipped revision: [%s]' % str(revision),
1936 BUILD_RESULT_SKIPPED)
1937
1938 # Obtain a build for this revision. This may be done by requesting a build
1939 # from another builder, waiting for it and downloading it.
1940 start_build_time = time.time()
1941 build_success = self.BuildCurrentRevision(depot, revision)
1942 if not build_success:
1943 return ('Failed to build revision: [%s]' % str(revision),
1944 BUILD_RESULT_FAIL)
1945 after_build_time = time.time()
1946
1947 # Possibly alter the command.
1948 command = self.GetCompatibleCommand(command, revision, depot)
1949
1950 # Run the command and get the results.
1951 results = self.RunPerformanceTestAndParseResults(command, metric)
1952
1953 # Restore build output directory once the tests are done, to avoid
1954 # any discrepancies.
1955 if self.IsDownloadable(depot) and revision:
1956 self.BackupOrRestoreOutputDirectory(restore=True)
1957
1958 # A value other than 0 indicates that the test couldn't be run, and results
1959 # should also include an error message.
1960 if results[1] != 0:
1961 return results
1962
1963 external_revisions = self._Get3rdPartyRevisions(depot)
1964
1965 if not external_revisions is None:
1966 return (results[0], results[1], external_revisions,
1967 time.time() - after_build_time, after_build_time -
1968 start_build_time)
1969 else:
1970 return ('Failed to parse DEPS file for external revisions.',
1971 BUILD_RESULT_FAIL)
1972
1973 def _SyncAllRevisions(self, revisions_to_sync, sync_client):
1974 """Syncs multiple depots to particular revisions.
1975
1976 Args:
1977 revisions_to_sync: A list of (depot, revision) pairs to be synced.
1978 sync_client: Program used to sync, e.g. "gclient", "repo". Can be None.
1979
1980 Returns:
1981 True if successful, False otherwise.
1982 """
1983 for depot, revision in revisions_to_sync:
1984 self.ChangeToDepotWorkingDirectory(depot)
1985
1986 if sync_client:
1987 self.PerformPreBuildCleanup()
1988
1989 # When using gclient to sync, you need to specify the depot you
1990 # want so that all the dependencies sync properly as well.
1991 # i.e. gclient sync src@<SHA1>
1992 if sync_client == 'gclient':
1993 revision = '%s@%s' % (DEPOT_DEPS_NAME[depot]['src'], revision)
1994
1995 sync_success = self.source_control.SyncToRevision(revision, sync_client)
1996 if not sync_success:
1997 return False
1998
1999 return True
2000
2001 def _CheckIfRunPassed(self, current_value, known_good_value, known_bad_value):
2002 """Given known good and bad values, decide if the current_value passed
2003 or failed.
2004
2005 Args:
2006 current_value: The value of the metric being checked.
2007 known_bad_value: The reference value for a "failed" run.
2008 known_good_value: The reference value for a "passed" run.
2009
2010 Returns:
2011 True if the current_value is closer to the known_good_value than the
2012 known_bad_value.
2013 """
2014 if self.opts.bisect_mode == BISECT_MODE_STD_DEV:
2015 dist_to_good_value = abs(current_value['std_dev'] -
2016 known_good_value['std_dev'])
2017 dist_to_bad_value = abs(current_value['std_dev'] -
2018 known_bad_value['std_dev'])
2019 else:
2020 dist_to_good_value = abs(current_value['mean'] - known_good_value['mean'])
2021 dist_to_bad_value = abs(current_value['mean'] - known_bad_value['mean'])
2022
2023 return dist_to_good_value < dist_to_bad_value
2024
2025 def _GetDepotDirectory(self, depot_name):
2026 if depot_name == 'chromium':
2027 return self.src_cwd
2028 elif depot_name == 'cros':
2029 return self.cros_cwd
2030 elif depot_name in DEPOT_NAMES:
2031 return self.depot_cwd[depot_name]
2032 else:
2033 assert False, ('Unknown depot [ %s ] encountered. Possibly a new one '
2034 'was added without proper support?' % depot_name)
2035
2036 def ChangeToDepotWorkingDirectory(self, depot_name):
2037 """Given a depot, changes to the appropriate working directory.
2038
2039 Args:
2040 depot_name: The name of the depot (see DEPOT_NAMES).
2041 """
2042 os.chdir(self._GetDepotDirectory(depot_name))
2043
2044 def _FillInV8BleedingEdgeInfo(self, min_revision_data, max_revision_data):
2045 r1 = self._GetNearestV8BleedingEdgeFromTrunk(min_revision_data['revision'],
2046 search_forward=True)
2047 r2 = self._GetNearestV8BleedingEdgeFromTrunk(max_revision_data['revision'],
2048 search_forward=False)
2049 min_revision_data['external']['v8_bleeding_edge'] = r1
2050 max_revision_data['external']['v8_bleeding_edge'] = r2
2051
2052 if (not self._GetV8BleedingEdgeFromV8TrunkIfMappable(
2053 min_revision_data['revision'])
2054 or not self._GetV8BleedingEdgeFromV8TrunkIfMappable(
2055 max_revision_data['revision'])):
2056 self.warnings.append(
2057 'Trunk revisions in V8 did not map directly to bleeding_edge. '
2058 'Attempted to expand the range to find V8 rolls which did map '
2059 'directly to bleeding_edge revisions, but results might not be '
2060 'valid.')
2061
2062 def _FindNextDepotToBisect(
2063 self, current_depot, min_revision_data, max_revision_data):
2064 """Decides which depot the script should dive into next (if any).
2065
2066 Args:
2067 current_depot: Current depot being bisected.
2068 min_revision_data: Data about the earliest revision in the bisect range.
2069 max_revision_data: Data about the latest revision in the bisect range.
2070
2071 Returns:
2072 Name of the depot to bisect next, or None.
2073 """
2074 external_depot = None
2075 for next_depot in DEPOT_NAMES:
2076 if DEPOT_DEPS_NAME[next_depot].has_key('platform'):
2077 if DEPOT_DEPS_NAME[next_depot]['platform'] != os.name:
2078 continue
2079
2080 if not (DEPOT_DEPS_NAME[next_depot]['recurse']
2081 and min_revision_data['depot']
2082 in DEPOT_DEPS_NAME[next_depot]['from']):
2083 continue
2084
2085 if current_depot == 'v8':
2086 # We grab the bleeding_edge info here rather than earlier because we
2087 # finally have the revision range. From that we can search forwards and
2088 # backwards to try to match trunk revisions to bleeding_edge.
2089 self._FillInV8BleedingEdgeInfo(min_revision_data, max_revision_data)
2090
2091 if (min_revision_data['external'].get(next_depot) ==
2092 max_revision_data['external'].get(next_depot)):
2093 continue
2094
2095 if (min_revision_data['external'].get(next_depot) and
2096 max_revision_data['external'].get(next_depot)):
2097 external_depot = next_depot
2098 break
2099
2100 return external_depot
2101
2102 def PrepareToBisectOnDepot(
2103 self, current_depot, end_revision, start_revision, previous_revision):
2104 """Changes to the appropriate directory and gathers a list of revisions
2105 to bisect between |start_revision| and |end_revision|.
2106
2107 Args:
2108 current_depot: The depot we want to bisect.
2109 end_revision: End of the revision range.
2110 start_revision: Start of the revision range.
2111 previous_revision: The last revision we synced to on |previous_depot|.
2112
2113 Returns:
2114 A list containing the revisions between |start_revision| and
2115 |end_revision| inclusive.
2116 """
2117 # Change into working directory of external library to run
2118 # subsequent commands.
2119 self.ChangeToDepotWorkingDirectory(current_depot)
2120
2121 # V8 (and possibly others) is merged in periodically. Bisecting
2122 # this directory directly won't give much good info.
2123 if DEPOT_DEPS_NAME[current_depot].has_key('custom_deps'):
2124 config_path = os.path.join(self.src_cwd, '..')
2125 if bisect_utils.RunGClientAndCreateConfig(self.opts,
2126 DEPOT_DEPS_NAME[current_depot]['custom_deps'], cwd=config_path):
2127 return []
2128 if bisect_utils.RunGClient(
2129 ['sync', '--revision', previous_revision], cwd=self.src_cwd):
2130 return []
2131
2132 if current_depot == 'v8_bleeding_edge':
2133 self.ChangeToDepotWorkingDirectory('chromium')
2134
2135 shutil.move('v8', 'v8.bak')
2136 shutil.move('v8_bleeding_edge', 'v8')
2137
2138 self.cleanup_commands.append(['mv', 'v8', 'v8_bleeding_edge'])
2139 self.cleanup_commands.append(['mv', 'v8.bak', 'v8'])
2140
2141 self.depot_cwd['v8_bleeding_edge'] = os.path.join(self.src_cwd, 'v8')
2142 self.depot_cwd['v8'] = os.path.join(self.src_cwd, 'v8.bak')
2143
2144 self.ChangeToDepotWorkingDirectory(current_depot)
2145
2146 depot_revision_list = self.GetRevisionList(current_depot,
2147 end_revision,
2148 start_revision)
2149
2150 self.ChangeToDepotWorkingDirectory('chromium')
2151
2152 return depot_revision_list
2153
2154 def GatherReferenceValues(self, good_rev, bad_rev, cmd, metric, target_depot):
2155 """Gathers reference values by running the performance tests on the
2156 known good and bad revisions.
2157
2158 Args:
2159 good_rev: The last known good revision where the performance regression
2160 has not occurred yet.
2161 bad_rev: A revision where the performance regression has already occurred.
2162 cmd: The command to execute the performance test.
2163 metric: The metric being tested for regression.
2164
2165 Returns:
2166 A tuple with the results of building and running each revision.
2167 """
2168 bad_run_results = self.RunTest(bad_rev, target_depot, cmd, metric)
2169
2170 good_run_results = None
2171
2172 if not bad_run_results[1]:
2173 good_run_results = self.RunTest(good_rev, target_depot, cmd, metric)
2174
2175 return (bad_run_results, good_run_results)
2176
2177 def PrintRevisionsToBisectMessage(self, revision_list, depot):
2178 if self.opts.output_buildbot_annotations:
2179 step_name = 'Bisection Range: [%s - %s]' % (
2180 revision_list[len(revision_list)-1], revision_list[0])
2181 bisect_utils.OutputAnnotationStepStart(step_name)
2182
2183 print
2184 print 'Revisions to bisect on [%s]:' % depot
2185 for revision_id in revision_list:
2186 print ' -> %s' % (revision_id, )
2187 print
2188
2189 if self.opts.output_buildbot_annotations:
2190 bisect_utils.OutputAnnotationStepClosed()
2191
2192 def NudgeRevisionsIfDEPSChange(self, bad_revision, good_revision,
2193 good_svn_revision=None):
2194 """Checks to see if changes to DEPS file occurred, and that the revision
2195 range also includes the change to .DEPS.git. If it doesn't, attempts to
2196 expand the revision range to include it.
2197
2198 Args:
2199 bad_revision: First known bad git revision.
2200 good_revision: Last known good git revision.
2201 good_svn_revision: Last known good svn revision.
2202
2203 Returns:
2204 A tuple with the new bad and good revisions.
2205 """
2206 # DONOT perform nudge because at revision 291563 .DEPS.git was removed
2207 # and source contain only DEPS file for dependency changes.
2208 if good_svn_revision >= 291563:
2209 return (bad_revision, good_revision)
2210
2211 if self.source_control.IsGit() and self.opts.target_platform == 'chromium':
2212 changes_to_deps = self.source_control.QueryFileRevisionHistory(
2213 'DEPS', good_revision, bad_revision)
2214
2215 if changes_to_deps:
2216 # DEPS file was changed, search from the oldest change to DEPS file to
2217 # bad_revision to see if there are matching .DEPS.git changes.
2218 oldest_deps_change = changes_to_deps[-1]
2219 changes_to_gitdeps = self.source_control.QueryFileRevisionHistory(
2220 bisect_utils.FILE_DEPS_GIT, oldest_deps_change, bad_revision)
2221
2222 if len(changes_to_deps) != len(changes_to_gitdeps):
2223 # Grab the timestamp of the last DEPS change
2224 cmd = ['log', '--format=%ct', '-1', changes_to_deps[0]]
2225 output = bisect_utils.CheckRunGit(cmd)
2226 commit_time = int(output)
2227
2228 # Try looking for a commit that touches the .DEPS.git file in the
2229 # next 15 minutes after the DEPS file change.
2230 cmd = ['log', '--format=%H', '-1',
2231 '--before=%d' % (commit_time + 900), '--after=%d' % commit_time,
2232 'origin/master', '--', bisect_utils.FILE_DEPS_GIT]
2233 output = bisect_utils.CheckRunGit(cmd)
2234 output = output.strip()
2235 if output:
2236 self.warnings.append('Detected change to DEPS and modified '
2237 'revision range to include change to .DEPS.git')
2238 return (output, good_revision)
2239 else:
2240 self.warnings.append('Detected change to DEPS but couldn\'t find '
2241 'matching change to .DEPS.git')
2242 return (bad_revision, good_revision)
2243
2244 def CheckIfRevisionsInProperOrder(
2245 self, target_depot, good_revision, bad_revision):
2246 """Checks that |good_revision| is an earlier revision than |bad_revision|.
2247
2248 Args:
2249 good_revision: Number/tag of the known good revision.
2250 bad_revision: Number/tag of the known bad revision.
2251
2252 Returns:
2253 True if the revisions are in the proper order (good earlier than bad).
2254 """
2255 if self.source_control.IsGit() and target_depot != 'cros':
2256 cmd = ['log', '--format=%ct', '-1', good_revision]
2257 cwd = self._GetDepotDirectory(target_depot)
2258
2259 output = bisect_utils.CheckRunGit(cmd, cwd=cwd)
2260 good_commit_time = int(output)
2261
2262 cmd = ['log', '--format=%ct', '-1', bad_revision]
2263 output = bisect_utils.CheckRunGit(cmd, cwd=cwd)
2264 bad_commit_time = int(output)
2265
2266 return good_commit_time <= bad_commit_time
2267 else:
2268 # CrOS and SVN use integers.
2269 return int(good_revision) <= int(bad_revision)
2270
2271 def CanPerformBisect(self, good_revision, bad_revision):
2272 """Checks whether a given revision is bisectable.
2273
2274 Checks for following:
2275 1. Non-bisectable revsions for android bots (refer to crbug.com/385324).
2276 2. Non-bisectable revsions for Windows bots (refer to crbug.com/405274).
2277
2278 Args:
2279 good_revision: Known good revision.
2280 bad_revision: Known bad revision.
2281
2282 Returns:
2283 A dictionary indicating the result. If revision is not bisectable,
2284 this will contain the field "error", otherwise None.
2285 """
2286 if self.opts.target_platform == 'android':
2287 revision_to_check = self.source_control.GetCommitPosition(good_revision)
2288 if (bisect_utils.IsStringInt(good_revision)
2289 and good_revision < 265549):
2290 return {'error': (
2291 'Bisect cannot continue for the given revision range.\n'
2292 'It is impossible to bisect Android regressions '
2293 'prior to r265549, which allows the bisect bot to '
2294 'rely on Telemetry to do apk installation of the most recently '
2295 'built local ChromeShell(refer to crbug.com/385324).\n'
2296 'Please try bisecting revisions greater than or equal to r265549.')}
2297
2298 if bisect_utils.IsWindowsHost():
2299 good_revision = self.source_control.GetCommitPosition(good_revision)
2300 bad_revision = self.source_control.GetCommitPosition(bad_revision)
2301 if (bisect_utils.IsStringInt(good_revision) and
2302 bisect_utils.IsStringInt(bad_revision)):
2303 if (289987 <= good_revision < 290716 or
2304 289987 <= bad_revision < 290716):
2305 return {'error': ('Oops! Revision between r289987 and r290716 are '
2306 'marked as dead zone for Windows due to '
2307 'crbug.com/405274. Please try another range.')}
2308
2309 return None
2310
2311 def Run(self, command_to_run, bad_revision_in, good_revision_in, metric):
2312 """Given known good and bad revisions, run a binary search on all
2313 intermediate revisions to determine the CL where the performance regression
2314 occurred.
2315
2316 Args:
2317 command_to_run: Specify the command to execute the performance test.
2318 good_revision: Number/tag of the known good revision.
2319 bad_revision: Number/tag of the known bad revision.
2320 metric: The performance metric to monitor.
2321
2322 Returns:
2323 A dict with 2 members, 'revision_data' and 'error'. On success,
2324 'revision_data' will contain a dict mapping revision ids to
2325 data about that revision. Each piece of revision data consists of a
2326 dict with the following keys:
2327
2328 'passed': Represents whether the performance test was successful at
2329 that revision. Possible values include: 1 (passed), 0 (failed),
2330 '?' (skipped), 'F' (build failed).
2331 'depot': The depot that this revision is from (i.e. WebKit)
2332 'external': If the revision is a 'src' revision, 'external' contains
2333 the revisions of each of the external libraries.
2334 'sort': A sort value for sorting the dict in order of commits.
2335
2336 For example:
2337 {
2338 'error':None,
2339 'revision_data':
2340 {
2341 'CL #1':
2342 {
2343 'passed': False,
2344 'depot': 'chromium',
2345 'external': None,
2346 'sort': 0
2347 }
2348 }
2349 }
2350
2351 If an error occurred, the 'error' field will contain the message and
2352 'revision_data' will be empty.
2353 """
2354 results = {
2355 'revision_data' : {},
2356 'error' : None,
2357 }
2358
2359 # Choose depot to bisect first
2360 target_depot = 'chromium'
2361 if self.opts.target_platform == 'cros':
2362 target_depot = 'cros'
2363 elif self.opts.target_platform == 'android-chrome':
2364 target_depot = 'android-chrome'
2365
2366 cwd = os.getcwd()
2367 self.ChangeToDepotWorkingDirectory(target_depot)
2368
2369 # If they passed SVN revisions, we can try match them to git SHA1 hashes.
2370 bad_revision = self.source_control.ResolveToRevision(
2371 bad_revision_in, target_depot, DEPOT_DEPS_NAME, 100)
2372 good_revision = self.source_control.ResolveToRevision(
2373 good_revision_in, target_depot, DEPOT_DEPS_NAME, -100)
2374
2375 os.chdir(cwd)
2376 if bad_revision is None:
2377 results['error'] = 'Couldn\'t resolve [%s] to SHA1.' % bad_revision_in
2378 return results
2379
2380 if good_revision is None:
2381 results['error'] = 'Couldn\'t resolve [%s] to SHA1.' % good_revision_in
2382 return results
2383
2384 # Check that they didn't accidentally swap good and bad revisions.
2385 if not self.CheckIfRevisionsInProperOrder(
2386 target_depot, good_revision, bad_revision):
2387 results['error'] = ('bad_revision < good_revision, did you swap these '
2388 'by mistake?')
2389 return results
2390 bad_revision, good_revision = self.NudgeRevisionsIfDEPSChange(
2391 bad_revision, good_revision, good_revision_in)
2392 if self.opts.output_buildbot_annotations:
2393 bisect_utils.OutputAnnotationStepStart('Gathering Revisions')
2394
2395 cannot_bisect = self.CanPerformBisect(good_revision, bad_revision)
2396 if cannot_bisect:
2397 results['error'] = cannot_bisect.get('error')
2398 return results
2399
2400 print 'Gathering revision range for bisection.'
2401 # Retrieve a list of revisions to do bisection on.
2402 src_revision_list = self.GetRevisionList(
2403 target_depot, bad_revision, good_revision)
2404
2405 if self.opts.output_buildbot_annotations:
2406 bisect_utils.OutputAnnotationStepClosed()
2407
2408 if src_revision_list:
2409 # revision_data will store information about a revision such as the
2410 # depot it came from, the webkit/V8 revision at that time,
2411 # performance timing, build state, etc...
2412 revision_data = results['revision_data']
2413
2414 # revision_list is the list we're binary searching through at the moment.
2415 revision_list = []
2416
2417 sort_key_ids = 0
2418
2419 for current_revision_id in src_revision_list:
2420 sort_key_ids += 1
2421
2422 revision_data[current_revision_id] = {
2423 'value' : None,
2424 'passed' : '?',
2425 'depot' : target_depot,
2426 'external' : None,
2427 'perf_time' : 0,
2428 'build_time' : 0,
2429 'sort' : sort_key_ids,
2430 }
2431 revision_list.append(current_revision_id)
2432
2433 min_revision = 0
2434 max_revision = len(revision_list) - 1
2435
2436 self.PrintRevisionsToBisectMessage(revision_list, target_depot)
2437
2438 if self.opts.output_buildbot_annotations:
2439 bisect_utils.OutputAnnotationStepStart('Gathering Reference Values')
2440
2441 print 'Gathering reference values for bisection.'
2442
2443 # Perform the performance tests on the good and bad revisions, to get
2444 # reference values.
2445 bad_results, good_results = self.GatherReferenceValues(good_revision,
2446 bad_revision,
2447 command_to_run,
2448 metric,
2449 target_depot)
2450
2451 if self.opts.output_buildbot_annotations:
2452 bisect_utils.OutputAnnotationStepClosed()
2453
2454 if bad_results[1]:
2455 results['error'] = ('An error occurred while building and running '
2456 'the \'bad\' reference value. The bisect cannot continue without '
2457 'a working \'bad\' revision to start from.\n\nError: %s' %
2458 bad_results[0])
2459 return results
2460
2461 if good_results[1]:
2462 results['error'] = ('An error occurred while building and running '
2463 'the \'good\' reference value. The bisect cannot continue without '
2464 'a working \'good\' revision to start from.\n\nError: %s' %
2465 good_results[0])
2466 return results
2467
2468
2469 # We need these reference values to determine if later runs should be
2470 # classified as pass or fail.
2471 known_bad_value = bad_results[0]
2472 known_good_value = good_results[0]
2473
2474 # Can just mark the good and bad revisions explicitly here since we
2475 # already know the results.
2476 bad_revision_data = revision_data[revision_list[0]]
2477 bad_revision_data['external'] = bad_results[2]
2478 bad_revision_data['perf_time'] = bad_results[3]
2479 bad_revision_data['build_time'] = bad_results[4]
2480 bad_revision_data['passed'] = False
2481 bad_revision_data['value'] = known_bad_value
2482
2483 good_revision_data = revision_data[revision_list[max_revision]]
2484 good_revision_data['external'] = good_results[2]
2485 good_revision_data['perf_time'] = good_results[3]
2486 good_revision_data['build_time'] = good_results[4]
2487 good_revision_data['passed'] = True
2488 good_revision_data['value'] = known_good_value
2489
2490 next_revision_depot = target_depot
2491
2492 while True:
2493 if not revision_list:
2494 break
2495
2496 min_revision_data = revision_data[revision_list[min_revision]]
2497 max_revision_data = revision_data[revision_list[max_revision]]
2498
2499 if max_revision - min_revision <= 1:
2500 current_depot = min_revision_data['depot']
2501 if min_revision_data['passed'] == '?':
2502 next_revision_index = min_revision
2503 elif max_revision_data['passed'] == '?':
2504 next_revision_index = max_revision
2505 elif current_depot in ['android-chrome', 'cros', 'chromium', 'v8']:
2506 previous_revision = revision_list[min_revision]
2507 # If there were changes to any of the external libraries we track,
2508 # should bisect the changes there as well.
2509 external_depot = self._FindNextDepotToBisect(
2510 current_depot, min_revision_data, max_revision_data)
2511 # If there was no change in any of the external depots, the search
2512 # is over.
2513 if not external_depot:
2514 if current_depot == 'v8':
2515 self.warnings.append('Unfortunately, V8 bisection couldn\'t '
2516 'continue any further. The script can only bisect into '
2517 'V8\'s bleeding_edge repository if both the current and '
2518 'previous revisions in trunk map directly to revisions in '
2519 'bleeding_edge.')
2520 break
2521
2522 earliest_revision = max_revision_data['external'][external_depot]
2523 latest_revision = min_revision_data['external'][external_depot]
2524
2525 new_revision_list = self.PrepareToBisectOnDepot(
2526 external_depot, latest_revision, earliest_revision,
2527 previous_revision)
2528
2529 if not new_revision_list:
2530 results['error'] = ('An error occurred attempting to retrieve '
2531 'revision range: [%s..%s]' %
2532 (earliest_revision, latest_revision))
2533 return results
2534
2535 _AddRevisionsIntoRevisionData(
2536 new_revision_list, external_depot, min_revision_data['sort'],
2537 revision_data)
2538
2539 # Reset the bisection and perform it on the newly inserted
2540 # changelists.
2541 revision_list = new_revision_list
2542 min_revision = 0
2543 max_revision = len(revision_list) - 1
2544 sort_key_ids += len(revision_list)
2545
2546 print ('Regression in metric %s appears to be the result of '
2547 'changes in [%s].' % (metric, external_depot))
2548
2549 self.PrintRevisionsToBisectMessage(revision_list, external_depot)
2550
2551 continue
2552 else:
2553 break
2554 else:
2555 next_revision_index = (int((max_revision - min_revision) / 2) +
2556 min_revision)
2557
2558 next_revision_id = revision_list[next_revision_index]
2559 next_revision_data = revision_data[next_revision_id]
2560 next_revision_depot = next_revision_data['depot']
2561
2562 self.ChangeToDepotWorkingDirectory(next_revision_depot)
2563
2564 if self.opts.output_buildbot_annotations:
2565 step_name = 'Working on [%s]' % next_revision_id
2566 bisect_utils.OutputAnnotationStepStart(step_name)
2567
2568 print 'Working on revision: [%s]' % next_revision_id
2569
2570 run_results = self.RunTest(
2571 next_revision_id, next_revision_depot, command_to_run, metric,
2572 skippable=True)
2573
2574 # If the build is successful, check whether or not the metric
2575 # had regressed.
2576 if not run_results[1]:
2577 if len(run_results) > 2:
2578 next_revision_data['external'] = run_results[2]
2579 next_revision_data['perf_time'] = run_results[3]
2580 next_revision_data['build_time'] = run_results[4]
2581
2582 passed_regression = self._CheckIfRunPassed(run_results[0],
2583 known_good_value,
2584 known_bad_value)
2585
2586 next_revision_data['passed'] = passed_regression
2587 next_revision_data['value'] = run_results[0]
2588
2589 if passed_regression:
2590 max_revision = next_revision_index
2591 else:
2592 min_revision = next_revision_index
2593 else:
2594 if run_results[1] == BUILD_RESULT_SKIPPED:
2595 next_revision_data['passed'] = 'Skipped'
2596 elif run_results[1] == BUILD_RESULT_FAIL:
2597 next_revision_data['passed'] = 'Build Failed'
2598
2599 print run_results[0]
2600
2601 # If the build is broken, remove it and redo search.
2602 revision_list.pop(next_revision_index)
2603
2604 max_revision -= 1
2605
2606 if self.opts.output_buildbot_annotations:
2607 self._PrintPartialResults(results)
2608 bisect_utils.OutputAnnotationStepClosed()
2609 else:
2610 # Weren't able to sync and retrieve the revision range.
2611 results['error'] = ('An error occurred attempting to retrieve revision '
2612 'range: [%s..%s]' % (good_revision, bad_revision))
2613
2614 return results
2615
2616 def _PrintPartialResults(self, results_dict):
2617 revision_data = results_dict['revision_data']
2618 revision_data_sorted = sorted(revision_data.iteritems(),
2619 key = lambda x: x[1]['sort'])
2620 results_dict = self._GetResultsDict(revision_data, revision_data_sorted)
2621
2622 self._PrintTestedCommitsTable(revision_data_sorted,
2623 results_dict['first_working_revision'],
2624 results_dict['last_broken_revision'],
2625 100, final_step=False)
2626
2627 def _ConfidenceLevelStatus(self, results_dict):
2628 if not results_dict['confidence']:
2629 return None
2630 confidence_status = 'Successful with %(level)s confidence%(warning)s.'
2631 if results_dict['confidence'] >= HIGH_CONFIDENCE:
2632 level = 'high'
2633 else:
2634 level = 'low'
2635 warning = ' and warnings'
2636 if not self.warnings:
2637 warning = ''
2638 return confidence_status % {'level': level, 'warning': warning}
2639
2640 def _GetViewVCLinkFromDepotAndHash(self, cl, depot):
2641 info = self.source_control.QueryRevisionInfo(cl,
2642 self._GetDepotDirectory(depot))
2643 if depot and DEPOT_DEPS_NAME[depot].has_key('viewvc'):
2644 try:
2645 # Format is "git-svn-id: svn://....@123456 <other data>"
2646 svn_line = [i for i in info['body'].splitlines() if 'git-svn-id:' in i]
2647 svn_revision = svn_line[0].split('@')
2648 svn_revision = svn_revision[1].split(' ')[0]
2649 return DEPOT_DEPS_NAME[depot]['viewvc'] + svn_revision
2650 except IndexError:
2651 return ''
2652 return ''
2653
2654 def _PrintRevisionInfo(self, cl, info, depot=None):
2655 email_info = ''
2656 if not info['email'].startswith(info['author']):
2657 email_info = '\nEmail : %s' % info['email']
2658 commit_link = self._GetViewVCLinkFromDepotAndHash(cl, depot)
2659 if commit_link:
2660 commit_info = '\nLink : %s' % commit_link
2661 else:
2662 commit_info = ('\nFailed to parse SVN revision from body:\n%s' %
2663 info['body'])
2664 print RESULTS_REVISION_INFO % {
2665 'subject': info['subject'],
2666 'author': info['author'],
2667 'email_info': email_info,
2668 'commit_info': commit_info,
2669 'cl': cl,
2670 'cl_date': info['date']
2671 }
2672
2673 def _PrintTestedCommitsHeader(self):
2674 if self.opts.bisect_mode == BISECT_MODE_MEAN:
2675 _PrintTableRow(
2676 [20, 70, 14, 12, 13],
2677 ['Depot', 'Commit SHA', 'Mean', 'Std. Error', 'State'])
2678 elif self.opts.bisect_mode == BISECT_MODE_STD_DEV:
2679 _PrintTableRow(
2680 [20, 70, 14, 12, 13],
2681 ['Depot', 'Commit SHA', 'Std. Error', 'Mean', 'State'])
2682 elif self.opts.bisect_mode == BISECT_MODE_RETURN_CODE:
2683 _PrintTableRow(
2684 [20, 70, 14, 13],
2685 ['Depot', 'Commit SHA', 'Return Code', 'State'])
2686 else:
2687 assert False, 'Invalid bisect_mode specified.'
2688
2689 def _PrintTestedCommitsEntry(self, current_data, cl_link, state_str):
2690 if self.opts.bisect_mode == BISECT_MODE_MEAN:
2691 std_error = '+-%.02f' % current_data['value']['std_err']
2692 mean = '%.02f' % current_data['value']['mean']
2693 _PrintTableRow(
2694 [20, 70, 12, 14, 13],
2695 [current_data['depot'], cl_link, mean, std_error, state_str])
2696 elif self.opts.bisect_mode == BISECT_MODE_STD_DEV:
2697 std_error = '+-%.02f' % current_data['value']['std_err']
2698 mean = '%.02f' % current_data['value']['mean']
2699 _PrintTableRow(
2700 [20, 70, 12, 14, 13],
2701 [current_data['depot'], cl_link, std_error, mean, state_str])
2702 elif self.opts.bisect_mode == BISECT_MODE_RETURN_CODE:
2703 mean = '%d' % current_data['value']['mean']
2704 _PrintTableRow(
2705 [20, 70, 14, 13],
2706 [current_data['depot'], cl_link, mean, state_str])
2707
2708 def _PrintTestedCommitsTable(
2709 self, revision_data_sorted, first_working_revision, last_broken_revision,
2710 confidence, final_step=True):
2711 print
2712 if final_step:
2713 print '===== TESTED COMMITS ====='
2714 else:
2715 print '===== PARTIAL RESULTS ====='
2716 self._PrintTestedCommitsHeader()
2717 state = 0
2718 for current_id, current_data in revision_data_sorted:
2719 if current_data['value']:
2720 if (current_id == last_broken_revision or
2721 current_id == first_working_revision):
2722 # If confidence is too low, don't add this empty line since it's
2723 # used to put focus on a suspected CL.
2724 if confidence and final_step:
2725 print
2726 state += 1
2727 if state == 2 and not final_step:
2728 # Just want a separation between "bad" and "good" cl's.
2729 print
2730
2731 state_str = 'Bad'
2732 if state == 1 and final_step:
2733 state_str = 'Suspected CL'
2734 elif state == 2:
2735 state_str = 'Good'
2736
2737 # If confidence is too low, don't bother outputting good/bad.
2738 if not confidence:
2739 state_str = ''
2740 state_str = state_str.center(13, ' ')
2741
2742 cl_link = self._GetViewVCLinkFromDepotAndHash(current_id,
2743 current_data['depot'])
2744 if not cl_link:
2745 cl_link = current_id
2746 self._PrintTestedCommitsEntry(current_data, cl_link, state_str)
2747
2748 def _PrintReproSteps(self):
2749 """Prints out a section of the results explaining how to run the test.
2750
2751 This message includes the command used to run the test.
2752 """
2753 command = '$ ' + self.opts.command
2754 if bisect_utils.IsTelemetryCommand(self.opts.command):
2755 command += ('\nAlso consider passing --profiler=list to see available '
2756 'profilers.')
2757 print REPRO_STEPS_LOCAL % {'command': command}
2758 if bisect_utils.IsTelemetryCommand(self.opts.command):
2759 telemetry_command = re.sub(r'--browser=[^\s]+',
2760 '--browser=<bot-name>',
2761 command)
2762 print REPRO_STEPS_TRYJOB_TELEMETRY % {'command': telemetry_command}
2763 else:
2764 print REPRO_STEPS_TRYJOB % {'command': command}
2765
2766 def _PrintOtherRegressions(self, other_regressions, revision_data):
2767 """Prints a section of the results about other potential regressions."""
2768 print
2769 print 'Other regressions may have occurred:'
2770 print ' %8s %70s %10s' % ('Depot'.center(8, ' '),
2771 'Range'.center(70, ' '), 'Confidence'.center(10, ' '))
2772 for regression in other_regressions:
2773 current_id, previous_id, confidence = regression
2774 current_data = revision_data[current_id]
2775 previous_data = revision_data[previous_id]
2776
2777 current_link = self._GetViewVCLinkFromDepotAndHash(current_id,
2778 current_data['depot'])
2779 previous_link = self._GetViewVCLinkFromDepotAndHash(previous_id,
2780 previous_data['depot'])
2781
2782 # If we can't map it to a viewable URL, at least show the original hash.
2783 if not current_link:
2784 current_link = current_id
2785 if not previous_link:
2786 previous_link = previous_id
2787
2788 print ' %8s %70s %s' % (
2789 current_data['depot'], current_link,
2790 ('%d%%' % confidence).center(10, ' '))
2791 print ' %8s %70s' % (
2792 previous_data['depot'], previous_link)
2793 print
2794
2795 def _GetResultsDict(self, revision_data, revision_data_sorted):
2796 # Find range where it possibly broke.
2797 first_working_revision = None
2798 first_working_revision_index = -1
2799 last_broken_revision = None
2800 last_broken_revision_index = -1
2801
2802 culprit_revisions = []
2803 other_regressions = []
2804 regression_size = 0.0
2805 regression_std_err = 0.0
2806 confidence = 0.0
2807
2808 for i in xrange(len(revision_data_sorted)):
2809 k, v = revision_data_sorted[i]
2810 if v['passed'] == 1:
2811 if not first_working_revision:
2812 first_working_revision = k
2813 first_working_revision_index = i
2814
2815 if not v['passed']:
2816 last_broken_revision = k
2817 last_broken_revision_index = i
2818
2819 if last_broken_revision != None and first_working_revision != None:
2820 broken_means = []
2821 for i in xrange(0, last_broken_revision_index + 1):
2822 if revision_data_sorted[i][1]['value']:
2823 broken_means.append(revision_data_sorted[i][1]['value']['values'])
2824
2825 working_means = []
2826 for i in xrange(first_working_revision_index, len(revision_data_sorted)):
2827 if revision_data_sorted[i][1]['value']:
2828 working_means.append(revision_data_sorted[i][1]['value']['values'])
2829
2830 # Flatten the lists to calculate mean of all values.
2831 working_mean = sum(working_means, [])
2832 broken_mean = sum(broken_means, [])
2833
2834 # Calculate the approximate size of the regression
2835 mean_of_bad_runs = math_utils.Mean(broken_mean)
2836 mean_of_good_runs = math_utils.Mean(working_mean)
2837
2838 regression_size = 100 * math_utils.RelativeChange(mean_of_good_runs,
2839 mean_of_bad_runs)
2840 if math.isnan(regression_size):
2841 regression_size = 'zero-to-nonzero'
2842
2843 regression_std_err = math.fabs(math_utils.PooledStandardError(
2844 [working_mean, broken_mean]) /
2845 max(0.0001, min(mean_of_good_runs, mean_of_bad_runs))) * 100.0
2846
2847 # Give a "confidence" in the bisect. At the moment we use how distinct the
2848 # values are before and after the last broken revision, and how noisy the
2849 # overall graph is.
2850 confidence = ConfidenceScore(working_means, broken_means)
2851
2852 culprit_revisions = []
2853
2854 cwd = os.getcwd()
2855 self.ChangeToDepotWorkingDirectory(
2856 revision_data[last_broken_revision]['depot'])
2857
2858 if revision_data[last_broken_revision]['depot'] == 'cros':
2859 # Want to get a list of all the commits and what depots they belong
2860 # to so that we can grab info about each.
2861 cmd = ['repo', 'forall', '-c',
2862 'pwd ; git log --pretty=oneline --before=%d --after=%d' % (
2863 last_broken_revision, first_working_revision + 1)]
2864 output, return_code = bisect_utils.RunProcessAndRetrieveOutput(cmd)
2865
2866 changes = []
2867 assert not return_code, ('An error occurred while running '
2868 '"%s"' % ' '.join(cmd))
2869 last_depot = None
2870 cwd = os.getcwd()
2871 for l in output.split('\n'):
2872 if l:
2873 # Output will be in form:
2874 # /path_to_depot
2875 # /path_to_other_depot
2876 # <SHA1>
2877 # /path_again
2878 # <SHA1>
2879 # etc.
2880 if l[0] == '/':
2881 last_depot = l
2882 else:
2883 contents = l.split(' ')
2884 if len(contents) > 1:
2885 changes.append([last_depot, contents[0]])
2886 for c in changes:
2887 os.chdir(c[0])
2888 info = self.source_control.QueryRevisionInfo(c[1])
2889 culprit_revisions.append((c[1], info, None))
2890 else:
2891 for i in xrange(last_broken_revision_index, len(revision_data_sorted)):
2892 k, v = revision_data_sorted[i]
2893 if k == first_working_revision:
2894 break
2895 self.ChangeToDepotWorkingDirectory(v['depot'])
2896 info = self.source_control.QueryRevisionInfo(k)
2897 culprit_revisions.append((k, info, v['depot']))
2898 os.chdir(cwd)
2899
2900 # Check for any other possible regression ranges.
2901 other_regressions = _FindOtherRegressions(
2902 revision_data_sorted, mean_of_bad_runs > mean_of_good_runs)
2903
2904 return {
2905 'first_working_revision': first_working_revision,
2906 'last_broken_revision': last_broken_revision,
2907 'culprit_revisions': culprit_revisions,
2908 'other_regressions': other_regressions,
2909 'regression_size': regression_size,
2910 'regression_std_err': regression_std_err,
2911 'confidence': confidence,
2912 }
2913
2914 def _CheckForWarnings(self, results_dict):
2915 if len(results_dict['culprit_revisions']) > 1:
2916 self.warnings.append('Due to build errors, regression range could '
2917 'not be narrowed down to a single commit.')
2918 if self.opts.repeat_test_count == 1:
2919 self.warnings.append('Tests were only set to run once. This may '
2920 'be insufficient to get meaningful results.')
2921 if 0 < results_dict['confidence'] < HIGH_CONFIDENCE:
2922 self.warnings.append('Confidence is not high. Try bisecting again '
2923 'with increased repeat_count, larger range, or '
2924 'on another metric.')
2925 if not results_dict['confidence']:
2926 self.warnings.append('Confidence score is 0%. Try bisecting again on '
2927 'another platform or another metric.')
2928
2929 def FormatAndPrintResults(self, bisect_results):
2930 """Prints the results from a bisection run in a readable format.
2931
2932 Args:
2933 bisect_results: The results from a bisection test run.
2934 """
2935 revision_data = bisect_results['revision_data']
2936 revision_data_sorted = sorted(revision_data.iteritems(),
2937 key = lambda x: x[1]['sort'])
2938 results_dict = self._GetResultsDict(revision_data, revision_data_sorted)
2939
2940 self._CheckForWarnings(results_dict)
2941
2942 if self.opts.output_buildbot_annotations:
2943 bisect_utils.OutputAnnotationStepStart('Build Status Per Revision')
2944
2945 print
2946 print 'Full results of bisection:'
2947 for current_id, current_data in revision_data_sorted:
2948 build_status = current_data['passed']
2949
2950 if type(build_status) is bool:
2951 if build_status:
2952 build_status = 'Good'
2953 else:
2954 build_status = 'Bad'
2955
2956 print ' %20s %40s %s' % (current_data['depot'],
2957 current_id, build_status)
2958 print
2959
2960 if self.opts.output_buildbot_annotations:
2961 bisect_utils.OutputAnnotationStepClosed()
2962 # The perf dashboard scrapes the "results" step in order to comment on
2963 # bugs. If you change this, please update the perf dashboard as well.
2964 bisect_utils.OutputAnnotationStepStart('Results')
2965
2966 self._PrintBanner(results_dict)
2967 self._PrintWarnings()
2968
2969 if results_dict['culprit_revisions'] and results_dict['confidence']:
2970 for culprit in results_dict['culprit_revisions']:
2971 cl, info, depot = culprit
2972 self._PrintRevisionInfo(cl, info, depot)
2973 if results_dict['other_regressions']:
2974 self._PrintOtherRegressions(results_dict['other_regressions'],
2975 revision_data)
2976 self._PrintTestedCommitsTable(revision_data_sorted,
2977 results_dict['first_working_revision'],
2978 results_dict['last_broken_revision'],
2979 results_dict['confidence'])
2980 _PrintStepTime(revision_data_sorted)
2981 self._PrintReproSteps()
2982 _PrintThankYou()
2983 if self.opts.output_buildbot_annotations:
2984 bisect_utils.OutputAnnotationStepClosed()
2985
2986 def _PrintBanner(self, results_dict):
2987 if self._IsBisectModeReturnCode():
2988 metrics = 'N/A'
2989 change = 'Yes'
2990 else:
2991 metrics = '/'.join(self.opts.metric)
2992 change = '%.02f%% (+/-%.02f%%)' % (
2993 results_dict['regression_size'], results_dict['regression_std_err'])
2994
2995 if results_dict['culprit_revisions'] and results_dict['confidence']:
2996 status = self._ConfidenceLevelStatus(results_dict)
2997 else:
2998 status = 'Failure, could not reproduce.'
2999 change = 'Bisect could not reproduce a change.'
3000
3001 print RESULTS_BANNER % {
3002 'status': status,
3003 'command': self.opts.command,
3004 'metrics': metrics,
3005 'change': change,
3006 'confidence': results_dict['confidence'],
3007 }
3008
3009 def _PrintWarnings(self):
3010 """Prints a list of warning strings if there are any."""
3011 if not self.warnings:
3012 return
3013 print
3014 print 'WARNINGS:'
3015 for w in set(self.warnings):
3016 print ' ! %s' % w
3017
3018
3019 def _IsPlatformSupported():
3020 """Checks that this platform and build system are supported.
3021
3022 Args:
3023 opts: The options parsed from the command line.
3024
3025 Returns:
3026 True if the platform and build system are supported.
3027 """
3028 # Haven't tested the script out on any other platforms yet.
3029 supported = ['posix', 'nt']
3030 return os.name in supported
3031
3032
3033 def RmTreeAndMkDir(path_to_dir, skip_makedir=False):
3034 """Removes the directory tree specified, and then creates an empty
3035 directory in the same location (if not specified to skip).
3036
3037 Args:
3038 path_to_dir: Path to the directory tree.
3039 skip_makedir: Whether to skip creating empty directory, default is False.
3040
3041 Returns:
3042 True if successful, False if an error occurred.
3043 """
3044 try:
3045 if os.path.exists(path_to_dir):
3046 shutil.rmtree(path_to_dir)
3047 except OSError, e:
3048 if e.errno != errno.ENOENT:
3049 return False
3050
3051 if not skip_makedir:
3052 return MaybeMakeDirectory(path_to_dir)
3053
3054 return True
3055
3056
3057 def RemoveBuildFiles(build_type):
3058 """Removes build files from previous runs."""
3059 if RmTreeAndMkDir(os.path.join('out', build_type)):
3060 if RmTreeAndMkDir(os.path.join('build', build_type)):
3061 return True
3062 return False
3063
3064
3065 class BisectOptions(object):
3066 """Options to be used when running bisection."""
3067 def __init__(self):
3068 super(BisectOptions, self).__init__()
3069
3070 self.target_platform = 'chromium'
3071 self.build_preference = None
3072 self.good_revision = None
3073 self.bad_revision = None
3074 self.use_goma = None
3075 self.goma_dir = None
3076 self.cros_board = None
3077 self.cros_remote_ip = None
3078 self.repeat_test_count = 20
3079 self.truncate_percent = 25
3080 self.max_time_minutes = 20
3081 self.metric = None
3082 self.command = None
3083 self.output_buildbot_annotations = None
3084 self.no_custom_deps = False
3085 self.working_directory = None
3086 self.extra_src = None
3087 self.debug_ignore_build = None
3088 self.debug_ignore_sync = None
3089 self.debug_ignore_perf_test = None
3090 self.gs_bucket = None
3091 self.target_arch = 'ia32'
3092 self.target_build_type = 'Release'
3093 self.builder_host = None
3094 self.builder_port = None
3095 self.bisect_mode = BISECT_MODE_MEAN
3096
3097 @staticmethod
3098 def _CreateCommandLineParser():
3099 """Creates a parser with bisect options.
3100
3101 Returns:
3102 An instance of optparse.OptionParser.
3103 """
3104 usage = ('%prog [options] [-- chromium-options]\n'
3105 'Perform binary search on revision history to find a minimal '
3106 'range of revisions where a performance metric regressed.\n')
3107
3108 parser = optparse.OptionParser(usage=usage)
3109
3110 group = optparse.OptionGroup(parser, 'Bisect options')
3111 group.add_option('-c', '--command',
3112 type='str',
3113 help='A command to execute your performance test at' +
3114 ' each point in the bisection.')
3115 group.add_option('-b', '--bad_revision',
3116 type='str',
3117 help='A bad revision to start bisection. ' +
3118 'Must be later than good revision. May be either a git' +
3119 ' or svn revision.')
3120 group.add_option('-g', '--good_revision',
3121 type='str',
3122 help='A revision to start bisection where performance' +
3123 ' test is known to pass. Must be earlier than the ' +
3124 'bad revision. May be either a git or svn revision.')
3125 group.add_option('-m', '--metric',
3126 type='str',
3127 help='The desired metric to bisect on. For example ' +
3128 '"vm_rss_final_b/vm_rss_f_b"')
3129 group.add_option('-r', '--repeat_test_count',
3130 type='int',
3131 default=20,
3132 help='The number of times to repeat the performance '
3133 'test. Values will be clamped to range [1, 100]. '
3134 'Default value is 20.')
3135 group.add_option('--max_time_minutes',
3136 type='int',
3137 default=20,
3138 help='The maximum time (in minutes) to take running the '
3139 'performance tests. The script will run the performance '
3140 'tests according to --repeat_test_count, so long as it '
3141 'doesn\'t exceed --max_time_minutes. Values will be '
3142 'clamped to range [1, 60].'
3143 'Default value is 20.')
3144 group.add_option('-t', '--truncate_percent',
3145 type='int',
3146 default=25,
3147 help='The highest/lowest % are discarded to form a '
3148 'truncated mean. Values will be clamped to range [0, '
3149 '25]. Default value is 25 (highest/lowest 25% will be '
3150 'discarded).')
3151 group.add_option('--bisect_mode',
3152 type='choice',
3153 choices=[BISECT_MODE_MEAN, BISECT_MODE_STD_DEV,
3154 BISECT_MODE_RETURN_CODE],
3155 default=BISECT_MODE_MEAN,
3156 help='The bisect mode. Choices are to bisect on the '
3157 'difference in mean, std_dev, or return_code.')
3158 parser.add_option_group(group)
3159
3160 group = optparse.OptionGroup(parser, 'Build options')
3161 group.add_option('-w', '--working_directory',
3162 type='str',
3163 help='Path to the working directory where the script '
3164 'will do an initial checkout of the chromium depot. The '
3165 'files will be placed in a subdirectory "bisect" under '
3166 'working_directory and that will be used to perform the '
3167 'bisection. This parameter is optional, if it is not '
3168 'supplied, the script will work from the current depot.')
3169 group.add_option('--build_preference',
3170 type='choice',
3171 choices=['msvs', 'ninja', 'make'],
3172 help='The preferred build system to use. On linux/mac '
3173 'the options are make/ninja. On Windows, the options '
3174 'are msvs/ninja.')
3175 group.add_option('--target_platform',
3176 type='choice',
3177 choices=['chromium', 'cros', 'android', 'android-chrome'],
3178 default='chromium',
3179 help='The target platform. Choices are "chromium" '
3180 '(current platform), "cros", or "android". If you '
3181 'specify something other than "chromium", you must be '
3182 'properly set up to build that platform.')
3183 group.add_option('--no_custom_deps',
3184 dest='no_custom_deps',
3185 action='store_true',
3186 default=False,
3187 help='Run the script with custom_deps or not.')
3188 group.add_option('--extra_src',
3189 type='str',
3190 help='Path to a script which can be used to modify '
3191 'the bisect script\'s behavior.')
3192 group.add_option('--cros_board',
3193 type='str',
3194 help='The cros board type to build.')
3195 group.add_option('--cros_remote_ip',
3196 type='str',
3197 help='The remote machine to image to.')
3198 group.add_option('--use_goma',
3199 action='store_true',
3200 help='Add a bunch of extra threads for goma, and enable '
3201 'goma')
3202 group.add_option('--goma_dir',
3203 help='Path to goma tools (or system default if not '
3204 'specified).')
3205 group.add_option('--output_buildbot_annotations',
3206 action='store_true',
3207 help='Add extra annotation output for buildbot.')
3208 group.add_option('--gs_bucket',
3209 default='',
3210 dest='gs_bucket',
3211 type='str',
3212 help=('Name of Google Storage bucket to upload or '
3213 'download build. e.g., chrome-perf'))
3214 group.add_option('--target_arch',
3215 type='choice',
3216 choices=['ia32', 'x64', 'arm'],
3217 default='ia32',
3218 dest='target_arch',
3219 help=('The target build architecture. Choices are "ia32" '
3220 '(default), "x64" or "arm".'))
3221 group.add_option('--target_build_type',
3222 type='choice',
3223 choices=['Release', 'Debug'],
3224 default='Release',
3225 help='The target build type. Choices are "Release" '
3226 '(default), or "Debug".')
3227 group.add_option('--builder_host',
3228 dest='builder_host',
3229 type='str',
3230 help=('Host address of server to produce build by posting'
3231 ' try job request.'))
3232 group.add_option('--builder_port',
3233 dest='builder_port',
3234 type='int',
3235 help=('HTTP port of the server to produce build by posting'
3236 ' try job request.'))
3237 parser.add_option_group(group)
3238
3239 group = optparse.OptionGroup(parser, 'Debug options')
3240 group.add_option('--debug_ignore_build',
3241 action='store_true',
3242 help='DEBUG: Don\'t perform builds.')
3243 group.add_option('--debug_ignore_sync',
3244 action='store_true',
3245 help='DEBUG: Don\'t perform syncs.')
3246 group.add_option('--debug_ignore_perf_test',
3247 action='store_true',
3248 help='DEBUG: Don\'t perform performance tests.')
3249 parser.add_option_group(group)
3250 return parser
3251
3252 def ParseCommandLine(self):
3253 """Parses the command line for bisect options."""
3254 parser = self._CreateCommandLineParser()
3255 opts, _ = parser.parse_args()
3256
3257 try:
3258 if not opts.command:
3259 raise RuntimeError('missing required parameter: --command')
3260
3261 if not opts.good_revision:
3262 raise RuntimeError('missing required parameter: --good_revision')
3263
3264 if not opts.bad_revision:
3265 raise RuntimeError('missing required parameter: --bad_revision')
3266
3267 if not opts.metric and opts.bisect_mode != BISECT_MODE_RETURN_CODE:
3268 raise RuntimeError('missing required parameter: --metric')
3269
3270 if opts.gs_bucket:
3271 if not cloud_storage.List(opts.gs_bucket):
3272 raise RuntimeError('Invalid Google Storage: gs://%s' % opts.gs_bucket)
3273 if not opts.builder_host:
3274 raise RuntimeError('Must specify try server host name using '
3275 '--builder_host when gs_bucket is used.')
3276 if not opts.builder_port:
3277 raise RuntimeError('Must specify try server port number using '
3278 '--builder_port when gs_bucket is used.')
3279 if opts.target_platform == 'cros':
3280 # Run sudo up front to make sure credentials are cached for later.
3281 print 'Sudo is required to build cros:'
3282 print
3283 bisect_utils.RunProcess(['sudo', 'true'])
3284
3285 if not opts.cros_board:
3286 raise RuntimeError('missing required parameter: --cros_board')
3287
3288 if not opts.cros_remote_ip:
3289 raise RuntimeError('missing required parameter: --cros_remote_ip')
3290
3291 if not opts.working_directory:
3292 raise RuntimeError('missing required parameter: --working_directory')
3293
3294 metric_values = opts.metric.split('/')
3295 if (len(metric_values) != 2 and
3296 opts.bisect_mode != BISECT_MODE_RETURN_CODE):
3297 raise RuntimeError('Invalid metric specified: [%s]' % opts.metric)
3298
3299 opts.metric = metric_values
3300 opts.repeat_test_count = min(max(opts.repeat_test_count, 1), 100)
3301 opts.max_time_minutes = min(max(opts.max_time_minutes, 1), 60)
3302 opts.truncate_percent = min(max(opts.truncate_percent, 0), 25)
3303 opts.truncate_percent = opts.truncate_percent / 100.0
3304
3305 for k, v in opts.__dict__.iteritems():
3306 assert hasattr(self, k), 'Invalid %s attribute in BisectOptions.' % k
3307 setattr(self, k, v)
3308 except RuntimeError, e:
3309 output_string = StringIO.StringIO()
3310 parser.print_help(file=output_string)
3311 error_message = '%s\n\n%s' % (e.message, output_string.getvalue())
3312 output_string.close()
3313 raise RuntimeError(error_message)
3314
3315 @staticmethod
3316 def FromDict(values):
3317 """Creates an instance of BisectOptions from a dictionary.
3318
3319 Args:
3320 values: a dict containing options to set.
3321
3322 Returns:
3323 An instance of BisectOptions.
3324 """
3325 opts = BisectOptions()
3326 for k, v in values.iteritems():
3327 assert hasattr(opts, k), 'Invalid %s attribute in BisectOptions.' % k
3328 setattr(opts, k, v)
3329
3330 if opts.metric:
3331 metric_values = opts.metric.split('/')
3332 if len(metric_values) != 2:
3333 raise RuntimeError('Invalid metric specified: [%s]' % opts.metric)
3334 opts.metric = metric_values
3335
3336 opts.repeat_test_count = min(max(opts.repeat_test_count, 1), 100)
3337 opts.max_time_minutes = min(max(opts.max_time_minutes, 1), 60)
3338 opts.truncate_percent = min(max(opts.truncate_percent, 0), 25)
3339 opts.truncate_percent = opts.truncate_percent / 100.0
3340
3341 return opts
3342
3343
3344 def main():
3345
3346 try:
3347 opts = BisectOptions()
3348 opts.ParseCommandLine()
3349
3350 if opts.extra_src:
3351 extra_src = bisect_utils.LoadExtraSrc(opts.extra_src)
3352 if not extra_src:
3353 raise RuntimeError('Invalid or missing --extra_src.')
3354 _AddAdditionalDepotInfo(extra_src.GetAdditionalDepotInfo())
3355
3356 if opts.working_directory:
3357 custom_deps = bisect_utils.DEFAULT_GCLIENT_CUSTOM_DEPS
3358 if opts.no_custom_deps:
3359 custom_deps = None
3360 bisect_utils.CreateBisectDirectoryAndSetupDepot(opts, custom_deps)
3361
3362 os.chdir(os.path.join(os.getcwd(), 'src'))
3363
3364 if not RemoveBuildFiles(opts.target_build_type):
3365 raise RuntimeError('Something went wrong removing the build files.')
3366
3367 if not _IsPlatformSupported():
3368 raise RuntimeError('Sorry, this platform isn\'t supported yet.')
3369
3370 # Check what source control method is being used, and create a
3371 # SourceControl object if possible.
3372 source_control = source_control_module.DetermineAndCreateSourceControl(opts)
3373
3374 if not source_control:
3375 raise RuntimeError(
3376 'Sorry, only the git workflow is supported at the moment.')
3377
3378 # gClient sync seems to fail if you're not in master branch.
3379 if (not source_control.IsInProperBranch() and
3380 not opts.debug_ignore_sync and
3381 not opts.working_directory):
3382 raise RuntimeError('You must switch to master branch to run bisection.')
3383 bisect_test = BisectPerformanceMetrics(source_control, opts)
3384 try:
3385 bisect_results = bisect_test.Run(opts.command,
3386 opts.bad_revision,
3387 opts.good_revision,
3388 opts.metric)
3389 if bisect_results['error']:
3390 raise RuntimeError(bisect_results['error'])
3391 bisect_test.FormatAndPrintResults(bisect_results)
3392 return 0
3393 finally:
3394 bisect_test.PerformCleanup()
3395 except RuntimeError, e:
3396 if opts.output_buildbot_annotations:
3397 # The perf dashboard scrapes the "results" step in order to comment on
3398 # bugs. If you change this, please update the perf dashboard as well.
3399 bisect_utils.OutputAnnotationStepStart('Results')
3400 print 'Error: %s' % e.message
3401 if opts.output_buildbot_annotations:
3402 bisect_utils.OutputAnnotationStepClosed()
3403 return 1
3404
3405
3406 if __name__ == '__main__':
3407 sys.exit(main())
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698