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