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 = [
'autoninja',
'-C', build_dir,
'pdfium_test']
390 RunCommandPropagateErr(cmd, stdout_has_errors=
True, exit_status_on_error=1)
394 PrintErr(
'Measuring...')
396 results = self.
_RunAsync(run_label, build_dir)
398 results = self.
_RunSync(run_label, build_dir)
404 """Profiles the test cases synchronously.
407 run_label: String to differentiate this version of the code in output
408 files from other versions.
409 build_dir: String with path to build directory
412 A dict mapping each test case name to the profiling values for that
419 if result
is not None:
420 results[test_case] = result
425 """Profiles the test cases asynchronously.
427 Uses as many workers as configured by --num-workers.
430 run_label: String to differentiate this version of the code in output
431 files from other versions.
432 build_dir: String with path to build directory
435 A dict mapping each test case name to the profiling values for that
439 pool = multiprocessing.Pool(self.
args.num_workers)
440 worker_func = functools.partial(RunSingleTestCaseParallel, self, run_label,
446 one_year_in_seconds = 3600 * 24 * 365
448 pool.map_async(worker_func, self.
test_cases).
get(one_year_in_seconds))
449 for worker_result
in worker_results:
450 test_case, result = worker_result
451 if result
is not None:
452 results[test_case] = result
453 except KeyboardInterrupt:
464 """Profiles a single test case.
467 run_label: String to differentiate this version of the code in output
468 files from other versions.
469 build_dir: String with path to build directory
470 test_case: Path to the test case.
473 The measured profiling value for that test case.
477 '--build-dir=%s' % build_dir
480 if self.
args.interesting_section:
481 command.append(
'--interesting-section')
483 if self.
args.profiler:
484 command.append(
'--profiler=%s' % self.
args.profiler)
487 if profile_file_path:
488 command.append(
'--output-path=%s' % profile_file_path)
490 if self.
args.png_dir:
491 command.append(
'--png')
494 command.extend([
'--pages', self.
args.pages])
496 output = RunCommandPropagateErr(command)
501 if self.
args.png_dir:
505 output = output.strip()
506 if re.match(
'^[0-9]+$', output):
512 png_dir = os.path.join(self.
args.png_dir, run_label)
513 if not os.path.exists(png_dir):
516 test_case_dir, test_case_filename = os.path.split(test_case)
517 test_case_png_matcher =
'%s.*.png' % test_case_filename
518 for output_png
in glob.glob(
519 os.path.join(test_case_dir, test_case_png_matcher)):
520 shutil.move(output_png, png_dir)
523 if self.
args.output_dir:
525 'callgrind.out.%s.%s' % (test_case.replace(
'/',
'_'), run_label))
526 return os.path.join(self.
args.output_dir, output_filename)
530 """Draws conclusions comparing results of test runs in two branches.
533 times_before_branch: A dict mapping each test case name to the
534 profiling values for that test case in the branch to be considered
536 times_after_branch: A dict mapping each test case name to the
537 profiling values for that test case in the branch to be considered
541 ComparisonConclusions with all test cases processed.
546 before = times_before_branch.get(test_case)
547 after = times_after_branch.get(test_case)
548 conclusions.ProcessCase(test_case, before, after)
553 """Prints the conclusions as the script output.
555 Depending on the script args, this can output a human or a machine-readable
556 version of the conclusions.
559 conclusions_dict: Dict to print returned from
560 ComparisonConclusions.GetOutputDict().
562 if self.
args.machine_readable:
563 print(json.dumps(conclusions_dict))
565 PrintConclusionsDictHumanReadable(
566 conclusions_dict, colored=
True, key=self.
args.case_order)
569 """Removes profile output files for uninteresting cases.
571 Cases without significant regressions or improvements and considered
575 conclusions: A ComparisonConclusions.
577 if not self.
args.output_dir:
580 if self.
args.profiler !=
'callgrind':
583 for case_result
in conclusions.GetCaseResults().values():
584 if case_result.rating
not in [RATING_REGRESSION, RATING_IMPROVEMENT]:
589 """Removes one profile output file.
591 If the output file does not exist, fails silently.
594 run_label: String to differentiate a version of the code in output
595 files from other versions.
596 case_name: String identifying test case for which to remove the output
606 parser = argparse.ArgumentParser()
610 help=
'pdf files or directories to search for pdf files '
611 'to run as test cases')
614 help=
'git branch to use as "before" for comparison. '
615 'Omitting this will use the current branch '
616 'without uncommitted changes as the baseline.')
619 help=
'git branch to use as "after" for comparison. '
620 'Omitting this will use the current branch '
621 'with uncommitted changes.')
624 default=os.path.join(
'out',
'Release'),
625 help=
'relative path from the base source directory '
626 'to the build directory')
628 '--build-dir-before',
629 help=
'relative path from the base source directory '
630 'to the build directory for the "before" branch, if '
631 'different from the build directory for the '
636 help=
'directory with a new or preexisting cache for '
637 'downloads. Default is to not use a cache.')
641 help=
'use the repository where the script is instead of '
642 'checking out a temporary one. This is faster and '
643 'does not require downloads, but although it '
644 'restores the state of the local repo, if the '
645 'script is killed or crashes the changes can remain '
646 'stashed and you may be on another branch.')
650 help=
'which profiler to use. Supports callgrind, '
651 'perfstat, and none. Default is callgrind.')
653 '--interesting-section',
655 help=
'whether to measure just the interesting section or '
656 'the whole test harness. Limiting to only the '
657 'interesting section does not work on Release since '
658 'the delimiters are optimized out')
661 help=
'selects some pages to be rendered. Page numbers '
662 'are 0-based. "--pages A" will render only page A. '
663 '"--pages A-B" will render pages A to B '
667 default=multiprocessing.cpu_count(),
669 help=
'run NUM_WORKERS jobs in parallel')
671 '--output-dir', help=
'directory to write the profile data output files')
675 help=
'outputs pngs to the specified directory that can '
676 'be compared with a static html generated. Will '
677 'affect performance measurements.')
682 help=
'Requires --png-dir. Threshold above which a png '
683 'is considered to have changed.')
685 '--threshold-significant',
688 help=
'variations in performance above this factor are '
689 'considered significant')
691 '--machine-readable',
693 help=
'whether to get output for machines. If enabled the '
694 'output will be a json with the format specified in '
695 'ComparisonConclusions.GetOutputDict(). Default is '
700 help=
'what key to use when sorting test cases in the '
701 'output. Accepted values are "after", "before", '
702 '"ratio" and "rating". Default is sorting by test '
705 args = parser.parse_args()
709 pdfium_src_dir = os.path.join(
710 os.path.dirname(__file__), os.path.pardir, os.path.pardir)
711 os.chdir(pdfium_src_dir)
715 if args.branch_after
and not args.branch_before:
716 PrintErr(
'--branch-after requires --branch-before to be specified.')
719 if args.branch_after
and not git.BranchExists(args.branch_after):
720 PrintErr(
'Branch "%s" does not exist' % args.branch_after)
723 if args.branch_before
and not git.BranchExists(args.branch_before):
724 PrintErr(
'Branch "%s" does not exist' % args.branch_before)
728 args.output_dir = os.path.expanduser(args.output_dir)
729 if not os.path.isdir(args.output_dir):
730 PrintErr(
'"%s" is not a directory' % args.output_dir)
734 args.png_dir = os.path.expanduser(args.png_dir)
735 if not os.path.isdir(args.png_dir):
736 PrintErr(
'"%s" is not a directory' % args.png_dir)
739 if args.threshold_significant <= 0.0:
740 PrintErr(
'--threshold-significant should receive a positive float')
743 if args.png_threshold:
745 PrintErr(
'--png-threshold requires --png-dir to be specified.')
748 if args.png_threshold <= 0.0:
749 PrintErr(
'--png-threshold should receive a positive float')
753 if not re.match(
r'^\d+(-\d+)?$', args.pages):
754 PrintErr(
'Supported formats for --pages are "--pages 7" and '
762if __name__ ==
'__main__':
static QDBusError::ErrorType get(const char *name)
QDebug print(QDebug debug, QSslError::SslError error)
multiPart append(textPart)