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', 'launch_activity', 'launch_inst_test',
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['launch_activity'] and not self.config['launch_inst_test']:
153 pid = self._find_app_process()
155 self.run_in_app_dir(['kill', '-9', str(pid)])
158 if self.config['launch_activity']:
159 activity = self.config['app_package_name'] + '/' + self.config['launch_activity']
160 result = self.adb.run(['shell', 'am', 'start', '-n', activity])
162 log_fatal("Can't start activity %s" % activity)
164 runner = self.config['app_package_name'] + '/android.support.test.runner.AndroidJUnitRunner'
165 result = self.adb.run(['shell', 'am', 'instrument', '-e', 'class',
166 self.config['launch_inst_test'], runner])
168 log_fatal("Can't start instrumentation test %s" % self.config['launch_inst_test'])
171 pid = self._find_app_process()
175 log_info('Wait for the app process for %d seconds' % (i + 1))
176 log_fatal("Can't find the app process")
179 def _find_app_process(self):
180 ps_args = ['-e'] if self.android_version >= 8 else []
181 result, output = self.adb.run_and_return_output(['shell', 'ps'] + ps_args)
184 output = output.split('\n')
187 if len(strs) > 2 and strs[-1].find(self.config['app_package_name']) != -1:
192 def _get_app_environment(self):
193 self.app_pid = self._find_app_process()
194 if self.app_pid is None:
195 log_fatal("can't find process for app [%s]" % self.config['app_package_name'])
196 if self.device_arch in ['aarch64', 'x86_64']:
197 output = self.run_in_app_dir(['cat', '/proc/%d/maps' % self.app_pid])
198 if output.find('linker64') != -1:
199 self.app_arch = self.device_arch
201 self.app_arch = 'arm' if self.device_arch == 'aarch64' else 'x86'
203 self.app_arch = self.device_arch
204 log_info('app_arch: %s' % self.app_arch)
207 def _download_simpleperf(self):
208 simpleperf_binary = get_target_binary_path(self.app_arch, 'simpleperf')
209 self.adb.check_run(['push', simpleperf_binary, '/data/local/tmp'])
210 self.run_in_app_dir(['cp', '/data/local/tmp/simpleperf', '.'])
211 self.run_in_app_dir(['chmod', 'a+x', 'simpleperf'])
214 def _download_native_libs(self):
215 if not self.config['native_lib_dir']:
217 filename_dict = dict()
218 for root, _, files in os.walk(self.config['native_lib_dir']):
220 if not file.endswith('.so'):
222 path = os.path.join(root, file)
223 old_path = filename_dict.get(file)
224 log_info('app_arch = %s' % self.app_arch)
225 if self._is_lib_better(path, old_path):
226 log_info('%s is better than %s' % (path, old_path))
227 filename_dict[file] = path
229 log_info('%s is worse than %s' % (path, old_path))
230 maps = self.run_in_app_dir(['cat', '/proc/%d/maps' % self.app_pid])
231 searched_lib = dict()
232 for item in maps.split():
233 if item.endswith('.so') and searched_lib.get(item) is None:
234 searched_lib[item] = True
235 # Use '/' as path separator as item comes from android environment.
236 filename = item[item.rfind('/') + 1:]
237 dirname = item[1:item.rfind('/')]
238 path = filename_dict.get(filename)
241 self.adb.check_run(['push', path, '/data/local/tmp'])
242 self.run_in_app_dir(['mkdir', '-p', dirname])
243 self.run_in_app_dir(['cp', '/data/local/tmp/' + filename, dirname])
246 def _is_lib_better(self, new_path, old_path):
247 """ Return true if new_path is more likely to be used on device. """
250 if self.app_arch == 'arm':
251 result1 = new_path.find('armeabi-v7a/') != -1
252 result2 = old_path.find('armeabi-v7a') != -1
253 if result1 != result2:
255 arch_dir = 'arm64' if self.app_arch == 'aarch64' else self.app_arch + '/'
256 result1 = new_path.find(arch_dir) != -1
257 result2 = old_path.find(arch_dir) != -1
258 if result1 != result2:
260 result1 = new_path.find('obj/') != -1
261 result2 = old_path.find('obj/') != -1
262 if result1 != result2:
267 def start_and_wait_profiling(self):
268 self.run_in_app_dir([
269 './simpleperf', 'record', self.config['record_options'], '-p',
270 str(self.app_pid), '--symfs', '.'])
273 def collect_profiling_data(self):
274 self.run_in_app_dir(['chmod', 'a+rw', 'perf.data'])
275 self.adb.check_run(['shell', 'cp',
276 '/data/data/%s/perf.data' % self.config['app_package_name'], '/data/local/tmp'])
277 self.adb.check_run(['pull', '/data/local/tmp/perf.data', self.config['perf_data_path']])
278 config = copy.copy(self.config)
279 config['symfs_dirs'] = []
280 if self.config['native_lib_dir']:
281 config['symfs_dirs'].append(self.config['native_lib_dir'])
282 binary_cache_builder = BinaryCacheBuilder(config)
283 binary_cache_builder.build_binary_cache()
286 def run_in_app_dir(self, args):
287 if self.is_root_device:
288 cmd = 'cd /data/data/' + self.config['app_package_name'] + ' && ' + (' '.join(args))
289 return self.adb.check_run_and_return_output(['shell', cmd])
291 return self.adb.check_run_and_return_output(
292 ['shell', 'run-as', self.config['app_package_name']] + args)
295 if __name__ == '__main__':
296 parser = argparse.ArgumentParser(
297 description='Profile an android app. See configurations in app_profiler.config.')
298 parser.add_argument('--config', default='app_profiler.config',
299 help='Set configuration file. Default is app_profiler.config.')
300 args = parser.parse_args()
301 config = load_config(args.config)
302 profiler = AppProfiler(config)