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

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

Issue 16023021: Try to expand the revision range to include a change to .DEPS.git if the script detects a change to… (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Changes from review. Created 7 years, 6 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
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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 233 matching lines...) Expand 10 before | Expand all | Expand 10 after
244 command: A list containing the args to git. 244 command: A list containing the args to git.
245 245
246 Returns: 246 Returns:
247 A tuple of the output and return code. 247 A tuple of the output and return code.
248 """ 248 """
249 command = ['git'] + command 249 command = ['git'] + command
250 250
251 return RunProcess(command) 251 return RunProcess(command)
252 252
253 253
254 def CheckRunGit(command):
255 """Run a git subcommand, returning its output and return code. Asserts if
256 the return code of the call is non-zero.
257
258 Args:
259 command: A list containing the args to git.
260
261 Returns:
262 A tuple of the output and return code.
263 """
264 (output, return_code) = RunGit(command)
265
266 assert not return_code, 'An error occurred while running'\
267 ' "git %s"' % ' '.join(command)
268 return output
269
270
254 def BuildWithMake(threads, targets, print_output): 271 def BuildWithMake(threads, targets, print_output):
255 cmd = ['make', 'BUILDTYPE=Release', '-j%d' % threads] + targets 272 cmd = ['make', 'BUILDTYPE=Release', '-j%d' % threads] + targets
256 273
257 (output, return_code) = RunProcess(cmd, print_output) 274 (output, return_code) = RunProcess(cmd, print_output)
258 275
259 return not return_code 276 return not return_code
260 277
261 278
262 def BuildWithNinja(threads, targets, print_output): 279 def BuildWithNinja(threads, targets, print_output):
263 cmd = ['ninja', '-C', os.path.join('out', 'Release'), 280 cmd = ['ninja', '-C', os.path.join('out', 'Release'),
(...skipping 55 matching lines...) Expand 10 before | Expand all | Expand 10 after
319 Args: 336 Args:
320 revision_range_end: The SHA1 for the end of the range. 337 revision_range_end: The SHA1 for the end of the range.
321 revision_range_start: The SHA1 for the beginning of the range. 338 revision_range_start: The SHA1 for the beginning of the range.
322 339
323 Returns: 340 Returns:
324 A list of the revisions between |revision_range_start| and 341 A list of the revisions between |revision_range_start| and
325 |revision_range_end| (inclusive). 342 |revision_range_end| (inclusive).
326 """ 343 """
327 revision_range = '%s..%s' % (revision_range_start, revision_range_end) 344 revision_range = '%s..%s' % (revision_range_start, revision_range_end)
328 cmd = ['log', '--format=%H', '-10000', '--first-parent', revision_range] 345 cmd = ['log', '--format=%H', '-10000', '--first-parent', revision_range]
329 (log_output, return_code) = RunGit(cmd) 346 log_output = CheckRunGit(cmd)
330
331 assert not return_code, 'An error occurred while running'\
332 ' "git %s"' % ' '.join(cmd)
333 347
334 revision_hash_list = log_output.split() 348 revision_hash_list = log_output.split()
335 revision_hash_list.append(revision_range_start) 349 revision_hash_list.append(revision_range_start)
336 350
337 return revision_hash_list 351 return revision_hash_list
338 352
339 def SyncToRevision(self, revision, use_gclient=True): 353 def SyncToRevision(self, revision, use_gclient=True):
340 """Syncs to the specified revision. 354 """Syncs to the specified revision.
341 355
342 Args: 356 Args:
(...skipping 39 matching lines...) Expand 10 before | Expand all | Expand 10 after
382 396
383 if search > 0: 397 if search > 0:
384 search_range = xrange(svn_revision, svn_revision + search, 1) 398 search_range = xrange(svn_revision, svn_revision + search, 1)
385 else: 399 else:
386 search_range = xrange(svn_revision, svn_revision + search, -1) 400 search_range = xrange(svn_revision, svn_revision + search, -1)
387 401
388 for i in search_range: 402 for i in search_range:
389 svn_pattern = 'git-svn-id: %s@%d' % (depot_svn, i) 403 svn_pattern = 'git-svn-id: %s@%d' % (depot_svn, i)
390 cmd = ['log', '--format=%H', '-1', '--grep', svn_pattern, 'origin/master'] 404 cmd = ['log', '--format=%H', '-1', '--grep', svn_pattern, 'origin/master']
391 405
392 (log_output, return_code) = RunGit(cmd) 406 log_output = CheckRunGit(cmd)
407 log_output = log_output.strip()
393 408
394 assert not return_code, 'An error occurred while running'\ 409 if log_output:
395 ' "git %s"' % ' '.join(cmd) 410 git_revision = log_output
396 411
397 if not return_code: 412 break
398 log_output = log_output.strip()
399
400 if log_output:
401 git_revision = log_output
402
403 break
404 413
405 return git_revision 414 return git_revision
406 415
407 def IsInProperBranch(self): 416 def IsInProperBranch(self):
408 """Confirms they're in the master branch for performing the bisection. 417 """Confirms they're in the master branch for performing the bisection.
409 This is needed or gclient will fail to sync properly. 418 This is needed or gclient will fail to sync properly.
410 419
411 Returns: 420 Returns:
412 True if the current branch on src is 'master' 421 True if the current branch on src is 'master'
413 """ 422 """
414 cmd = ['rev-parse', '--abbrev-ref', 'HEAD'] 423 cmd = ['rev-parse', '--abbrev-ref', 'HEAD']
415 (log_output, return_code) = RunGit(cmd) 424 log_output = CheckRunGit(cmd)
416
417 assert not return_code, 'An error occurred while running'\
418 ' "git %s"' % ' '.join(cmd)
419
420 log_output = log_output.strip() 425 log_output = log_output.strip()
421 426
422 return log_output == "master" 427 return log_output == "master"
423 428
424 def SVNFindRev(self, revision): 429 def SVNFindRev(self, revision):
425 """Maps directly to the 'git svn find-rev' command. 430 """Maps directly to the 'git svn find-rev' command.
426 431
427 Args: 432 Args:
428 revision: The git SHA1 to use. 433 revision: The git SHA1 to use.
429 434
430 Returns: 435 Returns:
431 An integer changelist #, otherwise None. 436 An integer changelist #, otherwise None.
432 """ 437 """
433 438
434 cmd = ['svn', 'find-rev', revision] 439 cmd = ['svn', 'find-rev', revision]
435 440
436 (output, return_code) = RunGit(cmd) 441 output = CheckRunGit(cmd)
437
438 assert not return_code, 'An error occurred while running'\
439 ' "git %s"' % ' '.join(cmd)
440
441 svn_revision = output.strip() 442 svn_revision = output.strip()
442 443
443 if IsStringInt(svn_revision): 444 if IsStringInt(svn_revision):
444 return int(svn_revision) 445 return int(svn_revision)
445 446
446 return None 447 return None
447 448
448 def QueryRevisionInfo(self, revision): 449 def QueryRevisionInfo(self, revision):
449 """Gathers information on a particular revision, such as author's name, 450 """Gathers information on a particular revision, such as author's name,
450 email, subject, and date. 451 email, subject, and date.
451 452
452 Args: 453 Args:
453 revision: Revision you want to gather information on. 454 revision: Revision you want to gather information on.
454 Returns: 455 Returns:
455 A dict in the following format: 456 A dict in the following format:
456 { 457 {
457 'author': %s, 458 'author': %s,
458 'email': %s, 459 'email': %s,
459 'date': %s, 460 'date': %s,
460 'subject': %s, 461 'subject': %s,
461 } 462 }
462 """ 463 """
463 commit_info = {} 464 commit_info = {}
464 465
465 formats = ['%cN', '%cE', '%s', '%cD'] 466 formats = ['%cN', '%cE', '%s', '%cD']
466 targets = ['author', 'email', 'subject', 'date'] 467 targets = ['author', 'email', 'subject', 'date']
467 468
468 for i in xrange(len(formats)): 469 for i in xrange(len(formats)):
469 cmd = ['log', '--format=%s' % formats[i], '-1', revision] 470 cmd = ['log', '--format=%s' % formats[i], '-1', revision]
470 (output, return_code) = RunGit(cmd) 471 output = CheckRunGit(cmd)
471 commit_info[targets[i]] = output.rstrip() 472 commit_info[targets[i]] = output.rstrip()
472 473
473 assert not return_code, 'An error occurred while running'\
474 ' "git %s"' % ' '.join(cmd)
475
476 return commit_info 474 return commit_info
477 475
478 def CheckoutFileAtRevision(self, file_name, revision): 476 def CheckoutFileAtRevision(self, file_name, revision):
479 """Performs a checkout on a file at the given revision. 477 """Performs a checkout on a file at the given revision.
480 478
481 Returns: 479 Returns:
482 True if successful. 480 True if successful.
483 """ 481 """
484 return not RunGit(['checkout', revision, file_name])[1] 482 return not RunGit(['checkout', revision, file_name])[1]
485 483
486 def RevertFileToHead(self, file_name): 484 def RevertFileToHead(self, file_name):
487 """Unstages a file and returns it to HEAD. 485 """Unstages a file and returns it to HEAD.
488 486
489 Returns: 487 Returns:
490 True if successful. 488 True if successful.
491 """ 489 """
492 # Reset doesn't seem to return 0 on success. 490 # Reset doesn't seem to return 0 on success.
493 RunGit(['reset', 'HEAD', bisect_utils.FILE_DEPS_GIT]) 491 RunGit(['reset', 'HEAD', bisect_utils.FILE_DEPS_GIT])
494 492
495 return not RunGit(['checkout', bisect_utils.FILE_DEPS_GIT])[1] 493 return not RunGit(['checkout', bisect_utils.FILE_DEPS_GIT])[1]
496 494
495 def QueryFileRevisionHistory(self, filename, revision_start, revision_end):
496 """Returns a list of commits that modified this file.
497
498 Args:
499 filename: Name of file.
500 revision_start: Start of revision range.
501 revision_end: End of revision range.
502
503 Returns:
504 Returns a list of commits that touched this file.
505 """
506 cmd = ['log', '--format=%H', '%s^1..%s' % (revision_start, revision_end),
507 filename]
508 output = CheckRunGit(cmd)
509
510 return [o for o in output.split('\n') if o]
511
497 class BisectPerformanceMetrics(object): 512 class BisectPerformanceMetrics(object):
498 """BisectPerformanceMetrics performs a bisection against a list of range 513 """BisectPerformanceMetrics performs a bisection against a list of range
499 of revisions to narrow down where performance regressions may have 514 of revisions to narrow down where performance regressions may have
500 occurred.""" 515 occurred."""
501 516
502 def __init__(self, source_control, opts): 517 def __init__(self, source_control, opts):
503 super(BisectPerformanceMetrics, self).__init__() 518 super(BisectPerformanceMetrics, self).__init__()
504 519
505 self.opts = opts 520 self.opts = opts
506 self.source_control = source_control 521 self.source_control = source_control
507 self.src_cwd = os.getcwd() 522 self.src_cwd = os.getcwd()
508 self.depot_cwd = {} 523 self.depot_cwd = {}
509 self.cleanup_commands = [] 524 self.cleanup_commands = []
525 self.warnings = []
510 526
511 # This always starts true since the script grabs latest first. 527 # This always starts true since the script grabs latest first.
512 self.was_blink = True 528 self.was_blink = True
513 529
514 for d in DEPOT_NAMES: 530 for d in DEPOT_NAMES:
515 # The working directory of each depot is just the path to the depot, but 531 # The working directory of each depot is just the path to the depot, but
516 # since we're already in 'src', we can skip that part. 532 # since we're already in 'src', we can skip that part.
517 533
518 self.depot_cwd[d] = self.src_cwd + DEPOT_DEPS_NAME[d]['src'][3:] 534 self.depot_cwd[d] = self.src_cwd + DEPOT_DEPS_NAME[d]['src'][3:]
519 535
(...skipping 517 matching lines...) Expand 10 before | Expand all | Expand 10 after
1037 1053
1038 print 1054 print
1039 print 'Revisions to bisect on [%s]:' % depot 1055 print 'Revisions to bisect on [%s]:' % depot
1040 for revision_id in revision_list: 1056 for revision_id in revision_list:
1041 print ' -> %s' % (revision_id, ) 1057 print ' -> %s' % (revision_id, )
1042 print 1058 print
1043 1059
1044 if self.opts.output_buildbot_annotations: 1060 if self.opts.output_buildbot_annotations:
1045 bisect_utils.OutputAnnotationStepClosed() 1061 bisect_utils.OutputAnnotationStepClosed()
1046 1062
1063 def NudgeRevisionsIfDEPSChange(self, bad_revision, good_revision):
1064 """Checks to see if changes to DEPS file occurred, and that the revision
1065 range also includes the change to .DEPS.git. If it doesn't, attempts to
1066 expand the revision range to include it.
1067
1068 Args:
1069 bad_rev: First known bad revision.
1070 good_revision: Last known good revision.
1071
1072 Returns:
1073 A tuple with the new bad and good revisions.
1074 """
1075 if self.source_control.IsGit():
1076 changes_to_deps = self.source_control.QueryFileRevisionHistory(
1077 'DEPS', good_revision, bad_revision)
1078
1079 if changes_to_deps:
1080 # DEPS file was changed, search from the oldest change to DEPS file to
1081 # bad_revision to see if there are matching .DEPS.git changes.
1082 oldest_deps_change = changes_to_deps[-1]
1083 changes_to_gitdeps = self.source_control.QueryFileRevisionHistory(
1084 bisect_utils.FILE_DEPS_GIT, oldest_deps_change, bad_revision)
1085
1086 if len(changes_to_deps) != len(changes_to_gitdeps):
1087 # Grab the timestamp of the last DEPS change
1088 cmd = ['log', '--format=%ct', '-1', changes_to_deps[0]]
1089 output = CheckRunGit(cmd)
1090 commit_time = int(output)
1091
1092 # Try looking for a commit that touches the .DEPS.git file in the
1093 # next 15 minutes after the DEPS file change.
1094 cmd = ['log', '--format=%H', '-1',
1095 '--before=%d' % (commit_time + 900), '--after=%d' % commit_time,
1096 'origin/master', bisect_utils.FILE_DEPS_GIT]
1097 output = CheckRunGit(cmd)
1098 output = output.strip()
1099 if output:
1100 self.warnings.append('Detected change to DEPS and modified '
1101 'revision range to include change to .DEPS.git')
1102 return (output, good_revision)
1103 else:
1104 self.warnings.append('Detected change to DEPS but couldn\'t find '
1105 'matching change to .DEPS.git')
1106 return (bad_revision, good_revision)
1107
1047 def Run(self, command_to_run, bad_revision_in, good_revision_in, metric): 1108 def Run(self, command_to_run, bad_revision_in, good_revision_in, metric):
1048 """Given known good and bad revisions, run a binary search on all 1109 """Given known good and bad revisions, run a binary search on all
1049 intermediate revisions to determine the CL where the performance regression 1110 intermediate revisions to determine the CL where the performance regression
1050 occurred. 1111 occurred.
1051 1112
1052 Args: 1113 Args:
1053 command_to_run: Specify the command to execute the performance test. 1114 command_to_run: Specify the command to execute the performance test.
1054 good_revision: Number/tag of the known good revision. 1115 good_revision: Number/tag of the known good revision.
1055 bad_revision: Number/tag of the known bad revision. 1116 bad_revision: Number/tag of the known bad revision.
1056 metric: The performance metric to monitor. 1117 metric: The performance metric to monitor.
(...skipping 41 matching lines...) Expand 10 before | Expand all | Expand 10 after
1098 'src', -100) 1159 'src', -100)
1099 1160
1100 if bad_revision is None: 1161 if bad_revision is None:
1101 results['error'] = 'Could\'t resolve [%s] to SHA1.' % (bad_revision_in,) 1162 results['error'] = 'Could\'t resolve [%s] to SHA1.' % (bad_revision_in,)
1102 return results 1163 return results
1103 1164
1104 if good_revision is None: 1165 if good_revision is None:
1105 results['error'] = 'Could\'t resolve [%s] to SHA1.' % (good_revision_in,) 1166 results['error'] = 'Could\'t resolve [%s] to SHA1.' % (good_revision_in,)
1106 return results 1167 return results
1107 1168
1169 (bad_revision, good_revision) = self.NudgeRevisionsIfDEPSChange(
1170 bad_revision, good_revision)
1171
1108 if self.opts.output_buildbot_annotations: 1172 if self.opts.output_buildbot_annotations:
1109 bisect_utils.OutputAnnotationStepStart('Gathering Revisions') 1173 bisect_utils.OutputAnnotationStepStart('Gathering Revisions')
1110 1174
1111 print 'Gathering revision range for bisection.' 1175 print 'Gathering revision range for bisection.'
1112 1176
1113 # Retrieve a list of revisions to do bisection on. 1177 # Retrieve a list of revisions to do bisection on.
1114 src_revision_list = self.GetRevisionList(bad_revision, good_revision) 1178 src_revision_list = self.GetRevisionList(bad_revision, good_revision)
1115 1179
1116 if self.opts.output_buildbot_annotations: 1180 if self.opts.output_buildbot_annotations:
1117 bisect_utils.OutputAnnotationStepClosed() 1181 bisect_utils.OutputAnnotationStepClosed()
(...skipping 255 matching lines...) Expand 10 before | Expand all | Expand 10 after
1373 good_std_dev = revision_data[first_working_revision]['value']['std_dev'] 1437 good_std_dev = revision_data[first_working_revision]['value']['std_dev']
1374 good_mean = revision_data[first_working_revision]['value']['mean'] 1438 good_mean = revision_data[first_working_revision]['value']['mean']
1375 bad_mean = revision_data[last_broken_revision]['value']['mean'] 1439 bad_mean = revision_data[last_broken_revision]['value']['mean']
1376 1440
1377 # A standard deviation of 0 could indicate either insufficient runs 1441 # A standard deviation of 0 could indicate either insufficient runs
1378 # or a test that consistently returns the same value. 1442 # or a test that consistently returns the same value.
1379 if good_std_dev > 0: 1443 if good_std_dev > 0:
1380 deviations = math.fabs(bad_mean - good_mean) / good_std_dev 1444 deviations = math.fabs(bad_mean - good_mean) / good_std_dev
1381 1445
1382 if deviations < 1.5: 1446 if deviations < 1.5:
1383 print 'Warning: Regression was less than 1.5 standard deviations '\ 1447 self.warnings.append('Regression was less than 1.5 standard '
1384 'from "good" value. Results may not be accurate.' 1448 'deviations from "good" value. Results may not be accurate.')
1385 print
1386 elif self.opts.repeat_test_count == 1: 1449 elif self.opts.repeat_test_count == 1:
1387 print 'Warning: Tests were only set to run once. This may be '\ 1450 self.warnings.append('Tests were only set to run once. This '
1388 'insufficient to get meaningful results.' 1451 'may be insufficient to get meaningful results.')
1389 print
1390 1452
1391 # Check for any other possible regression ranges 1453 # Check for any other possible regression ranges
1392 prev_revision_data = revision_data_sorted[0][1] 1454 prev_revision_data = revision_data_sorted[0][1]
1393 prev_revision_id = revision_data_sorted[0][0] 1455 prev_revision_id = revision_data_sorted[0][0]
1394 possible_regressions = [] 1456 possible_regressions = []
1395 for current_id, current_data in revision_data_sorted: 1457 for current_id, current_data in revision_data_sorted:
1396 if current_data['value']: 1458 if current_data['value']:
1397 prev_mean = prev_revision_data['value']['mean'] 1459 prev_mean = prev_revision_data['value']['mean']
1398 cur_mean = current_data['value']['mean'] 1460 cur_mean = current_data['value']['mean']
1399 1461
(...skipping 40 matching lines...) Expand 10 before | Expand all | Expand 10 after
1440 if percent_change is None: 1502 if percent_change is None:
1441 percent_change = 0 1503 percent_change = 0
1442 1504
1443 print ' %8s %s [%.2f%%, %s x std.dev]' % ( 1505 print ' %8s %s [%.2f%%, %s x std.dev]' % (
1444 previous_data['depot'], previous_id, 100 * percent_change, 1506 previous_data['depot'], previous_id, 100 * percent_change,
1445 deviations) 1507 deviations)
1446 print ' %8s %s' % ( 1508 print ' %8s %s' % (
1447 current_data['depot'], current_id) 1509 current_data['depot'], current_id)
1448 print 1510 print
1449 1511
1512 if self.warnings:
1513 print
1514 print 'The following warnings were generated:'
1515 print
1516 for w in self.warnings:
1517 print ' - %s' % w
1518 print
1519
1450 if self.opts.output_buildbot_annotations: 1520 if self.opts.output_buildbot_annotations:
1451 bisect_utils.OutputAnnotationStepClosed() 1521 bisect_utils.OutputAnnotationStepClosed()
1452 1522
1453 1523
1454 def DetermineAndCreateSourceControl(): 1524 def DetermineAndCreateSourceControl():
1455 """Attempts to determine the underlying source control workflow and returns 1525 """Attempts to determine the underlying source control workflow and returns
1456 a SourceControl object. 1526 a SourceControl object.
1457 1527
1458 Returns: 1528 Returns:
1459 An instance of a SourceControl object, or None if the current workflow 1529 An instance of a SourceControl object, or None if the current workflow
(...skipping 269 matching lines...) Expand 10 before | Expand all | Expand 10 after
1729 1799
1730 if not(bisect_results['error']): 1800 if not(bisect_results['error']):
1731 return 0 1801 return 0
1732 else: 1802 else:
1733 print 'Error: ' + bisect_results['error'] 1803 print 'Error: ' + bisect_results['error']
1734 print 1804 print
1735 return 1 1805 return 1
1736 1806
1737 if __name__ == '__main__': 1807 if __name__ == '__main__':
1738 sys.exit(main()) 1808 sys.exit(main())
OLDNEW
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698