| #!/usr/bin/env vpython3 | 
 | # | 
 | # Copyright 2019 The Chromium Authors | 
 | # Use of this source code is governed by a BSD-style license that can be | 
 | # found in the LICENSE file. | 
 |  | 
 | """A simple tool to run simpleperf to get sampling-based perf traces. | 
 |  | 
 | Typical Usage: | 
 |   android_webview/tools/run_simpleperf.py \ | 
 |     --report-path report.html \ | 
 |     --output-directory out/Debug/ | 
 | """ | 
 |  | 
 | import argparse | 
 | import html | 
 | import logging | 
 | import os | 
 | import re | 
 | import subprocess | 
 | import sys | 
 |  | 
 | sys.path.append(os.path.join( | 
 |     os.path.dirname(__file__), os.pardir, os.pardir, 'build', 'android')) | 
 | # pylint: disable=wrong-import-position,import-error | 
 | import devil_chromium | 
 | from devil.android import apk_helper | 
 | from devil.android import device_errors | 
 | from devil.android.ndk import abis | 
 | from devil.android.tools import script_common | 
 | from devil.utils import logging_common | 
 | from py_utils import tempfile_ext | 
 |  | 
 | _SUPPORTED_ARCH_DICT = { | 
 |     abis.ARM: 'arm', | 
 |     abis.ARM_64: 'arm64', | 
 |     abis.X86: 'x86', | 
 |     # Note: x86_64 isn't tested yet. | 
 | } | 
 |  | 
 |  | 
 | class StackAddressInterpreter: | 
 |   """A class to interpret addresses in simpleperf using stack script.""" | 
 |   def __init__(self, args, tmp_dir): | 
 |     self.args = args | 
 |     self.tmp_dir = tmp_dir | 
 |  | 
 |   @staticmethod | 
 |   def RunStackScript(output_dir, stack_input_path): | 
 |     """Run the stack script. | 
 |  | 
 |     Args: | 
 |       output_dir: The directory of Chromium output. | 
 |       stack_input_path: The path to the stack input file. | 
 |  | 
 |     Returns: | 
 |       The output of running the stack script (stack.py). | 
 |     """ | 
 |     # Note that stack script is not designed to be used in a stand-alone way. | 
 |     # Therefore, it is better off to call it as a command line. | 
 |     # TODO(changwan): consider using llvm symbolizer directly. | 
 |     cmd = ['third_party/android_platform/development/scripts/stack', | 
 |            '--output-directory', output_dir, | 
 |            stack_input_path] | 
 |     return subprocess.check_output(cmd, universal_newlines=True).splitlines() | 
 |  | 
 |   @staticmethod | 
 |   def _ConvertAddressToFakeTraceLine(address, lib_path): | 
 |     formatted_address = '0x' + '0' * (16 - len(address)) + address | 
 |     # Pretend that this is Chromium's stack traces output in logcat. | 
 |     # Note that the date, time, pid, tid, frame number, and frame address | 
 |     # are all fake and they are irrelevant. | 
 |     return ('11-15 00:00:00.000 11111 11111 ' | 
 |             'E chromium: #00 0x0000001111111111 %s+%s') % ( | 
 |                 lib_path, formatted_address) | 
 |  | 
 |   def Interpret(self, addresses, lib_path): | 
 |     """Interpret the given addresses. | 
 |  | 
 |     Args: | 
 |       addresses: A collection of addresses. | 
 |       lib_path: The path to the WebView library. | 
 |  | 
 |     Returns: | 
 |       A list of (address, function_info) where function_info is the function | 
 |       name, plus file name and line if args.show_file_line is set. | 
 |     """ | 
 |     stack_input_path = os.path.join(self.tmp_dir, 'stack_input.txt') | 
 |     with open(stack_input_path, 'w') as f: | 
 |       for address in addresses: | 
 |         f.write(StackAddressInterpreter._ConvertAddressToFakeTraceLine( | 
 |             address, lib_path) + '\n') | 
 |  | 
 |     stack_output = StackAddressInterpreter.RunStackScript( | 
 |         self.args.output_directory, stack_input_path) | 
 |  | 
 |     if self.args.debug: | 
 |       logging.debug('First 10 lines of stack output:') | 
 |       for i in range(max(10, len(stack_output))): | 
 |         logging.debug(stack_output[i]) | 
 |  | 
 |     logging.info('We got the results from the stack script. Translating the ' | 
 |                  'addresses...') | 
 |  | 
 |     address_function_pairs = [] | 
 |     pattern = re.compile(r'  0*(?P<address>[1-9a-f][0-9a-f]+)  (?P<function>.*)' | 
 |                          r'  (?P<file_name_line>.*)') | 
 |     for line in stack_output: | 
 |       m = pattern.match(line) | 
 |       if m: | 
 |         function_info = m.group('function') | 
 |         if self.args.show_file_line: | 
 |           function_info += " | " + m.group('file_name_line') | 
 |  | 
 |         address_function_pairs.append((m.group('address'), function_info)) | 
 |  | 
 |     logging.info('The translation is done.') | 
 |     return address_function_pairs | 
 |  | 
 |  | 
 | class SimplePerfRunner: | 
 |   """A runner for simpleperf and its postprocessing.""" | 
 |  | 
 |   def __init__(self, device, args, tmp_dir, address_interpreter): | 
 |     self.device = device | 
 |     self.address_interpreter = address_interpreter | 
 |     self.args = args | 
 |     self.apk_helper = None | 
 |     self.tmp_dir = tmp_dir | 
 |  | 
 |   def _GetFormattedArch(self): | 
 |     arch = _SUPPORTED_ARCH_DICT.get( | 
 |         self.device.product_cpu_abi) | 
 |     if not arch: | 
 |       raise Exception('Your device arch (' + | 
 |                       self.device.product_cpu_abi + ') is not supported.') | 
 |     logging.info('Guessing arch=%s because product.cpu.abi=%s', arch, | 
 |                  self.device.product_cpu_abi) | 
 |     return arch | 
 |  | 
 |   def GetWebViewLibraryNameAndPath(self, package_name): | 
 |     """Get WebView library name and path on the device.""" | 
 |     apk_path = self._GetWebViewApkPath(package_name) | 
 |     logging.debug('WebView APK path: %s', apk_path) | 
 |     # TODO(changwan): check if we need support for bundle. | 
 |     tmp_apk_path = os.path.join(self.tmp_dir, 'base.apk') | 
 |     self.device.adb.Pull(apk_path, tmp_apk_path) | 
 |     self.apk_helper = apk_helper.ToHelper(tmp_apk_path) | 
 |     metadata = self.apk_helper.GetAllMetadata() | 
 |     lib_name = None | 
 |     for key, value in metadata: | 
 |       if key == 'com.android.webview.WebViewLibrary': | 
 |         lib_name = value | 
 |  | 
 |     lib_path = os.path.join(apk_path, 'lib', self._GetFormattedArch(), lib_name) | 
 |     logging.debug("WebView's library path on the device should be: %s", | 
 |                   lib_path) | 
 |     return lib_name, lib_path | 
 |  | 
 |   def Run(self): | 
 |     """Run the simpleperf and do the post processing.""" | 
 |     package_name = self.GetCurrentWebViewProvider() | 
 |     SimplePerfRunner.RunPackageCompile(package_name) | 
 |     perf_data_path = os.path.join(self.tmp_dir, 'perf.data') | 
 |     SimplePerfRunner.RunSimplePerf(perf_data_path, self.args) | 
 |     lines = SimplePerfRunner.GetOriginalReportHtml( | 
 |         perf_data_path, | 
 |         os.path.join(self.tmp_dir, 'unprocessed_report.html')) | 
 |     lib_name, lib_path = self.GetWebViewLibraryNameAndPath(package_name) | 
 |     addresses = SimplePerfRunner.CollectAddresses(lines, lib_name) | 
 |     logging.info("Extracted %d addresses", len(addresses)) | 
 |     address_function_pairs = self.address_interpreter.Interpret( | 
 |         addresses, lib_path) | 
 |  | 
 |     lines = SimplePerfRunner.ReplaceAddressesWithFunctionInfos( | 
 |         lines, address_function_pairs, lib_name) | 
 |  | 
 |     with open(self.args.report_path, 'w') as f: | 
 |       for line in lines: | 
 |         f.write(line + '\n') | 
 |  | 
 |     logging.info("The final report has been generated at '%s'.", | 
 |                  self.args.report_path) | 
 |  | 
 |   @staticmethod | 
 |   def RunSimplePerf(perf_data_path, args): | 
 |     """Runs the simple perf commandline.""" | 
 |     cmd = [ | 
 |         'third_party/android_toolchain/ndk/simpleperf/app_profiler.py', | 
 |         '--perf_data_path', perf_data_path, '--skip_collect_binaries' | 
 |     ] | 
 |     if args.system_wide: | 
 |       cmd.append('--system_wide') | 
 |     else: | 
 |       cmd.extend([ | 
 |           '--app', 'org.chromium.webview_shell', '--activity', | 
 |           '.TelemetryActivity' | 
 |       ]) | 
 |  | 
 |     if args.record_options: | 
 |       cmd.extend(['--record_options', args.record_options]) | 
 |  | 
 |     logging.info("Profile has started.") | 
 |     subprocess.check_call(cmd) | 
 |     logging.info("Profile has finished, processing the results...") | 
 |  | 
 |   @staticmethod | 
 |   def RunPackageCompile(package_name): | 
 |     """Compile the package (dex optimization).""" | 
 |     cmd = [ | 
 |         'adb', 'shell', 'cmd', 'package', 'compile', '-m', 'speed', '-f', | 
 |         package_name | 
 |     ] | 
 |     subprocess.check_call(cmd) | 
 |  | 
 |   def GetCurrentWebViewProvider(self): | 
 |     return self.device.GetWebViewUpdateServiceDump()['CurrentWebViewPackage'] | 
 |  | 
 |   def _GetWebViewApkPath(self, package_name): | 
 |     return self.device.GetApplicationPaths(package_name)[0] | 
 |  | 
 |   @staticmethod | 
 |   def GetOriginalReportHtml(perf_data_path, report_html_path): | 
 |     """Gets the original report.html from running simpleperf.""" | 
 |     cmd = [ | 
 |         'third_party/android_toolchain/ndk/simpleperf/report_html.py', | 
 |         '--record_file', perf_data_path, '--report_path', report_html_path, | 
 |         '--no_browser' | 
 |     ] | 
 |     subprocess.check_call(cmd) | 
 |     lines = [] | 
 |     with open(report_html_path, 'r') as f: | 
 |       lines = f.readlines() | 
 |     return lines | 
 |  | 
 |   @staticmethod | 
 |   def CollectAddresses(lines, lib_name): | 
 |     """Collect address-looking texts from lines. | 
 |  | 
 |     Args: | 
 |       lines: A list of strings that may contain addresses. | 
 |       lib_name: The name of the WebView library. | 
 |  | 
 |     Returns: | 
 |       A set containing the addresses that were found in the lines. | 
 |     """ | 
 |     addresses = set() | 
 |     for line in lines: | 
 |       for address in re.findall(lib_name + r'\[\+([0-9a-f]+)\]', line): | 
 |         addresses.add(address) | 
 |     return addresses | 
 |  | 
 |   @staticmethod | 
 |   def ReplaceAddressesWithFunctionInfos(lines, address_function_pairs, | 
 |                                         lib_name): | 
 |     """Replaces the addresses with function names. | 
 |  | 
 |     Args: | 
 |       lines: A list of strings that may contain addresses. | 
 |       address_function_pairs: A list of pairs of (address, function_name). | 
 |       lib_name: The name of the WebView library. | 
 |  | 
 |     Returns: | 
 |       A list of strings with addresses replaced by function names. | 
 |     """ | 
 |  | 
 |     logging.info('Replacing the HTML content with new function names...') | 
 |  | 
 |     # Note: Using a lenient pattern matching and a hashmap (dict) is much faster | 
 |     # than using a double loop (by the order of 1,000). | 
 |     # '+address' will be replaced by function name. | 
 |     address_function_dict = { | 
 |         '+' + k: html.escape(v, quote=False) | 
 |         for k, v in address_function_pairs | 
 |     } | 
 |     # Look behind the lib_name and '[' which will not be substituted. Note that | 
 |     # '+' is used in the pattern but will be removed. | 
 |     pattern = re.compile(r'(?<=' + lib_name + r'\[)\+([a-f0-9]+)(?=\])') | 
 |  | 
 |     def replace_fn(match): | 
 |       address = match.group(0) | 
 |       if address in address_function_dict: | 
 |         return address_function_dict[address] | 
 |       return address | 
 |  | 
 |     # Line-by-line assignment to avoid creating a temp list. | 
 |     for i, line in enumerate(lines): | 
 |       lines[i] = pattern.sub(replace_fn, line) | 
 |  | 
 |     logging.info('Replacing is done.') | 
 |     return lines | 
 |  | 
 |  | 
 | def main(raw_args): | 
 |   parser = argparse.ArgumentParser() | 
 |   parser.add_argument('--debug', action='store_true', | 
 |                       help='Get additional debugging mode') | 
 |   parser.add_argument( | 
 |       '--output-directory', | 
 |       help='the path to the build output directory, such as out/Debug') | 
 |   parser.add_argument('--report-path', | 
 |                       default='report.html', help='Report path') | 
 |   parser.add_argument('--adb-path', | 
 |                       help='Absolute path to the adb binary to use.') | 
 |   parser.add_argument('--record-options', | 
 |                       help=('Set recording options for app_profiler.py command.' | 
 |                             ' Example: "-e task-clock:u -f 1000 -g --duration' | 
 |                             ' 10" where -f means sampling frequency per second.' | 
 |                             ' Try `app_profiler.py record -h` for more ' | 
 |                             ' information. Note that not setting this defaults' | 
 |                             ' to the default record options.')) | 
 |   parser.add_argument('--show-file-line', action='store_true', | 
 |                       help='Show file name and lines in the result.') | 
 |   parser.add_argument( | 
 |       '--system-wide', | 
 |       action='store_true', | 
 |       help=('Whether to profile system wide (without launching' | 
 |             'an app).')) | 
 |  | 
 |   script_common.AddDeviceArguments(parser) | 
 |   logging_common.AddLoggingArguments(parser) | 
 |  | 
 |   args = parser.parse_args(raw_args) | 
 |   logging_common.InitializeLogging(args) | 
 |   devil_chromium.Initialize(adb_path=args.adb_path) | 
 |  | 
 |   devices = script_common.GetDevices(args.devices, args.denylist_file) | 
 |   device = devices[0] | 
 |  | 
 |   if len(devices) > 1: | 
 |     raise device_errors.MultipleDevicesError(devices) | 
 |  | 
 |   with tempfile_ext.NamedTemporaryDirectory( | 
 |       prefix='tmp_simpleperf') as tmp_dir: | 
 |     runner = SimplePerfRunner( | 
 |         device, args, tmp_dir, | 
 |         StackAddressInterpreter(args, tmp_dir)) | 
 |     runner.Run() | 
 |  | 
 |  | 
 | if __name__ == '__main__': | 
 |   main(sys.argv[1:]) |