From 926c65a49ca4769274022678e53144fe335c70b5 Mon Sep 17 00:00:00 2001 From: Henrique Nakashima Date: Thu, 20 Jul 2017 15:48:06 -0400 Subject: Add safetynet_measure.py to measure pdfium performance. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a script that measures the performance of pdfium when loading and rendering a single pdf file. Supported profilers for now are callgrind (default) and perfstat. It will be used by the compare script, which will compare measurements this script takes. Change-Id: I9c78ec879a7f4839e60f217d5dd8ee35b1e9a051 Reviewed-on: https://pdfium-review.googlesource.com/7354 Reviewed-by: Lei Zhang Reviewed-by: Nicolás Peña Commit-Queue: Henrique Nakashima --- testing/tools/safetynet_measure.py | 151 +++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100755 testing/tools/safetynet_measure.py diff --git a/testing/tools/safetynet_measure.py b/testing/tools/safetynet_measure.py new file mode 100755 index 0000000000..32fd7baa88 --- /dev/null +++ b/testing/tools/safetynet_measure.py @@ -0,0 +1,151 @@ +#!/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. +""" + +import argparse +import os +import re +import subprocess +import sys + + +CALLGRIND_PROFILER = 'callgrind' +PERFSTAT_PROFILER = 'perfstat' + + +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): + print "FAILURE: Can't find test executable '%s'" % self.pdfium_test_path + print 'Use --build-dir to specify its location.' + return False + if not os.access(self.pdfium_test_path, os.X_OK): + print ("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() + else: + print '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 _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') + 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 from the base source directory') + parser.add_argument('--profiler', default=CALLGRIND_PROFILER, + help='which profiler to use. Supports callgrind and ' + 'perfstat for now.') + 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('--output-path', + help='where to write the profile data output file') + args = parser.parse_args() + + if args.interesting_section and args.profiler != CALLGRIND_PROFILER: + print '--interesting-section requires profiler to be callgrind.' + return 1 + + run = PerformanceRun(args) + return run.Run() + + +if __name__ == '__main__': + sys.exit(main()) -- cgit v1.2.3