#!/usr/bin/env python3 # Copyright (C) 2020 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import absolute_import from __future__ import division from __future__ import print_function import argparse import os import subprocess import sys import tempfile import time import uuid NULL = open(os.devnull) PACKAGES_LIST_CFG = '''data_sources { config { name: "android.packages_list" } } ''' CFG_INDENT = ' ' CFG = '''buffers {{ size_kb: {size_kb} fill_policy: DISCARD }} data_sources {{ config {{ name: "android.java_hprof" java_hprof_config {{ {target_cfg} {continuous_dump_config} }} }} }} data_source_stop_timeout_ms: {data_source_stop_timeout_ms} duration_ms: {duration_ms} ''' OOM_CFG = '''buffers: {{ size_kb: {size_kb} fill_policy: DISCARD }} data_sources: {{ config {{ name: "android.java_hprof.oom" java_hprof_config {{ {process_cfg} }} }} }} data_source_stop_timeout_ms: 100000 trigger_config {{ trigger_mode: START_TRACING trigger_timeout_ms: {wait_duration_ms} triggers {{ name: "com.android.telemetry.art-outofmemory" stop_delay_ms: 500 }} }} ''' CONTINUOUS_DUMP = """ continuous_dump_config {{ dump_phase_ms: 0 dump_interval_ms: {dump_interval} }} """ UUID = str(uuid.uuid4())[-6:] PROFILE_PATH = '/data/misc/perfetto-traces/java-profile-' + UUID PERFETTO_CMD = ('CFG=\'{cfg}\'; echo ${{CFG}} | ' 'perfetto --txt -c - -o ' + PROFILE_PATH + ' -d') SDK = { 'S': 31, 'UpsideDownCake': 34, } def release_or_newer(release): sdk = int( subprocess.check_output( ['adb', 'shell', 'getprop', 'ro.system.build.version.sdk']).decode('utf-8').strip()) if sdk >= SDK[release]: return True codename = subprocess.check_output( ['adb', 'shell', 'getprop', 'ro.build.version.codename']).decode('utf-8').strip() return codename == release def convert_size_to_kb(size): if size.endswith("kb"): return int(size[:-2]) elif size.endswith("mb"): return int(size[:-2]) * 1024 elif size.endswith("gb"): return int(size[:-2]) * 1024 * 1024 else: return int(size) def generate_heap_dump_config(args): fail = False if args.pid is None and args.name is None: print("FATAL: Neither PID nor NAME given.", file=sys.stderr) fail = True target_cfg = "" if args.pid: for pid in args.pid.split(','): try: pid = int(pid) except ValueError: print("FATAL: invalid PID %s" % pid, file=sys.stderr) fail = True target_cfg += '{}pid: {}\n'.format(CFG_INDENT, pid) if args.name: for name in args.name.split(','): target_cfg += '{}process_cmdline: "{}"\n'.format(CFG_INDENT, name) if args.dump_smaps: target_cfg += '{}dump_smaps: true\n'.format(CFG_INDENT) if fail: return None continuous_dump_cfg = "" if args.continuous_dump: continuous_dump_cfg = CONTINUOUS_DUMP.format( dump_interval=args.continuous_dump) if args.continuous_dump: # Unlimited trace duration duration_ms = 0 elif args.stop_when_done: # Oneshot heapdump and the system supports data_source_stop_timeout_ms, we # can use a short duration. duration_ms = 1000 else: # Oneshot heapdump, but the system doesn't supports # data_source_stop_timeout_ms, we have to use a longer duration in the hope # of giving enough time to capture the whole dump. duration_ms = 20000 if args.stop_when_done: data_source_stop_timeout_ms = 100000 else: data_source_stop_timeout_ms = 0 return CFG.format( size_kb=convert_size_to_kb(args.buffer_size), target_cfg=target_cfg, continuous_dump_config=continuous_dump_cfg, duration_ms=duration_ms, data_source_stop_timeout_ms=data_source_stop_timeout_ms) def generate_oom_config(args): if not release_or_newer('UpsideDownCake'): print("FATAL: OOM mode not supported for this android version", file=sys.stderr) return None if args.pid: print("FATAL: Specifying pid not supported in OOM mode", file=sys.stderr) return None if not args.name: print("FATAL: Must specify process in OOM mode (use --name '*' to match all)", file=sys.stderr) return None if args.continuous_dump: print("FATAL: Specifying continuous dump not supported in OOM mode", file=sys.stderr) return None if args.dump_smaps: print("FATAL: Dumping smaps not supported in OOM mode", file=sys.stderr) return None process_cfg = '' for name in args.name.split(','): process_cfg += '{}process_cmdline: "{}"\n'.format(CFG_INDENT, name) return OOM_CFG.format( size_kb=convert_size_to_kb(args.buffer_size), wait_duration_ms=args.oom_wait_seconds * 1000, process_cfg=process_cfg) def main(argv): parser = argparse.ArgumentParser() parser.add_argument( "-o", "--output", help="Filename to save profile to.", metavar="FILE", default=None) parser.add_argument( "-p", "--pid", help="Comma-separated list of PIDs to " "profile.", metavar="PIDS") parser.add_argument( "-n", "--name", help="Comma-separated list of process " "names to profile.", metavar="NAMES") parser.add_argument( "-b", "--buffer-size", help="Buffer size in memory that store the whole java heap graph. N(kb|mb|gb)", type=str, default="100024kb") parser.add_argument( "-c", "--continuous-dump", help="Dump interval in ms. 0 to disable continuous dump. When continuous " "dump is enabled, use CTRL+C to stop", type=int, default=0) parser.add_argument( "--no-versions", action="store_true", help="Do not get version information about APKs.") parser.add_argument( "--dump-smaps", action="store_true", help="Get information about /proc/$PID/smaps of target.") parser.add_argument( "--print-config", action="store_true", help="Print config instead of running. For debugging.") parser.add_argument( "--stop-when-done", action="store_true", default=None, help="Use a new method to stop the profile when the dump is done. " "Previously, we would hardcode a duration. Available and default on S.") parser.add_argument( "--no-stop-when-done", action="store_false", dest='stop_when_done', help="Do not use a new method to stop the profile when the dump is done.") parser.add_argument( "--wait-for-oom", action="store_true", dest='wait_for_oom', help="Starts a tracing session waiting for an OutOfMemoryError to be " "thrown. Available on U.") parser.add_argument( "--oom-wait-seconds", type=int, default=60, help="Seconds to wait for an OutOfMemoryError to be thrown. " "Defaults to 60.") args = parser.parse_args() if args.stop_when_done is None: args.stop_when_done = release_or_newer('S') cfg = None if args.wait_for_oom: cfg = generate_oom_config(args) else: cfg = generate_heap_dump_config(args) if not cfg: parser.print_help() return 1 if not args.no_versions: cfg += PACKAGES_LIST_CFG if args.print_config: print(cfg) return 0 output_file = args.output if output_file is None: fd, name = tempfile.mkstemp('profile') os.close(fd) output_file = name user = subprocess.check_output(['adb', 'shell', 'whoami']).strip().decode('utf8') perfetto_pid = subprocess.check_output( ['adb', 'exec-out', PERFETTO_CMD.format(cfg=cfg, user=user)]).strip().decode('utf8') try: int(perfetto_pid.strip()) except ValueError: print("Failed to invoke perfetto: {}".format(perfetto_pid), file=sys.stderr) return 1 if args.wait_for_oom: print("Waiting for OutOfMemoryError") else: print("Dumping Java Heap.") exists = True ctrl_c_count = 0 # Wait for perfetto cmd to return. while exists: try: exists = subprocess.call( ['adb', 'shell', '[ -d /proc/{} ]'.format(perfetto_pid)]) == 0 time.sleep(1) except KeyboardInterrupt as e: ctrl_c_count += 1 subprocess.check_call( ['adb', 'shell', 'kill -TERM {}'.format(perfetto_pid)]) if ctrl_c_count == 1: print("Stopping perfetto and waiting for data...") else: raise e subprocess.check_call(['adb', 'pull', PROFILE_PATH, output_file], stdout=NULL) subprocess.check_call(['adb', 'shell', 'rm', '-f', PROFILE_PATH], stdout=NULL) print("Wrote profile to {}".format(output_file)) print("This can be viewed using https://ui.perfetto.dev.") if __name__ == '__main__': sys.exit(main(sys.argv))