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