1#!/usr/bin/env python 2# 3# Copyright (C) 2016 The Android Open Source Project 4# 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 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 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. 16# 17 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. 21""" 22 23from __future__ import print_function 24import argparse 25import copy 26import os 27import os.path 28import shutil 29import subprocess 30import sys 31import time 32 33from binary_cache_builder import BinaryCacheBuilder 34from simpleperf_report_lib import * 35from utils import * 36 37class AppProfiler(object): 38 """Used to manage the process of profiling an android app. 39 40 There are three steps: 41 1. Prepare profiling. 42 2. Profile the app. 43 3. Collect profiling data. 44 """ 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', 50 'binary_cache_dir'] 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) 60 self.config = config 61 self.adb = AdbHelper(self.config['adb_path']) 62 self.is_root_device = False 63 self.android_version = 0 64 self.device_arch = None 65 self.app_arch = None 66 self.app_pid = None 67 68 69 def profile(self): 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.') 77 78 79 def prepare_profiling(self): 80 self._get_device_environment() 81 self._enable_profiling() 82 self._recompile_app() 83 self._restart_app() 84 self._get_app_environment() 85 self._download_simpleperf() 86 self._download_native_libs() 87 88 89 def _get_device_environment(self): 90 self.is_root_device = self.adb.switch_to_root() 91 92 # Get android version. 93 build_version = self.adb.get_property('ro.build.version.release') 94 if build_version: 95 if not build_version[0].isdigit(): 96 c = build_version[0].upper() 97 if c < 'L': 98 self.android_version = 0 99 else: 100 self.android_version = ord(c) - ord('L') + 5 101 else: 102 strs = build_version.split('.') 103 if strs: 104 self.android_version = int(strs[0]) 105 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' 116 else: 117 log_fatal('unsupported architecture: %s' % output.strip()) 118 119 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']) 125 126 127 def _recompile_app(self): 128 if not self.config['recompile_app']: 129 return 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.") 137 else: 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']]) 145 else: 146 log_fatal('unreachable') 147 148 149 def _restart_app(self): 150 if not self.config['launch_activity'] and not self.config['launch_inst_test']: 151 return 152 153 pid = self._find_app_process() 154 if pid is not None: 155 self.run_in_app_dir(['kill', '-9', str(pid)]) 156 time.sleep(1) 157 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]) 161 if not result: 162 log_fatal("Can't start activity %s" % activity) 163 else: 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]) 167 if not result: 168 log_fatal("Can't start instrumentation test %s" % self.config['launch_inst_test']) 169 170 for i in range(10): 171 pid = self._find_app_process() 172 if pid is not None: 173 return 174 time.sleep(1) 175 log_info('Wait for the app process for %d seconds' % (i + 1)) 176 log_fatal("Can't find the app process") 177 178 179 def _find_app_process(self): 180 result, output = self.adb.run_and_return_output(['shell', 'ps']) 181 if not result: 182 return None 183 output = output.split('\n') 184 for line in output: 185 strs = line.split() 186 if len(strs) > 2 and strs[-1].find(self.config['app_package_name']) != -1: 187 return int(strs[1]) 188 return None 189 190 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 199 else: 200 self.app_arch = 'arm' if self.device_arch == 'aarch64' else 'x86' 201 else: 202 self.app_arch = self.device_arch 203 log_info('app_arch: %s' % self.app_arch) 204 205 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']) 211 212 213 def _download_native_libs(self): 214 if not self.config['native_lib_dir']: 215 return 216 filename_dict = dict() 217 for root, _, files in os.walk(self.config['native_lib_dir']): 218 for file in files: 219 if not file.endswith('.so'): 220 continue 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 227 else: 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) 238 if path is None: 239 continue 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]) 243 244 245 def _is_lib_better(self, new_path, old_path): 246 """ Return true if new_path is more likely to be used on device. """ 247 if old_path is None: 248 return True 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: 253 return result1 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: 258 return result1 259 result1 = new_path.find('obj/') != -1 260 result2 = old_path.find('obj/') != -1 261 if result1 != result2: 262 return result1 263 return False 264 265 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', '.']) 270 271 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() 283 284 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]) 289 else: 290 return self.adb.check_run_and_return_output( 291 ['shell', 'run-as', self.config['app_package_name']] + args) 292 293 294if __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) 302 profiler.profile() 303