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 result, output = self.adb.run_and_return_output(['shell', 'ps'])
183 output = output.split('\n')
186 if len(strs) > 2 and strs[-1].find(self.config['app_package_name']) != -1:
191 def _get_app_environment(self):
192 self.app_pid = self._find_app_process()
193 if self.app_pid is None:
194 log_fatal("can't find process for app [%s]" % self.config['app_package_name'])
195 if self.device_arch in ['aarch64', 'x86_64']:
196 output = self.run_in_app_dir(['cat', '/proc/%d/maps' % self.app_pid])
197 if output.find('linker64') != -1:
198 self.app_arch = self.device_arch
200 self.app_arch = 'arm' if self.device_arch == 'aarch64' else 'x86'
202 self.app_arch = self.device_arch
203 log_info('app_arch: %s' % self.app_arch)
206 def _download_simpleperf(self):
207 simpleperf_binary = get_target_binary_path(self.app_arch, 'simpleperf')
208 self.adb.check_run(['push', simpleperf_binary, '/data/local/tmp'])
209 self.run_in_app_dir(['cp', '/data/local/tmp/simpleperf', '.'])
210 self.run_in_app_dir(['chmod', 'a+x', 'simpleperf'])
213 def _download_native_libs(self):
214 if not self.config['native_lib_dir']:
216 filename_dict = dict()
217 for root, _, files in os.walk(self.config['native_lib_dir']):
219 if not file.endswith('.so'):
221 path = os.path.join(root, file)
222 old_path = filename_dict.get(file)
223 log_info('app_arch = %s' % self.app_arch)
224 if self._is_lib_better(path, old_path):
225 log_info('%s is better than %s' % (path, old_path))
226 filename_dict[file] = path
228 log_info('%s is worse than %s' % (path, old_path))
229 maps = self.run_in_app_dir(['cat', '/proc/%d/maps' % self.app_pid])
230 searched_lib = dict()
231 for item in maps.split():
232 if item.endswith('.so') and searched_lib.get(item) is None:
233 searched_lib[item] = True
234 # Use '/' as path separator as item comes from android environment.
235 filename = item[item.rfind('/') + 1:]
236 dirname = item[1:item.rfind('/')]
237 path = filename_dict.get(filename)
240 self.adb.check_run(['push', path, '/data/local/tmp'])
241 self.run_in_app_dir(['mkdir', '-p', dirname])
242 self.run_in_app_dir(['cp', '/data/local/tmp/' + filename, dirname])
245 def _is_lib_better(self, new_path, old_path):
246 """ Return true if new_path is more likely to be used on device. """
249 if self.app_arch == 'arm':
250 result1 = new_path.find('armeabi-v7a/') != -1
251 result2 = old_path.find('armeabi-v7a') != -1
252 if result1 != result2:
254 arch_dir = 'arm64' if self.app_arch == 'aarch64' else self.app_arch + '/'
255 result1 = new_path.find(arch_dir) != -1
256 result2 = old_path.find(arch_dir) != -1
257 if result1 != result2:
259 result1 = new_path.find('obj/') != -1
260 result2 = old_path.find('obj/') != -1
261 if result1 != result2:
266 def start_and_wait_profiling(self):
267 self.run_in_app_dir([
268 './simpleperf', 'record', self.config['record_options'], '-p',
269 str(self.app_pid), '--symfs', '.'])
272 def collect_profiling_data(self):
273 self.run_in_app_dir(['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 def run_in_app_dir(self, args):
286 if self.is_root_device:
287 cmd = 'cd /data/data/' + self.config['app_package_name'] + ' && ' + (' '.join(args))
288 return self.adb.check_run_and_return_output(['shell', cmd])
290 return self.adb.check_run_and_return_output(
291 ['shell', 'run-as', self.config['app_package_name']] + args)
294 if __name__ == '__main__':
295 parser = argparse.ArgumentParser(
296 description='Profile an android app. See configurations in app_profiler.config.')
297 parser.add_argument('--config', default='app_profiler.config',
298 help='Set configuration file. Default is app_profiler.config.')
299 args = parser.parse_args()
300 config = load_config(args.config)
301 profiler = AppProfiler(config)