1#!/usr/bin/env python3 2 3# Copyright (C) 2020 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 17from __future__ import absolute_import 18from __future__ import division 19from __future__ import print_function 20 21import argparse 22import os 23import subprocess 24import sys 25import tempfile 26import time 27import uuid 28 29NULL = open(os.devnull) 30 31PACKAGES_LIST_CFG = '''data_sources { 32 config { 33 name: "android.packages_list" 34 } 35} 36''' 37 38CFG_IDENT = ' ' 39CFG = '''buffers {{ 40 size_kb: 100024 41 fill_policy: RING_BUFFER 42}} 43 44data_sources {{ 45 config {{ 46 name: "android.java_hprof" 47 java_hprof_config {{ 48{target_cfg} 49{continuous_dump_config} 50 }} 51 }} 52}} 53 54data_source_stop_timeout_ms: {data_source_stop_timeout_ms} 55duration_ms: {duration_ms} 56''' 57 58CONTINUOUS_DUMP = """ 59 continuous_dump_config {{ 60 dump_phase_ms: 0 61 dump_interval_ms: {dump_interval} 62 }} 63""" 64 65UUID = str(uuid.uuid4())[-6:] 66PROFILE_PATH = '/data/misc/perfetto-traces/java-profile-' + UUID 67 68PERFETTO_CMD = ('CFG=\'{cfg}\'; echo ${{CFG}} | ' 69 'perfetto --txt -c - -o ' + PROFILE_PATH + ' -d') 70 71SDK = { 72 'S': 31, 73} 74 75def release_or_newer(release): 76 sdk = int(subprocess.check_output( 77 ['adb', 'shell', 'getprop', 'ro.system.build.version.sdk'] 78 ).decode('utf-8').strip()) 79 if sdk >= SDK[release]: 80 return True 81 codename = subprocess.check_output( 82 ['adb', 'shell', 'getprop', 'ro.build.version.codename'] 83 ).decode('utf-8').strip() 84 return codename == release 85 86def main(argv): 87 parser = argparse.ArgumentParser() 88 parser.add_argument( 89 "-o", 90 "--output", 91 help="Filename to save profile to.", 92 metavar="FILE", 93 default=None) 94 parser.add_argument( 95 "-p", 96 "--pid", 97 help="Comma-separated list of PIDs to " 98 "profile.", 99 metavar="PIDS") 100 parser.add_argument( 101 "-n", 102 "--name", 103 help="Comma-separated list of process " 104 "names to profile.", 105 metavar="NAMES") 106 parser.add_argument( 107 "-c", 108 "--continuous-dump", 109 help="Dump interval in ms. 0 to disable continuous dump.", 110 type=int, 111 default=0) 112 parser.add_argument( 113 "--no-versions", 114 action="store_true", 115 help="Do not get version information about APKs.") 116 parser.add_argument( 117 "--dump-smaps", 118 action="store_true", 119 help="Get information about /proc/$PID/smaps of target.") 120 parser.add_argument( 121 "--print-config", 122 action="store_true", 123 help="Print config instead of running. For debugging.") 124 parser.add_argument( 125 "--stop-when-done", 126 action="store_true", 127 default=None, 128 help="Use a new method to stop the profile when the dump is done. " 129 "Previously, we would hardcode a duration. Available and default on S.") 130 parser.add_argument( 131 "--no-stop-when-done", 132 action="store_false", 133 dest='stop_when_done', 134 help="Do not use a new method to stop the profile when the dump is done.") 135 136 args = parser.parse_args() 137 138 if args.stop_when_done is None: 139 args.stop_when_done = release_or_newer('S') 140 141 fail = False 142 if args.pid is None and args.name is None: 143 print("FATAL: Neither PID nor NAME given.", file=sys.stderr) 144 fail = True 145 146 target_cfg = "" 147 if args.pid: 148 for pid in args.pid.split(','): 149 try: 150 pid = int(pid) 151 except ValueError: 152 print("FATAL: invalid PID %s" % pid, file=sys.stderr) 153 fail = True 154 target_cfg += '{}pid: {}\n'.format(CFG_IDENT, pid) 155 if args.name: 156 for name in args.name.split(','): 157 target_cfg += '{}process_cmdline: "{}"\n'.format(CFG_IDENT, name) 158 if args.dump_smaps: 159 target_cfg += '{}dump_smaps: true\n'.format(CFG_IDENT) 160 161 if fail: 162 parser.print_help() 163 return 1 164 165 output_file = args.output 166 if output_file is None: 167 fd, name = tempfile.mkstemp('profile') 168 os.close(fd) 169 output_file = name 170 171 continuous_dump_cfg = "" 172 if args.continuous_dump: 173 continuous_dump_cfg = CONTINUOUS_DUMP.format( 174 dump_interval=args.continuous_dump) 175 176 if args.stop_when_done: 177 duration_ms = 1000 178 data_source_stop_timeout_ms = 100000 179 else: 180 duration_ms = 20000 181 data_source_stop_timeout_ms = 0 182 183 cfg = CFG.format( 184 target_cfg=target_cfg, 185 continuous_dump_config=continuous_dump_cfg, 186 duration_ms=duration_ms, 187 data_source_stop_timeout_ms=data_source_stop_timeout_ms) 188 if not args.no_versions: 189 cfg += PACKAGES_LIST_CFG 190 191 if args.print_config: 192 print(cfg) 193 return 0 194 195 user = subprocess.check_output( 196 ['adb', 'shell', 'whoami']).strip().decode('utf8') 197 perfetto_pid = subprocess.check_output( 198 ['adb', 'exec-out', 199 PERFETTO_CMD.format(cfg=cfg, user=user)]).strip().decode('utf8') 200 try: 201 int(perfetto_pid.strip()) 202 except ValueError: 203 print("Failed to invoke perfetto: {}".format(perfetto_pid), file=sys.stderr) 204 return 1 205 206 print("Dumping Java Heap.") 207 exists = True 208 209 # Wait for perfetto cmd to return. 210 while exists: 211 exists = subprocess.call( 212 ['adb', 'shell', '[ -d /proc/{} ]'.format(perfetto_pid)]) == 0 213 time.sleep(1) 214 215 subprocess.check_call( 216 ['adb', 'pull', PROFILE_PATH, output_file], stdout=NULL) 217 218 subprocess.check_call( 219 ['adb', 'shell', 'rm', PROFILE_PATH], stdout=NULL) 220 221 print("Wrote profile to {}".format(output_file)) 222 print("This can be viewed using https://ui.perfetto.dev.") 223 224 225if __name__ == '__main__': 226 sys.exit(main(sys.argv)) 227