Chromium Code Reviews| 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 |