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 233 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
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 Loading... |
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 Loading... |
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 Loading... |
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 Loading... |
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 Loading... |
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 Loading... |
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 Loading... |
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()) |
OLD | NEW |