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.run_in_app_dir(['kill', '-9', str(pid)])
156 activity = self.config['app_package_name'] + '/' + self.config['main_activity']
157 result = self.adb.run(['shell', 'am', 'start', '-n', activity])
159 log_fatal("Can't start activity %s" % activity)
161 pid = self._find_app_process()
165 log_info('Wait for the app process for %d seconds' % (i + 1))
166 log_fatal("Can't find the app process")
169 def _find_app_process(self):
170 result, output = self.adb.run_and_return_output(['shell', 'ps'])
173 output = output.split('\n')
176 if len(strs) > 2 and strs[-1].find(self.config['app_package_name']) != -1:
181 def _get_app_environment(self):
182 self.app_pid = self._find_app_process()
183 if self.app_pid is None:
184 log_fatal("can't find process for app [%s]" % self.config['app_package_name'])
185 if self.device_arch in ['aarch64', 'x86_64']:
186 output = self.run_in_app_dir(['cat', '/proc/%d/maps' % self.app_pid])
187 if output.find('linker64') != -1:
188 self.app_arch = self.device_arch
190 self.app_arch = 'arm' if self.device_arch == 'aarch64' else 'x86'
192 self.app_arch = self.device_arch
193 log_info('app_arch: %s' % self.app_arch)
196 def _download_simpleperf(self):
197 simpleperf_binary = get_target_binary_path(self.app_arch, 'simpleperf')
198 self.adb.check_run(['push', simpleperf_binary, '/data/local/tmp'])
199 self.run_in_app_dir(['cp', '/data/local/tmp/simpleperf', '.'])
200 self.run_in_app_dir(['chmod', 'a+x', 'simpleperf'])
203 def _download_native_libs(self):
204 if not self.config['native_lib_dir']:
206 filename_dict = dict()
207 for root, _, files in os.walk(self.config['native_lib_dir']):
209 if not file.endswith('.so'):
211 path = os.path.join(root, file)
212 old_path = filename_dict.get(file)
213 log_info('app_arch = %s' % self.app_arch)
214 if self._is_lib_better(path, old_path):
215 log_info('%s is better than %s' % (path, old_path))
216 filename_dict[file] = path
218 log_info('%s is worse than %s' % (path, old_path))
219 maps = self.run_in_app_dir(['cat', '/proc/%d/maps' % self.app_pid])
220 searched_lib = dict()
221 for item in maps.split():
222 if item.endswith('.so') and searched_lib.get(item) is None:
223 searched_lib[item] = True
224 # Use '/' as path separator as item comes from android environment.
225 filename = item[item.rfind('/') + 1:]
226 dirname = item[1:item.rfind('/')]
227 path = filename_dict.get(filename)
230 self.adb.check_run(['push', path, '/data/local/tmp'])
231 self.run_in_app_dir(['mkdir', '-p', dirname])
232 self.run_in_app_dir(['cp', '/data/local/tmp/' + filename, dirname])
235 def _is_lib_better(self, new_path, old_path):
236 """ Return true if new_path is more likely to be used on device. """
239 if self.app_arch == 'arm':
240 result1 = new_path.find('armeabi-v7a/') != -1
241 result2 = old_path.find('armeabi-v7a') != -1
242 if result1 != result2:
244 arch_dir = 'arm64' if self.app_arch == 'aarch64' else self.app_arch + '/'
245 result1 = new_path.find(arch_dir) != -1
246 result2 = old_path.find(arch_dir) != -1
247 if result1 != result2:
249 result1 = new_path.find('obj/') != -1
250 result2 = old_path.find('obj/') != -1
251 if result1 != result2:
256 def start_and_wait_profiling(self):
257 self.run_in_app_dir([
258 './simpleperf', 'record', self.config['record_options'], '-p',
259 str(self.app_pid), '--symfs', '.'])
262 def collect_profiling_data(self):
263 self.run_in_app_dir(['chmod', 'a+rw', 'perf.data'])
264 self.adb.check_run(['shell', 'cp',
265 '/data/data/%s/perf.data' % self.config['app_package_name'], '/data/local/tmp'])
266 self.adb.check_run(['pull', '/data/local/tmp/perf.data', self.config['perf_data_path']])
267 config = copy.copy(self.config)
268 config['symfs_dirs'] = []
269 if self.config['native_lib_dir']:
270 config['symfs_dirs'].append(self.config['native_lib_dir'])
271 binary_cache_builder = BinaryCacheBuilder(config)
272 binary_cache_builder.build_binary_cache()
275 def run_in_app_dir(self, args):
276 if self.is_root_device:
277 cmd = 'cd /data/data/' + self.config['app_package_name'] + ' && ' + (' '.join(args))
278 return self.adb.check_run_and_return_output(['shell', cmd])
280 return self.adb.check_run_and_return_output(
281 ['shell', 'run-as', self.config['app_package_name']] + args)
284 if __name__ == '__main__':
285 parser = argparse.ArgumentParser(
286 description='Profile an android app. See configurations in app_profiler.config.')
287 parser.add_argument('--config', default='app_profiler.config',
288 help='Set configuration file. Default is app_profiler.config.')
289 args = parser.parse_args()
290 config = load_config(args.config)
291 profiler = AppProfiler(config)