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