| #!/usr/bin/env python |
| # |
| # 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 cgi |
| 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')) |
| import devil_chromium # pylint: disable=import-error |
| from devil.android import apk_helper # pylint: disable=import-error |
| from devil.android import device_errors # pylint: disable=import-error |
| from devil.android.ndk import abis # pylint: disable=import-error |
| from devil.android.tools import script_common # pylint: disable=import-error |
| from devil.utils import logging_common # pylint: disable=import-error |
| from py_utils import tempfile_ext # pylint: disable=import-error |
| |
| _SUPPORTED_ARCH_DICT = { |
| abis.ARM: 'arm', |
| abis.ARM_64: 'arm64', |
| abis.X86: 'x86', |
| # Note: x86_64 isn't tested yet. |
| } |
| |
| |
| class StackAddressInterpreter(object): |
| """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).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_name). |
| """ |
| 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]) |
| |
| 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: |
| # We haven't yet seen a case where |file_name_line| could be useful. |
| address_function_pairs.append((m.group('address'), m.group('function'))) |
| return address_function_pairs |
| |
| |
| class SimplePerfRunner(object): |
| """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): |
| """Get WebView library name and path on the device.""" |
| package_name = self._GetCurrentWebViewProvider() |
| apk_path = self._GetWebViewApkPath(package_name) |
| logging.debug('WebView APK path:' + 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:" + lib_path) |
| return lib_name, lib_path |
| |
| def Run(self): |
| """Run the simpleperf and do the post processing.""" |
| perf_data_path = os.path.join(self.tmp_dir, 'perf.data') |
| SimplePerfRunner.RunSimplePerf(perf_data_path, self.args.record_options) |
| lines = SimplePerfRunner.GetOriginalReportHtml( |
| perf_data_path, |
| os.path.join(self.tmp_dir, 'unprocessed_report.html')) |
| lib_name, lib_path = self.GetWebViewLibraryNameAndPath() |
| 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.ReplaceAddressesWithFunctionNames( |
| 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, record_options): |
| """Runs the simple perf commandline.""" |
| cmd = ['third_party/android_ndk/simpleperf/app_profiler.py', |
| '--app', 'org.chromium.webview_shell', |
| '--activity', '.TelemetryActivity', |
| '--perf_data_path', perf_data_path, |
| '--skip_collect_binaries'] |
| if record_options: |
| cmd.extend(['--record_options', record_options]) |
| 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 ReplaceAddressesWithFunctionNames(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. |
| """ |
| |
| address_count = 0 |
| for address, function in address_function_pairs: |
| pattern = re.compile(lib_name + r'\[\+' + address + r'\]') |
| for i, line in enumerate(lines): |
| address_count += len(pattern.findall(line)) |
| lines[i] = pattern.sub( |
| lib_name + '[' + cgi.escape(function) + ']', line) |
| logging.info('There were %d matching addresses', address_count) |
| 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.')) |
| |
| 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.blacklist_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:]) |