| #!/usr/bin/env python |
| # Copyright 2017 The PDFium Authors. All rights reserved. |
| # 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. |
| """ |
| |
| from __future__ import print_function |
| |
| import argparse |
| import os |
| import re |
| import subprocess |
| import sys |
| |
| # pylint: disable=relative-import |
| from common import PrintErr |
| |
| CALLGRIND_PROFILER = 'callgrind' |
| PERFSTAT_PROFILER = 'perfstat' |
| NONE_PROFILER = 'none' |
| |
| PDFIUM_TEST = 'pdfium_test' |
| |
| |
| class PerformanceRun(object): |
| """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) |
| |
| # 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) |
| |
| # 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()) |