blob: 9b1c549f068fdbe36edc2039878725ee2e0d7995 [file] [log] [blame]
#!/usr/bin/env vpython
# 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/ \
--report-path report.html \
--output-directory out/Debug/
import argparse
import cgi
import logging
import os
import re
import subprocess
import sys
os.path.dirname(__file__), os.pardir, os.pardir, 'build', 'android'))
import devil_chromium
from import apk_helper
from import device_errors
from import abis
from import script_common
from devil.utils import logging_common
from py_utils import tempfile_ext
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
def RunStackScript(output_dir, stack_input_path):
"""Run the stack script.
output_dir: The directory of Chromium output.
stack_input_path: The path to the stack input file.
The output of running the stack script (
# 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,
return subprocess.check_output(cmd).splitlines()
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.
addresses: A collection of addresses.
lib_path: The path to the WebView library.
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:
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])'We got the results from the stack script. Translating the '
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 ='function')
if self.args.show_file_line:
function_info += " | " +'file_name_line')
address_function_pairs.append(('address'), function_info))'The translation is done.')
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):
if not arch:
raise Exception('Your device arch (' +
self.device.product_cpu_abi + ') is not supported.')'Guessing arch=%s because product.cpu.abi=%s', arch,
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:' + 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 == '':
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."""
package_name = self.GetCurrentWebViewProvider()
perf_data_path = os.path.join(self.tmp_dir, '')
SimplePerfRunner.RunSimplePerf(perf_data_path, self.args)
lines = SimplePerfRunner.GetOriginalReportHtml(
os.path.join(self.tmp_dir, 'unprocessed_report.html'))
lib_name, lib_path = self.GetWebViewLibraryNameAndPath(package_name)
addresses = SimplePerfRunner.CollectAddresses(lines, lib_name)"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')"The final report has been generated at '%s'.",
def RunSimplePerf(perf_data_path, args):
"""Runs the simple perf commandline."""
cmd = ['third_party/android_ndk/simpleperf/',
'--perf_data_path', perf_data_path,
if args.system_wide:
'--app', 'org.chromium.webview_shell', '--activity',
if args.record_options:
cmd.extend(['--record_options', args.record_options])"Profile has started.")
subprocess.check_call(cmd)"Profile has finished, processing the results...")
def RunPackageCompile(package_name):
"""Compile the package (dex optimization)."""
cmd = [
'adb', 'shell', 'cmd', 'package', 'compile', '-m', 'speed', '-f',
def GetCurrentWebViewProvider(self):
return self.device.GetWebViewUpdateServiceDump()['CurrentWebViewPackage']
def _GetWebViewApkPath(self, package_name):
return self.device.GetApplicationPaths(package_name)[0]
def GetOriginalReportHtml(perf_data_path, report_html_path):
"""Gets the original report.html from running simpleperf."""
cmd = ['third_party/android_ndk/simpleperf/',
'--record_file', perf_data_path,
'--report_path', report_html_path,
lines = []
with open(report_html_path, 'r') as f:
lines = f.readlines()
return lines
def CollectAddresses(lines, lib_name):
"""Collect address-looking texts from lines.
lines: A list of strings that may contain addresses.
lib_name: The name of the WebView library.
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):
return addresses
def ReplaceAddressesWithFunctionInfos(lines, address_function_pairs,
"""Replaces the addresses with function names.
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.
A list of strings with addresses replaced by function names.
"""'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: cgi.escape(v)
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 =
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)'Replacing is done.')
return lines
def main(raw_args):
parser = argparse.ArgumentParser()
parser.add_argument('--debug', action='store_true',
help='Get additional debugging mode')
help='the path to the build output directory, such as out/Debug')
default='report.html', help='Report path')
help='Absolute path to the adb binary to use.')
help=('Set recording options for command.'
' Example: "-e task-clock:u -f 1000 -g --duration'
' 10" where -f means sampling frequency per second.'
' Try ` 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.')
help=('Whether to profile system wide (without launching'
'an app).'))
args = parser.parse_args(raw_args)
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))
if __name__ == '__main__':