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 476 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
487 """Unstages a file and returns it to HEAD. | 487 """Unstages a file and returns it to HEAD. |
488 | 488 |
489 Returns: | 489 Returns: |
490 True if successful. | 490 True if successful. |
491 """ | 491 """ |
492 # Reset doesn't seem to return 0 on success. | 492 # Reset doesn't seem to return 0 on success. |
493 RunGit(['reset', 'HEAD', bisect_utils.FILE_DEPS_GIT]) | 493 RunGit(['reset', 'HEAD', bisect_utils.FILE_DEPS_GIT]) |
494 | 494 |
495 return not RunGit(['checkout', bisect_utils.FILE_DEPS_GIT])[1] | 495 return not RunGit(['checkout', bisect_utils.FILE_DEPS_GIT])[1] |
496 | 496 |
497 def QueryFileRevisionHistory(self, filename, revision_start, revision_end): | |
498 """Returns a list of commits that modified this file. | |
499 | |
500 Args: | |
501 filename: Name of file. | |
502 revision_start: Start of revision range. | |
503 revision_end: End of revision range. | |
504 | |
505 Returns: | |
506 Returns a list of commits that touched this file. | |
507 """ | |
508 cmd = ['log', '--format=%H', '%s^1..%s' % (revision_start, revision_end), | |
509 filename] | |
510 (output, return_code) = RunGit(cmd) | |
511 | |
512 assert not return_code, 'An error occurred while running'\ | |
tonyg
2013/05/28 23:16:07
We already do this assert not return_code dance in
shatch
2013/05/28 23:38:17
Done.
| |
513 ' "git %s"' % ' '.join(cmd) | |
514 | |
515 return [o for o in output.split('\n') if o] | |
516 | |
497 class BisectPerformanceMetrics(object): | 517 class BisectPerformanceMetrics(object): |
498 """BisectPerformanceMetrics performs a bisection against a list of range | 518 """BisectPerformanceMetrics performs a bisection against a list of range |
499 of revisions to narrow down where performance regressions may have | 519 of revisions to narrow down where performance regressions may have |
500 occurred.""" | 520 occurred.""" |
501 | 521 |
502 def __init__(self, source_control, opts): | 522 def __init__(self, source_control, opts): |
503 super(BisectPerformanceMetrics, self).__init__() | 523 super(BisectPerformanceMetrics, self).__init__() |
504 | 524 |
505 self.opts = opts | 525 self.opts = opts |
506 self.source_control = source_control | 526 self.source_control = source_control |
507 self.src_cwd = os.getcwd() | 527 self.src_cwd = os.getcwd() |
508 self.depot_cwd = {} | 528 self.depot_cwd = {} |
509 self.cleanup_commands = [] | 529 self.cleanup_commands = [] |
530 self.warnings = [] | |
510 | 531 |
511 # This always starts true since the script grabs latest first. | 532 # This always starts true since the script grabs latest first. |
512 self.was_blink = True | 533 self.was_blink = True |
513 | 534 |
514 for d in DEPOT_NAMES: | 535 for d in DEPOT_NAMES: |
515 # The working directory of each depot is just the path to the depot, but | 536 # 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. | 537 # since we're already in 'src', we can skip that part. |
517 | 538 |
518 self.depot_cwd[d] = self.src_cwd + DEPOT_DEPS_NAME[d]['src'][3:] | 539 self.depot_cwd[d] = self.src_cwd + DEPOT_DEPS_NAME[d]['src'][3:] |
519 | 540 |
(...skipping 517 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
1037 | 1058 |
1038 print | 1059 print |
1039 print 'Revisions to bisect on [%s]:' % depot | 1060 print 'Revisions to bisect on [%s]:' % depot |
1040 for revision_id in revision_list: | 1061 for revision_id in revision_list: |
1041 print ' -> %s' % (revision_id, ) | 1062 print ' -> %s' % (revision_id, ) |
1042 print | 1063 print |
1043 | 1064 |
1044 if self.opts.output_buildbot_annotations: | 1065 if self.opts.output_buildbot_annotations: |
1045 bisect_utils.OutputAnnotationStepClosed() | 1066 bisect_utils.OutputAnnotationStepClosed() |
1046 | 1067 |
1068 def NudgeRevisionsIfDEPSChange(self, bad_revision, good_revision): | |
1069 """Checks to see if changes to DEPS file occurred, and that the revision | |
1070 range also includes the change to .DEPS.git. If it doesn't, attempts to | |
1071 expand the revision range to include it. | |
1072 | |
1073 Args: | |
1074 good_revision: Last known good revision. | |
1075 bad_rev: First known bad revision. | |
tonyg
2013/05/28 23:16:07
Nit: args are in the wrong order here
shatch
2013/05/28 23:38:17
Done.
| |
1076 | |
1077 Returns: | |
1078 A tuple with the new good and bad revisions. | |
tonyg
2013/05/28 23:16:07
A tuple with the new bad and good revisions
shatch
2013/05/28 23:38:17
Done.
| |
1079 """ | |
1080 if self.source_control.IsGit(): | |
1081 changes_to_deps = self.source_control.QueryFileRevisionHistory( | |
1082 'DEPS', good_revision, bad_revision) | |
1083 | |
1084 if changes_to_deps: | |
1085 # DEPS file was changed, search from the oldest change to DEPS file to | |
1086 # bad_revision to see if there are matching .DEPS.git changes. | |
1087 oldest_deps_change = changes_to_deps[len(changes_to_deps) - 1] | |
tonyg
2013/05/28 23:16:07
Neat python trick:
oldest_deps_change = changes_to
shatch
2013/05/28 23:38:17
Cool!
| |
1088 changes_to_gitdeps = self.source_control.QueryFileRevisionHistory( | |
1089 bisect_utils.FILE_DEPS_GIT, oldest_deps_change, bad_revision) | |
1090 | |
1091 if len(changes_to_deps) != len(changes_to_gitdeps): | |
1092 # Grab the timestamp of the last DEPS change | |
1093 cmd = ['log', '--format=%ct', '-1', changes_to_deps[0]] | |
1094 (output, return_code) = RunGit(cmd) | |
1095 | |
1096 assert not return_code, 'An error occurred while running'\ | |
1097 ' "git %s"' % ' '.join(cmd) | |
1098 | |
1099 commit_time = int(output) | |
1100 | |
1101 # Try looking for a commit that touches the .DEPS.git file in the | |
1102 # next 5 minutes after the DEPS file change. | |
tonyg
2013/05/28 23:16:07
I didn't look how long it usually takes, but 5 min
shatch
2013/05/28 23:38:17
Done.
| |
1103 cmd = ['log', '--format=%H', '-1', | |
1104 '--before=%d' % (commit_time + 300), '--after=%d' % commit_time, | |
1105 'origin/master', bisect_utils.FILE_DEPS_GIT] | |
1106 (output, return_code) = RunGit(cmd) | |
1107 | |
1108 assert not return_code, 'An error occurred while running'\ | |
1109 ' "git %s"' % ' '.join(cmd) | |
1110 | |
1111 output = output.strip() | |
1112 if output: | |
1113 self.warnings.append('Detected change to DEPS and modified ' | |
1114 'revision range to include change to .DEPS.git') | |
1115 return (output, good_revision) | |
1116 else: | |
1117 self.warnings.append('Detected change to DEPS but couldn\'t find ' | |
1118 'matching change to .DEPS.git') | |
1119 return (bad_revision, good_revision) | |
1120 | |
1047 def Run(self, command_to_run, bad_revision_in, good_revision_in, metric): | 1121 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 | 1122 """Given known good and bad revisions, run a binary search on all |
1049 intermediate revisions to determine the CL where the performance regression | 1123 intermediate revisions to determine the CL where the performance regression |
1050 occurred. | 1124 occurred. |
1051 | 1125 |
1052 Args: | 1126 Args: |
1053 command_to_run: Specify the command to execute the performance test. | 1127 command_to_run: Specify the command to execute the performance test. |
1054 good_revision: Number/tag of the known good revision. | 1128 good_revision: Number/tag of the known good revision. |
1055 bad_revision: Number/tag of the known bad revision. | 1129 bad_revision: Number/tag of the known bad revision. |
1056 metric: The performance metric to monitor. | 1130 metric: The performance metric to monitor. |
(...skipping 41 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
1098 'src', -100) | 1172 'src', -100) |
1099 | 1173 |
1100 if bad_revision is None: | 1174 if bad_revision is None: |
1101 results['error'] = 'Could\'t resolve [%s] to SHA1.' % (bad_revision_in,) | 1175 results['error'] = 'Could\'t resolve [%s] to SHA1.' % (bad_revision_in,) |
1102 return results | 1176 return results |
1103 | 1177 |
1104 if good_revision is None: | 1178 if good_revision is None: |
1105 results['error'] = 'Could\'t resolve [%s] to SHA1.' % (good_revision_in,) | 1179 results['error'] = 'Could\'t resolve [%s] to SHA1.' % (good_revision_in,) |
1106 return results | 1180 return results |
1107 | 1181 |
1182 (bad_revision, good_revision) = self.NudgeRevisionsIfDEPSChange( | |
1183 bad_revision, good_revision) | |
1184 | |
1108 if self.opts.output_buildbot_annotations: | 1185 if self.opts.output_buildbot_annotations: |
1109 bisect_utils.OutputAnnotationStepStart('Gathering Revisions') | 1186 bisect_utils.OutputAnnotationStepStart('Gathering Revisions') |
1110 | 1187 |
1111 print 'Gathering revision range for bisection.' | 1188 print 'Gathering revision range for bisection.' |
1112 | 1189 |
1113 # Retrieve a list of revisions to do bisection on. | 1190 # Retrieve a list of revisions to do bisection on. |
1114 src_revision_list = self.GetRevisionList(bad_revision, good_revision) | 1191 src_revision_list = self.GetRevisionList(bad_revision, good_revision) |
1115 | 1192 |
1116 if self.opts.output_buildbot_annotations: | 1193 if self.opts.output_buildbot_annotations: |
1117 bisect_utils.OutputAnnotationStepClosed() | 1194 bisect_utils.OutputAnnotationStepClosed() |
(...skipping 255 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
1373 good_std_dev = revision_data[first_working_revision]['value']['std_dev'] | 1450 good_std_dev = revision_data[first_working_revision]['value']['std_dev'] |
1374 good_mean = revision_data[first_working_revision]['value']['mean'] | 1451 good_mean = revision_data[first_working_revision]['value']['mean'] |
1375 bad_mean = revision_data[last_broken_revision]['value']['mean'] | 1452 bad_mean = revision_data[last_broken_revision]['value']['mean'] |
1376 | 1453 |
1377 # A standard deviation of 0 could indicate either insufficient runs | 1454 # A standard deviation of 0 could indicate either insufficient runs |
1378 # or a test that consistently returns the same value. | 1455 # or a test that consistently returns the same value. |
1379 if good_std_dev > 0: | 1456 if good_std_dev > 0: |
1380 deviations = math.fabs(bad_mean - good_mean) / good_std_dev | 1457 deviations = math.fabs(bad_mean - good_mean) / good_std_dev |
1381 | 1458 |
1382 if deviations < 1.5: | 1459 if deviations < 1.5: |
1383 print 'Warning: Regression was less than 1.5 standard deviations '\ | 1460 self.warnings.append('Regression was less than 1.5 standard ' |
1384 'from "good" value. Results may not be accurate.' | 1461 'deviations from "good" value. Results may not be accurate.') |
tonyg
2013/05/28 23:16:07
Something for a future CL that I'll mention while
shatch
2013/05/28 23:38:17
Yeah that sounds reasonable, I'll get a version wi
| |
1385 print | |
1386 elif self.opts.repeat_test_count == 1: | 1462 elif self.opts.repeat_test_count == 1: |
1387 print 'Warning: Tests were only set to run once. This may be '\ | 1463 self.warnings.append('Tests were only set to run once. This ' |
1388 'insufficient to get meaningful results.' | 1464 'may be insufficient to get meaningful results.') |
1389 print | |
1390 | 1465 |
1391 # Check for any other possible regression ranges | 1466 # Check for any other possible regression ranges |
1392 prev_revision_data = revision_data_sorted[0][1] | 1467 prev_revision_data = revision_data_sorted[0][1] |
1393 prev_revision_id = revision_data_sorted[0][0] | 1468 prev_revision_id = revision_data_sorted[0][0] |
1394 possible_regressions = [] | 1469 possible_regressions = [] |
1395 for current_id, current_data in revision_data_sorted: | 1470 for current_id, current_data in revision_data_sorted: |
1396 if current_data['value']: | 1471 if current_data['value']: |
1397 prev_mean = prev_revision_data['value']['mean'] | 1472 prev_mean = prev_revision_data['value']['mean'] |
1398 cur_mean = current_data['value']['mean'] | 1473 cur_mean = current_data['value']['mean'] |
1399 | 1474 |
(...skipping 40 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
1440 if percent_change is None: | 1515 if percent_change is None: |
1441 percent_change = 0 | 1516 percent_change = 0 |
1442 | 1517 |
1443 print ' %8s %s [%.2f%%, %s x std.dev]' % ( | 1518 print ' %8s %s [%.2f%%, %s x std.dev]' % ( |
1444 previous_data['depot'], previous_id, 100 * percent_change, | 1519 previous_data['depot'], previous_id, 100 * percent_change, |
1445 deviations) | 1520 deviations) |
1446 print ' %8s %s' % ( | 1521 print ' %8s %s' % ( |
1447 current_data['depot'], current_id) | 1522 current_data['depot'], current_id) |
1448 print | 1523 print |
1449 | 1524 |
1525 if self.warnings: | |
1526 print | |
1527 print 'The following warnings were generated:' | |
1528 print | |
1529 for w in self.warnings: | |
1530 print ' - %s' % w | |
1531 print | |
1532 | |
1450 if self.opts.output_buildbot_annotations: | 1533 if self.opts.output_buildbot_annotations: |
1451 bisect_utils.OutputAnnotationStepClosed() | 1534 bisect_utils.OutputAnnotationStepClosed() |
1452 | 1535 |
1453 | 1536 |
1454 def DetermineAndCreateSourceControl(): | 1537 def DetermineAndCreateSourceControl(): |
1455 """Attempts to determine the underlying source control workflow and returns | 1538 """Attempts to determine the underlying source control workflow and returns |
1456 a SourceControl object. | 1539 a SourceControl object. |
1457 | 1540 |
1458 Returns: | 1541 Returns: |
1459 An instance of a SourceControl object, or None if the current workflow | 1542 An instance of a SourceControl object, or None if the current workflow |
(...skipping 269 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
1729 | 1812 |
1730 if not(bisect_results['error']): | 1813 if not(bisect_results['error']): |
1731 return 0 | 1814 return 0 |
1732 else: | 1815 else: |
1733 print 'Error: ' + bisect_results['error'] | 1816 print 'Error: ' + bisect_results['error'] |
1734 print | 1817 print |
1735 return 1 | 1818 return 1 |
1736 | 1819 |
1737 if __name__ == '__main__': | 1820 if __name__ == '__main__': |
1738 sys.exit(main()) | 1821 sys.exit(main()) |
OLD | NEW |