3 # Copyright (C) 2016 The Android Open Source Project
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
18 """app_profiler.py: manage the process of profiling an android app.
19 It downloads simpleperf on device, uses it to collect samples from
20 user's app, and pulls perf.data and needed binaries on host.
23 from __future__ import print_function
33 from binary_cache_builder import BinaryCacheBuilder
34 from simpleperf_report_lib import *
37 class AppProfiler(object):
38 """Used to manage the process of profiling an android app.
40 There are three steps:
43 3. Collect profiling data.
45 def __init__(self, config):
46 # check config variables
47 config_names = ['app_package_name', 'native_lib_dir', 'apk_file_path',
48 'recompile_app', 'restart_app', 'main_activity',
49 'record_options', 'perf_data_path', 'adb_path', 'readelf_path',
51 for name in config_names:
52 if not config.has_key(name):
53 log_fatal('config [%s] is missing' % name)
54 native_lib_dir = config.get('native_lib_dir')
55 if native_lib_dir and not os.path.isdir(native_lib_dir):
56 log_fatal('[native_lib_dir] "%s" is not a dir' % native_lib_dir)
57 apk_file_path = config.get('apk_file_path')
58 if apk_file_path and not os.path.isfile(apk_file_path):
59 log_fatal('[apk_file_path] "%s" is not a file' % apk_file_path)
61 self.adb = AdbHelper(self.config['adb_path'])
62 self.is_root_device = False
63 self.android_version = 0
64 self.device_arch = None
70 log_info('prepare profiling')
71 self.prepare_profiling()
72 log_info('start profiling')
73 self.start_and_wait_profiling()
74 log_info('collect profiling data')
75 self.collect_profiling_data()
76 log_info('profiling is finished.')
79 def prepare_profiling(self):
80 self._get_device_environment()
81 self._enable_profiling()
84 self._get_app_environment()
85 self._download_simpleperf()
86 self._download_native_libs()
89 def _get_device_environment(self):
90 self.is_root_device = self.adb.switch_to_root()
92 # Get android version.
93 build_version = self.adb.get_property('ro.build.version.release')
95 if not build_version[0].isdigit():
96 c = build_version[0].upper()
98 self.android_version = 0
100 self.android_version = ord(c) - ord('L') + 5
102 strs = build_version.split('.')
104 self.android_version = int(strs[0])
106 # Get device architecture.
107 output = self.adb.check_run_and_return_output(['shell', 'uname', '-m'])
108 if output.find('aarch64') != -1:
109 self.device_arch = 'aarch64'
110 elif output.find('arm') != -1:
111 self.device_arch = 'arm'
112 elif output.find('x86_64') != -1:
113 self.device_arch = 'x86_64'
114 elif output.find('86') != -1:
115 self.device_arch = 'x86'
117 log_fatal('unsupported architecture: %s' % output.strip())
120 def _enable_profiling(self):
121 self.adb.set_property('security.perf_harden', '0')
122 if self.is_root_device:
123 # We can enable kernel symbols
124 self.adb.run(['shell', 'echo', '0', '>/proc/sys/kernel/kptr_restrict'])
127 def _recompile_app(self):
128 if not self.config['recompile_app']:
130 if self.android_version == 0:
131 log_warning("Can't fully compile an app on android version < L.")
132 elif self.android_version == 5 or self.android_version == 6:
133 if not self.is_root_device:
134 log_warning("Can't fully compile an app on android version < N on non-root devices.")
135 elif not self.config['apk_file_path']:
136 log_warning("apk file is needed to reinstall the app on android version < N.")
138 flag = '-g' if self.android_version == 6 else '--include-debug-symbols'
139 self.adb.set_property('dalvik.vm.dex2oat-flags', flag)
140 self.adb.check_run(['install', '-r', self.config['apk_file_path']])
141 elif self.android_version >= 7:
142 self.adb.set_property('debug.generate-debug-info', 'true')
143 self.adb.check_run(['shell', 'cmd', 'package', 'compile', '-f', '-m', 'speed',
144 self.config['app_package_name']])
146 log_fatal('unreachable')
149 def _restart_app(self):
150 if not self.config['restart_app']:
152 pid = self._find_app_process()
154 self.adb.run(['shell', 'run-as', self.config['app_package_name'],
155 'kill', '-9', str(pid)])
157 activity = self.config['app_package_name'] + '/' + self.config['main_activity']
158 result = self.adb.run(['shell', 'am', 'start', '-n', activity])
160 log_fatal("Can't start activity %s" % activity)
162 pid = self._find_app_process()
166 log_info('Wait for the app process for %d seconds' % (i + 1))
167 log_fatal("Can't find the app process")
170 def _find_app_process(self):
171 result, output = self.adb.run_and_return_output(
172 ['shell', 'run-as', self.config['app_package_name'], 'ps'])
175 output = output.split('\n')
178 if len(strs) > 2 and strs[-1].find(self.config['app_package_name']) != -1:
183 def _get_app_environment(self):
184 self.app_pid = self._find_app_process()
185 if self.app_pid is None:
186 log_fatal("can't find process for app [%s]" % self.config['app_package_name'])
187 if self.device_arch in ['aarch64', 'x86_64']:
188 output = self.adb.check_run_and_return_output(
189 ['shell', 'run-as', self.config['app_package_name'],
190 'cat', '/proc/%d/maps' % self.app_pid])
191 if output.find('linker64') != -1:
192 self.app_arch = self.device_arch
194 self.app_arch = 'arm' if self.device_arch == 'aarch64' else 'x86'
196 self.app_arch = self.device_arch
197 log_info('app_arch: %s' % self.app_arch)
200 def _download_simpleperf(self):
201 simpleperf_binary = get_target_binary_path(self.app_arch, 'simpleperf')
202 self.adb.check_run(['push', simpleperf_binary, '/data/local/tmp'])
203 self.adb.check_run(['shell', 'run-as', self.config['app_package_name'],
204 'cp', '/data/local/tmp/simpleperf', '.'])
205 self.adb.check_run(['shell', 'run-as', self.config['app_package_name'],
206 'chmod', 'a+x', 'simpleperf'])
209 def _download_native_libs(self):
210 if not self.config['native_lib_dir']:
212 filename_dict = dict()
213 for root, _, files in os.walk(self.config['native_lib_dir']):
215 if not file.endswith('.so'):
217 path = os.path.join(root, file)
218 old_path = filename_dict.get(file)
219 log_info('app_arch = %s' % self.app_arch)
220 if self._is_lib_better(path, old_path):
221 log_info('%s is better than %s' % (path, old_path))
222 filename_dict[file] = path
224 log_info('%s is worse than %s' % (path, old_path))
225 maps = self.adb.check_run_and_return_output(['shell', 'run-as',
226 self.config['app_package_name'], 'cat', '/proc/%d/maps' % self.app_pid])
227 searched_lib = dict()
228 for item in maps.split():
229 if item.endswith('.so') and searched_lib.get(item) is None:
230 searched_lib[item] = True
231 # Use '/' as path separator as item comes from android environment.
232 filename = item[item.rfind('/') + 1:]
233 dirname = item[1:item.rfind('/')]
234 path = filename_dict.get(filename)
237 self.adb.check_run(['push', path, '/data/local/tmp'])
238 self.adb.check_run(['shell', 'run-as', self.config['app_package_name'],
239 'mkdir', '-p', dirname])
240 self.adb.check_run(['shell', 'run-as', self.config['app_package_name'],
241 'cp', '/data/local/tmp/' + filename, dirname])
244 def _is_lib_better(self, new_path, old_path):
245 """ Return true if new_path is more likely to be used on device. """
248 if self.app_arch == 'arm':
249 result1 = new_path.find('armeabi-v7a/') != -1
250 result2 = old_path.find('armeabi-v7a') != -1
251 if result1 != result2:
253 arch_dir = 'arm64' if self.app_arch == 'aarch64' else self.app_arch + '/'
254 result1 = new_path.find(arch_dir) != -1
255 result2 = old_path.find(arch_dir) != -1
256 if result1 != result2:
258 result1 = new_path.find('obj/') != -1
259 result2 = old_path.find('obj/') != -1
260 if result1 != result2:
265 def start_and_wait_profiling(self):
266 self.adb.check_run(['shell', 'run-as', self.config['app_package_name'],
267 './simpleperf', 'record', self.config['record_options'], '-p',
268 str(self.app_pid), '--symfs', '.'])
271 def collect_profiling_data(self):
272 self.adb.check_run(['shell', 'run-as', self.config['app_package_name'],
273 'chmod', 'a+rw', 'perf.data'])
274 self.adb.check_run(['shell', 'cp',
275 '/data/data/%s/perf.data' % self.config['app_package_name'], '/data/local/tmp'])
276 self.adb.check_run(['pull', '/data/local/tmp/perf.data', self.config['perf_data_path']])
277 config = copy.copy(self.config)
278 config['symfs_dirs'] = []
279 if self.config['native_lib_dir']:
280 config['symfs_dirs'].append(self.config['native_lib_dir'])
281 binary_cache_builder = BinaryCacheBuilder(config)
282 binary_cache_builder.build_binary_cache()
285 if __name__ == '__main__':
286 parser = argparse.ArgumentParser(
287 description='Profile an android app. See configurations in app_profiler.config.')
288 parser.add_argument('--config', default='app_profiler.config',
289 help='Set configuration file. Default is app_profiler.config.')
290 args = parser.parse_args()
291 config = load_config(args.config)
292 profiler = AppProfiler(config)