run_simpleperf.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. #!/usr/bin/env vpython3
  2. #
  3. # Copyright 2019 The Chromium Authors. All rights reserved.
  4. # Use of this source code is governed by a BSD-style license that can be
  5. # found in the LICENSE file.
  6. """A simple tool to run simpleperf to get sampling-based perf traces.
  7. Typical Usage:
  8. android_webview/tools/run_simpleperf.py \
  9. --report-path report.html \
  10. --output-directory out/Debug/
  11. """
  12. import argparse
  13. import logging
  14. import os
  15. import re
  16. import subprocess
  17. import sys
  18. import six
  19. if six.PY2:
  20. import cgi as html
  21. else:
  22. import html
  23. sys.path.append(os.path.join(
  24. os.path.dirname(__file__), os.pardir, os.pardir, 'build', 'android'))
  25. # pylint: disable=wrong-import-position,import-error
  26. import devil_chromium
  27. from devil.android import apk_helper
  28. from devil.android import device_errors
  29. from devil.android.ndk import abis
  30. from devil.android.tools import script_common
  31. from devil.utils import logging_common
  32. from py_utils import tempfile_ext
  33. _SUPPORTED_ARCH_DICT = {
  34. abis.ARM: 'arm',
  35. abis.ARM_64: 'arm64',
  36. abis.X86: 'x86',
  37. # Note: x86_64 isn't tested yet.
  38. }
  39. class StackAddressInterpreter:
  40. """A class to interpret addresses in simpleperf using stack script."""
  41. def __init__(self, args, tmp_dir):
  42. self.args = args
  43. self.tmp_dir = tmp_dir
  44. @staticmethod
  45. def RunStackScript(output_dir, stack_input_path):
  46. """Run the stack script.
  47. Args:
  48. output_dir: The directory of Chromium output.
  49. stack_input_path: The path to the stack input file.
  50. Returns:
  51. The output of running the stack script (stack.py).
  52. """
  53. # Note that stack script is not designed to be used in a stand-alone way.
  54. # Therefore, it is better off to call it as a command line.
  55. # TODO(changwan): consider using llvm symbolizer directly.
  56. cmd = ['third_party/android_platform/development/scripts/stack',
  57. '--output-directory', output_dir,
  58. stack_input_path]
  59. return subprocess.check_output(cmd, universal_newlines=True).splitlines()
  60. @staticmethod
  61. def _ConvertAddressToFakeTraceLine(address, lib_path):
  62. formatted_address = '0x' + '0' * (16 - len(address)) + address
  63. # Pretend that this is Chromium's stack traces output in logcat.
  64. # Note that the date, time, pid, tid, frame number, and frame address
  65. # are all fake and they are irrelevant.
  66. return ('11-15 00:00:00.000 11111 11111 '
  67. 'E chromium: #00 0x0000001111111111 %s+%s') % (
  68. lib_path, formatted_address)
  69. def Interpret(self, addresses, lib_path):
  70. """Interpret the given addresses.
  71. Args:
  72. addresses: A collection of addresses.
  73. lib_path: The path to the WebView library.
  74. Returns:
  75. A list of (address, function_info) where function_info is the function
  76. name, plus file name and line if args.show_file_line is set.
  77. """
  78. stack_input_path = os.path.join(self.tmp_dir, 'stack_input.txt')
  79. with open(stack_input_path, 'w') as f:
  80. for address in addresses:
  81. f.write(StackAddressInterpreter._ConvertAddressToFakeTraceLine(
  82. address, lib_path) + '\n')
  83. stack_output = StackAddressInterpreter.RunStackScript(
  84. self.args.output_directory, stack_input_path)
  85. if self.args.debug:
  86. logging.debug('First 10 lines of stack output:')
  87. for i in range(max(10, len(stack_output))):
  88. logging.debug(stack_output[i])
  89. logging.info('We got the results from the stack script. Translating the '
  90. 'addresses...')
  91. address_function_pairs = []
  92. pattern = re.compile(r' 0*(?P<address>[1-9a-f][0-9a-f]+) (?P<function>.*)'
  93. r' (?P<file_name_line>.*)')
  94. for line in stack_output:
  95. m = pattern.match(line)
  96. if m:
  97. function_info = m.group('function')
  98. if self.args.show_file_line:
  99. function_info += " | " + m.group('file_name_line')
  100. address_function_pairs.append((m.group('address'), function_info))
  101. logging.info('The translation is done.')
  102. return address_function_pairs
  103. class SimplePerfRunner:
  104. """A runner for simpleperf and its postprocessing."""
  105. def __init__(self, device, args, tmp_dir, address_interpreter):
  106. self.device = device
  107. self.address_interpreter = address_interpreter
  108. self.args = args
  109. self.apk_helper = None
  110. self.tmp_dir = tmp_dir
  111. def _GetFormattedArch(self):
  112. arch = _SUPPORTED_ARCH_DICT.get(
  113. self.device.product_cpu_abi)
  114. if not arch:
  115. raise Exception('Your device arch (' +
  116. self.device.product_cpu_abi + ') is not supported.')
  117. logging.info('Guessing arch=%s because product.cpu.abi=%s', arch,
  118. self.device.product_cpu_abi)
  119. return arch
  120. def GetWebViewLibraryNameAndPath(self, package_name):
  121. """Get WebView library name and path on the device."""
  122. apk_path = self._GetWebViewApkPath(package_name)
  123. logging.debug('WebView APK path: %s', apk_path)
  124. # TODO(changwan): check if we need support for bundle.
  125. tmp_apk_path = os.path.join(self.tmp_dir, 'base.apk')
  126. self.device.adb.Pull(apk_path, tmp_apk_path)
  127. self.apk_helper = apk_helper.ToHelper(tmp_apk_path)
  128. metadata = self.apk_helper.GetAllMetadata()
  129. lib_name = None
  130. for key, value in metadata:
  131. if key == 'com.android.webview.WebViewLibrary':
  132. lib_name = value
  133. lib_path = os.path.join(apk_path, 'lib', self._GetFormattedArch(), lib_name)
  134. logging.debug("WebView's library path on the device should be: %s",
  135. lib_path)
  136. return lib_name, lib_path
  137. def Run(self):
  138. """Run the simpleperf and do the post processing."""
  139. package_name = self.GetCurrentWebViewProvider()
  140. SimplePerfRunner.RunPackageCompile(package_name)
  141. perf_data_path = os.path.join(self.tmp_dir, 'perf.data')
  142. SimplePerfRunner.RunSimplePerf(perf_data_path, self.args)
  143. lines = SimplePerfRunner.GetOriginalReportHtml(
  144. perf_data_path,
  145. os.path.join(self.tmp_dir, 'unprocessed_report.html'))
  146. lib_name, lib_path = self.GetWebViewLibraryNameAndPath(package_name)
  147. addresses = SimplePerfRunner.CollectAddresses(lines, lib_name)
  148. logging.info("Extracted %d addresses", len(addresses))
  149. address_function_pairs = self.address_interpreter.Interpret(
  150. addresses, lib_path)
  151. lines = SimplePerfRunner.ReplaceAddressesWithFunctionInfos(
  152. lines, address_function_pairs, lib_name)
  153. with open(self.args.report_path, 'w') as f:
  154. for line in lines:
  155. f.write(line + '\n')
  156. logging.info("The final report has been generated at '%s'.",
  157. self.args.report_path)
  158. @staticmethod
  159. def RunSimplePerf(perf_data_path, args):
  160. """Runs the simple perf commandline."""
  161. cmd = ['third_party/android_ndk/simpleperf/app_profiler.py',
  162. '--perf_data_path', perf_data_path,
  163. '--skip_collect_binaries']
  164. if args.system_wide:
  165. cmd.append('--system_wide')
  166. else:
  167. cmd.extend([
  168. '--app', 'org.chromium.webview_shell', '--activity',
  169. '.TelemetryActivity'
  170. ])
  171. if args.record_options:
  172. cmd.extend(['--record_options', args.record_options])
  173. logging.info("Profile has started.")
  174. subprocess.check_call(cmd)
  175. logging.info("Profile has finished, processing the results...")
  176. @staticmethod
  177. def RunPackageCompile(package_name):
  178. """Compile the package (dex optimization)."""
  179. cmd = [
  180. 'adb', 'shell', 'cmd', 'package', 'compile', '-m', 'speed', '-f',
  181. package_name
  182. ]
  183. subprocess.check_call(cmd)
  184. def GetCurrentWebViewProvider(self):
  185. return self.device.GetWebViewUpdateServiceDump()['CurrentWebViewPackage']
  186. def _GetWebViewApkPath(self, package_name):
  187. return self.device.GetApplicationPaths(package_name)[0]
  188. @staticmethod
  189. def GetOriginalReportHtml(perf_data_path, report_html_path):
  190. """Gets the original report.html from running simpleperf."""
  191. cmd = ['third_party/android_ndk/simpleperf/report_html.py',
  192. '--record_file', perf_data_path,
  193. '--report_path', report_html_path,
  194. '--no_browser']
  195. subprocess.check_call(cmd)
  196. lines = []
  197. with open(report_html_path, 'r') as f:
  198. lines = f.readlines()
  199. return lines
  200. @staticmethod
  201. def CollectAddresses(lines, lib_name):
  202. """Collect address-looking texts from lines.
  203. Args:
  204. lines: A list of strings that may contain addresses.
  205. lib_name: The name of the WebView library.
  206. Returns:
  207. A set containing the addresses that were found in the lines.
  208. """
  209. addresses = set()
  210. for line in lines:
  211. for address in re.findall(lib_name + r'\[\+([0-9a-f]+)\]', line):
  212. addresses.add(address)
  213. return addresses
  214. @staticmethod
  215. def ReplaceAddressesWithFunctionInfos(lines, address_function_pairs,
  216. lib_name):
  217. """Replaces the addresses with function names.
  218. Args:
  219. lines: A list of strings that may contain addresses.
  220. address_function_pairs: A list of pairs of (address, function_name).
  221. lib_name: The name of the WebView library.
  222. Returns:
  223. A list of strings with addresses replaced by function names.
  224. """
  225. logging.info('Replacing the HTML content with new function names...')
  226. # Note: Using a lenient pattern matching and a hashmap (dict) is much faster
  227. # than using a double loop (by the order of 1,000).
  228. # '+address' will be replaced by function name.
  229. address_function_dict = {
  230. '+' + k: html.escape(v, quote=False)
  231. for k, v in address_function_pairs
  232. }
  233. # Look behind the lib_name and '[' which will not be substituted. Note that
  234. # '+' is used in the pattern but will be removed.
  235. pattern = re.compile(r'(?<=' + lib_name + r'\[)\+([a-f0-9]+)(?=\])')
  236. def replace_fn(match):
  237. address = match.group(0)
  238. if address in address_function_dict:
  239. return address_function_dict[address]
  240. return address
  241. # Line-by-line assignment to avoid creating a temp list.
  242. for i, line in enumerate(lines):
  243. lines[i] = pattern.sub(replace_fn, line)
  244. logging.info('Replacing is done.')
  245. return lines
  246. def main(raw_args):
  247. parser = argparse.ArgumentParser()
  248. parser.add_argument('--debug', action='store_true',
  249. help='Get additional debugging mode')
  250. parser.add_argument(
  251. '--output-directory',
  252. help='the path to the build output directory, such as out/Debug')
  253. parser.add_argument('--report-path',
  254. default='report.html', help='Report path')
  255. parser.add_argument('--adb-path',
  256. help='Absolute path to the adb binary to use.')
  257. parser.add_argument('--record-options',
  258. help=('Set recording options for app_profiler.py command.'
  259. ' Example: "-e task-clock:u -f 1000 -g --duration'
  260. ' 10" where -f means sampling frequency per second.'
  261. ' Try `app_profiler.py record -h` for more '
  262. ' information. Note that not setting this defaults'
  263. ' to the default record options.'))
  264. parser.add_argument('--show-file-line', action='store_true',
  265. help='Show file name and lines in the result.')
  266. parser.add_argument(
  267. '--system-wide',
  268. action='store_true',
  269. help=('Whether to profile system wide (without launching'
  270. 'an app).'))
  271. script_common.AddDeviceArguments(parser)
  272. logging_common.AddLoggingArguments(parser)
  273. args = parser.parse_args(raw_args)
  274. logging_common.InitializeLogging(args)
  275. devil_chromium.Initialize(adb_path=args.adb_path)
  276. devices = script_common.GetDevices(args.devices, args.denylist_file)
  277. device = devices[0]
  278. if len(devices) > 1:
  279. raise device_errors.MultipleDevicesError(devices)
  280. with tempfile_ext.NamedTemporaryDirectory(
  281. prefix='tmp_simpleperf') as tmp_dir:
  282. runner = SimplePerfRunner(
  283. device, args, tmp_dir,
  284. StackAddressInterpreter(args, tmp_dir))
  285. runner.Run()
  286. if __name__ == '__main__':
  287. main(sys.argv[1:])