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