#!/usr/bin/env vpython3 # # Copyright 2019 The Chromium Authors. All rights reserved. # 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 logging import os import re import subprocess import sys import six if six.PY2: import cgi as html else: import html 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
[1-9a-f][0-9a-f]+) (?P.*)' r' (?P.*)') 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_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_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:])