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