123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345 |
- #!/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<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_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:])
|