1*288bf522SAndroid Build Coastguard Worker#!/usr/bin/env python3 2*288bf522SAndroid Build Coastguard Worker# 3*288bf522SAndroid Build Coastguard Worker# Copyright (C) 2016 The Android Open Source Project 4*288bf522SAndroid Build Coastguard Worker# 5*288bf522SAndroid Build Coastguard Worker# Licensed under the Apache License, Version 2.0 (the "License"); 6*288bf522SAndroid Build Coastguard Worker# you may not use this file except in compliance with the License. 7*288bf522SAndroid Build Coastguard Worker# You may obtain a copy of the License at 8*288bf522SAndroid Build Coastguard Worker# 9*288bf522SAndroid Build Coastguard Worker# http://www.apache.org/licenses/LICENSE-2.0 10*288bf522SAndroid Build Coastguard Worker# 11*288bf522SAndroid Build Coastguard Worker# Unless required by applicable law or agreed to in writing, software 12*288bf522SAndroid Build Coastguard Worker# distributed under the License is distributed on an "AS IS" BASIS, 13*288bf522SAndroid Build Coastguard Worker# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14*288bf522SAndroid Build Coastguard Worker# See the License for the specific language governing permissions and 15*288bf522SAndroid Build Coastguard Worker# limitations under the License. 16*288bf522SAndroid Build Coastguard Worker# 17*288bf522SAndroid Build Coastguard Worker 18*288bf522SAndroid Build Coastguard Worker"""app_profiler.py: Record cpu profiling data of an android app or native program. 19*288bf522SAndroid Build Coastguard Worker 20*288bf522SAndroid Build Coastguard Worker It downloads simpleperf on device, uses it to collect profiling data on the selected app, 21*288bf522SAndroid Build Coastguard Worker and pulls profiling data and related binaries on host. 22*288bf522SAndroid Build Coastguard Worker""" 23*288bf522SAndroid Build Coastguard Worker 24*288bf522SAndroid Build Coastguard Workerimport logging 25*288bf522SAndroid Build Coastguard Workerimport os 26*288bf522SAndroid Build Coastguard Workerimport os.path 27*288bf522SAndroid Build Coastguard Workerimport re 28*288bf522SAndroid Build Coastguard Workerimport subprocess 29*288bf522SAndroid Build Coastguard Workerimport sys 30*288bf522SAndroid Build Coastguard Workerimport time 31*288bf522SAndroid Build Coastguard Workerfrom typing import Optional 32*288bf522SAndroid Build Coastguard Worker 33*288bf522SAndroid Build Coastguard Workerfrom simpleperf_utils import ( 34*288bf522SAndroid Build Coastguard Worker AdbHelper, BaseArgumentParser, bytes_to_str, extant_dir, get_script_dir, get_target_binary_path, 35*288bf522SAndroid Build Coastguard Worker log_exit, ReadElf, remove, str_to_bytes) 36*288bf522SAndroid Build Coastguard Worker 37*288bf522SAndroid Build Coastguard WorkerNATIVE_LIBS_DIR_ON_DEVICE = '/data/local/tmp/native_libs/' 38*288bf522SAndroid Build Coastguard Worker 39*288bf522SAndroid Build Coastguard WorkerSHELL_PS_UID_PATTERN = re.compile(r'USER.*\nu(\d+)_.*') 40*288bf522SAndroid Build Coastguard Worker 41*288bf522SAndroid Build Coastguard Worker 42*288bf522SAndroid Build Coastguard Workerclass HostElfEntry(object): 43*288bf522SAndroid Build Coastguard Worker """Represent a native lib on host in NativeLibDownloader.""" 44*288bf522SAndroid Build Coastguard Worker 45*288bf522SAndroid Build Coastguard Worker def __init__(self, path, name, score): 46*288bf522SAndroid Build Coastguard Worker self.path = path 47*288bf522SAndroid Build Coastguard Worker self.name = name 48*288bf522SAndroid Build Coastguard Worker self.score = score 49*288bf522SAndroid Build Coastguard Worker 50*288bf522SAndroid Build Coastguard Worker def __repr__(self): 51*288bf522SAndroid Build Coastguard Worker return self.__str__() 52*288bf522SAndroid Build Coastguard Worker 53*288bf522SAndroid Build Coastguard Worker def __str__(self): 54*288bf522SAndroid Build Coastguard Worker return '[path: %s, name %s, score %s]' % (self.path, self.name, self.score) 55*288bf522SAndroid Build Coastguard Worker 56*288bf522SAndroid Build Coastguard Worker 57*288bf522SAndroid Build Coastguard Workerclass NativeLibDownloader(object): 58*288bf522SAndroid Build Coastguard Worker """Download native libs on device. 59*288bf522SAndroid Build Coastguard Worker 60*288bf522SAndroid Build Coastguard Worker 1. Collect info of all native libs in the native_lib_dir on host. 61*288bf522SAndroid Build Coastguard Worker 2. Check the available native libs in /data/local/tmp/native_libs on device. 62*288bf522SAndroid Build Coastguard Worker 3. Sync native libs on device. 63*288bf522SAndroid Build Coastguard Worker """ 64*288bf522SAndroid Build Coastguard Worker 65*288bf522SAndroid Build Coastguard Worker def __init__(self, ndk_path, device_arch, adb): 66*288bf522SAndroid Build Coastguard Worker self.adb = adb 67*288bf522SAndroid Build Coastguard Worker self.readelf = ReadElf(ndk_path) 68*288bf522SAndroid Build Coastguard Worker self.device_arch = device_arch 69*288bf522SAndroid Build Coastguard Worker self.need_archs = self._get_need_archs() 70*288bf522SAndroid Build Coastguard Worker self.host_build_id_map = {} # Map from build_id to HostElfEntry. 71*288bf522SAndroid Build Coastguard Worker self.device_build_id_map = {} # Map from build_id to relative_path on device. 72*288bf522SAndroid Build Coastguard Worker # Map from filename to HostElfEntry for elf files without build id. 73*288bf522SAndroid Build Coastguard Worker self.no_build_id_file_map = {} 74*288bf522SAndroid Build Coastguard Worker self.name_count_map = {} # Used to give a unique name for each library. 75*288bf522SAndroid Build Coastguard Worker self.dir_on_device = NATIVE_LIBS_DIR_ON_DEVICE 76*288bf522SAndroid Build Coastguard Worker self.build_id_list_file = 'build_id_list' 77*288bf522SAndroid Build Coastguard Worker 78*288bf522SAndroid Build Coastguard Worker def _get_need_archs(self): 79*288bf522SAndroid Build Coastguard Worker """Return the archs of binaries needed on device.""" 80*288bf522SAndroid Build Coastguard Worker if self.device_arch == 'arm64': 81*288bf522SAndroid Build Coastguard Worker return ['arm', 'arm64'] 82*288bf522SAndroid Build Coastguard Worker if self.device_arch == 'arm': 83*288bf522SAndroid Build Coastguard Worker return ['arm'] 84*288bf522SAndroid Build Coastguard Worker if self.device_arch == 'x86_64': 85*288bf522SAndroid Build Coastguard Worker return ['x86', 'x86_64'] 86*288bf522SAndroid Build Coastguard Worker if self.device_arch == 'x86': 87*288bf522SAndroid Build Coastguard Worker return ['x86'] 88*288bf522SAndroid Build Coastguard Worker return [] 89*288bf522SAndroid Build Coastguard Worker 90*288bf522SAndroid Build Coastguard Worker def collect_native_libs_on_host(self, native_lib_dir): 91*288bf522SAndroid Build Coastguard Worker self.host_build_id_map.clear() 92*288bf522SAndroid Build Coastguard Worker for root, _, files in os.walk(native_lib_dir): 93*288bf522SAndroid Build Coastguard Worker for name in files: 94*288bf522SAndroid Build Coastguard Worker if not name.endswith('.so'): 95*288bf522SAndroid Build Coastguard Worker continue 96*288bf522SAndroid Build Coastguard Worker self.add_native_lib_on_host(os.path.join(root, name), name) 97*288bf522SAndroid Build Coastguard Worker 98*288bf522SAndroid Build Coastguard Worker def add_native_lib_on_host(self, path, name): 99*288bf522SAndroid Build Coastguard Worker arch = self.readelf.get_arch(path) 100*288bf522SAndroid Build Coastguard Worker if arch not in self.need_archs: 101*288bf522SAndroid Build Coastguard Worker return 102*288bf522SAndroid Build Coastguard Worker sections = self.readelf.get_sections(path) 103*288bf522SAndroid Build Coastguard Worker score = 0 104*288bf522SAndroid Build Coastguard Worker if '.debug_info' in sections: 105*288bf522SAndroid Build Coastguard Worker score = 3 106*288bf522SAndroid Build Coastguard Worker elif '.gnu_debugdata' in sections: 107*288bf522SAndroid Build Coastguard Worker score = 2 108*288bf522SAndroid Build Coastguard Worker elif '.symtab' in sections: 109*288bf522SAndroid Build Coastguard Worker score = 1 110*288bf522SAndroid Build Coastguard Worker build_id = self.readelf.get_build_id(path) 111*288bf522SAndroid Build Coastguard Worker if build_id: 112*288bf522SAndroid Build Coastguard Worker entry = self.host_build_id_map.get(build_id) 113*288bf522SAndroid Build Coastguard Worker if entry: 114*288bf522SAndroid Build Coastguard Worker if entry.score < score: 115*288bf522SAndroid Build Coastguard Worker entry.path = path 116*288bf522SAndroid Build Coastguard Worker entry.score = score 117*288bf522SAndroid Build Coastguard Worker else: 118*288bf522SAndroid Build Coastguard Worker repeat_count = self.name_count_map.get(name, 0) 119*288bf522SAndroid Build Coastguard Worker self.name_count_map[name] = repeat_count + 1 120*288bf522SAndroid Build Coastguard Worker unique_name = name if repeat_count == 0 else name + '_' + str(repeat_count) 121*288bf522SAndroid Build Coastguard Worker self.host_build_id_map[build_id] = HostElfEntry(path, unique_name, score) 122*288bf522SAndroid Build Coastguard Worker else: 123*288bf522SAndroid Build Coastguard Worker entry = self.no_build_id_file_map.get(name) 124*288bf522SAndroid Build Coastguard Worker if entry: 125*288bf522SAndroid Build Coastguard Worker if entry.score < score: 126*288bf522SAndroid Build Coastguard Worker entry.path = path 127*288bf522SAndroid Build Coastguard Worker entry.score = score 128*288bf522SAndroid Build Coastguard Worker else: 129*288bf522SAndroid Build Coastguard Worker self.no_build_id_file_map[name] = HostElfEntry(path, name, score) 130*288bf522SAndroid Build Coastguard Worker 131*288bf522SAndroid Build Coastguard Worker def collect_native_libs_on_device(self): 132*288bf522SAndroid Build Coastguard Worker self.device_build_id_map.clear() 133*288bf522SAndroid Build Coastguard Worker self.adb.check_run(['shell', 'mkdir', '-p', self.dir_on_device]) 134*288bf522SAndroid Build Coastguard Worker if os.path.exists(self.build_id_list_file): 135*288bf522SAndroid Build Coastguard Worker os.remove(self.build_id_list_file) 136*288bf522SAndroid Build Coastguard Worker result, output = self.adb.run_and_return_output(['shell', 'ls', self.dir_on_device]) 137*288bf522SAndroid Build Coastguard Worker if not result: 138*288bf522SAndroid Build Coastguard Worker return 139*288bf522SAndroid Build Coastguard Worker file_set = set(output.strip().split()) 140*288bf522SAndroid Build Coastguard Worker if self.build_id_list_file not in file_set: 141*288bf522SAndroid Build Coastguard Worker return 142*288bf522SAndroid Build Coastguard Worker self.adb.run(['pull', self.dir_on_device + self.build_id_list_file]) 143*288bf522SAndroid Build Coastguard Worker if os.path.exists(self.build_id_list_file): 144*288bf522SAndroid Build Coastguard Worker with open(self.build_id_list_file, 'rb') as fh: 145*288bf522SAndroid Build Coastguard Worker for line in fh.readlines(): 146*288bf522SAndroid Build Coastguard Worker line = bytes_to_str(line).strip() 147*288bf522SAndroid Build Coastguard Worker items = line.split('=') 148*288bf522SAndroid Build Coastguard Worker if len(items) == 2: 149*288bf522SAndroid Build Coastguard Worker build_id, filename = items 150*288bf522SAndroid Build Coastguard Worker if filename in file_set: 151*288bf522SAndroid Build Coastguard Worker self.device_build_id_map[build_id] = filename 152*288bf522SAndroid Build Coastguard Worker remove(self.build_id_list_file) 153*288bf522SAndroid Build Coastguard Worker 154*288bf522SAndroid Build Coastguard Worker def sync_native_libs_on_device(self): 155*288bf522SAndroid Build Coastguard Worker # Push missing native libs on device. 156*288bf522SAndroid Build Coastguard Worker for build_id in self.host_build_id_map: 157*288bf522SAndroid Build Coastguard Worker if build_id not in self.device_build_id_map: 158*288bf522SAndroid Build Coastguard Worker entry = self.host_build_id_map[build_id] 159*288bf522SAndroid Build Coastguard Worker self.adb.check_run(['push', entry.path, self.dir_on_device + entry.name]) 160*288bf522SAndroid Build Coastguard Worker # Remove native libs not exist on host. 161*288bf522SAndroid Build Coastguard Worker for build_id in self.device_build_id_map: 162*288bf522SAndroid Build Coastguard Worker if build_id not in self.host_build_id_map: 163*288bf522SAndroid Build Coastguard Worker name = self.device_build_id_map[build_id] 164*288bf522SAndroid Build Coastguard Worker self.adb.run(['shell', 'rm', self.dir_on_device + name]) 165*288bf522SAndroid Build Coastguard Worker # Push new build_id_list on device. 166*288bf522SAndroid Build Coastguard Worker with open(self.build_id_list_file, 'wb') as fh: 167*288bf522SAndroid Build Coastguard Worker for build_id in self.host_build_id_map: 168*288bf522SAndroid Build Coastguard Worker s = str_to_bytes('%s=%s\n' % (build_id, self.host_build_id_map[build_id].name)) 169*288bf522SAndroid Build Coastguard Worker fh.write(s) 170*288bf522SAndroid Build Coastguard Worker self.adb.check_run(['push', self.build_id_list_file, 171*288bf522SAndroid Build Coastguard Worker self.dir_on_device + self.build_id_list_file]) 172*288bf522SAndroid Build Coastguard Worker os.remove(self.build_id_list_file) 173*288bf522SAndroid Build Coastguard Worker 174*288bf522SAndroid Build Coastguard Worker # Push elf files without build id on device. 175*288bf522SAndroid Build Coastguard Worker for entry in self.no_build_id_file_map.values(): 176*288bf522SAndroid Build Coastguard Worker target = self.dir_on_device + entry.name 177*288bf522SAndroid Build Coastguard Worker 178*288bf522SAndroid Build Coastguard Worker # Skip download if we have a file with the same name and size on device. 179*288bf522SAndroid Build Coastguard Worker result, output = self.adb.run_and_return_output(['shell', 'ls', '-l', target]) 180*288bf522SAndroid Build Coastguard Worker if result: 181*288bf522SAndroid Build Coastguard Worker items = output.split() 182*288bf522SAndroid Build Coastguard Worker if len(items) > 5: 183*288bf522SAndroid Build Coastguard Worker try: 184*288bf522SAndroid Build Coastguard Worker file_size = int(items[4]) 185*288bf522SAndroid Build Coastguard Worker except ValueError: 186*288bf522SAndroid Build Coastguard Worker file_size = 0 187*288bf522SAndroid Build Coastguard Worker if file_size == os.path.getsize(entry.path): 188*288bf522SAndroid Build Coastguard Worker continue 189*288bf522SAndroid Build Coastguard Worker self.adb.check_run(['push', entry.path, target]) 190*288bf522SAndroid Build Coastguard Worker 191*288bf522SAndroid Build Coastguard Worker 192*288bf522SAndroid Build Coastguard Workerclass ProfilerBase(object): 193*288bf522SAndroid Build Coastguard Worker """Base class of all Profilers.""" 194*288bf522SAndroid Build Coastguard Worker 195*288bf522SAndroid Build Coastguard Worker def __init__(self, args): 196*288bf522SAndroid Build Coastguard Worker self.args = args 197*288bf522SAndroid Build Coastguard Worker self.adb = AdbHelper(enable_switch_to_root=not args.disable_adb_root) 198*288bf522SAndroid Build Coastguard Worker if not self.adb.is_device_available(): 199*288bf522SAndroid Build Coastguard Worker log_exit('No Android device is connected via ADB.') 200*288bf522SAndroid Build Coastguard Worker self.is_root_device = self.adb.switch_to_root() 201*288bf522SAndroid Build Coastguard Worker self.android_version = self.adb.get_android_version() 202*288bf522SAndroid Build Coastguard Worker if self.android_version < 7: 203*288bf522SAndroid Build Coastguard Worker log_exit("""app_profiler.py isn't supported on Android < N, please switch to use 204*288bf522SAndroid Build Coastguard Worker simpleperf binary directly.""") 205*288bf522SAndroid Build Coastguard Worker self.device_arch = self.adb.get_device_arch() 206*288bf522SAndroid Build Coastguard Worker self.record_subproc = None 207*288bf522SAndroid Build Coastguard Worker 208*288bf522SAndroid Build Coastguard Worker def profile(self): 209*288bf522SAndroid Build Coastguard Worker logging.info('prepare profiling') 210*288bf522SAndroid Build Coastguard Worker self.prepare() 211*288bf522SAndroid Build Coastguard Worker logging.info('start profiling') 212*288bf522SAndroid Build Coastguard Worker self.start() 213*288bf522SAndroid Build Coastguard Worker self.wait_profiling() 214*288bf522SAndroid Build Coastguard Worker logging.info('collect profiling data') 215*288bf522SAndroid Build Coastguard Worker self.collect_profiling_data() 216*288bf522SAndroid Build Coastguard Worker logging.info('profiling is finished.') 217*288bf522SAndroid Build Coastguard Worker 218*288bf522SAndroid Build Coastguard Worker def prepare(self): 219*288bf522SAndroid Build Coastguard Worker """Prepare recording. """ 220*288bf522SAndroid Build Coastguard Worker self.download_simpleperf() 221*288bf522SAndroid Build Coastguard Worker if self.args.native_lib_dir: 222*288bf522SAndroid Build Coastguard Worker self.download_libs() 223*288bf522SAndroid Build Coastguard Worker 224*288bf522SAndroid Build Coastguard Worker def download_simpleperf(self): 225*288bf522SAndroid Build Coastguard Worker simpleperf_binary = get_target_binary_path(self.device_arch, 'simpleperf') 226*288bf522SAndroid Build Coastguard Worker self.adb.check_run(['push', simpleperf_binary, '/data/local/tmp']) 227*288bf522SAndroid Build Coastguard Worker self.adb.check_run(['shell', 'chmod', 'a+x', '/data/local/tmp/simpleperf']) 228*288bf522SAndroid Build Coastguard Worker 229*288bf522SAndroid Build Coastguard Worker def download_libs(self): 230*288bf522SAndroid Build Coastguard Worker downloader = NativeLibDownloader(self.args.ndk_path, self.device_arch, self.adb) 231*288bf522SAndroid Build Coastguard Worker downloader.collect_native_libs_on_host(self.args.native_lib_dir) 232*288bf522SAndroid Build Coastguard Worker downloader.collect_native_libs_on_device() 233*288bf522SAndroid Build Coastguard Worker downloader.sync_native_libs_on_device() 234*288bf522SAndroid Build Coastguard Worker 235*288bf522SAndroid Build Coastguard Worker def start(self): 236*288bf522SAndroid Build Coastguard Worker raise NotImplementedError 237*288bf522SAndroid Build Coastguard Worker 238*288bf522SAndroid Build Coastguard Worker def start_profiling(self, target_args): 239*288bf522SAndroid Build Coastguard Worker """Start simpleperf record process on device.""" 240*288bf522SAndroid Build Coastguard Worker args = ['/data/local/tmp/simpleperf', 'record', '-o', '/data/local/tmp/perf.data', 241*288bf522SAndroid Build Coastguard Worker self.args.record_options] 242*288bf522SAndroid Build Coastguard Worker if self.adb.run(['shell', 'ls', NATIVE_LIBS_DIR_ON_DEVICE]): 243*288bf522SAndroid Build Coastguard Worker args += ['--symfs', NATIVE_LIBS_DIR_ON_DEVICE] 244*288bf522SAndroid Build Coastguard Worker args += ['--log', self.args.log] 245*288bf522SAndroid Build Coastguard Worker args += target_args 246*288bf522SAndroid Build Coastguard Worker adb_args = [self.adb.adb_path, 'shell'] + args 247*288bf522SAndroid Build Coastguard Worker logging.info('run adb cmd: %s' % adb_args) 248*288bf522SAndroid Build Coastguard Worker self.record_subproc = subprocess.Popen(adb_args) 249*288bf522SAndroid Build Coastguard Worker 250*288bf522SAndroid Build Coastguard Worker def wait_profiling(self): 251*288bf522SAndroid Build Coastguard Worker """Wait until profiling finishes, or stop profiling when user presses Ctrl-C.""" 252*288bf522SAndroid Build Coastguard Worker returncode = None 253*288bf522SAndroid Build Coastguard Worker try: 254*288bf522SAndroid Build Coastguard Worker returncode = self.record_subproc.wait() 255*288bf522SAndroid Build Coastguard Worker except KeyboardInterrupt: 256*288bf522SAndroid Build Coastguard Worker self.stop_profiling() 257*288bf522SAndroid Build Coastguard Worker self.record_subproc = None 258*288bf522SAndroid Build Coastguard Worker # Don't check return value of record_subproc. Because record_subproc also 259*288bf522SAndroid Build Coastguard Worker # receives Ctrl-C, and always returns non-zero. 260*288bf522SAndroid Build Coastguard Worker returncode = 0 261*288bf522SAndroid Build Coastguard Worker logging.debug('profiling result [%s]' % (returncode == 0)) 262*288bf522SAndroid Build Coastguard Worker if returncode != 0: 263*288bf522SAndroid Build Coastguard Worker log_exit('Failed to record profiling data.') 264*288bf522SAndroid Build Coastguard Worker 265*288bf522SAndroid Build Coastguard Worker def stop_profiling(self): 266*288bf522SAndroid Build Coastguard Worker """Stop profiling by sending SIGINT to simpleperf, and wait until it exits 267*288bf522SAndroid Build Coastguard Worker to make sure perf.data is completely generated.""" 268*288bf522SAndroid Build Coastguard Worker has_killed = False 269*288bf522SAndroid Build Coastguard Worker while True: 270*288bf522SAndroid Build Coastguard Worker (result, _) = self.adb.run_and_return_output(['shell', 'pidof', 'simpleperf']) 271*288bf522SAndroid Build Coastguard Worker if not result: 272*288bf522SAndroid Build Coastguard Worker break 273*288bf522SAndroid Build Coastguard Worker if not has_killed: 274*288bf522SAndroid Build Coastguard Worker has_killed = True 275*288bf522SAndroid Build Coastguard Worker self.adb.run_and_return_output(['shell', 'pkill', '-l', '2', 'simpleperf']) 276*288bf522SAndroid Build Coastguard Worker time.sleep(1) 277*288bf522SAndroid Build Coastguard Worker 278*288bf522SAndroid Build Coastguard Worker def collect_profiling_data(self): 279*288bf522SAndroid Build Coastguard Worker self.adb.check_run_and_return_output(['pull', '/data/local/tmp/perf.data', 280*288bf522SAndroid Build Coastguard Worker self.args.perf_data_path]) 281*288bf522SAndroid Build Coastguard Worker if not self.args.skip_collect_binaries: 282*288bf522SAndroid Build Coastguard Worker binary_cache_args = [sys.executable, 283*288bf522SAndroid Build Coastguard Worker os.path.join(get_script_dir(), 'binary_cache_builder.py')] 284*288bf522SAndroid Build Coastguard Worker binary_cache_args += ['-i', self.args.perf_data_path, '--log', self.args.log] 285*288bf522SAndroid Build Coastguard Worker if self.args.native_lib_dir: 286*288bf522SAndroid Build Coastguard Worker binary_cache_args += ['-lib', self.args.native_lib_dir] 287*288bf522SAndroid Build Coastguard Worker if self.args.disable_adb_root: 288*288bf522SAndroid Build Coastguard Worker binary_cache_args += ['--disable_adb_root'] 289*288bf522SAndroid Build Coastguard Worker if self.args.ndk_path: 290*288bf522SAndroid Build Coastguard Worker binary_cache_args += ['--ndk_path', self.args.ndk_path] 291*288bf522SAndroid Build Coastguard Worker subprocess.check_call(binary_cache_args) 292*288bf522SAndroid Build Coastguard Worker 293*288bf522SAndroid Build Coastguard Worker 294*288bf522SAndroid Build Coastguard Workerclass AppProfiler(ProfilerBase): 295*288bf522SAndroid Build Coastguard Worker """Profile an Android app.""" 296*288bf522SAndroid Build Coastguard Worker 297*288bf522SAndroid Build Coastguard Worker def prepare(self): 298*288bf522SAndroid Build Coastguard Worker super(AppProfiler, self).prepare() 299*288bf522SAndroid Build Coastguard Worker self.app_versioncode = self.get_app_versioncode() 300*288bf522SAndroid Build Coastguard Worker if self.args.compile_java_code: 301*288bf522SAndroid Build Coastguard Worker self.compile_java_code() 302*288bf522SAndroid Build Coastguard Worker 303*288bf522SAndroid Build Coastguard Worker def get_app_versioncode(self) -> Optional[str]: 304*288bf522SAndroid Build Coastguard Worker result, output = self.adb.run_and_return_output( 305*288bf522SAndroid Build Coastguard Worker ['shell', 'pm', 'list', 'packages', '--show-versioncode']) 306*288bf522SAndroid Build Coastguard Worker if not result: 307*288bf522SAndroid Build Coastguard Worker return None 308*288bf522SAndroid Build Coastguard Worker prefix = f'package:{self.args.app} ' 309*288bf522SAndroid Build Coastguard Worker for line in output.splitlines(): 310*288bf522SAndroid Build Coastguard Worker if line.startswith(prefix): 311*288bf522SAndroid Build Coastguard Worker pos = line.find('versionCode:') 312*288bf522SAndroid Build Coastguard Worker if pos != -1: 313*288bf522SAndroid Build Coastguard Worker return line[pos + len('versionCode:'):].strip() 314*288bf522SAndroid Build Coastguard Worker return None 315*288bf522SAndroid Build Coastguard Worker 316*288bf522SAndroid Build Coastguard Worker def compile_java_code(self): 317*288bf522SAndroid Build Coastguard Worker self.kill_app_process() 318*288bf522SAndroid Build Coastguard Worker # Fully compile Java code on Android >= N. 319*288bf522SAndroid Build Coastguard Worker self.adb.set_property('debug.generate-debug-info', 'true') 320*288bf522SAndroid Build Coastguard Worker self.adb.check_run(['shell', 'cmd', 'package', 'compile', '-f', '-m', 'speed', 321*288bf522SAndroid Build Coastguard Worker self.args.app]) 322*288bf522SAndroid Build Coastguard Worker 323*288bf522SAndroid Build Coastguard Worker def kill_app_process(self): 324*288bf522SAndroid Build Coastguard Worker if self.find_app_process(): 325*288bf522SAndroid Build Coastguard Worker self.adb.check_run(['shell', 'am', 'force-stop', self.args.app]) 326*288bf522SAndroid Build Coastguard Worker count = 0 327*288bf522SAndroid Build Coastguard Worker while True: 328*288bf522SAndroid Build Coastguard Worker time.sleep(1) 329*288bf522SAndroid Build Coastguard Worker pid = self.find_app_process() 330*288bf522SAndroid Build Coastguard Worker if not pid: 331*288bf522SAndroid Build Coastguard Worker break 332*288bf522SAndroid Build Coastguard Worker count += 1 333*288bf522SAndroid Build Coastguard Worker if count >= 5: 334*288bf522SAndroid Build Coastguard Worker logging.info('unable to kill %s, skipping...' % self.args.app) 335*288bf522SAndroid Build Coastguard Worker break 336*288bf522SAndroid Build Coastguard Worker # When testing on Android N, `am force-stop` sometimes can't kill 337*288bf522SAndroid Build Coastguard Worker # com.example.simpleperf.simpleperfexampleofkotlin. So use kill when this happens. 338*288bf522SAndroid Build Coastguard Worker if count >= 3: 339*288bf522SAndroid Build Coastguard Worker self.run_in_app_dir(['kill', '-9', str(pid)]) 340*288bf522SAndroid Build Coastguard Worker 341*288bf522SAndroid Build Coastguard Worker def find_app_process(self): 342*288bf522SAndroid Build Coastguard Worker result, pidof_output = self.adb.run_and_return_output( 343*288bf522SAndroid Build Coastguard Worker ['shell', 'pidof', self.args.app]) 344*288bf522SAndroid Build Coastguard Worker if not result: 345*288bf522SAndroid Build Coastguard Worker return None 346*288bf522SAndroid Build Coastguard Worker result, current_user = self.adb.run_and_return_output( 347*288bf522SAndroid Build Coastguard Worker ['shell', 'am', 'get-current-user']) 348*288bf522SAndroid Build Coastguard Worker if not result: 349*288bf522SAndroid Build Coastguard Worker return None 350*288bf522SAndroid Build Coastguard Worker pids = pidof_output.split() 351*288bf522SAndroid Build Coastguard Worker for pid in pids: 352*288bf522SAndroid Build Coastguard Worker result, ps_output = self.adb.run_and_return_output( 353*288bf522SAndroid Build Coastguard Worker ['shell', 'ps', '-p', pid, '-o', 'USER']) 354*288bf522SAndroid Build Coastguard Worker if not result: 355*288bf522SAndroid Build Coastguard Worker return None 356*288bf522SAndroid Build Coastguard Worker uid = SHELL_PS_UID_PATTERN.search(ps_output).group(1) 357*288bf522SAndroid Build Coastguard Worker if uid == current_user.strip(): 358*288bf522SAndroid Build Coastguard Worker return int(pid) 359*288bf522SAndroid Build Coastguard Worker return None 360*288bf522SAndroid Build Coastguard Worker 361*288bf522SAndroid Build Coastguard Worker def run_in_app_dir(self, args): 362*288bf522SAndroid Build Coastguard Worker if self.is_root_device: 363*288bf522SAndroid Build Coastguard Worker adb_args = ['shell', 'cd /data/data/' + self.args.app + ' && ' + (' '.join(args))] 364*288bf522SAndroid Build Coastguard Worker else: 365*288bf522SAndroid Build Coastguard Worker adb_args = ['shell', 'run-as', self.args.app] + args 366*288bf522SAndroid Build Coastguard Worker return self.adb.run_and_return_output(adb_args) 367*288bf522SAndroid Build Coastguard Worker 368*288bf522SAndroid Build Coastguard Worker def start(self): 369*288bf522SAndroid Build Coastguard Worker if self.args.launch or self.args.activity or self.args.test: 370*288bf522SAndroid Build Coastguard Worker self.kill_app_process() 371*288bf522SAndroid Build Coastguard Worker args = ['--app', self.args.app] 372*288bf522SAndroid Build Coastguard Worker if self.app_versioncode: 373*288bf522SAndroid Build Coastguard Worker args += ['--add-meta-info', f'app_versioncode={self.app_versioncode}'] 374*288bf522SAndroid Build Coastguard Worker self.start_profiling(args) 375*288bf522SAndroid Build Coastguard Worker if self.args.launch: 376*288bf522SAndroid Build Coastguard Worker self.start_app() 377*288bf522SAndroid Build Coastguard Worker if self.args.activity: 378*288bf522SAndroid Build Coastguard Worker self.start_activity() 379*288bf522SAndroid Build Coastguard Worker elif self.args.test: 380*288bf522SAndroid Build Coastguard Worker self.start_test() 381*288bf522SAndroid Build Coastguard Worker # else: no need to start an activity or test. 382*288bf522SAndroid Build Coastguard Worker 383*288bf522SAndroid Build Coastguard Worker def start_app(self): 384*288bf522SAndroid Build Coastguard Worker result = self.adb.run(['shell', 'monkey', '-p', self.args.app, '1']) 385*288bf522SAndroid Build Coastguard Worker if not result: 386*288bf522SAndroid Build Coastguard Worker self.record_subproc.terminate() 387*288bf522SAndroid Build Coastguard Worker log_exit(f"Can't start {self.args.app}") 388*288bf522SAndroid Build Coastguard Worker 389*288bf522SAndroid Build Coastguard Worker def start_activity(self): 390*288bf522SAndroid Build Coastguard Worker activity = self.args.app + '/' + self.args.activity 391*288bf522SAndroid Build Coastguard Worker result = self.adb.run(['shell', 'am', 'start', '-n', activity]) 392*288bf522SAndroid Build Coastguard Worker if not result: 393*288bf522SAndroid Build Coastguard Worker self.record_subproc.terminate() 394*288bf522SAndroid Build Coastguard Worker log_exit("Can't start activity %s" % activity) 395*288bf522SAndroid Build Coastguard Worker 396*288bf522SAndroid Build Coastguard Worker def start_test(self): 397*288bf522SAndroid Build Coastguard Worker runner = self.args.app + '/androidx.test.runner.AndroidJUnitRunner' 398*288bf522SAndroid Build Coastguard Worker result = self.adb.run(['shell', 'am', 'instrument', '-e', 'class', 399*288bf522SAndroid Build Coastguard Worker self.args.test, runner]) 400*288bf522SAndroid Build Coastguard Worker if not result: 401*288bf522SAndroid Build Coastguard Worker self.record_subproc.terminate() 402*288bf522SAndroid Build Coastguard Worker log_exit("Can't start instrumentation test %s" % self.args.test) 403*288bf522SAndroid Build Coastguard Worker 404*288bf522SAndroid Build Coastguard Worker 405*288bf522SAndroid Build Coastguard Workerclass NativeProgramProfiler(ProfilerBase): 406*288bf522SAndroid Build Coastguard Worker """Profile a native program.""" 407*288bf522SAndroid Build Coastguard Worker 408*288bf522SAndroid Build Coastguard Worker def start(self): 409*288bf522SAndroid Build Coastguard Worker logging.info('Waiting for native process %s' % self.args.native_program) 410*288bf522SAndroid Build Coastguard Worker while True: 411*288bf522SAndroid Build Coastguard Worker (result, pid) = self.adb.run_and_return_output(['shell', 'pidof', 412*288bf522SAndroid Build Coastguard Worker self.args.native_program]) 413*288bf522SAndroid Build Coastguard Worker if not result: 414*288bf522SAndroid Build Coastguard Worker # Wait for 1 millisecond. 415*288bf522SAndroid Build Coastguard Worker time.sleep(0.001) 416*288bf522SAndroid Build Coastguard Worker else: 417*288bf522SAndroid Build Coastguard Worker self.start_profiling(['-p', str(int(pid))]) 418*288bf522SAndroid Build Coastguard Worker break 419*288bf522SAndroid Build Coastguard Worker 420*288bf522SAndroid Build Coastguard Worker 421*288bf522SAndroid Build Coastguard Workerclass NativeCommandProfiler(ProfilerBase): 422*288bf522SAndroid Build Coastguard Worker """Profile running a native command.""" 423*288bf522SAndroid Build Coastguard Worker 424*288bf522SAndroid Build Coastguard Worker def start(self): 425*288bf522SAndroid Build Coastguard Worker self.start_profiling([self.args.cmd]) 426*288bf522SAndroid Build Coastguard Worker 427*288bf522SAndroid Build Coastguard Worker 428*288bf522SAndroid Build Coastguard Workerclass NativeProcessProfiler(ProfilerBase): 429*288bf522SAndroid Build Coastguard Worker """Profile processes given their pids.""" 430*288bf522SAndroid Build Coastguard Worker 431*288bf522SAndroid Build Coastguard Worker def start(self): 432*288bf522SAndroid Build Coastguard Worker self.start_profiling(['-p', ','.join(self.args.pid)]) 433*288bf522SAndroid Build Coastguard Worker 434*288bf522SAndroid Build Coastguard Worker 435*288bf522SAndroid Build Coastguard Workerclass NativeThreadProfiler(ProfilerBase): 436*288bf522SAndroid Build Coastguard Worker """Profile threads given their tids.""" 437*288bf522SAndroid Build Coastguard Worker 438*288bf522SAndroid Build Coastguard Worker def start(self): 439*288bf522SAndroid Build Coastguard Worker self.start_profiling(['-t', ','.join(self.args.tid)]) 440*288bf522SAndroid Build Coastguard Worker 441*288bf522SAndroid Build Coastguard Worker 442*288bf522SAndroid Build Coastguard Workerclass SystemWideProfiler(ProfilerBase): 443*288bf522SAndroid Build Coastguard Worker """Profile system wide.""" 444*288bf522SAndroid Build Coastguard Worker 445*288bf522SAndroid Build Coastguard Worker def start(self): 446*288bf522SAndroid Build Coastguard Worker self.start_profiling(['-a']) 447*288bf522SAndroid Build Coastguard Worker 448*288bf522SAndroid Build Coastguard Worker 449*288bf522SAndroid Build Coastguard Workerdef main(): 450*288bf522SAndroid Build Coastguard Worker parser = BaseArgumentParser(description=__doc__) 451*288bf522SAndroid Build Coastguard Worker 452*288bf522SAndroid Build Coastguard Worker target_group = parser.add_argument_group(title='Select profiling target' 453*288bf522SAndroid Build Coastguard Worker ).add_mutually_exclusive_group(required=True) 454*288bf522SAndroid Build Coastguard Worker target_group.add_argument('-p', '--app', help="""Profile an Android app, given the package name. 455*288bf522SAndroid Build Coastguard Worker Like `-p com.example.android.myapp`.""") 456*288bf522SAndroid Build Coastguard Worker 457*288bf522SAndroid Build Coastguard Worker target_group.add_argument('-np', '--native_program', help="""Profile a native program running on 458*288bf522SAndroid Build Coastguard Worker the Android device. Like `-np surfaceflinger`.""") 459*288bf522SAndroid Build Coastguard Worker 460*288bf522SAndroid Build Coastguard Worker target_group.add_argument('-cmd', help="""Profile running a command on the Android device. 461*288bf522SAndroid Build Coastguard Worker Like `-cmd "pm -l"`.""") 462*288bf522SAndroid Build Coastguard Worker 463*288bf522SAndroid Build Coastguard Worker target_group.add_argument('--pid', nargs='+', help="""Profile native processes running on device 464*288bf522SAndroid Build Coastguard Worker given their process ids.""") 465*288bf522SAndroid Build Coastguard Worker 466*288bf522SAndroid Build Coastguard Worker target_group.add_argument('--tid', nargs='+', help="""Profile native threads running on device 467*288bf522SAndroid Build Coastguard Worker given their thread ids.""") 468*288bf522SAndroid Build Coastguard Worker 469*288bf522SAndroid Build Coastguard Worker target_group.add_argument('--system_wide', action='store_true', help="""Profile system wide.""") 470*288bf522SAndroid Build Coastguard Worker 471*288bf522SAndroid Build Coastguard Worker app_target_group = parser.add_argument_group(title='Extra options for profiling an app') 472*288bf522SAndroid Build Coastguard Worker app_target_group.add_argument('--compile_java_code', action='store_true', help="""Used with -p. 473*288bf522SAndroid Build Coastguard Worker On Android N and Android O, we need to compile Java code into 474*288bf522SAndroid Build Coastguard Worker native instructions to profile Java code. Android O also needs 475*288bf522SAndroid Build Coastguard Worker wrap.sh in the apk to use the native instructions.""") 476*288bf522SAndroid Build Coastguard Worker 477*288bf522SAndroid Build Coastguard Worker app_start_group = app_target_group.add_mutually_exclusive_group() 478*288bf522SAndroid Build Coastguard Worker app_start_group.add_argument('--launch', action='store_true', help="""Used with -p. Profile the 479*288bf522SAndroid Build Coastguard Worker launch time of an Android app. The app will be started or 480*288bf522SAndroid Build Coastguard Worker restarted.""") 481*288bf522SAndroid Build Coastguard Worker app_start_group.add_argument('-a', '--activity', help="""Used with -p. Profile the launch time 482*288bf522SAndroid Build Coastguard Worker of an activity in an Android app. The app will be started or 483*288bf522SAndroid Build Coastguard Worker restarted to run the activity. Like `-a .MainActivity`.""") 484*288bf522SAndroid Build Coastguard Worker 485*288bf522SAndroid Build Coastguard Worker app_start_group.add_argument('-t', '--test', help="""Used with -p. Profile the launch time of an 486*288bf522SAndroid Build Coastguard Worker instrumentation test in an Android app. The app will be started or 487*288bf522SAndroid Build Coastguard Worker restarted to run the instrumentation test. Like 488*288bf522SAndroid Build Coastguard Worker `-t test_class_name`.""") 489*288bf522SAndroid Build Coastguard Worker 490*288bf522SAndroid Build Coastguard Worker record_group = parser.add_argument_group('Select recording options') 491*288bf522SAndroid Build Coastguard Worker record_group.add_argument('-r', '--record_options', 492*288bf522SAndroid Build Coastguard Worker default='-e task-clock:u -f 1000 -g --duration 10', help="""Set 493*288bf522SAndroid Build Coastguard Worker recording options for `simpleperf record` command. Use 494*288bf522SAndroid Build Coastguard Worker `run_simpleperf_on_device.py record -h` to see all accepted options. 495*288bf522SAndroid Build Coastguard Worker Default is "-e task-clock:u -f 1000 -g --duration 10".""") 496*288bf522SAndroid Build Coastguard Worker 497*288bf522SAndroid Build Coastguard Worker record_group.add_argument('-lib', '--native_lib_dir', type=extant_dir, 498*288bf522SAndroid Build Coastguard Worker help="""When profiling an Android app containing native libraries, 499*288bf522SAndroid Build Coastguard Worker the native libraries are usually stripped and lake of symbols 500*288bf522SAndroid Build Coastguard Worker and debug information to provide good profiling result. By 501*288bf522SAndroid Build Coastguard Worker using -lib, you tell app_profiler.py the path storing 502*288bf522SAndroid Build Coastguard Worker unstripped native libraries, and app_profiler.py will search 503*288bf522SAndroid Build Coastguard Worker all shared libraries with suffix .so in the directory. Then 504*288bf522SAndroid Build Coastguard Worker the native libraries will be downloaded on device and 505*288bf522SAndroid Build Coastguard Worker collected in build_cache.""") 506*288bf522SAndroid Build Coastguard Worker 507*288bf522SAndroid Build Coastguard Worker record_group.add_argument('-o', '--perf_data_path', default='perf.data', 508*288bf522SAndroid Build Coastguard Worker help='The path to store profiling data. Default is perf.data.') 509*288bf522SAndroid Build Coastguard Worker 510*288bf522SAndroid Build Coastguard Worker record_group.add_argument('-nb', '--skip_collect_binaries', action='store_true', 511*288bf522SAndroid Build Coastguard Worker help="""By default we collect binaries used in profiling data from 512*288bf522SAndroid Build Coastguard Worker device to binary_cache directory. It can be used to annotate 513*288bf522SAndroid Build Coastguard Worker source code and disassembly. This option skips it.""") 514*288bf522SAndroid Build Coastguard Worker 515*288bf522SAndroid Build Coastguard Worker other_group = parser.add_argument_group('Other options') 516*288bf522SAndroid Build Coastguard Worker other_group.add_argument('--ndk_path', type=extant_dir, 517*288bf522SAndroid Build Coastguard Worker help="""Set the path of a ndk release. app_profiler.py needs some 518*288bf522SAndroid Build Coastguard Worker tools in ndk, like readelf.""") 519*288bf522SAndroid Build Coastguard Worker 520*288bf522SAndroid Build Coastguard Worker other_group.add_argument('--disable_adb_root', action='store_true', 521*288bf522SAndroid Build Coastguard Worker help="""Force adb to run in non root mode. By default, app_profiler.py 522*288bf522SAndroid Build Coastguard Worker will try to switch to root mode to be able to profile released 523*288bf522SAndroid Build Coastguard Worker Android apps.""") 524*288bf522SAndroid Build Coastguard Worker 525*288bf522SAndroid Build Coastguard Worker def check_args(args): 526*288bf522SAndroid Build Coastguard Worker if (not args.app) and (args.compile_java_code or args.activity or args.test): 527*288bf522SAndroid Build Coastguard Worker log_exit('--compile_java_code, -a, -t can only be used when profiling an Android app.') 528*288bf522SAndroid Build Coastguard Worker 529*288bf522SAndroid Build Coastguard Worker args = parser.parse_args() 530*288bf522SAndroid Build Coastguard Worker check_args(args) 531*288bf522SAndroid Build Coastguard Worker if args.app: 532*288bf522SAndroid Build Coastguard Worker profiler = AppProfiler(args) 533*288bf522SAndroid Build Coastguard Worker elif args.native_program: 534*288bf522SAndroid Build Coastguard Worker profiler = NativeProgramProfiler(args) 535*288bf522SAndroid Build Coastguard Worker elif args.cmd: 536*288bf522SAndroid Build Coastguard Worker profiler = NativeCommandProfiler(args) 537*288bf522SAndroid Build Coastguard Worker elif args.pid: 538*288bf522SAndroid Build Coastguard Worker profiler = NativeProcessProfiler(args) 539*288bf522SAndroid Build Coastguard Worker elif args.tid: 540*288bf522SAndroid Build Coastguard Worker profiler = NativeThreadProfiler(args) 541*288bf522SAndroid Build Coastguard Worker elif args.system_wide: 542*288bf522SAndroid Build Coastguard Worker profiler = SystemWideProfiler(args) 543*288bf522SAndroid Build Coastguard Worker profiler.profile() 544*288bf522SAndroid Build Coastguard Worker 545*288bf522SAndroid Build Coastguard Worker 546*288bf522SAndroid Build Coastguard Workerif __name__ == '__main__': 547*288bf522SAndroid Build Coastguard Worker main() 548