1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
android_webview / tools / run_simpleperf.py [blame]
#!/usr/bin/env vpython3
#
# Copyright 2019 The Chromium Authors
# 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 html
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'))
# 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_toolchain/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_toolchain/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:])