5"""Compares the performance of two versions of the pdfium code."""
19from common
import GetBooleanGnArg
20from common
import PrintErr
21from common
import RunCommandPropagateErr
22from githelper
import GitHelper
23from safetynet_conclusions
import ComparisonConclusions
24from safetynet_conclusions
import PrintConclusionsDictHumanReadable
25from safetynet_conclusions
import RATING_IMPROVEMENT
26from safetynet_conclusions
import RATING_REGRESSION
27from safetynet_image
import ImageComparison
31 result = this.RunSingleTestCase(run_label, build_dir, test_case)
32 return (test_case, result)
36 """A comparison between two branches of pdfium."""
44 if self.
args.this_repo:
52 input_file_re = re.compile(
'^.+[.]pdf$')
54 for input_path
in self.
args.input_paths:
55 if os.path.isfile(input_path):
57 elif os.path.isdir(input_path):
58 for file_dir, _, filename_list
in os.walk(input_path):
59 for input_filename
in filename_list:
60 if input_file_re.match(input_filename):
61 file_path = os.path.join(file_dir, input_filename)
62 if os.path.isfile(file_path):
66 if self.
args.build_dir_before:
72 """Runs comparison by checking out branches, building and measuring them.
75 Exit code for the script.
77 if self.
args.this_repo:
80 if self.
args.branch_after:
81 if self.
args.this_repo:
83 self.
args.branch_before, self.
args.branch_after)
86 self.
args.branch_after)
87 elif self.
args.branch_before:
88 if self.
args.this_repo:
90 self.
args.branch_before)
93 self.
args.branch_before)
95 if self.
args.this_repo:
101 conclusions_dict = conclusions.GetOutputDict()
102 conclusions_dict.setdefault(
'metadata', {})[
'profiler'] = self.
args.profiler
108 if self.
args.png_dir:
111 self.
args.num_workers, self.
args.png_threshold)
112 image_comparison.Run(open_in_browser=
not self.
args.machine_readable)
117 """Freezes a version of the measuring script.
119 This is needed to make sure we are comparing the pdfium library changes and
120 not script changes that may happen between the two branches.
122 self.
__FreezeFile(os.path.join(
'testing',
'tools',
'safetynet_measure.py'))
123 self.
__FreezeFile(os.path.join(
'testing',
'tools',
'common.py'))
125 def __FreezeFile(self, filename):
127 exit_status_on_error=1)
130 """Profiles two branches that are not the current branch.
132 This is done in the local repository and changes may not be restored if the
133 script fails or is interrupted.
135 after_branch does not need to descend from before_branch, they will be
136 measured the same way
139 before_branch: One branch to profile.
140 after_branch: Other branch to profile.
143 A tuple (before, after), where each of before and after is a dict
144 mapping a test case name to the profiling values for that test case
147 branch_to_restore = self.
git.GetCurrentBranchName()
165 """Profiles two branches that are not the current branch.
167 This is done in new, cloned repositories, therefore it is safer but slower
168 and requires downloads.
170 after_branch does not need to descend from before_branch, they will be
171 measured the same way
174 before_branch: One branch to profile.
175 after_branch: Other branch to profile.
178 A tuple (before, after), where each of before and after is a dict
179 mapping a test case name to the profiling values for that test case
189 """Profiles the current branch (with uncommitted changes) and another one.
191 This is done in the local repository and changes may not be restored if the
192 script fails or is interrupted.
194 The current branch does not need to descend from other_branch.
197 other_branch: Other branch to profile that is not the current.
200 A tuple (before, after), where each of before and after is a dict
201 mapping a test case name to the profiling values for that test case
202 in the given branch. The current branch is considered to be "after" and
203 the other branch is considered to be "before".
205 branch_to_restore = self.
git.GetCurrentBranchName()
222 """Profiles the current branch (with uncommitted changes) and another one.
224 This is done in new, cloned repositories, therefore it is safer but slower
225 and requires downloads.
227 The current branch does not need to descend from other_branch.
230 other_branch: Other branch to profile that is not the current. None will
231 compare to the same branch.
234 A tuple (before, after), where each of before and after is a dict
235 mapping a test case name to the profiling values for that test case
236 in the given branch. The current branch is considered to be "after" and
237 the other branch is considered to be "before".
248 """Profiles the current branch with and without uncommitted changes.
250 This is done in the local repository and changes may not be restored if the
251 script fails or is interrupted.
254 A tuple (before, after), where each of before and after is a dict
255 mapping a test case name to the profiling values for that test case
256 using the given version. The current branch without uncommitted changes is
257 considered to be "before" and with uncommitted changes is considered to be
264 if not pushed
and not self.
args.build_dir_before:
265 PrintErr(
'Warning: No local changes to compare')
277 """Profiles the current branch with and without uncommitted changes.
279 This is done in new, cloned repositories, therefore it is safer but slower
280 and requires downloads.
283 A tuple (before, after), where each of before and after is a dict
284 mapping a test case name to the profiling values for that test case
285 using the given version. The current branch without uncommitted changes is
286 considered to be "before" and with uncommitted changes is considered to be
292 """Profiles a branch in a a temporary git repository.
295 run_label: String to differentiate this version of the code in output
296 files from other versions.
297 relative_build_dir: Path to the build dir in the current working dir to
298 clone build args from.
299 branch: Branch to checkout in the new repository. None will
300 profile the same branch checked out in the original repo.
302 A dict mapping each test case name to the profiling values for that
305 build_dir = self.
_CreateTempRepo(
'repo_%s' % run_label, relative_build_dir,
312 """Clones a temporary git repository out of the current working dir.
315 dir_name: Name for the temporary repository directory
316 relative_build_dir: Path to the build dir in the current working dir to
317 clone build args from.
318 branch: Branch to checkout in the new repository. None will keep checked
319 out the same branch as the local repo.
321 Path to the build directory of the new repository.
325 repo_dir = tempfile.mkdtemp(suffix=
'-%s' % dir_name)
326 src_dir = os.path.join(repo_dir,
'pdfium')
328 self.
git.CloneLocal(os.getcwd(), src_dir)
330 if branch
is not None:
332 self.
git.Checkout(branch)
335 PrintErr(
'Syncing...')
338 'gclient',
'config',
'--unmanaged',
339 'https://pdfium.googlesource.com/pdfium.git'
341 if self.
args.cache_dir:
342 cmd.append(
'--cache-dir=%s' % self.
args.cache_dir)
343 RunCommandPropagateErr(cmd, exit_status_on_error=1)
345 RunCommandPropagateErr([
'gclient',
'sync',
'--force'],
346 exit_status_on_error=1)
350 build_dir = os.path.join(src_dir, relative_build_dir)
351 os.makedirs(build_dir)
354 source_gn_args = os.path.join(cwd, relative_build_dir,
'args.gn')
355 dest_gn_args = os.path.join(build_dir,
'args.gn')
356 shutil.copy(source_gn_args, dest_gn_args)
358 RunCommandPropagateErr([
'gn',
'gen', relative_build_dir],
359 exit_status_on_error=1)
366 PrintErr(
"Checking out branch '%s'" % branch)
367 self.
git.Checkout(branch)
370 PrintErr(
'Stashing local changes')
371 return self.
git.StashPush()
374 PrintErr(
'Restoring local changes')
375 self.
git.StashPopAll()
378 """Synchronizes and builds the current version of pdfium.
381 build_dir: String with path to build directory
383 PrintErr(
'Syncing...')
384 RunCommandPropagateErr([
'gclient',
'sync',
'--force'],
385 exit_status_on_error=1)
388 PrintErr(
'Building...')
389 cmd = [
'ninja',
'-C', build_dir,
'pdfium_test']
390 if GetBooleanGnArg(
'use_goma', build_dir):
391 cmd.extend([
'-j',
'250'])
392 RunCommandPropagateErr(cmd, stdout_has_errors=
True, exit_status_on_error=1)
396 PrintErr(
'Measuring...')
398 results = self.
_RunAsync(run_label, build_dir)
400 results = self.
_RunSync(run_label, build_dir)
406 """Profiles the test cases synchronously.
409 run_label: String to differentiate this version of the code in output
410 files from other versions.
411 build_dir: String with path to build directory
414 A dict mapping each test case name to the profiling values for that
421 if result
is not None:
422 results[test_case] = result
427 """Profiles the test cases asynchronously.
429 Uses as many workers as configured by --num-workers.
432 run_label: String to differentiate this version of the code in output
433 files from other versions.
434 build_dir: String with path to build directory
437 A dict mapping each test case name to the profiling values for that
441 pool = multiprocessing.Pool(self.
args.num_workers)
442 worker_func = functools.partial(RunSingleTestCaseParallel, self, run_label,
448 one_year_in_seconds = 3600 * 24 * 365
450 pool.map_async(worker_func, self.
test_cases).
get(one_year_in_seconds))
451 for worker_result
in worker_results:
452 test_case, result = worker_result
453 if result
is not None:
454 results[test_case] = result
455 except KeyboardInterrupt:
466 """Profiles a single test case.
469 run_label: String to differentiate this version of the code in output
470 files from other versions.
471 build_dir: String with path to build directory
472 test_case: Path to the test case.
475 The measured profiling value for that test case.
479 '--build-dir=%s' % build_dir
482 if self.
args.interesting_section:
483 command.append(
'--interesting-section')
485 if self.
args.profiler:
486 command.append(
'--profiler=%s' % self.
args.profiler)
489 if profile_file_path:
490 command.append(
'--output-path=%s' % profile_file_path)
492 if self.
args.png_dir:
493 command.append(
'--png')
496 command.extend([
'--pages', self.
args.pages])
498 output = RunCommandPropagateErr(command)
503 if self.
args.png_dir:
507 output = output.strip()
508 if re.match(
'^[0-9]+$', output):
514 png_dir = os.path.join(self.
args.png_dir, run_label)
515 if not os.path.exists(png_dir):
518 test_case_dir, test_case_filename = os.path.split(test_case)
519 test_case_png_matcher =
'%s.*.png' % test_case_filename
520 for output_png
in glob.glob(
521 os.path.join(test_case_dir, test_case_png_matcher)):
522 shutil.move(output_png, png_dir)
525 if self.
args.output_dir:
527 'callgrind.out.%s.%s' % (test_case.replace(
'/',
'_'), run_label))
528 return os.path.join(self.
args.output_dir, output_filename)
532 """Draws conclusions comparing results of test runs in two branches.
535 times_before_branch: A dict mapping each test case name to the
536 profiling values for that test case in the branch to be considered
538 times_after_branch: A dict mapping each test case name to the
539 profiling values for that test case in the branch to be considered
543 ComparisonConclusions with all test cases processed.
548 before = times_before_branch.get(test_case)
549 after = times_after_branch.get(test_case)
550 conclusions.ProcessCase(test_case, before, after)
555 """Prints the conclusions as the script output.
557 Depending on the script args, this can output a human or a machine-readable
558 version of the conclusions.
561 conclusions_dict: Dict to print returned from
562 ComparisonConclusions.GetOutputDict().
564 if self.
args.machine_readable:
565 print(json.dumps(conclusions_dict))
567 PrintConclusionsDictHumanReadable(
568 conclusions_dict, colored=
True, key=self.
args.case_order)
571 """Removes profile output files for uninteresting cases.
573 Cases without significant regressions or improvements and considered
577 conclusions: A ComparisonConclusions.
579 if not self.
args.output_dir:
582 if self.
args.profiler !=
'callgrind':
585 for case_result
in conclusions.GetCaseResults().values():
586 if case_result.rating
not in [RATING_REGRESSION, RATING_IMPROVEMENT]:
591 """Removes one profile output file.
593 If the output file does not exist, fails silently.
596 run_label: String to differentiate a version of the code in output
597 files from other versions.
598 case_name: String identifying test case for which to remove the output
608 parser = argparse.ArgumentParser()
612 help=
'pdf files or directories to search for pdf files '
613 'to run as test cases')
616 help=
'git branch to use as "before" for comparison. '
617 'Omitting this will use the current branch '
618 'without uncommitted changes as the baseline.')
621 help=
'git branch to use as "after" for comparison. '
622 'Omitting this will use the current branch '
623 'with uncommitted changes.')
626 default=os.path.join(
'out',
'Release'),
627 help=
'relative path from the base source directory '
628 'to the build directory')
630 '--build-dir-before',
631 help=
'relative path from the base source directory '
632 'to the build directory for the "before" branch, if '
633 'different from the build directory for the '
638 help=
'directory with a new or preexisting cache for '
639 'downloads. Default is to not use a cache.')
643 help=
'use the repository where the script is instead of '
644 'checking out a temporary one. This is faster and '
645 'does not require downloads, but although it '
646 'restores the state of the local repo, if the '
647 'script is killed or crashes the changes can remain '
648 'stashed and you may be on another branch.')
652 help=
'which profiler to use. Supports callgrind, '
653 'perfstat, and none. Default is callgrind.')
655 '--interesting-section',
657 help=
'whether to measure just the interesting section or '
658 'the whole test harness. Limiting to only the '
659 'interesting section does not work on Release since '
660 'the delimiters are optimized out')
663 help=
'selects some pages to be rendered. Page numbers '
664 'are 0-based. "--pages A" will render only page A. '
665 '"--pages A-B" will render pages A to B '
669 default=multiprocessing.cpu_count(),
671 help=
'run NUM_WORKERS jobs in parallel')
673 '--output-dir', help=
'directory to write the profile data output files')
677 help=
'outputs pngs to the specified directory that can '
678 'be compared with a static html generated. Will '
679 'affect performance measurements.')
684 help=
'Requires --png-dir. Threshold above which a png '
685 'is considered to have changed.')
687 '--threshold-significant',
690 help=
'variations in performance above this factor are '
691 'considered significant')
693 '--machine-readable',
695 help=
'whether to get output for machines. If enabled the '
696 'output will be a json with the format specified in '
697 'ComparisonConclusions.GetOutputDict(). Default is '
702 help=
'what key to use when sorting test cases in the '
703 'output. Accepted values are "after", "before", '
704 '"ratio" and "rating". Default is sorting by test '
707 args = parser.parse_args()
711 pdfium_src_dir = os.path.join(
712 os.path.dirname(__file__), os.path.pardir, os.path.pardir)
713 os.chdir(pdfium_src_dir)
717 if args.branch_after
and not args.branch_before:
718 PrintErr(
'--branch-after requires --branch-before to be specified.')
721 if args.branch_after
and not git.BranchExists(args.branch_after):
722 PrintErr(
'Branch "%s" does not exist' % args.branch_after)
725 if args.branch_before
and not git.BranchExists(args.branch_before):
726 PrintErr(
'Branch "%s" does not exist' % args.branch_before)
730 args.output_dir = os.path.expanduser(args.output_dir)
731 if not os.path.isdir(args.output_dir):
732 PrintErr(
'"%s" is not a directory' % args.output_dir)
736 args.png_dir = os.path.expanduser(args.png_dir)
737 if not os.path.isdir(args.png_dir):
738 PrintErr(
'"%s" is not a directory' % args.png_dir)
741 if args.threshold_significant <= 0.0:
742 PrintErr(
'--threshold-significant should receive a positive float')
745 if args.png_threshold:
747 PrintErr(
'--png-threshold requires --png-dir to be specified.')
750 if args.png_threshold <= 0.0:
751 PrintErr(
'--png-threshold should receive a positive float')
755 if not re.match(
r'^\d+(-\d+)?$', args.pages):
756 PrintErr(
'Supported formats for --pages are "--pages 7" and '
764if __name__ ==
'__main__':
list append(new Employee("Blackpool", "Stephen"))
static QDBusError::ErrorType get(const char *name)
QDebug print(QDebug debug, QSslError::SslError error)