OLD | NEW |
---|---|
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # Copyright (c) 2013 The Chromium Authors. All rights reserved. | 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 | 3 # Use of this source code is governed by a BSD-style license that can be |
4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
5 | 5 |
6 """Performance Test Bisect Tool | 6 """Performance Test Bisect Tool |
7 | 7 |
8 This script bisects a series of changelists using binary search. It starts at | 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 | 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 | 10 known-good revision. It will then binary search across this revision range by |
(...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
45 import re | 45 import re |
46 import shlex | 46 import shlex |
47 import shutil | 47 import shutil |
48 import StringIO | 48 import StringIO |
49 import subprocess | 49 import subprocess |
50 import sys | 50 import sys |
51 import time | 51 import time |
52 import zipfile | 52 import zipfile |
53 | 53 |
54 import bisect_utils | 54 import bisect_utils |
55 import post_perf_builder_job | |
56 | |
55 | 57 |
56 try: | 58 try: |
57 from telemetry.page import cloud_storage | 59 from telemetry.page import cloud_storage |
58 except ImportError: | 60 except ImportError: |
59 sys.path.append(os.path.join(os.path.dirname(sys.argv[0]), 'telemetry')) | 61 sys.path.append(os.path.join(os.path.dirname(sys.argv[0]), 'telemetry')) |
60 from telemetry.page import cloud_storage | 62 from telemetry.page import cloud_storage |
61 | 63 |
62 # The additional repositories that might need to be bisected. | 64 # The additional repositories that might need to be bisected. |
63 # If the repository has any dependant repositories (such as skia/src needs | 65 # If the repository has any dependant repositories (such as skia/src needs |
64 # skia/include and skia/gyp to be updated), specify them in the 'depends' | 66 # skia/include and skia/gyp to be updated), specify them in the 'depends' |
(...skipping 1241 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
1306 source_dir = os.path.join(build_dir, build_type) | 1308 source_dir = os.path.join(build_dir, build_type) |
1307 destination_dir = os.path.join(build_dir, '%s.bak' % build_type) | 1309 destination_dir = os.path.join(build_dir, '%s.bak' % build_type) |
1308 if restore: | 1310 if restore: |
1309 source_dir, destination_dir = destination_dir, source_dir | 1311 source_dir, destination_dir = destination_dir, source_dir |
1310 if os.path.exists(source_dir): | 1312 if os.path.exists(source_dir): |
1311 RmTreeAndMkDir(destination_dir, skip_makedir=True) | 1313 RmTreeAndMkDir(destination_dir, skip_makedir=True) |
1312 shutil.move(source_dir, destination_dir) | 1314 shutil.move(source_dir, destination_dir) |
1313 return destination_dir | 1315 return destination_dir |
1314 return None | 1316 return None |
1315 | 1317 |
1316 def DownloadCurrentBuild(self, sha_revision, build_type='Release'): | 1318 def DownloadCurrentBuild(self, revision, build_type='Release'): |
1317 """Download the build archive for the given revision. | 1319 """Download the build archive for the given revision. |
1318 | 1320 |
1319 Args: | 1321 Args: |
1320 sha_revision: The git SHA1 for the revision. | 1322 revision: The SVN revision to build. |
1321 build_type: Target build type ('Release', 'Debug', 'Release_x64' etc.) | 1323 build_type: Target build type ('Release', 'Debug', 'Release_x64' etc.) |
1322 | 1324 |
1323 Returns: | 1325 Returns: |
1324 True if download succeeds, otherwise False. | 1326 True if download succeeds, otherwise False. |
1325 """ | 1327 """ |
1326 # Get SVN revision for the given SHA, since builds are archived using SVN | |
1327 # revision. | |
1328 revision = self.source_control.SVNFindRev(sha_revision) | |
1329 if not revision: | |
1330 raise RuntimeError( | |
1331 'Failed to determine SVN revision for %s' % sha_revision) | |
1332 | |
1333 abs_build_dir = os.path.abspath( | 1328 abs_build_dir = os.path.abspath( |
1334 self.builder.GetBuildOutputDirectory(self.opts, self.src_cwd)) | 1329 self.builder.GetBuildOutputDirectory(self.opts, self.src_cwd)) |
1335 target_build_output_dir = os.path.join(abs_build_dir, build_type) | 1330 target_build_output_dir = os.path.join(abs_build_dir, build_type) |
1336 # Get build target architecture. | 1331 # Get build target architecture. |
1337 build_arch = self.opts.target_arch | 1332 build_arch = self.opts.target_arch |
1338 # File path of the downloaded archive file. | 1333 # File path of the downloaded archive file. |
1339 archive_file_dest = os.path.join(abs_build_dir, | 1334 archive_file_dest = os.path.join(abs_build_dir, |
1340 GetZipFileName(revision, build_arch)) | 1335 GetZipFileName(revision, build_arch)) |
1341 if FetchFromCloudStorage(self.opts.gs_bucket, | 1336 remote_build = GetRemoteBuildPath(revision, build_arch) |
1342 GetRemoteBuildPath(revision, build_arch), | 1337 fetch_build_func = lambda: FetchFromCloudStorage(self.opts.gs_bucket, |
1343 abs_build_dir): | 1338 remote_build, |
1344 # Generic name for the archive, created when archive file is extracted. | 1339 abs_build_dir) |
1345 output_dir = os.path.join(abs_build_dir, | 1340 if not fetch_build_func(): |
1346 GetZipFileName(target_arch=build_arch)) | 1341 if not self.PostBuildRequestAndWait(revision, condition=fetch_build_func): |
1347 # Unzip build archive directory. | 1342 raise RuntimeError('Somewthing went wrong while processing build' |
1348 try: | 1343 'request for: %s' % revision) |
1344 | |
1345 # Generic name for the archive, created when archive file is extracted. | |
1346 output_dir = os.path.join(abs_build_dir, | |
1347 GetZipFileName(target_arch=build_arch)) | |
1348 # Unzip build archive directory. | |
1349 try: | |
1350 RmTreeAndMkDir(output_dir, skip_makedir=True) | |
1351 ExtractZip(archive_file_dest, abs_build_dir) | |
1352 if os.path.exists(output_dir): | |
1353 self.BackupOrRestoreOutputdirectory(restore=False) | |
1354 print 'Moving build from %s to %s' % ( | |
1355 output_dir, target_build_output_dir) | |
1356 shutil.move(output_dir, target_build_output_dir) | |
1357 return True | |
1358 raise IOError('Missing extracted folder %s ' % output_dir) | |
1359 except e: | |
1360 print 'Somewthing went wrong while extracting archive file: %s' % e | |
1361 self.BackupOrRestoreOutputdirectory(restore=True) | |
1362 # Cleanup any leftovers from unzipping. | |
1363 if os.path.exists(output_dir): | |
1349 RmTreeAndMkDir(output_dir, skip_makedir=True) | 1364 RmTreeAndMkDir(output_dir, skip_makedir=True) |
1350 ExtractZip(archive_file_dest, abs_build_dir) | 1365 finally: |
1351 if os.path.exists(output_dir): | 1366 # Delete downloaded archive |
1352 self.BackupOrRestoreOutputdirectory(restore=False) | 1367 if os.path.exists(archive_file_dest): |
1353 print 'Moving build from %s to %s' % ( | 1368 os.remove(archive_file_dest) |
1354 output_dir, target_build_output_dir) | 1369 return False |
1355 shutil.move(output_dir, target_build_output_dir) | 1370 |
1356 return True | 1371 |
qyearsley
2014/03/06 23:40:45
Style: Both DownloadCurrentBuild and PostBuildRequ
prasadv
2014/03/07 01:07:44
Done.
| |
1357 raise IOError('Missing extracted folder %s ' % output_dir) | 1372 def PostBuildRequestAndWait(self, revision, condition, patch=None): |
1358 except e: | 1373 """POSTs the build request job to the tryserver instance.""" |
1359 print 'Somewthing went wrong while extracting archive file: %s' % e | 1374 |
1360 self.BackupOrRestoreOutputdirectory(restore=True) | 1375 def GetBuilderNameAndBuildTime(target_arch='ia32'): |
1361 # Cleanup any leftovers from unzipping. | 1376 """Gets builder name and buildtime in seconds based on platform.""" |
1362 if os.path.exists(output_dir): | 1377 if IsWindows(): |
1363 RmTreeAndMkDir(output_dir, skip_makedir=True) | 1378 if Is64BitWindows() and target_arch == 'x64': |
1364 finally: | 1379 return ('Win x64 Bisect Builder', 3600) # 60 mins |
qyearsley
2014/03/06 23:40:45
Style nit: The style guide suggests spacing inline
prasadv
2014/03/07 01:07:44
Done.
| |
1365 # Delete downloaded archive | 1380 return ('Win Bisect Builder', 3600) |
1366 if os.path.exists(archive_file_dest): | 1381 if IsLinux(): |
1367 os.remove(archive_file_dest) | 1382 return ('Linux Bisect Builder', 1800) # 30 mins |
1383 if IsMac(): | |
1384 return ('Mac Bisect Builder', 2700) # 45 mins | |
1385 raise NotImplementedError('Unsupported Platform "%s".' % sys.platform) | |
1386 if not condition: | |
1387 return False | |
1388 | |
1389 bot_name, build_timeout = GetBuilderNameAndBuildTime(self.opts.target_arch) | |
1390 | |
1391 # Creates a try job description. | |
1392 job_args = {'host': self.opts.host, | |
1393 'port': self.opts.port, | |
1394 'revision': revision, | |
1395 'bot': bot_name, | |
1396 'name': 'Bisect Job-%s' % revision | |
1397 } | |
1398 # Update patch information if supplied. | |
1399 if patch: | |
1400 job_args['patch'] = patch | |
1401 #Posts job to build the revision on the server. | |
qyearsley
2014/03/06 23:40:45
Style: Should add space after #.
prasadv
2014/03/07 01:07:44
Done.
| |
1402 if post_perf_builder_job.PostTryJob(job_args): | |
1403 poll_interval = 60 | |
qyearsley
2014/03/06 23:40:45
Style: Unnecessary spaces.
prasadv
2014/03/07 01:07:44
Done.
| |
1404 start_time = time.time() | |
1405 while True: | |
1406 res = condition() | |
1407 if res: | |
1408 return res | |
1409 elapsed_time = time.time() - start_time | |
1410 if elapsed_time > build_timeout: | |
1411 raise RuntimeError('Timed out while waiting %ds for %s build.' % | |
1412 (build_timeout, revision)) | |
1413 print ('Time elapsed: %ss, still waiting for %s build' % | |
1414 (elapsed_time, revision)) | |
1415 time.sleep(poll_interval) | |
1368 return False | 1416 return False |
1369 | 1417 |
1370 def BuildCurrentRevision(self, depot, revision=None): | 1418 def BuildCurrentRevision(self, depot, revision=None): |
1371 """Builds chrome and performance_ui_tests on the current revision. | 1419 """Builds chrome and performance_ui_tests on the current revision. |
1372 | 1420 |
1373 Returns: | 1421 Returns: |
1374 True if the build was successful. | 1422 True if the build was successful. |
1375 """ | 1423 """ |
1376 if self.opts.debug_ignore_build: | 1424 if self.opts.debug_ignore_build: |
1377 return True | 1425 return True |
1378 cwd = os.getcwd() | 1426 cwd = os.getcwd() |
1379 os.chdir(self.src_cwd) | 1427 os.chdir(self.src_cwd) |
1380 # Fetch build archive for the given revision from the cloud storage when | 1428 # Fetch build archive for the given revision from the cloud storage when |
1381 # the storage bucket is passed. | 1429 # the storage bucket is passed. |
1382 if depot == 'chromium' and self.opts.gs_bucket and revision: | 1430 if depot == 'chromium' and self.opts.gs_bucket and revision: |
1431 # Get SVN revision for the given SHA, since builds are archived using SVN | |
1432 # revision. | |
1433 revision = self.source_control.SVNFindRev(revision) | |
1434 if not revision: | |
1435 raise RuntimeError( | |
1436 'Failed to determine SVN revision for %s' % sha_revision) | |
1383 if self.DownloadCurrentBuild(revision): | 1437 if self.DownloadCurrentBuild(revision): |
1384 os.chdir(cwd) | 1438 os.chdir(cwd) |
1385 return True | 1439 return True |
1386 raise RuntimeError('Failed to download build archive for revision %s.\n' | 1440 raise RuntimeError('Failed to download build archive for revision %s.\n' |
1387 'Unfortunately, bisection couldn\'t continue any ' | 1441 'Unfortunately, bisection couldn\'t continue any ' |
1388 'further. Please try running script without ' | 1442 'further. Please try running script without ' |
1389 '--gs_bucket flag to produce local builds.' % revision) | 1443 '--gs_bucket flag to produce local builds.' % revision) |
1390 | 1444 |
1445 | |
1391 build_success = self.builder.Build(depot, self.opts) | 1446 build_success = self.builder.Build(depot, self.opts) |
1392 os.chdir(cwd) | 1447 os.chdir(cwd) |
1393 return build_success | 1448 return build_success |
1394 | 1449 |
1395 def RunGClientHooks(self): | 1450 def RunGClientHooks(self): |
1396 """Runs gclient with runhooks command. | 1451 """Runs gclient with runhooks command. |
1397 | 1452 |
1398 Returns: | 1453 Returns: |
1399 True if gclient reports no errors. | 1454 True if gclient reports no errors. |
1400 """ | 1455 """ |
(...skipping 1614 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
3015 self.command = None | 3070 self.command = None |
3016 self.output_buildbot_annotations = None | 3071 self.output_buildbot_annotations = None |
3017 self.no_custom_deps = False | 3072 self.no_custom_deps = False |
3018 self.working_directory = None | 3073 self.working_directory = None |
3019 self.extra_src = None | 3074 self.extra_src = None |
3020 self.debug_ignore_build = None | 3075 self.debug_ignore_build = None |
3021 self.debug_ignore_sync = None | 3076 self.debug_ignore_sync = None |
3022 self.debug_ignore_perf_test = None | 3077 self.debug_ignore_perf_test = None |
3023 self.gs_bucket = None | 3078 self.gs_bucket = None |
3024 self.target_arch = 'ia32' | 3079 self.target_arch = 'ia32' |
3080 self.host = None | |
3081 self.port = None | |
3025 | 3082 |
3026 def _CreateCommandLineParser(self): | 3083 def _CreateCommandLineParser(self): |
3027 """Creates a parser with bisect options. | 3084 """Creates a parser with bisect options. |
3028 | 3085 |
3029 Returns: | 3086 Returns: |
3030 An instance of optparse.OptionParser. | 3087 An instance of optparse.OptionParser. |
3031 """ | 3088 """ |
3032 usage = ('%prog [options] [-- chromium-options]\n' | 3089 usage = ('%prog [options] [-- chromium-options]\n' |
3033 'Perform binary search on revision history to find a minimal ' | 3090 'Perform binary search on revision history to find a minimal ' |
3034 'range of revisions where a peformance metric regressed.\n') | 3091 'range of revisions where a peformance metric regressed.\n') |
(...skipping 93 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
3128 type='str', | 3185 type='str', |
3129 help=('Name of Google Storage bucket to upload or ' | 3186 help=('Name of Google Storage bucket to upload or ' |
3130 'download build. e.g., chrome-perf')) | 3187 'download build. e.g., chrome-perf')) |
3131 group.add_option('--target_arch', | 3188 group.add_option('--target_arch', |
3132 type='choice', | 3189 type='choice', |
3133 choices=['ia32', 'x64', 'arm'], | 3190 choices=['ia32', 'x64', 'arm'], |
3134 default='ia32', | 3191 default='ia32', |
3135 dest='target_arch', | 3192 dest='target_arch', |
3136 help=('The target build architecture. Choices are "ia32" ' | 3193 help=('The target build architecture. Choices are "ia32" ' |
3137 '(default), "x64" or "arm".')) | 3194 '(default), "x64" or "arm".')) |
3138 | 3195 group.add_option('--host', |
3196 dest='host', | |
3197 type='str', | |
3198 help=('Host address of server to produce build by posting' | |
3199 ' try job request.')) | |
3200 group.add_option('--port', | |
3201 dest='port', | |
3202 type='int', | |
3203 help=('HTTP port of the server to produce build by posting' | |
3204 ' try job request.')) | |
3139 parser.add_option_group(group) | 3205 parser.add_option_group(group) |
3140 | 3206 |
3141 group = optparse.OptionGroup(parser, 'Debug options') | 3207 group = optparse.OptionGroup(parser, 'Debug options') |
3142 group.add_option('--debug_ignore_build', | 3208 group.add_option('--debug_ignore_build', |
3143 action="store_true", | 3209 action="store_true", |
3144 help='DEBUG: Don\'t perform builds.') | 3210 help='DEBUG: Don\'t perform builds.') |
3145 group.add_option('--debug_ignore_sync', | 3211 group.add_option('--debug_ignore_sync', |
3146 action="store_true", | 3212 action="store_true", |
3147 help='DEBUG: Don\'t perform syncs.') | 3213 help='DEBUG: Don\'t perform syncs.') |
3148 group.add_option('--debug_ignore_perf_test', | 3214 group.add_option('--debug_ignore_perf_test', |
3149 action="store_true", | 3215 action="store_true", |
3150 help='DEBUG: Don\'t perform performance tests.') | 3216 help='DEBUG: Don\'t perform performance tests.') |
3151 parser.add_option_group(group) | 3217 parser.add_option_group(group) |
3152 | |
3153 | |
3154 return parser | 3218 return parser |
3155 | 3219 |
3156 def ParseCommandLine(self): | 3220 def ParseCommandLine(self): |
3157 """Parses the command line for bisect options.""" | 3221 """Parses the command line for bisect options.""" |
3158 parser = self._CreateCommandLineParser() | 3222 parser = self._CreateCommandLineParser() |
3159 (opts, args) = parser.parse_args() | 3223 (opts, args) = parser.parse_args() |
3160 | 3224 |
3161 try: | 3225 try: |
3162 if not opts.command: | 3226 if not opts.command: |
3163 raise RuntimeError('missing required parameter: --command') | 3227 raise RuntimeError('missing required parameter: --command') |
3164 | 3228 |
3165 if not opts.good_revision: | 3229 if not opts.good_revision: |
3166 raise RuntimeError('missing required parameter: --good_revision') | 3230 raise RuntimeError('missing required parameter: --good_revision') |
3167 | 3231 |
3168 if not opts.bad_revision: | 3232 if not opts.bad_revision: |
3169 raise RuntimeError('missing required parameter: --bad_revision') | 3233 raise RuntimeError('missing required parameter: --bad_revision') |
3170 | 3234 |
3171 if not opts.metric: | 3235 if not opts.metric: |
3172 raise RuntimeError('missing required parameter: --metric') | 3236 raise RuntimeError('missing required parameter: --metric') |
3173 | 3237 |
3174 if opts.gs_bucket: | 3238 if opts.gs_bucket: |
3175 if not cloud_storage.List(opts.gs_bucket): | 3239 if not cloud_storage.List(opts.gs_bucket): |
3176 raise RuntimeError('Invalid Google Storage URL: [%s]', e) | 3240 raise RuntimeError('Invalid Google Storage: gs://%s' % opts.gs_bucket) |
3177 | 3241 if not opts.host: |
3242 raise RuntimeError('Must specify try server hostname, when ' | |
3243 'gs_bucket is used: --host') | |
3244 if not opts.port: | |
3245 raise RuntimeError('Must specify try server port number, when ' | |
3246 'gs_bucket is used: --port') | |
3178 if opts.target_platform == 'cros': | 3247 if opts.target_platform == 'cros': |
3179 # Run sudo up front to make sure credentials are cached for later. | 3248 # Run sudo up front to make sure credentials are cached for later. |
3180 print 'Sudo is required to build cros:' | 3249 print 'Sudo is required to build cros:' |
3181 print | 3250 print |
3182 RunProcess(['sudo', 'true']) | 3251 RunProcess(['sudo', 'true']) |
3183 | 3252 |
3184 if not opts.cros_board: | 3253 if not opts.cros_board: |
3185 raise RuntimeError('missing required parameter: --cros_board') | 3254 raise RuntimeError('missing required parameter: --cros_board') |
3186 | 3255 |
3187 if not opts.cros_remote_ip: | 3256 if not opts.cros_remote_ip: |
(...skipping 84 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
3272 | 3341 |
3273 if not source_control: | 3342 if not source_control: |
3274 raise RuntimeError("Sorry, only the git workflow is supported at the " | 3343 raise RuntimeError("Sorry, only the git workflow is supported at the " |
3275 "moment.") | 3344 "moment.") |
3276 | 3345 |
3277 # gClient sync seems to fail if you're not in master branch. | 3346 # gClient sync seems to fail if you're not in master branch. |
3278 if (not source_control.IsInProperBranch() and | 3347 if (not source_control.IsInProperBranch() and |
3279 not opts.debug_ignore_sync and | 3348 not opts.debug_ignore_sync and |
3280 not opts.working_directory): | 3349 not opts.working_directory): |
3281 raise RuntimeError("You must switch to master branch to run bisection.") | 3350 raise RuntimeError("You must switch to master branch to run bisection.") |
3282 | |
3283 bisect_test = BisectPerformanceMetrics(source_control, opts) | 3351 bisect_test = BisectPerformanceMetrics(source_control, opts) |
3284 try: | 3352 try: |
3285 bisect_results = bisect_test.Run(opts.command, | 3353 bisect_results = bisect_test.Run(opts.command, |
3286 opts.bad_revision, | 3354 opts.bad_revision, |
3287 opts.good_revision, | 3355 opts.good_revision, |
3288 opts.metric) | 3356 opts.metric) |
3289 if bisect_results['error']: | 3357 if bisect_results['error']: |
3290 raise RuntimeError(bisect_results['error']) | 3358 raise RuntimeError(bisect_results['error']) |
3291 bisect_test.FormatAndPrintResults(bisect_results) | 3359 bisect_test.FormatAndPrintResults(bisect_results) |
3292 return 0 | 3360 return 0 |
3293 finally: | 3361 finally: |
3294 bisect_test.PerformCleanup() | 3362 bisect_test.PerformCleanup() |
3295 except RuntimeError, e: | 3363 except RuntimeError, e: |
3296 if opts.output_buildbot_annotations: | 3364 if opts.output_buildbot_annotations: |
3297 # The perf dashboard scrapes the "results" step in order to comment on | 3365 # The perf dashboard scrapes the "results" step in order to comment on |
3298 # bugs. If you change this, please update the perf dashboard as well. | 3366 # bugs. If you change this, please update the perf dashboard as well. |
3299 bisect_utils.OutputAnnotationStepStart('Results') | 3367 bisect_utils.OutputAnnotationStepStart('Results') |
3300 print 'Error: %s' % e.message | 3368 print 'Error: %s' % e.message |
3301 if opts.output_buildbot_annotations: | 3369 if opts.output_buildbot_annotations: |
3302 bisect_utils.OutputAnnotationStepClosed() | 3370 bisect_utils.OutputAnnotationStepClosed() |
3303 return 1 | 3371 return 1 |
3304 | 3372 |
3305 if __name__ == '__main__': | 3373 if __name__ == '__main__': |
3306 sys.exit(main()) | 3374 sys.exit(main()) |
OLD | NEW |