Commit bf802924 authored by Changwan Ryu's avatar Changwan Ryu Committed by Commit Bot

Add run_simpleperf.py

Adding a simple script to run simpleperf for WebView.
Unfortunately for WebView, there is no easy way to symbolize the native
stack output.

This is a somewhat quick and dirty workaround to use stack script to do
the post-processing on the original report HTML file that simpleperf
produces.

Bug: 1015236
Change-Id: Ica49b8af3af809162d2f895663c37f913c646028
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1929765Reviewed-by: default avatarNate Fischer <ntfschr@chromium.org>
Reviewed-by: default avatarAndrew Luo <aluo@chromium.org>
Commit-Queue: Changwan Ryu <changwan@chromium.org>
Cr-Commit-Position: refs/heads/master@{#719485}
parent 8545ae68
#!/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:
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.
f.write(('11-15 00:00:00.000 11111 11111 '
'E chromium: #00 0x0000001111111111 %s+%s\n') % (
lib_path, formatted_address))
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)
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):
"""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']
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.')
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:])
#!/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.
"""Unit tests for run_simpleperf.py"""
import os
import unittest
import mock # pylint: disable=import-error
from run_simpleperf import SimplePerfRunner
from run_simpleperf import StackAddressInterpreter
_EXAMPLE_STACK_SCRIPT_INPUT = [
("11-15 00:00:00.000 11111 11111 E chromium: #00 0x0000001111111111 "
"/data/app/com.google.android.webview--8E2vMMZTpLVeEKY7ZgoHQ=="
"/lib/arm64/libwebviewchromium.so+0x00000000083a4db8"),
("11-15 00:00:00.000 11111 11111 E chromium: #00 0x0000001111111111 "
"/data/app/com.google.android.webview--8E2vMMZTpLVeEKY7ZgoHQ=="
"/lib/arm64/libwebviewchromium.so+0x00000000083db114"),
("11-15 00:00:00.000 11111 11111 E chromium: #00 0x0000001111111111 "
"/data/app/com.google.android.webview--8E2vMMZTpLVeEKY7ZgoHQ=="
"/lib/arm64/libwebviewchromium.so+0x000000000abcdef0")]
_EXAMPLE_STACK_SCRIPT_OUTPUT = [
"Stack Trace:",
" RELADDR FUNCTION",
(" 00000000083a4db8 mojo::core::ports::(anonymous namespace)::UpdateTLS("
"mojo::core::ports::PortLocker*, mojo::core::ports::PortLocker*) "
"../../mojo/core/ports/port_locker.cc:26:3"),
"",
"-----------------------------------------------------",
"",
"Stack Trace:",
" RELADDR FUNCTION",
(" 00000000083db114 viz::GLRenderer::SetUseProgram(viz::ProgramKey "
"const&, gfx::ColorSpace const&, gfx::ColorSpace const&) "
"../../components/viz/service/display/gl_renderer.cc:3267:14"),
"",
"-----------------------------------------------------"]
_ADDRESSES = ['83a4db8', '83db114', 'abcdef0'] # 3rd one is ignored
_WEBVIEW_LIB_NAME = 'libwebviewchromium.so'
_WEBVIEW_LIB_PATH = (
'/data/app/com.google.android.webview'
'--8E2vMMZTpLVeEKY7ZgoHQ==/lib/arm64/libwebviewchromium.so')
_EXAMPLE_INTERPRETER_OUTPUT = [
('83a4db8',
('mojo::core::ports::(anonymous namespace)::UpdateTLS('
'mojo::core::ports::PortLocker*, mojo::core::ports::PortLocker*)')),
('83db114',
('viz::GLRenderer::SetUseProgram(viz::ProgramKey const&, '
'gfx::ColorSpace const&, gfx::ColorSpace const&)'))]
_MOCK_ORIGINAL_REPORT = [
'"442": {"l": 28, "f": "libwebviewchromium.so[+3db7d84]"},',
'"443": {"l": 28, "f": "libwebviewchromium.so[+3db7a5c]"},']
_MOCK_ADDRESSES = ['3db7d84', '3db7a5c']
_MOCK_ADDRESS_FUNCTION_NAME_PAIRS = [
('3db7d84', 'MyClass::FirstMethod(const char*)'),
('3db7a5c', 'MyClass::SecondMethod(int)')]
_MOCK_FINAL_REPORT = [
('"442": {"l": 28, "f": "libwebviewchromium.so[MyClass::'
'FirstMethod(const char*)]"},'),
('"443": {"l": 28, "f": "libwebviewchromium.so[MyClass::'
'SecondMethod(int)]"},')]
class _RunSimpleperfTest(unittest.TestCase):
"""Unit tests for the run_simpleperf module. """
def _AssertFileLines(self, mock_open, expected_lines):
handle = mock_open()
# Get 'str1', 'str2', ... from the call to f.write(str_i + '\n') which is
# saved as [(str1 + '\n'), (str2 + '\n'), ...].
actual_lines = [args[0][:-1] for (args, _) in
handle.write.call_args_list]
self.assertEquals(expected_lines, actual_lines)
def setUp(self):
self.tmp_dir = '/tmp' # the actual directory won't be used in this test.
self.args = mock.Mock(
report_path=os.path.join(self.tmp_dir, 'report.html'))
self.device = mock.Mock()
self.stack_address_interpreter = StackAddressInterpreter(self.args,
self.tmp_dir)
self.simple_perf_runner = SimplePerfRunner(
self.device, self.args, self.tmp_dir, self.stack_address_interpreter)
@mock.patch('run_simpleperf.open', new_callable=mock.mock_open)
def testStackAddressInterpreter(self, mock_open):
StackAddressInterpreter.RunStackScript = mock.Mock(
return_value=_EXAMPLE_STACK_SCRIPT_OUTPUT)
self.assertEquals(_EXAMPLE_INTERPRETER_OUTPUT,
self.stack_address_interpreter.Interpret(
_ADDRESSES, _WEBVIEW_LIB_PATH))
self._AssertFileLines(mock_open, _EXAMPLE_STACK_SCRIPT_INPUT)
def testSimplePerfRunner_CollectAddresses(self):
addresses = self.simple_perf_runner.CollectAddresses(
_MOCK_ORIGINAL_REPORT, 'libwebviewchromium.so')
self.assertEquals(set(_MOCK_ADDRESSES), addresses)
def testSimplePerfRunner_ReplaceAddresses(self):
postprocessed_report = (
self.simple_perf_runner.ReplaceAddressesWithFunctionNames(
_MOCK_ORIGINAL_REPORT, _MOCK_ADDRESS_FUNCTION_NAME_PAIRS,
'libwebviewchromium.so'))
self.assertEquals(_MOCK_FINAL_REPORT, postprocessed_report)
@mock.patch('run_simpleperf.open', new_callable=mock.mock_open)
def testSimplePerfRunner_Run(self, mock_open):
self.stack_address_interpreter.Interpret = mock.Mock(
return_value=_MOCK_ADDRESS_FUNCTION_NAME_PAIRS)
SimplePerfRunner.RunSimplePerf = mock.Mock()
SimplePerfRunner.GetOriginalReportHtml = mock.Mock(
return_value=_MOCK_ORIGINAL_REPORT)
self.simple_perf_runner.GetWebViewLibraryNameAndPath = mock.Mock(
return_value=(_WEBVIEW_LIB_NAME, _WEBVIEW_LIB_PATH))
self.simple_perf_runner.Run()
self._AssertFileLines(mock_open, _MOCK_FINAL_REPORT)
if __name__ == '__main__':
unittest.main()
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment