|  | #!/usr/bin/env python3 | 
|  | # Copyright 2017 The PDFium Authors | 
|  | # 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: | 
|  | """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, filename): | 
|  | RunCommandPropagateErr(['cp', filename, 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 = ['autoninja', '-C', build_dir, 'pdfium_test'] | 
|  | 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) | 
|  | 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()) |