1#!/usr/bin/env python3 2# Copyright 2017 The PDFium Authors 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5"""Measures performance for rendering a single test case with pdfium. 6 7The output is a number that is a metric which depends on the profiler specified. 8""" 9 10import argparse 11import os 12import re 13import subprocess 14import sys 15 16from common import PrintErr 17 18CALLGRIND_PROFILER = 'callgrind' 19PERFSTAT_PROFILER = 'perfstat' 20NONE_PROFILER = 'none' 21 22PDFIUM_TEST = 'pdfium_test' 23 24 25class PerformanceRun: 26 """A single measurement of a test case.""" 27 28 def __init__(self, args): 29 self.args = args 30 self.pdfium_test_path = os.path.join(self.args.build_dir, PDFIUM_TEST) 31 32 def _CheckTools(self): 33 """Returns whether the tool file paths are sane.""" 34 if not os.path.exists(self.pdfium_test_path): 35 PrintErr( 36 "FAILURE: Can't find test executable '%s'" % self.pdfium_test_path) 37 PrintErr('Use --build-dir to specify its location.') 38 return False 39 if not os.access(self.pdfium_test_path, os.X_OK): 40 PrintErr("FAILURE: Test executable '%s' lacks execution permissions" % 41 self.pdfium_test_path) 42 return False 43 return True 44 45 def Run(self): 46 """Runs test harness and measures performance with the given profiler. 47 48 Returns: 49 Exit code for the script. 50 """ 51 if not self._CheckTools(): 52 return 1 53 54 if self.args.profiler == CALLGRIND_PROFILER: 55 time = self._RunCallgrind() 56 elif self.args.profiler == PERFSTAT_PROFILER: 57 time = self._RunPerfStat() 58 elif self.args.profiler == NONE_PROFILER: 59 time = self._RunWithoutProfiler() 60 else: 61 PrintErr('profiler=%s not supported, aborting' % self.args.profiler) 62 return 1 63 64 if time is None: 65 return 1 66 67 print(time) 68 return 0 69 70 def _RunCallgrind(self): 71 """Runs test harness and measures performance with callgrind. 72 73 Returns: 74 int with the result of the measurement, in instructions or time. 75 """ 76 # Whether to turn instrument the whole run or to use the callgrind macro 77 # delimiters in pdfium_test. 78 instrument_at_start = 'no' if self.args.interesting_section else 'yes' 79 output_path = self.args.output_path or '/dev/null' 80 81 valgrind_cmd = ([ 82 'valgrind', '--tool=callgrind', 83 '--instr-atstart=%s' % instrument_at_start, 84 '--callgrind-out-file=%s' % output_path 85 ] + self._BuildTestHarnessCommand()) 86 output = subprocess.check_output( 87 valgrind_cmd, stderr=subprocess.STDOUT).decode('utf-8') 88 89 # Match the line with the instruction count, eg. 90 # '==98765== Collected : 12345' 91 return self._ExtractIrCount(r'\bCollected\b *: *\b(\d+)', output) 92 93 def _RunPerfStat(self): 94 """Runs test harness and measures performance with perf stat. 95 96 Returns: 97 int with the result of the measurement, in instructions or time. 98 """ 99 # --no-big-num: do not add thousands separators 100 # -einstructions: print only instruction count 101 cmd_to_run = (['perf', 'stat', '--no-big-num', '-einstructions'] + 102 self._BuildTestHarnessCommand()) 103 output = subprocess.check_output( 104 cmd_to_run, stderr=subprocess.STDOUT).decode('utf-8') 105 106 # Match the line with the instruction count, eg. 107 # ' 12345 instructions' 108 return self._ExtractIrCount(r'\b(\d+)\b.*\binstructions\b', output) 109 110 def _RunWithoutProfiler(self): 111 """Runs test harness and measures performance without a profiler. 112 113 Returns: 114 int with the result of the measurement, in instructions or time. In this 115 case, always return 1 since no profiler is being used. 116 """ 117 cmd_to_run = self._BuildTestHarnessCommand() 118 subprocess.check_output(cmd_to_run, stderr=subprocess.STDOUT) 119 120 # Return 1 for every run. 121 return 1 122 123 def _BuildTestHarnessCommand(self): 124 """Builds command to run the test harness.""" 125 cmd = [self.pdfium_test_path, '--send-events'] 126 127 if self.args.interesting_section: 128 cmd.append('--callgrind-delim') 129 if self.args.png: 130 cmd.append('--png') 131 if self.args.pages: 132 cmd.append('--pages=%s' % self.args.pages) 133 134 cmd.append(self.args.pdf_path) 135 return cmd 136 137 def _ExtractIrCount(self, regex, output): 138 """Extracts a number from the output with a regex.""" 139 matched = re.search(regex, output) 140 141 if not matched: 142 return None 143 144 # Group 1 is the instruction number, eg. 12345 145 return int(matched.group(1)) 146 147 148def main(): 149 parser = argparse.ArgumentParser() 150 parser.add_argument( 151 'pdf_path', help='test case to measure load and rendering time') 152 parser.add_argument( 153 '--build-dir', 154 default=os.path.join('out', 'Release'), 155 help='relative path to the build directory with ' 156 '%s' % PDFIUM_TEST) 157 parser.add_argument( 158 '--profiler', 159 default=CALLGRIND_PROFILER, 160 help='which profiler to use. Supports callgrind, ' 161 'perfstat, and none.') 162 parser.add_argument( 163 '--interesting-section', 164 action='store_true', 165 help='whether to measure just the interesting section or ' 166 'the whole test harness. The interesting section is ' 167 'pdfium reading a pdf from memory and rendering ' 168 'it, which omits loading the time to load the file, ' 169 'initialize the library, terminate it, etc. ' 170 'Limiting to only the interesting section does not ' 171 'work on Release since the delimiters are optimized ' 172 'out. Callgrind only.') 173 parser.add_argument( 174 '--png', 175 action='store_true', 176 help='outputs a png image on the same location as the ' 177 'pdf file') 178 parser.add_argument( 179 '--pages', 180 help='selects some pages to be rendered. Page numbers ' 181 'are 0-based. "--pages A" will render only page A. ' 182 '"--pages A-B" will render pages A to B ' 183 '(inclusive).') 184 parser.add_argument( 185 '--output-path', help='where to write the profile data output file') 186 args = parser.parse_args() 187 188 if args.interesting_section and args.profiler != CALLGRIND_PROFILER: 189 PrintErr('--interesting-section requires profiler to be callgrind.') 190 return 1 191 192 run = PerformanceRun(args) 193 return run.Run() 194 195 196if __name__ == '__main__': 197 sys.exit(main()) 198