| #!/usr/bin/env python | 
 | # Copyright 2017 The PDFium Authors. All rights reserved. | 
 | # Use of this source code is governed by a BSD-style license that can be | 
 | # found in the LICENSE file. | 
 |  | 
 | """Compares the performance of two versions of the pdfium code.""" | 
 |  | 
 | import argparse | 
 | import functools | 
 | import glob | 
 | import json | 
 | import multiprocessing | 
 | import os | 
 | import re | 
 | import shutil | 
 | import subprocess | 
 | import sys | 
 | import tempfile | 
 |  | 
 | from common import GetBooleanGnArg | 
 | from common import PrintErr | 
 | from common import RunCommandPropagateErr | 
 | from githelper import GitHelper | 
 | from safetynet_conclusions import ComparisonConclusions | 
 | from safetynet_conclusions import PrintConclusionsDictHumanReadable | 
 | from safetynet_conclusions import RATING_IMPROVEMENT | 
 | from safetynet_conclusions import RATING_REGRESSION | 
 | from safetynet_image import ImageComparison | 
 |  | 
 |  | 
 | def RunSingleTestCaseParallel(this, run_label, build_dir, test_case): | 
 |   result = this.RunSingleTestCase(run_label, build_dir, test_case) | 
 |   return (test_case, result) | 
 |  | 
 |  | 
 | class CompareRun(object): | 
 |   """A comparison between two branches of pdfium.""" | 
 |  | 
 |   def __init__(self, args): | 
 |     self.git = GitHelper() | 
 |     self.args = args | 
 |     self._InitPaths() | 
 |  | 
 |   def _InitPaths(self): | 
 |     if self.args.this_repo: | 
 |       self.safe_script_dir = self.args.build_dir | 
 |     else: | 
 |       self.safe_script_dir = os.path.join('testing', 'tools') | 
 |  | 
 |     self.safe_measure_script_path = os.path.abspath( | 
 |         os.path.join(self.safe_script_dir, | 
 |                      'safetynet_measure.py')) | 
 |  | 
 |     input_file_re = re.compile('^.+[.]pdf$') | 
 |     self.test_cases = [] | 
 |     for input_path in self.args.input_paths: | 
 |       if os.path.isfile(input_path): | 
 |         self.test_cases.append(input_path) | 
 |       elif os.path.isdir(input_path): | 
 |         for file_dir, _, filename_list in os.walk(input_path): | 
 |           for input_filename in filename_list: | 
 |             if input_file_re.match(input_filename): | 
 |               file_path = os.path.join(file_dir, input_filename) | 
 |               if os.path.isfile(file_path): | 
 |                 self.test_cases.append(file_path) | 
 |  | 
 |     self.after_build_dir = self.args.build_dir | 
 |     if self.args.build_dir_before: | 
 |       self.before_build_dir = self.args.build_dir_before | 
 |     else: | 
 |       self.before_build_dir = self.after_build_dir | 
 |  | 
 |   def Run(self): | 
 |     """Runs comparison by checking out branches, building and measuring them. | 
 |  | 
 |     Returns: | 
 |       Exit code for the script. | 
 |     """ | 
 |     if self.args.this_repo: | 
 |       self._FreezeMeasureScript() | 
 |  | 
 |     if self.args.branch_after: | 
 |       if self.args.this_repo: | 
 |         before, after = self._ProfileTwoOtherBranchesInThisRepo( | 
 |             self.args.branch_before, | 
 |             self.args.branch_after) | 
 |       else: | 
 |         before, after = self._ProfileTwoOtherBranches( | 
 |             self.args.branch_before, | 
 |             self.args.branch_after) | 
 |     elif self.args.branch_before: | 
 |       if self.args.this_repo: | 
 |         before, after = self._ProfileCurrentAndOtherBranchInThisRepo( | 
 |             self.args.branch_before) | 
 |       else: | 
 |         before, after = self._ProfileCurrentAndOtherBranch( | 
 |             self.args.branch_before) | 
 |     else: | 
 |       if self.args.this_repo: | 
 |         before, after = self._ProfileLocalChangesAndCurrentBranchInThisRepo() | 
 |       else: | 
 |         before, after = self._ProfileLocalChangesAndCurrentBranch() | 
 |  | 
 |     conclusions = self._DrawConclusions(before, after) | 
 |     conclusions_dict = conclusions.GetOutputDict() | 
 |     conclusions_dict.setdefault('metadata', {})['profiler'] = self.args.profiler | 
 |  | 
 |     self._PrintConclusions(conclusions_dict) | 
 |  | 
 |     self._CleanUp(conclusions) | 
 |  | 
 |     if self.args.png_dir: | 
 |       image_comparison = ImageComparison( | 
 |           self.after_build_dir, | 
 |           self.args.png_dir, | 
 |           ('before', 'after'), | 
 |           self.args.num_workers, | 
 |           self.args.png_threshold) | 
 |       image_comparison.Run(open_in_browser=not self.args.machine_readable) | 
 |  | 
 |     return 0 | 
 |  | 
 |   def _FreezeMeasureScript(self): | 
 |     """Freezes a version of the measuring script. | 
 |  | 
 |     This is needed to make sure we are comparing the pdfium library changes and | 
 |     not script changes that may happen between the two branches. | 
 |     """ | 
 |     self.__FreezeFile(os.path.join('testing', 'tools', 'safetynet_measure.py')) | 
 |     self.__FreezeFile(os.path.join('testing', 'tools', 'common.py')) | 
 |  | 
 |   def __FreezeFile(self, file): | 
 |     RunCommandPropagateErr(['cp', file, self.safe_script_dir], | 
 |                            exit_status_on_error=1) | 
 |  | 
 |   def _ProfileTwoOtherBranchesInThisRepo(self, before_branch, after_branch): | 
 |     """Profiles two branches that are not the current branch. | 
 |  | 
 |     This is done in the local repository and changes may not be restored if the | 
 |     script fails or is interrupted. | 
 |  | 
 |     after_branch does not need to descend from before_branch, they will be | 
 |     measured the same way | 
 |  | 
 |     Args: | 
 |       before_branch: One branch to profile. | 
 |       after_branch: Other branch to profile. | 
 |  | 
 |     Returns: | 
 |       A tuple (before, after), where each of before and after is a dict | 
 |       mapping a test case name to the profiling values for that test case | 
 |       in the given branch. | 
 |     """ | 
 |     branch_to_restore = self.git.GetCurrentBranchName() | 
 |  | 
 |     self._StashLocalChanges() | 
 |  | 
 |     self._CheckoutBranch(after_branch) | 
 |     self._BuildCurrentBranch(self.after_build_dir) | 
 |     after = self._MeasureCurrentBranch('after', self.after_build_dir) | 
 |  | 
 |     self._CheckoutBranch(before_branch) | 
 |     self._BuildCurrentBranch(self.before_build_dir) | 
 |     before = self._MeasureCurrentBranch('before', self.before_build_dir) | 
 |  | 
 |     self._CheckoutBranch(branch_to_restore) | 
 |     self._RestoreLocalChanges() | 
 |  | 
 |     return before, after | 
 |  | 
 |   def _ProfileTwoOtherBranches(self, before_branch, after_branch): | 
 |     """Profiles two branches that are not the current branch. | 
 |  | 
 |     This is done in new, cloned repositories, therefore it is safer but slower | 
 |     and requires downloads. | 
 |  | 
 |     after_branch does not need to descend from before_branch, they will be | 
 |     measured the same way | 
 |  | 
 |     Args: | 
 |       before_branch: One branch to profile. | 
 |       after_branch: Other branch to profile. | 
 |  | 
 |     Returns: | 
 |       A tuple (before, after), where each of before and after is a dict | 
 |       mapping a test case name to the profiling values for that test case | 
 |       in the given branch. | 
 |     """ | 
 |     after = self._ProfileSeparateRepo('after', | 
 |                                       self.after_build_dir, | 
 |                                       after_branch) | 
 |     before = self._ProfileSeparateRepo('before', | 
 |                                        self.before_build_dir, | 
 |                                        before_branch) | 
 |     return before, after | 
 |  | 
 |   def _ProfileCurrentAndOtherBranchInThisRepo(self, other_branch): | 
 |     """Profiles the current branch (with uncommitted changes) and another one. | 
 |  | 
 |     This is done in the local repository and changes may not be restored if the | 
 |     script fails or is interrupted. | 
 |  | 
 |     The current branch does not need to descend from other_branch. | 
 |  | 
 |     Args: | 
 |       other_branch: Other branch to profile that is not the current. | 
 |  | 
 |     Returns: | 
 |       A tuple (before, after), where each of before and after is a dict | 
 |       mapping a test case name to the profiling values for that test case | 
 |       in the given branch. The current branch is considered to be "after" and | 
 |       the other branch is considered to be "before". | 
 |     """ | 
 |     branch_to_restore = self.git.GetCurrentBranchName() | 
 |  | 
 |     self._BuildCurrentBranch(self.after_build_dir) | 
 |     after = self._MeasureCurrentBranch('after', self.after_build_dir) | 
 |  | 
 |     self._StashLocalChanges() | 
 |  | 
 |     self._CheckoutBranch(other_branch) | 
 |     self._BuildCurrentBranch(self.before_build_dir) | 
 |     before = self._MeasureCurrentBranch('before', self.before_build_dir) | 
 |  | 
 |     self._CheckoutBranch(branch_to_restore) | 
 |     self._RestoreLocalChanges() | 
 |  | 
 |     return before, after | 
 |  | 
 |   def _ProfileCurrentAndOtherBranch(self, other_branch): | 
 |     """Profiles the current branch (with uncommitted changes) and another one. | 
 |  | 
 |     This is done in new, cloned repositories, therefore it is safer but slower | 
 |     and requires downloads. | 
 |  | 
 |     The current branch does not need to descend from other_branch. | 
 |  | 
 |     Args: | 
 |       other_branch: Other branch to profile that is not the current. None will | 
 |           compare to the same branch. | 
 |  | 
 |     Returns: | 
 |       A tuple (before, after), where each of before and after is a dict | 
 |       mapping a test case name to the profiling values for that test case | 
 |       in the given branch. The current branch is considered to be "after" and | 
 |       the other branch is considered to be "before". | 
 |     """ | 
 |     self._BuildCurrentBranch(self.after_build_dir) | 
 |     after = self._MeasureCurrentBranch('after', self.after_build_dir) | 
 |  | 
 |     before = self._ProfileSeparateRepo('before', | 
 |                                        self.before_build_dir, | 
 |                                        other_branch) | 
 |  | 
 |     return before, after | 
 |  | 
 |   def _ProfileLocalChangesAndCurrentBranchInThisRepo(self): | 
 |     """Profiles the current branch with and without uncommitted changes. | 
 |  | 
 |     This is done in the local repository and changes may not be restored if the | 
 |     script fails or is interrupted. | 
 |  | 
 |     Returns: | 
 |       A tuple (before, after), where each of before and after is a dict | 
 |       mapping a test case name to the profiling values for that test case | 
 |       using the given version. The current branch without uncommitted changes is | 
 |       considered to be "before" and with uncommitted changes is considered to be | 
 |       "after". | 
 |     """ | 
 |     self._BuildCurrentBranch(self.after_build_dir) | 
 |     after = self._MeasureCurrentBranch('after', self.after_build_dir) | 
 |  | 
 |     pushed = self._StashLocalChanges() | 
 |     if not pushed and not self.args.build_dir_before: | 
 |       PrintErr('Warning: No local changes to compare') | 
 |  | 
 |     before_build_dir = self.before_build_dir | 
 |  | 
 |     self._BuildCurrentBranch(before_build_dir) | 
 |     before = self._MeasureCurrentBranch('before', before_build_dir) | 
 |  | 
 |     self._RestoreLocalChanges() | 
 |  | 
 |     return before, after | 
 |  | 
 |   def _ProfileLocalChangesAndCurrentBranch(self): | 
 |     """Profiles the current branch with and without uncommitted changes. | 
 |  | 
 |     This is done in new, cloned repositories, therefore it is safer but slower | 
 |     and requires downloads. | 
 |  | 
 |     Returns: | 
 |       A tuple (before, after), where each of before and after is a dict | 
 |       mapping a test case name to the profiling values for that test case | 
 |       using the given version. The current branch without uncommitted changes is | 
 |       considered to be "before" and with uncommitted changes is considered to be | 
 |       "after". | 
 |     """ | 
 |     return self._ProfileCurrentAndOtherBranch(other_branch=None) | 
 |  | 
 |   def _ProfileSeparateRepo(self, run_label, relative_build_dir, branch): | 
 |     """Profiles a branch in a a temporary git repository. | 
 |  | 
 |     Args: | 
 |       run_label: String to differentiate this version of the code in output | 
 |           files from other versions. | 
 |       relative_build_dir: Path to the build dir in the current working dir to | 
 |           clone build args from. | 
 |       branch: Branch to checkout in the new repository. None will | 
 |           profile the same branch checked out in the original repo. | 
 |     Returns: | 
 |       A dict mapping each test case name to the profiling values for that | 
 |       test case. | 
 |     """ | 
 |     build_dir = self._CreateTempRepo('repo_%s' % run_label, | 
 |                                      relative_build_dir, | 
 |                                      branch) | 
 |  | 
 |     self._BuildCurrentBranch(build_dir) | 
 |     return self._MeasureCurrentBranch(run_label, build_dir) | 
 |  | 
 |   def _CreateTempRepo(self, dir_name, relative_build_dir, branch): | 
 |     """Clones a temporary git repository out of the current working dir. | 
 |  | 
 |     Args: | 
 |       dir_name: Name for the temporary repository directory | 
 |       relative_build_dir: Path to the build dir in the current working dir to | 
 |           clone build args from. | 
 |       branch: Branch to checkout in the new repository. None will keep checked | 
 |           out the same branch as the local repo. | 
 |     Returns: | 
 |       Path to the build directory of the new repository. | 
 |     """ | 
 |     cwd = os.getcwd() | 
 |  | 
 |     repo_dir = tempfile.mkdtemp(suffix='-%s' % dir_name) | 
 |     src_dir = os.path.join(repo_dir, 'pdfium') | 
 |  | 
 |     self.git.CloneLocal(os.getcwd(), src_dir) | 
 |  | 
 |     if branch is not None: | 
 |       os.chdir(src_dir) | 
 |       self.git.Checkout(branch) | 
 |  | 
 |     os.chdir(repo_dir) | 
 |     PrintErr('Syncing...') | 
 |  | 
 |     cmd = ['gclient', 'config', '--unmanaged', | 
 |            'https://pdfium.googlesource.com/pdfium.git'] | 
 |     if self.args.cache_dir: | 
 |       cmd.append('--cache-dir=%s' % self.args.cache_dir) | 
 |     RunCommandPropagateErr(cmd, exit_status_on_error=1) | 
 |  | 
 |     RunCommandPropagateErr(['gclient', 'sync', '--force'], | 
 |                            exit_status_on_error=1) | 
 |  | 
 |     PrintErr('Done.') | 
 |  | 
 |     build_dir = os.path.join(src_dir, relative_build_dir) | 
 |     os.makedirs(build_dir) | 
 |     os.chdir(src_dir) | 
 |  | 
 |     source_gn_args = os.path.join(cwd, relative_build_dir, 'args.gn') | 
 |     dest_gn_args = os.path.join(build_dir, 'args.gn') | 
 |     shutil.copy(source_gn_args, dest_gn_args) | 
 |  | 
 |     RunCommandPropagateErr(['gn', 'gen', relative_build_dir], | 
 |                            exit_status_on_error=1) | 
 |  | 
 |     os.chdir(cwd) | 
 |  | 
 |     return build_dir | 
 |  | 
 |  | 
 |   def _CheckoutBranch(self, branch): | 
 |     PrintErr("Checking out branch '%s'" % branch) | 
 |     self.git.Checkout(branch) | 
 |  | 
 |   def _StashLocalChanges(self): | 
 |     PrintErr('Stashing local changes') | 
 |     return self.git.StashPush() | 
 |  | 
 |   def _RestoreLocalChanges(self): | 
 |     PrintErr('Restoring local changes') | 
 |     self.git.StashPopAll() | 
 |  | 
 |   def _BuildCurrentBranch(self, build_dir): | 
 |     """Synchronizes and builds the current version of pdfium. | 
 |  | 
 |     Args: | 
 |       build_dir: String with path to build directory | 
 |     """ | 
 |     PrintErr('Syncing...') | 
 |     RunCommandPropagateErr(['gclient', 'sync', '--force'], | 
 |                            exit_status_on_error=1) | 
 |     PrintErr('Done.') | 
 |  | 
 |     PrintErr('Building...') | 
 |     cmd = ['ninja', '-C', build_dir, 'pdfium_test'] | 
 |     if GetBooleanGnArg('use_goma', build_dir): | 
 |       cmd.extend(['-j', '250']) | 
 |     RunCommandPropagateErr(cmd, stdout_has_errors=True, exit_status_on_error=1) | 
 |     PrintErr('Done.') | 
 |  | 
 |   def _MeasureCurrentBranch(self, run_label, build_dir): | 
 |     PrintErr('Measuring...') | 
 |     if self.args.num_workers > 1 and len(self.test_cases) > 1: | 
 |       results = self._RunAsync(run_label, build_dir) | 
 |     else: | 
 |       results = self._RunSync(run_label, build_dir) | 
 |     PrintErr('Done.') | 
 |  | 
 |     return results | 
 |  | 
 |   def _RunSync(self, run_label, build_dir): | 
 |     """Profiles the test cases synchronously. | 
 |  | 
 |     Args: | 
 |       run_label: String to differentiate this version of the code in output | 
 |           files from other versions. | 
 |       build_dir: String with path to build directory | 
 |  | 
 |     Returns: | 
 |       A dict mapping each test case name to the profiling values for that | 
 |       test case. | 
 |     """ | 
 |     results = {} | 
 |  | 
 |     for test_case in self.test_cases: | 
 |       result = self.RunSingleTestCase(run_label, build_dir, test_case) | 
 |       if result is not None: | 
 |         results[test_case] = result | 
 |  | 
 |     return results | 
 |  | 
 |   def _RunAsync(self, run_label, build_dir): | 
 |     """Profiles the test cases asynchronously. | 
 |  | 
 |     Uses as many workers as configured by --num-workers. | 
 |  | 
 |     Args: | 
 |       run_label: String to differentiate this version of the code in output | 
 |           files from other versions. | 
 |       build_dir: String with path to build directory | 
 |  | 
 |     Returns: | 
 |       A dict mapping each test case name to the profiling values for that | 
 |       test case. | 
 |     """ | 
 |     results = {} | 
 |     pool = multiprocessing.Pool(self.args.num_workers) | 
 |     worker_func = functools.partial( | 
 |         RunSingleTestCaseParallel, self, run_label, build_dir) | 
 |  | 
 |     try: | 
 |       # The timeout is a workaround for http://bugs.python.org/issue8296 | 
 |       # which prevents KeyboardInterrupt from working. | 
 |       one_year_in_seconds = 3600 * 24 * 365 | 
 |       worker_results = (pool.map_async(worker_func, self.test_cases) | 
 |                         .get(one_year_in_seconds)) | 
 |       for worker_result in worker_results: | 
 |         test_case, result = worker_result | 
 |         if result is not None: | 
 |           results[test_case] = result | 
 |     except KeyboardInterrupt: | 
 |       pool.terminate() | 
 |       sys.exit(1) | 
 |     else: | 
 |       pool.close() | 
 |  | 
 |     pool.join() | 
 |  | 
 |     return results | 
 |  | 
 |   def RunSingleTestCase(self, run_label, build_dir, test_case): | 
 |     """Profiles a single test case. | 
 |  | 
 |     Args: | 
 |       run_label: String to differentiate this version of the code in output | 
 |           files from other versions. | 
 |       build_dir: String with path to build directory | 
 |       test_case: Path to the test case. | 
 |  | 
 |     Returns: | 
 |       The measured profiling value for that test case. | 
 |     """ | 
 |     command = [self.safe_measure_script_path, test_case, | 
 |                '--build-dir=%s' % build_dir] | 
 |  | 
 |     if self.args.interesting_section: | 
 |       command.append('--interesting-section') | 
 |  | 
 |     if self.args.profiler: | 
 |       command.append('--profiler=%s' % self.args.profiler) | 
 |  | 
 |     profile_file_path = self._GetProfileFilePath(run_label, test_case) | 
 |     if profile_file_path: | 
 |       command.append('--output-path=%s' % profile_file_path) | 
 |  | 
 |     if self.args.png_dir: | 
 |       command.append('--png') | 
 |  | 
 |     if self.args.pages: | 
 |       command.extend(['--pages', self.args.pages]) | 
 |  | 
 |     output = RunCommandPropagateErr(command) | 
 |  | 
 |     if output is None: | 
 |       return None | 
 |  | 
 |     if self.args.png_dir: | 
 |       self._MoveImages(test_case, run_label) | 
 |  | 
 |     # Get the time number as output, making sure it's just a number | 
 |     output = output.strip() | 
 |     if re.match('^[0-9]+$', output): | 
 |       return int(output) | 
 |  | 
 |     return None | 
 |  | 
 |   def _MoveImages(self, test_case, run_label): | 
 |     png_dir = os.path.join(self.args.png_dir, run_label) | 
 |     if not os.path.exists(png_dir): | 
 |       os.makedirs(png_dir) | 
 |  | 
 |     test_case_dir, test_case_filename = os.path.split(test_case) | 
 |     test_case_png_matcher = '%s.*.png' % test_case_filename | 
 |     for output_png in glob.glob(os.path.join(test_case_dir, | 
 |                                              test_case_png_matcher)): | 
 |       shutil.move(output_png, png_dir) | 
 |  | 
 |   def _GetProfileFilePath(self, run_label, test_case): | 
 |     if self.args.output_dir: | 
 |       output_filename = ('callgrind.out.%s.%s' | 
 |                          % (test_case.replace('/', '_'), | 
 |                             run_label)) | 
 |       return os.path.join(self.args.output_dir, output_filename) | 
 |     else: | 
 |       return None | 
 |  | 
 |   def _DrawConclusions(self, times_before_branch, times_after_branch): | 
 |     """Draws conclusions comparing results of test runs in two branches. | 
 |  | 
 |     Args: | 
 |       times_before_branch: A dict mapping each test case name to the | 
 |           profiling values for that test case in the branch to be considered | 
 |           as the baseline. | 
 |       times_after_branch: A dict mapping each test case name to the | 
 |           profiling values for that test case in the branch to be considered | 
 |           as the new version. | 
 |  | 
 |     Returns: | 
 |       ComparisonConclusions with all test cases processed. | 
 |     """ | 
 |     conclusions = ComparisonConclusions(self.args.threshold_significant) | 
 |  | 
 |     for test_case in sorted(self.test_cases): | 
 |       before = times_before_branch.get(test_case) | 
 |       after = times_after_branch.get(test_case) | 
 |       conclusions.ProcessCase(test_case, before, after) | 
 |  | 
 |     return conclusions | 
 |  | 
 |   def _PrintConclusions(self, conclusions_dict): | 
 |     """Prints the conclusions as the script output. | 
 |  | 
 |     Depending on the script args, this can output a human or a machine-readable | 
 |     version of the conclusions. | 
 |  | 
 |     Args: | 
 |       conclusions_dict: Dict to print returned from | 
 |           ComparisonConclusions.GetOutputDict(). | 
 |     """ | 
 |     if self.args.machine_readable: | 
 |       print json.dumps(conclusions_dict) | 
 |     else: | 
 |       PrintConclusionsDictHumanReadable( | 
 |           conclusions_dict, colored=True, key=self.args.case_order) | 
 |  | 
 |   def _CleanUp(self, conclusions): | 
 |     """Removes profile output files for uninteresting cases. | 
 |  | 
 |     Cases without significant regressions or improvements and considered | 
 |     uninteresting. | 
 |  | 
 |     Args: | 
 |       conclusions: A ComparisonConclusions. | 
 |     """ | 
 |     if not self.args.output_dir: | 
 |       return | 
 |  | 
 |     if self.args.profiler != 'callgrind': | 
 |       return | 
 |  | 
 |     for case_result in conclusions.GetCaseResults().values(): | 
 |       if case_result.rating not in [RATING_REGRESSION, RATING_IMPROVEMENT]: | 
 |         self._CleanUpOutputFile('before', case_result.case_name) | 
 |         self._CleanUpOutputFile('after', case_result.case_name) | 
 |  | 
 |   def _CleanUpOutputFile(self, run_label, case_name): | 
 |     """Removes one profile output file. | 
 |  | 
 |     If the output file does not exist, fails silently. | 
 |  | 
 |     Args: | 
 |       run_label: String to differentiate a version of the code in output | 
 |           files from other versions. | 
 |       case_name: String identifying test case for which to remove the output | 
 |           file. | 
 |     """ | 
 |     try: | 
 |       os.remove(self._GetProfileFilePath(run_label, case_name)) | 
 |     except OSError: | 
 |       pass | 
 |  | 
 |  | 
 | def main(): | 
 |   parser = argparse.ArgumentParser() | 
 |   parser.add_argument('input_paths', nargs='+', | 
 |                       help='pdf files or directories to search for pdf files ' | 
 |                            'to run as test cases') | 
 |   parser.add_argument('--branch-before', | 
 |                       help='git branch to use as "before" for comparison. ' | 
 |                            'Omitting this will use the current branch ' | 
 |                            'without uncommitted changes as the baseline.') | 
 |   parser.add_argument('--branch-after', | 
 |                       help='git branch to use as "after" for comparison. ' | 
 |                            'Omitting this will use the current branch ' | 
 |                            'with uncommitted changes.') | 
 |   parser.add_argument('--build-dir', default=os.path.join('out', 'Release'), | 
 |                       help='relative path from the base source directory ' | 
 |                            'to the build directory') | 
 |   parser.add_argument('--build-dir-before', | 
 |                       help='relative path from the base source directory ' | 
 |                            'to the build directory for the "before" branch, if ' | 
 |                            'different from the build directory for the ' | 
 |                            '"after" branch') | 
 |   parser.add_argument('--cache-dir', default=None, | 
 |                       help='directory with a new or preexisting cache for ' | 
 |                            'downloads. Default is to not use a cache.') | 
 |   parser.add_argument('--this-repo', action='store_true', | 
 |                       help='use the repository where the script is instead of ' | 
 |                            'checking out a temporary one. This is faster and ' | 
 |                            'does not require downloads, but although it ' | 
 |                            'restores the state of the local repo, if the ' | 
 |                            'script is killed or crashes the changes can remain ' | 
 |                            'stashed and you may be on another branch.') | 
 |   parser.add_argument('--profiler', default='callgrind', | 
 |                       help='which profiler to use. Supports callgrind, ' | 
 |                            'perfstat, and none. Default is callgrind.') | 
 |   parser.add_argument('--interesting-section', action='store_true', | 
 |                       help='whether to measure just the interesting section or ' | 
 |                            'the whole test harness. Limiting to only the ' | 
 |                            'interesting section does not work on Release since ' | 
 |                            'the delimiters are optimized out') | 
 |   parser.add_argument('--pages', | 
 |                       help='selects some pages to be rendered. Page numbers ' | 
 |                            'are 0-based. "--pages A" will render only page A. ' | 
 |                            '"--pages A-B" will render pages A to B ' | 
 |                            '(inclusive).') | 
 |   parser.add_argument('--num-workers', default=multiprocessing.cpu_count(), | 
 |                       type=int, help='run NUM_WORKERS jobs in parallel') | 
 |   parser.add_argument('--output-dir', | 
 |                       help='directory to write the profile data output files') | 
 |   parser.add_argument('--png-dir', default=None, | 
 |                       help='outputs pngs to the specified directory that can ' | 
 |                            'be compared with a static html generated. Will ' | 
 |                            'affect performance measurements.') | 
 |   parser.add_argument('--png-threshold', default=0.0, type=float, | 
 |                       help='Requires --png-dir. Threshold above which a png ' | 
 |                            'is considered to have changed.') | 
 |   parser.add_argument('--threshold-significant', default=0.02, type=float, | 
 |                       help='variations in performance above this factor are ' | 
 |                            'considered significant') | 
 |   parser.add_argument('--machine-readable', action='store_true', | 
 |                       help='whether to get output for machines. If enabled the ' | 
 |                            'output will be a json with the format specified in ' | 
 |                            'ComparisonConclusions.GetOutputDict(). Default is ' | 
 |                            'human-readable.') | 
 |   parser.add_argument('--case-order', default=None, | 
 |                       help='what key to use when sorting test cases in the ' | 
 |                            'output. Accepted values are "after", "before", ' | 
 |                            '"ratio" and "rating". Default is sorting by test ' | 
 |                            'case path.') | 
 |  | 
 |   args = parser.parse_args() | 
 |  | 
 |   # Always start at the pdfium src dir, which is assumed to be two level above | 
 |   # this script. | 
 |   pdfium_src_dir = os.path.join( | 
 |       os.path.dirname(__file__), | 
 |       os.path.pardir, | 
 |       os.path.pardir) | 
 |   os.chdir(pdfium_src_dir) | 
 |  | 
 |   git = GitHelper() | 
 |  | 
 |   if args.branch_after and not args.branch_before: | 
 |     PrintErr('--branch-after requires --branch-before to be specified.') | 
 |     return 1 | 
 |  | 
 |   if args.branch_after and not git.BranchExists(args.branch_after): | 
 |     PrintErr('Branch "%s" does not exist' % args.branch_after) | 
 |     return 1 | 
 |  | 
 |   if args.branch_before and not git.BranchExists(args.branch_before): | 
 |     PrintErr('Branch "%s" does not exist' % args.branch_before) | 
 |     return 1 | 
 |  | 
 |   if args.output_dir: | 
 |     args.output_dir = os.path.expanduser(args.output_dir) | 
 |     if not os.path.isdir(args.output_dir): | 
 |       PrintErr('"%s" is not a directory' % args.output_dir) | 
 |       return 1 | 
 |  | 
 |   if args.png_dir: | 
 |     args.png_dir = os.path.expanduser(args.png_dir) | 
 |     if not os.path.isdir(args.png_dir): | 
 |       PrintErr('"%s" is not a directory' % args.png_dir) | 
 |       return 1 | 
 |  | 
 |   if args.threshold_significant <= 0.0: | 
 |     PrintErr('--threshold-significant should receive a positive float') | 
 |     return 1 | 
 |  | 
 |   if args.png_threshold: | 
 |     if not args.png_dir: | 
 |       PrintErr('--png-threshold requires --png-dir to be specified.') | 
 |       return 1 | 
 |  | 
 |     if args.png_threshold <= 0.0: | 
 |       PrintErr('--png-threshold should receive a positive float') | 
 |       return 1 | 
 |  | 
 |   if args.pages: | 
 |     if not re.match(r'^\d+(-\d+)?$', args.pages): | 
 |       PrintErr('Supported formats for --pages are "--pages 7" and ' | 
 |                '"--pages 3-6"') | 
 |       return 1 | 
 |  | 
 |   run = CompareRun(args) | 
 |   return run.Run() | 
 |  | 
 |  | 
 | if __name__ == '__main__': | 
 |   sys.exit(main()) |