|  | #!/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. | 
|  | """Measures performance for rendering a single test case with pdfium. | 
|  |  | 
|  | The output is a number that is a metric which depends on the profiler specified. | 
|  | """ | 
|  |  | 
|  | import argparse | 
|  | import os | 
|  | import re | 
|  | import subprocess | 
|  | import sys | 
|  |  | 
|  | from common import PrintErr | 
|  |  | 
|  | CALLGRIND_PROFILER = 'callgrind' | 
|  | PERFSTAT_PROFILER = 'perfstat' | 
|  | NONE_PROFILER = 'none' | 
|  |  | 
|  | PDFIUM_TEST = 'pdfium_test' | 
|  |  | 
|  |  | 
|  | class PerformanceRun: | 
|  | """A single measurement of a test case.""" | 
|  |  | 
|  | def __init__(self, args): | 
|  | self.args = args | 
|  | self.pdfium_test_path = os.path.join(self.args.build_dir, PDFIUM_TEST) | 
|  |  | 
|  | def _CheckTools(self): | 
|  | """Returns whether the tool file paths are sane.""" | 
|  | if not os.path.exists(self.pdfium_test_path): | 
|  | PrintErr( | 
|  | "FAILURE: Can't find test executable '%s'" % self.pdfium_test_path) | 
|  | PrintErr('Use --build-dir to specify its location.') | 
|  | return False | 
|  | if not os.access(self.pdfium_test_path, os.X_OK): | 
|  | PrintErr("FAILURE: Test executable '%s' lacks execution permissions" % | 
|  | self.pdfium_test_path) | 
|  | return False | 
|  | return True | 
|  |  | 
|  | def Run(self): | 
|  | """Runs test harness and measures performance with the given profiler. | 
|  |  | 
|  | Returns: | 
|  | Exit code for the script. | 
|  | """ | 
|  | if not self._CheckTools(): | 
|  | return 1 | 
|  |  | 
|  | if self.args.profiler == CALLGRIND_PROFILER: | 
|  | time = self._RunCallgrind() | 
|  | elif self.args.profiler == PERFSTAT_PROFILER: | 
|  | time = self._RunPerfStat() | 
|  | elif self.args.profiler == NONE_PROFILER: | 
|  | time = self._RunWithoutProfiler() | 
|  | else: | 
|  | PrintErr('profiler=%s not supported, aborting' % self.args.profiler) | 
|  | return 1 | 
|  |  | 
|  | if time is None: | 
|  | return 1 | 
|  |  | 
|  | print(time) | 
|  | return 0 | 
|  |  | 
|  | def _RunCallgrind(self): | 
|  | """Runs test harness and measures performance with callgrind. | 
|  |  | 
|  | Returns: | 
|  | int with the result of the measurement, in instructions or time. | 
|  | """ | 
|  | # Whether to turn instrument the whole run or to use the callgrind macro | 
|  | # delimiters in pdfium_test. | 
|  | instrument_at_start = 'no' if self.args.interesting_section else 'yes' | 
|  | output_path = self.args.output_path or '/dev/null' | 
|  |  | 
|  | valgrind_cmd = ([ | 
|  | 'valgrind', '--tool=callgrind', | 
|  | '--instr-atstart=%s' % instrument_at_start, | 
|  | '--callgrind-out-file=%s' % output_path | 
|  | ] + self._BuildTestHarnessCommand()) | 
|  | output = subprocess.check_output( | 
|  | valgrind_cmd, stderr=subprocess.STDOUT).decode('utf-8') | 
|  |  | 
|  | # Match the line with the instruction count, eg. | 
|  | # '==98765== Collected : 12345' | 
|  | return self._ExtractIrCount(r'\bCollected\b *: *\b(\d+)', output) | 
|  |  | 
|  | def _RunPerfStat(self): | 
|  | """Runs test harness and measures performance with perf stat. | 
|  |  | 
|  | Returns: | 
|  | int with the result of the measurement, in instructions or time. | 
|  | """ | 
|  | # --no-big-num: do not add thousands separators | 
|  | # -einstructions: print only instruction count | 
|  | cmd_to_run = (['perf', 'stat', '--no-big-num', '-einstructions'] + | 
|  | self._BuildTestHarnessCommand()) | 
|  | output = subprocess.check_output( | 
|  | cmd_to_run, stderr=subprocess.STDOUT).decode('utf-8') | 
|  |  | 
|  | # Match the line with the instruction count, eg. | 
|  | # '        12345      instructions' | 
|  | return self._ExtractIrCount(r'\b(\d+)\b.*\binstructions\b', output) | 
|  |  | 
|  | def _RunWithoutProfiler(self): | 
|  | """Runs test harness and measures performance without a profiler. | 
|  |  | 
|  | Returns: | 
|  | int with the result of the measurement, in instructions or time. In this | 
|  | case, always return 1 since no profiler is being used. | 
|  | """ | 
|  | cmd_to_run = self._BuildTestHarnessCommand() | 
|  | subprocess.check_output(cmd_to_run, stderr=subprocess.STDOUT) | 
|  |  | 
|  | # Return 1 for every run. | 
|  | return 1 | 
|  |  | 
|  | def _BuildTestHarnessCommand(self): | 
|  | """Builds command to run the test harness.""" | 
|  | cmd = [self.pdfium_test_path, '--send-events'] | 
|  |  | 
|  | if self.args.interesting_section: | 
|  | cmd.append('--callgrind-delim') | 
|  | if self.args.png: | 
|  | cmd.append('--png') | 
|  | if self.args.pages: | 
|  | cmd.append('--pages=%s' % self.args.pages) | 
|  |  | 
|  | cmd.append(self.args.pdf_path) | 
|  | return cmd | 
|  |  | 
|  | def _ExtractIrCount(self, regex, output): | 
|  | """Extracts a number from the output with a regex.""" | 
|  | matched = re.search(regex, output) | 
|  |  | 
|  | if not matched: | 
|  | return None | 
|  |  | 
|  | # Group 1 is the instruction number, eg. 12345 | 
|  | return int(matched.group(1)) | 
|  |  | 
|  |  | 
|  | def main(): | 
|  | parser = argparse.ArgumentParser() | 
|  | parser.add_argument( | 
|  | 'pdf_path', help='test case to measure load and rendering time') | 
|  | parser.add_argument( | 
|  | '--build-dir', | 
|  | default=os.path.join('out', 'Release'), | 
|  | help='relative path to the build directory with ' | 
|  | '%s' % PDFIUM_TEST) | 
|  | parser.add_argument( | 
|  | '--profiler', | 
|  | default=CALLGRIND_PROFILER, | 
|  | help='which profiler to use. Supports callgrind, ' | 
|  | 'perfstat, and none.') | 
|  | parser.add_argument( | 
|  | '--interesting-section', | 
|  | action='store_true', | 
|  | help='whether to measure just the interesting section or ' | 
|  | 'the whole test harness. The interesting section is ' | 
|  | 'pdfium reading a pdf from memory and rendering ' | 
|  | 'it, which omits loading the time to load the file, ' | 
|  | 'initialize the library, terminate it, etc. ' | 
|  | 'Limiting to only the interesting section does not ' | 
|  | 'work on Release since the delimiters are optimized ' | 
|  | 'out. Callgrind only.') | 
|  | parser.add_argument( | 
|  | '--png', | 
|  | action='store_true', | 
|  | help='outputs a png image on the same location as the ' | 
|  | 'pdf file') | 
|  | 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( | 
|  | '--output-path', help='where to write the profile data output file') | 
|  | args = parser.parse_args() | 
|  |  | 
|  | if args.interesting_section and args.profiler != CALLGRIND_PROFILER: | 
|  | PrintErr('--interesting-section requires profiler to be callgrind.') | 
|  | return 1 | 
|  |  | 
|  | run = PerformanceRun(args) | 
|  | return run.Run() | 
|  |  | 
|  |  | 
|  | if __name__ == '__main__': | 
|  | sys.exit(main()) |