1#!/usr/bin/env python 2# 3# Copyright 2019 The Chromium Authors. All rights reserved. 4# Use of this source code is governed by a BSD-style license that can be 5# found in the LICENSE file. 6 7"""A simple tool to run simpleperf to get sampling-based perf traces. 8 9Typical Usage: 10 android_webview/tools/run_simpleperf.py \ 11 --report-path report.html \ 12 --output-directory out/Debug/ 13""" 14 15import argparse 16import cgi 17import logging 18import os 19import re 20import subprocess 21import sys 22 23sys.path.append(os.path.join( 24 os.path.dirname(__file__), os.pardir, os.pardir, 'build', 'android')) 25import devil_chromium 26from devil.android import apk_helper 27from devil.android import device_errors 28from devil.android.ndk import abis 29from devil.android.tools import script_common 30from devil.utils import logging_common 31from py_utils import tempfile_ext 32 33_SUPPORTED_ARCH_DICT = { 34 abis.ARM: 'arm', 35 abis.ARM_64: 'arm64', 36 abis.X86: 'x86', 37 # Note: x86_64 isn't tested yet. 38} 39 40 41class StackAddressInterpreter(object): 42 """A class to interpret addresses in simpleperf using stack script.""" 43 def __init__(self, args, tmp_dir): 44 self.args = args 45 self.tmp_dir = tmp_dir 46 47 @staticmethod 48 def RunStackScript(output_dir, stack_input_path): 49 """Run the stack script. 50 51 Args: 52 output_dir: The directory of Chromium output. 53 stack_input_path: The path to the stack input file. 54 55 Returns: 56 The output of running the stack script (stack.py). 57 """ 58 # Note that stack script is not designed to be used in a stand-alone way. 59 # Therefore, it is better off to call it as a command line. 60 # TODO(changwan): consider using llvm symbolizer directly. 61 cmd = ['third_party/android_platform/development/scripts/stack', 62 '--output-directory', output_dir, 63 stack_input_path] 64 return subprocess.check_output(cmd).splitlines() 65 66 @staticmethod 67 def _ConvertAddressToFakeTraceLine(address, lib_path): 68 formatted_address = '0x' + '0' * (16 - len(address)) + address 69 # Pretend that this is Chromium's stack traces output in logcat. 70 # Note that the date, time, pid, tid, frame number, and frame address 71 # are all fake and they are irrelevant. 72 return ('11-15 00:00:00.000 11111 11111 ' 73 'E chromium: #00 0x0000001111111111 %s+%s') % ( 74 lib_path, formatted_address) 75 76 def Interpret(self, addresses, lib_path): 77 """Interpret the given addresses. 78 79 Args: 80 addresses: A collection of addresses. 81 lib_path: The path to the WebView library. 82 83 Returns: 84 A list of (address, function_info) where function_info is the function 85 name, plus file name and line if args.show_file_line is set. 86 """ 87 stack_input_path = os.path.join(self.tmp_dir, 'stack_input.txt') 88 with open(stack_input_path, 'w') as f: 89 for address in addresses: 90 f.write(StackAddressInterpreter._ConvertAddressToFakeTraceLine( 91 address, lib_path) + '\n') 92 93 stack_output = StackAddressInterpreter.RunStackScript( 94 self.args.output_directory, stack_input_path) 95 96 if self.args.debug: 97 logging.debug('First 10 lines of stack output:') 98 for i in range(max(10, len(stack_output))): 99 logging.debug(stack_output[i]) 100 101 logging.info('We got the results from the stack script. Translating the ' 102 'addresses...') 103 104 address_function_pairs = [] 105 pattern = re.compile(r' 0*(?P<address>[1-9a-f][0-9a-f]+) (?P<function>.*)' 106 r' (?P<file_name_line>.*)') 107 for line in stack_output: 108 m = pattern.match(line) 109 if m: 110 function_info = m.group('function') 111 if self.args.show_file_line: 112 function_info += " | " + m.group('file_name_line') 113 114 address_function_pairs.append((m.group('address'), function_info)) 115 116 logging.info('The translation is done.') 117 return address_function_pairs 118 119 120class SimplePerfRunner(object): 121 """A runner for simpleperf and its postprocessing.""" 122 123 def __init__(self, device, args, tmp_dir, address_interpreter): 124 self.device = device 125 self.address_interpreter = address_interpreter 126 self.args = args 127 self.apk_helper = None 128 self.tmp_dir = tmp_dir 129 130 def _GetFormattedArch(self): 131 arch = _SUPPORTED_ARCH_DICT.get( 132 self.device.product_cpu_abi) 133 if not arch: 134 raise Exception('Your device arch (' + 135 self.device.product_cpu_abi + ') is not supported.') 136 logging.info('Guessing arch=%s because product.cpu.abi=%s', arch, 137 self.device.product_cpu_abi) 138 return arch 139 140 def GetWebViewLibraryNameAndPath(self, package_name): 141 """Get WebView library name and path on the device.""" 142 apk_path = self._GetWebViewApkPath(package_name) 143 logging.debug('WebView APK path:' + apk_path) 144 # TODO(changwan): check if we need support for bundle. 145 tmp_apk_path = os.path.join(self.tmp_dir, 'base.apk') 146 self.device.adb.Pull(apk_path, tmp_apk_path) 147 self.apk_helper = apk_helper.ToHelper(tmp_apk_path) 148 metadata = self.apk_helper.GetAllMetadata() 149 lib_name = None 150 for key, value in metadata: 151 if key == 'com.android.webview.WebViewLibrary': 152 lib_name = value 153 154 lib_path = os.path.join(apk_path, 'lib', self._GetFormattedArch(), lib_name) 155 logging.debug("WebView's library path on the device should be:" + lib_path) 156 return lib_name, lib_path 157 158 def Run(self): 159 """Run the simpleperf and do the post processing.""" 160 package_name = self.GetCurrentWebViewProvider() 161 SimplePerfRunner.RunPackageCompile(package_name) 162 perf_data_path = os.path.join(self.tmp_dir, 'perf.data') 163 SimplePerfRunner.RunSimplePerf(perf_data_path, self.args) 164 lines = SimplePerfRunner.GetOriginalReportHtml( 165 perf_data_path, 166 os.path.join(self.tmp_dir, 'unprocessed_report.html')) 167 lib_name, lib_path = self.GetWebViewLibraryNameAndPath(package_name) 168 addresses = SimplePerfRunner.CollectAddresses(lines, lib_name) 169 logging.info("Extracted %d addresses", len(addresses)) 170 address_function_pairs = self.address_interpreter.Interpret( 171 addresses, lib_path) 172 173 lines = SimplePerfRunner.ReplaceAddressesWithFunctionInfos( 174 lines, address_function_pairs, lib_name) 175 176 with open(self.args.report_path, 'w') as f: 177 for line in lines: 178 f.write(line + '\n') 179 180 logging.info("The final report has been generated at '%s'.", 181 self.args.report_path) 182 183 @staticmethod 184 def RunSimplePerf(perf_data_path, args): 185 """Runs the simple perf commandline.""" 186 cmd = ['third_party/android_ndk/simpleperf/app_profiler.py', 187 '--perf_data_path', perf_data_path, 188 '--skip_collect_binaries'] 189 if args.system_wide: 190 cmd.append('--system_wide') 191 else: 192 cmd.extend([ 193 '--app', 'org.chromium.webview_shell', '--activity', 194 '.TelemetryActivity' 195 ]) 196 197 if args.record_options: 198 cmd.extend(['--record_options', args.record_options]) 199 200 logging.info("Profile has started.") 201 subprocess.check_call(cmd) 202 logging.info("Profile has finished, processing the results...") 203 204 @staticmethod 205 def RunPackageCompile(package_name): 206 """Compile the package (dex optimization).""" 207 cmd = [ 208 'adb', 'shell', 'cmd', 'package', 'compile', '-m', 'speed', '-f', 209 package_name 210 ] 211 subprocess.check_call(cmd) 212 213 def GetCurrentWebViewProvider(self): 214 return self.device.GetWebViewUpdateServiceDump()['CurrentWebViewPackage'] 215 216 def _GetWebViewApkPath(self, package_name): 217 return self.device.GetApplicationPaths(package_name)[0] 218 219 @staticmethod 220 def GetOriginalReportHtml(perf_data_path, report_html_path): 221 """Gets the original report.html from running simpleperf.""" 222 cmd = ['third_party/android_ndk/simpleperf/report_html.py', 223 '--record_file', perf_data_path, 224 '--report_path', report_html_path, 225 '--no_browser'] 226 subprocess.check_call(cmd) 227 lines = [] 228 with open(report_html_path, 'r') as f: 229 lines = f.readlines() 230 return lines 231 232 @staticmethod 233 def CollectAddresses(lines, lib_name): 234 """Collect address-looking texts from lines. 235 236 Args: 237 lines: A list of strings that may contain addresses. 238 lib_name: The name of the WebView library. 239 240 Returns: 241 A set containing the addresses that were found in the lines. 242 """ 243 addresses = set() 244 for line in lines: 245 for address in re.findall(lib_name + r'\[\+([0-9a-f]+)\]', line): 246 addresses.add(address) 247 return addresses 248 249 @staticmethod 250 def ReplaceAddressesWithFunctionInfos(lines, address_function_pairs, 251 lib_name): 252 """Replaces the addresses with function names. 253 254 Args: 255 lines: A list of strings that may contain addresses. 256 address_function_pairs: A list of pairs of (address, function_name). 257 lib_name: The name of the WebView library. 258 259 Returns: 260 A list of strings with addresses replaced by function names. 261 """ 262 263 logging.info('Replacing the HTML content with new function names...') 264 265 # Note: Using a lenient pattern matching and a hashmap (dict) is much faster 266 # than using a double loop (by the order of 1,000). 267 # '+address' will be replaced by function name. 268 address_function_dict = { 269 '+' + k: cgi.escape(v) 270 for k, v in address_function_pairs 271 } 272 # Look behind the lib_name and '[' which will not be substituted. Note that 273 # '+' is used in the pattern but will be removed. 274 pattern = re.compile(r'(?<=' + lib_name + r'\[)\+([a-f0-9]+)(?=\])') 275 276 def replace_fn(match): 277 address = match.group(0) 278 if address in address_function_dict: 279 return address_function_dict[address] 280 else: 281 return address 282 283 # Line-by-line assignment to avoid creating a temp list. 284 for i, line in enumerate(lines): 285 lines[i] = pattern.sub(replace_fn, line) 286 287 logging.info('Replacing is done.') 288 return lines 289 290 291def main(raw_args): 292 parser = argparse.ArgumentParser() 293 parser.add_argument('--debug', action='store_true', 294 help='Get additional debugging mode') 295 parser.add_argument( 296 '--output-directory', 297 help='the path to the build output directory, such as out/Debug') 298 parser.add_argument('--report-path', 299 default='report.html', help='Report path') 300 parser.add_argument('--adb-path', 301 help='Absolute path to the adb binary to use.') 302 parser.add_argument('--record-options', 303 help=('Set recording options for app_profiler.py command.' 304 ' Example: "-e task-clock:u -f 1000 -g --duration' 305 ' 10" where -f means sampling frequency per second.' 306 ' Try `app_profiler.py record -h` for more ' 307 ' information. Note that not setting this defaults' 308 ' to the default record options.')) 309 parser.add_argument('--show-file-line', action='store_true', 310 help='Show file name and lines in the result.') 311 parser.add_argument( 312 '--system-wide', 313 action='store_true', 314 help=('Whether to profile system wide (without launching' 315 'an app).')) 316 317 script_common.AddDeviceArguments(parser) 318 logging_common.AddLoggingArguments(parser) 319 320 args = parser.parse_args(raw_args) 321 logging_common.InitializeLogging(args) 322 devil_chromium.Initialize(adb_path=args.adb_path) 323 324 devices = script_common.GetDevices(args.devices, args.denylist_file) 325 device = devices[0] 326 327 if len(devices) > 1: 328 raise device_errors.MultipleDevicesError(devices) 329 330 with tempfile_ext.NamedTemporaryDirectory( 331 prefix='tmp_simpleperf') as tmp_dir: 332 runner = SimplePerfRunner( 333 device, args, tmp_dir, 334 StackAddressInterpreter(args, tmp_dir)) 335 runner.Run() 336 337 338if __name__ == '__main__': 339 main(sys.argv[1:]) 340