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 6"""Measures performance for rendering a single test case with pdfium. 7 8The output is a number that is a metric which depends on the profiler specified. 9""" 10 11import argparse 12import os 13import re 14import subprocess 15import sys 16 17from common import PrintErr 18 19 20CALLGRIND_PROFILER = 'callgrind' 21PERFSTAT_PROFILER = 'perfstat' 22 23PDFIUM_TEST = 'pdfium_test' 24 25class PerformanceRun(object): 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("FAILURE: Can't find test executable '%s'" 36 % 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 else: 59 PrintErr('profiler=%s not supported, aborting' % self.args.profiler) 60 return 1 61 62 if time is None: 63 return 1 64 65 print time 66 return 0 67 68 def _RunCallgrind(self): 69 """Runs test harness and measures performance with callgrind. 70 71 Returns: 72 int with the result of the measurement, in instructions or time. 73 """ 74 # Whether to turn instrument the whole run or to use the callgrind macro 75 # delimiters in pdfium_test. 76 instrument_at_start = 'no' if self.args.interesting_section else 'yes' 77 output_path = self.args.output_path or '/dev/null' 78 79 valgrind_cmd = (['valgrind', '--tool=callgrind', 80 '--instr-atstart=%s' % instrument_at_start, 81 '--callgrind-out-file=%s' % output_path] 82 + self._BuildTestHarnessCommand()) 83 output = subprocess.check_output(valgrind_cmd, stderr=subprocess.STDOUT) 84 85 # Match the line with the instruction count, eg. 86 # '==98765== Collected : 12345' 87 return self._ExtractIrCount(r'\bCollected\b *: *\b(\d+)', output) 88 89 def _RunPerfStat(self): 90 """Runs test harness and measures performance with perf stat. 91 92 Returns: 93 int with the result of the measurement, in instructions or time. 94 """ 95 # --no-big-num: do not add thousands separators 96 # -einstructions: print only instruction count 97 cmd_to_run = (['perf', 'stat', '--no-big-num', '-einstructions'] 98 + self._BuildTestHarnessCommand()) 99 output = subprocess.check_output(cmd_to_run, stderr=subprocess.STDOUT) 100 101 # Match the line with the instruction count, eg. 102 # ' 12345 instructions' 103 return self._ExtractIrCount(r'\b(\d+)\b.*\binstructions\b', output) 104 105 def _BuildTestHarnessCommand(self): 106 """Builds command to run the test harness.""" 107 cmd = [self.pdfium_test_path, '--send-events'] 108 if self.args.interesting_section: 109 cmd.append('--callgrind-delim') 110 cmd.append(self.args.pdf_path) 111 return cmd 112 113 def _ExtractIrCount(self, regex, output): 114 """Extracts a number from the output with a regex.""" 115 matched = re.search(regex, output) 116 117 if not matched: 118 return None 119 120 # Group 1 is the instruction number, eg. 12345 121 return int(matched.group(1)) 122 123 124def main(): 125 parser = argparse.ArgumentParser() 126 parser.add_argument('pdf_path', 127 help='test case to measure load and rendering time') 128 parser.add_argument('--build-dir', default=os.path.join('out', 'Release'), 129 help='relative path to the build directory with ' 130 '%s' % PDFIUM_TEST) 131 parser.add_argument('--profiler', default=CALLGRIND_PROFILER, 132 help='which profiler to use. Supports callgrind and ' 133 'perfstat for now.') 134 parser.add_argument('--interesting-section', action='store_true', 135 help='whether to measure just the interesting section or ' 136 'the whole test harness. The interesting section is ' 137 'pdfium reading a pdf from memory and rendering ' 138 'it, which omits loading the time to load the file, ' 139 'initialize the library, terminate it, etc. ' 140 'Limiting to only the interesting section does not ' 141 'work on Release since the delimiters are optimized ' 142 'out. Callgrind only.') 143 parser.add_argument('--output-path', 144 help='where to write the profile data output file') 145 args = parser.parse_args() 146 147 if args.interesting_section and args.profiler != CALLGRIND_PROFILER: 148 PrintErr('--interesting-section requires profiler to be callgrind.') 149 return 1 150 151 run = PerformanceRun(args) 152 return run.Run() 153 154 155if __name__ == '__main__': 156 sys.exit(main()) 157