1#!/usr/bin/env python3 2# Copyright (C) 2017 The Android Open Source Project 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16from __future__ import absolute_import 17from __future__ import division 18from __future__ import print_function 19 20import argparse 21import atexit 22import os 23import shutil 24import signal 25import subprocess 26import sys 27import tempfile 28import time 29import uuid 30 31from perfetto.prebuilts.manifests.traceconv import * 32from perfetto.prebuilts.perfetto_prebuilts import * 33 34NULL = open(os.devnull) 35NOOUT = { 36 'stdout': NULL, 37 'stderr': NULL, 38} 39 40UUID = str(uuid.uuid4())[-6:] 41 42PACKAGES_LIST_CFG = '''data_sources { 43 config { 44 name: "android.packages_list" 45 } 46} 47''' 48 49CFG_INDENT = ' ' 50CFG = '''buffers {{ 51 size_kb: 63488 52}} 53 54data_sources {{ 55 config {{ 56 name: "android.heapprofd" 57 heapprofd_config {{ 58 shmem_size_bytes: {shmem_size} 59 sampling_interval_bytes: {interval} 60{target_cfg} 61 }} 62 }} 63}} 64 65duration_ms: {duration} 66write_into_file: true 67flush_timeout_ms: 30000 68flush_period_ms: 604800000 69''' 70 71# flush_period_ms of 1 week to suppress trace_processor_shell warning. 72 73CONTINUOUS_DUMP = """ 74 continuous_dump_config {{ 75 dump_phase_ms: 0 76 dump_interval_ms: {dump_interval} 77 }} 78""" 79 80PROFILE_LOCAL_PATH = os.path.join(tempfile.gettempdir(), UUID) 81 82IS_INTERRUPTED = False 83 84 85def sigint_handler(sig, frame): 86 global IS_INTERRUPTED 87 IS_INTERRUPTED = True 88 89 90def print_no_profile_error(): 91 print("No profiles generated", file=sys.stderr) 92 print( 93 "If this is unexpected, check " 94 "https://perfetto.dev/docs/data-sources/native-heap-profiler#troubleshooting.", 95 file=sys.stderr) 96 97 98def known_issues_url(number): 99 return ('https://perfetto.dev/docs/data-sources/native-heap-profiler' 100 '#known-issues-android{}'.format(number)) 101 102 103KNOWN_ISSUES = { 104 '10': known_issues_url(10), 105 'Q': known_issues_url(10), 106 '11': known_issues_url(11), 107 'R': known_issues_url(11), 108} 109 110 111def maybe_known_issues(): 112 release_or_codename = subprocess.check_output( 113 ['adb', 'shell', 'getprop', 114 'ro.build.version.release_or_codename']).decode('utf-8').strip() 115 return KNOWN_ISSUES.get(release_or_codename, None) 116 117 118SDK = { 119 'R': 30, 120} 121 122 123def release_or_newer(release): 124 sdk = int( 125 subprocess.check_output( 126 ['adb', 'shell', 'getprop', 127 'ro.system.build.version.sdk']).decode('utf-8').strip()) 128 if sdk >= SDK[release]: 129 return True 130 codename = subprocess.check_output( 131 ['adb', 'shell', 'getprop', 132 'ro.build.version.codename']).decode('utf-8').strip() 133 return codename == release 134 135 136ORDER = ['-n', '-p', '-i', '-o'] 137 138 139def arg_order(action): 140 result = len(ORDER) 141 for opt in action.option_strings: 142 if opt in ORDER: 143 result = min(ORDER.index(opt), result) 144 return result, action.option_strings[0].strip('-') 145 146 147def print_options(parser): 148 for action in sorted(parser._actions, key=arg_order): 149 if action.help is argparse.SUPPRESS: 150 continue 151 opts = ', '.join('`' + x + '`' for x in action.option_strings) 152 metavar = '' if action.metavar is None else ' _' + action.metavar + '_' 153 print('{}{}'.format(opts, metavar)) 154 print(': {}'.format(action.help)) 155 print() 156 157 158def main(argv): 159 parser = argparse.ArgumentParser(description="""Collect a heap profile 160 161 The PERFETTO_PROGUARD_MAP=packagename=map_filename.txt[:packagename=map_filename.txt...] environment variable can be used to pass proguard deobfuscation maps for different packages""", formatter_class=argparse.RawDescriptionHelpFormatter) 162 163 parser.add_argument( 164 "-i", 165 "--interval", 166 help="Sampling interval. " 167 "Default 4096 (4KiB)", 168 type=int, 169 default=4096) 170 parser.add_argument( 171 "-d", 172 "--duration", 173 help="Duration of profile (ms). 0 to run until interrupted. " 174 "Default: until interrupted by user.", 175 type=int, 176 default=0) 177 # This flag is a no-op now. We never start heapprofd explicitly using system 178 # properties. 179 parser.add_argument( 180 "--no-start", help="Do not start heapprofd.", action='store_true') 181 parser.add_argument( 182 "-p", 183 "--pid", 184 help="Comma-separated list of PIDs to " 185 "profile.", 186 metavar="PIDS") 187 parser.add_argument( 188 "-n", 189 "--name", 190 help="Comma-separated list of process " 191 "names to profile.", 192 metavar="NAMES") 193 parser.add_argument( 194 "-c", 195 "--continuous-dump", 196 help="Dump interval in ms. 0 to disable continuous dump.", 197 type=int, 198 default=0) 199 parser.add_argument( 200 "--heaps", 201 help="Comma-separated list of heaps to collect, e.g: malloc,art. " 202 "Requires Android 12.", 203 metavar="HEAPS") 204 parser.add_argument( 205 "--all-heaps", 206 action="store_true", 207 help="Collect allocations from all heaps registered by target.") 208 parser.add_argument( 209 "--no-android-tree-symbolization", 210 action="store_true", 211 help="Do not symbolize using currently lunched target in the " 212 "Android tree.") 213 parser.add_argument( 214 "--disable-selinux", 215 action="store_true", 216 help="Disable SELinux enforcement for duration of " 217 "profile.") 218 parser.add_argument( 219 "--no-versions", 220 action="store_true", 221 help="Do not get version information about APKs.") 222 parser.add_argument( 223 "--no-running", 224 action="store_true", 225 help="Do not target already running processes. Requires Android 11.") 226 parser.add_argument( 227 "--no-startup", 228 action="store_true", 229 help="Do not target processes that start during " 230 "the profile. Requires Android 11.") 231 parser.add_argument( 232 "--shmem-size", 233 help="Size of buffer between client and " 234 "heapprofd. Default 8MiB. Needs to be a power of two " 235 "multiple of 4096, at least 8192.", 236 type=int, 237 default=8 * 1048576) 238 parser.add_argument( 239 "--block-client", 240 help="When buffer is full, block the " 241 "client to wait for buffer space. Use with caution as " 242 "this can significantly slow down the client. " 243 "This is the default", 244 action="store_true") 245 parser.add_argument( 246 "--block-client-timeout", 247 help="If --block-client is given, do not block any allocation for " 248 "longer than this timeout (us).", 249 type=int) 250 parser.add_argument( 251 "--no-block-client", 252 help="When buffer is full, stop the " 253 "profile early.", 254 action="store_true") 255 parser.add_argument( 256 "--idle-allocations", 257 help="Keep track of how many " 258 "bytes were unused since the last dump, per " 259 "callstack", 260 action="store_true") 261 parser.add_argument( 262 "--dump-at-max", 263 help="Dump the maximum memory usage " 264 "rather than at the time of the dump.", 265 action="store_true") 266 parser.add_argument( 267 "--disable-fork-teardown", 268 help="Do not tear down client in forks. This can be useful for programs " 269 "that use vfork. Android 11+ only.", 270 action="store_true") 271 parser.add_argument( 272 "--simpleperf", 273 action="store_true", 274 help="Get simpleperf profile of heapprofd. This is " 275 "only for heapprofd development.") 276 parser.add_argument( 277 "--traceconv-binary", help="Path to local trace to text. For debugging.") 278 parser.add_argument( 279 "--no-annotations", 280 help="Do not suffix the pprof function names with Android ART mode " 281 "annotations such as [jit].", 282 action="store_true") 283 parser.add_argument( 284 "--print-config", 285 action="store_true", 286 help="Print config instead of running. For debugging.") 287 parser.add_argument( 288 "-o", 289 "--output", 290 help="Output directory.", 291 metavar="DIRECTORY", 292 default=None) 293 parser.add_argument( 294 "--print-options", action="store_true", help=argparse.SUPPRESS) 295 296 args = parser.parse_args() 297 if args.print_options: 298 print_options(parser) 299 return 0 300 fail = False 301 if args.block_client and args.no_block_client: 302 print( 303 "FATAL: Both block-client and no-block-client given.", file=sys.stderr) 304 fail = True 305 if args.pid is None and args.name is None: 306 print("FATAL: Neither PID nor NAME given.", file=sys.stderr) 307 fail = True 308 if args.duration is None: 309 print("FATAL: No duration given.", file=sys.stderr) 310 fail = True 311 if args.interval is None: 312 print("FATAL: No interval given.", file=sys.stderr) 313 fail = True 314 if args.shmem_size % 4096: 315 print("FATAL: shmem-size is not a multiple of 4096.", file=sys.stderr) 316 fail = True 317 if args.shmem_size < 8192: 318 print("FATAL: shmem-size is less than 8192.", file=sys.stderr) 319 fail = True 320 if args.shmem_size & (args.shmem_size - 1): 321 print("FATAL: shmem-size is not a power of two.", file=sys.stderr) 322 fail = True 323 324 target_cfg = "" 325 if not args.no_block_client: 326 target_cfg += CFG_INDENT + "block_client: true\n" 327 if args.block_client_timeout: 328 target_cfg += ( 329 CFG_INDENT + 330 "block_client_timeout_us: %s\n" % args.block_client_timeout) 331 if args.no_startup: 332 target_cfg += CFG_INDENT + "no_startup: true\n" 333 if args.no_running: 334 target_cfg += CFG_INDENT + "no_running: true\n" 335 if args.dump_at_max: 336 target_cfg += CFG_INDENT + "dump_at_max: true\n" 337 if args.disable_fork_teardown: 338 target_cfg += CFG_INDENT + "disable_fork_teardown: true\n" 339 if args.all_heaps: 340 target_cfg += CFG_INDENT + "all_heaps: true\n" 341 if args.pid: 342 for pid in args.pid.split(','): 343 try: 344 pid = int(pid) 345 except ValueError: 346 print("FATAL: invalid PID %s" % pid, file=sys.stderr) 347 fail = True 348 target_cfg += CFG_INDENT + 'pid: {}\n'.format(pid) 349 if args.name: 350 for name in args.name.split(','): 351 target_cfg += CFG_INDENT + 'process_cmdline: "{}"\n'.format(name) 352 if args.heaps: 353 for heap in args.heaps.split(','): 354 target_cfg += CFG_INDENT + 'heaps: "{}"\n'.format(heap) 355 356 if fail: 357 parser.print_help() 358 return 1 359 360 traceconv_binary = args.traceconv_binary 361 362 if args.continuous_dump: 363 target_cfg += CONTINUOUS_DUMP.format(dump_interval=args.continuous_dump) 364 cfg = CFG.format( 365 interval=args.interval, 366 duration=args.duration, 367 target_cfg=target_cfg, 368 shmem_size=args.shmem_size) 369 if not args.no_versions: 370 cfg += PACKAGES_LIST_CFG 371 372 if args.print_config: 373 print(cfg) 374 return 0 375 376 # Do this AFTER print_config so we do not download traceconv only to 377 # print out the config. 378 if traceconv_binary is None: 379 traceconv_binary = get_perfetto_prebuilt(TRACECONV_MANIFEST, soft_fail=True) 380 381 known_issues = maybe_known_issues() 382 if known_issues: 383 print('If you are experiencing problems, please see the known issues for ' 384 'your release: {}.'.format(known_issues)) 385 386 # TODO(fmayer): Maybe feature detect whether we can remove traces instead of 387 # this. 388 uuid_trace = release_or_newer('R') 389 if uuid_trace: 390 profile_device_path = '/data/misc/perfetto-traces/profile-' + UUID 391 else: 392 user = subprocess.check_output(['adb', 'shell', 393 'whoami']).decode('utf-8').strip() 394 profile_device_path = '/data/misc/perfetto-traces/profile-' + user 395 396 perfetto_cmd = ('CFG=\'{cfg}\'; echo ${{CFG}} | ' 397 'perfetto --txt -c - -o ' + profile_device_path + ' -d') 398 399 if args.disable_selinux: 400 enforcing = subprocess.check_output(['adb', 'shell', 401 'getenforce']).decode('utf-8').strip() 402 atexit.register( 403 subprocess.check_call, 404 ['adb', 'shell', 'su root setenforce %s' % enforcing]) 405 subprocess.check_call(['adb', 'shell', 'su root setenforce 0']) 406 407 if args.simpleperf: 408 subprocess.check_call([ 409 'adb', 'shell', 'mkdir -p /data/local/tmp/heapprofd_profile && ' 410 'cd /data/local/tmp/heapprofd_profile &&' 411 '(nohup simpleperf record -g -p $(pidof heapprofd) 2>&1 &) ' 412 '> /dev/null' 413 ]) 414 415 profile_target = PROFILE_LOCAL_PATH 416 if args.output is not None: 417 profile_target = args.output 418 else: 419 os.mkdir(profile_target) 420 421 if not os.path.isdir(profile_target): 422 print( 423 "Output directory {} not found".format(profile_target), file=sys.stderr) 424 return 1 425 426 if os.listdir(profile_target): 427 print( 428 "Output directory {} not empty".format(profile_target), file=sys.stderr) 429 return 1 430 431 perfetto_pid = subprocess.check_output( 432 ['adb', 'exec-out', perfetto_cmd.format(cfg=cfg)]).strip() 433 try: 434 perfetto_pid = int(perfetto_pid.strip()) 435 except ValueError: 436 print("Failed to invoke perfetto: {}".format(perfetto_pid), file=sys.stderr) 437 return 1 438 439 old_handler = signal.signal(signal.SIGINT, sigint_handler) 440 print("Profiling active. Press Ctrl+C to terminate.") 441 print("You may disconnect your device.") 442 print() 443 exists = True 444 device_connected = True 445 while not device_connected or (exists and not IS_INTERRUPTED): 446 exists = subprocess.call( 447 ['adb', 'shell', '[ -d /proc/{} ]'.format(perfetto_pid)], **NOOUT) == 0 448 device_connected = subprocess.call(['adb', 'shell', 'true'], **NOOUT) == 0 449 time.sleep(1) 450 print("Waiting for profiler shutdown...") 451 signal.signal(signal.SIGINT, old_handler) 452 if IS_INTERRUPTED: 453 # Not check_call because it could have existed in the meantime. 454 subprocess.call(['adb', 'shell', 'kill', '-INT', str(perfetto_pid)]) 455 if args.simpleperf: 456 subprocess.check_call(['adb', 'shell', 'killall', '-INT', 'simpleperf']) 457 print("Waiting for simpleperf to exit.") 458 while subprocess.call( 459 ['adb', 'shell', '[ -f /proc/$(pidof simpleperf)/exe ]'], **NOOUT) == 0: 460 time.sleep(1) 461 subprocess.check_call( 462 ['adb', 'pull', '/data/local/tmp/heapprofd_profile', profile_target]) 463 print("Pulled simpleperf profile to " + profile_target + 464 "/heapprofd_profile") 465 466 # Wait for perfetto cmd to return. 467 while exists: 468 exists = subprocess.call( 469 ['adb', 'shell', '[ -d /proc/{} ]'.format(perfetto_pid)]) == 0 470 time.sleep(1) 471 472 profile_host_path = os.path.join(profile_target, 'raw-trace') 473 subprocess.check_call(['adb', 'pull', profile_device_path, profile_host_path], 474 stdout=NULL) 475 if uuid_trace: 476 subprocess.check_call(['adb', 'shell', 'rm', profile_device_path], 477 stdout=NULL) 478 479 if traceconv_binary is None: 480 print('Wrote profile to {}'.format(profile_host_path)) 481 print( 482 'This file can be opened using the Perfetto UI, https://ui.perfetto.dev' 483 ) 484 return 0 485 486 binary_path = os.getenv('PERFETTO_BINARY_PATH') 487 if not args.no_android_tree_symbolization: 488 product_out = os.getenv('ANDROID_PRODUCT_OUT') 489 if product_out: 490 product_out_symbols = product_out + '/symbols' 491 else: 492 product_out_symbols = None 493 494 if binary_path is None: 495 binary_path = product_out_symbols 496 elif product_out_symbols is not None: 497 binary_path += os.pathsep + product_out_symbols 498 499 trace_file = os.path.join(profile_target, 'raw-trace') 500 concat_files = [trace_file] 501 502 if binary_path is not None: 503 with open(os.path.join(profile_target, 'symbols'), 'w') as fd: 504 ret = subprocess.call([ 505 traceconv_binary, 'symbolize', 506 os.path.join(profile_target, 'raw-trace') 507 ], 508 env=dict( 509 os.environ, PERFETTO_BINARY_PATH=binary_path), 510 stdout=fd) 511 if ret == 0: 512 concat_files.append(os.path.join(profile_target, 'symbols')) 513 else: 514 print("Failed to symbolize. Continuing without symbols.", file=sys.stderr) 515 516 proguard_map = os.getenv('PERFETTO_PROGUARD_MAP') 517 if proguard_map is not None: 518 with open(os.path.join(profile_target, 'deobfuscation-packets'), 'w') as fd: 519 ret = subprocess.call([ 520 traceconv_binary, 'deobfuscate', 521 os.path.join(profile_target, 'raw-trace') 522 ], 523 env=dict( 524 os.environ, PERFETTO_PROGUARD_MAP=proguard_map), 525 stdout=fd) 526 if ret == 0: 527 concat_files.append(os.path.join(profile_target, 'deobfuscation-packets')) 528 else: 529 print( 530 "Failed to deobfuscate. Continuing without deobfuscated.", 531 file=sys.stderr) 532 533 if len(concat_files) > 1: 534 with open(os.path.join(profile_target, 'symbolized-trace'), 'wb') as out: 535 for fn in concat_files: 536 with open(fn, 'rb') as inp: 537 while True: 538 buf = inp.read(4096) 539 if not buf: 540 break 541 out.write(buf) 542 trace_file = os.path.join(profile_target, 'symbolized-trace') 543 544 conversion_args = [traceconv_binary, 'profile'] + ( 545 ['--no-annotations'] if args.no_annotations else []) + [trace_file] 546 traceconv_output = subprocess.check_output(conversion_args) 547 profile_path = None 548 for word in traceconv_output.decode('utf-8').split(): 549 if 'heap_profile-' in word: 550 profile_path = word 551 if profile_path is None: 552 print_no_profile_error() 553 return 1 554 555 profile_files = os.listdir(profile_path) 556 if not profile_files: 557 print_no_profile_error() 558 return 1 559 560 for profile_file in profile_files: 561 shutil.copy(os.path.join(profile_path, profile_file), profile_target) 562 563 symlink_path = None 564 if not sys.platform.startswith('win'): 565 subprocess.check_call( 566 ['gzip'] + [os.path.join(profile_target, x) for x in profile_files]) 567 if args.output is None: 568 symlink_path = os.path.join( 569 os.path.dirname(profile_target), "heap_profile-latest") 570 if os.path.lexists(symlink_path): 571 os.unlink(symlink_path) 572 os.symlink(profile_target, symlink_path) 573 574 if symlink_path is not None: 575 print("Wrote profiles to {} (symlink {})".format(profile_target, 576 symlink_path)) 577 else: 578 print("Wrote profiles to {}".format(profile_target)) 579 580 print("The raw-trace file can be viewed using https://ui.perfetto.dev.") 581 print("The heap_dump.* files can be viewed using pprof/ (Googlers only) " + 582 "or https://www.speedscope.app/.") 583 print("The two above are equivalent. The raw-trace contains the union of " + 584 "all the heap dumps.") 585 586 587if __name__ == '__main__': 588 sys.exit(main(sys.argv)) 589