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_INDENT = ' ' 39CFG = '''buffers {{ 40 size_kb: {size_kb} 41 fill_policy: DISCARD 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 58OOM_CFG = '''buffers: {{ 59 size_kb: {size_kb} 60 fill_policy: DISCARD 61}} 62 63data_sources: {{ 64 config {{ 65 name: "android.java_hprof.oom" 66 java_hprof_config {{ 67{process_cfg} 68 }} 69 }} 70}} 71 72data_source_stop_timeout_ms: 100000 73 74trigger_config {{ 75 trigger_mode: START_TRACING 76 trigger_timeout_ms: {wait_duration_ms} 77 triggers {{ 78 name: "com.android.telemetry.art-outofmemory" 79 stop_delay_ms: 500 80 }} 81}} 82''' 83 84CONTINUOUS_DUMP = """ 85 continuous_dump_config {{ 86 dump_phase_ms: 0 87 dump_interval_ms: {dump_interval} 88 }} 89""" 90 91UUID = str(uuid.uuid4())[-6:] 92PROFILE_PATH = '/data/misc/perfetto-traces/java-profile-' + UUID 93 94PERFETTO_CMD = ('CFG=\'{cfg}\'; echo ${{CFG}} | ' 95 'perfetto --txt -c - -o ' + PROFILE_PATH + ' -d') 96 97SDK = { 98 'S': 31, 99 'UpsideDownCake': 34, 100} 101 102 103def release_or_newer(release): 104 sdk = int( 105 subprocess.check_output( 106 ['adb', 'shell', 'getprop', 107 'ro.system.build.version.sdk']).decode('utf-8').strip()) 108 if sdk >= SDK[release]: 109 return True 110 codename = subprocess.check_output( 111 ['adb', 'shell', 'getprop', 112 'ro.build.version.codename']).decode('utf-8').strip() 113 return codename == release 114 115def convert_size_to_kb(size): 116 if size.endswith("kb"): 117 return int(size[:-2]) 118 elif size.endswith("mb"): 119 return int(size[:-2]) * 1024 120 elif size.endswith("gb"): 121 return int(size[:-2]) * 1024 * 1024 122 else: 123 return int(size) 124 125def generate_heap_dump_config(args): 126 fail = False 127 if args.pid is None and args.name is None: 128 print("FATAL: Neither PID nor NAME given.", file=sys.stderr) 129 fail = True 130 131 target_cfg = "" 132 if args.pid: 133 for pid in args.pid.split(','): 134 try: 135 pid = int(pid) 136 except ValueError: 137 print("FATAL: invalid PID %s" % pid, file=sys.stderr) 138 fail = True 139 target_cfg += '{}pid: {}\n'.format(CFG_INDENT, pid) 140 if args.name: 141 for name in args.name.split(','): 142 target_cfg += '{}process_cmdline: "{}"\n'.format(CFG_INDENT, name) 143 if args.dump_smaps: 144 target_cfg += '{}dump_smaps: true\n'.format(CFG_INDENT) 145 146 if fail: 147 return None 148 149 continuous_dump_cfg = "" 150 if args.continuous_dump: 151 continuous_dump_cfg = CONTINUOUS_DUMP.format( 152 dump_interval=args.continuous_dump) 153 154 if args.continuous_dump: 155 # Unlimited trace duration 156 duration_ms = 0 157 elif args.stop_when_done: 158 # Oneshot heapdump and the system supports data_source_stop_timeout_ms, we 159 # can use a short duration. 160 duration_ms = 1000 161 else: 162 # Oneshot heapdump, but the system doesn't supports 163 # data_source_stop_timeout_ms, we have to use a longer duration in the hope 164 # of giving enough time to capture the whole dump. 165 duration_ms = 20000 166 167 if args.stop_when_done: 168 data_source_stop_timeout_ms = 100000 169 else: 170 data_source_stop_timeout_ms = 0 171 172 return CFG.format( 173 size_kb=convert_size_to_kb(args.buffer_size), 174 target_cfg=target_cfg, 175 continuous_dump_config=continuous_dump_cfg, 176 duration_ms=duration_ms, 177 data_source_stop_timeout_ms=data_source_stop_timeout_ms) 178 179def generate_oom_config(args): 180 if not release_or_newer('UpsideDownCake'): 181 print("FATAL: OOM mode not supported for this android version", 182 file=sys.stderr) 183 return None 184 185 if args.pid: 186 print("FATAL: Specifying pid not supported in OOM mode", 187 file=sys.stderr) 188 return None 189 190 if not args.name: 191 print("FATAL: Must specify process in OOM mode (use --name '*' to match all)", 192 file=sys.stderr) 193 return None 194 195 if args.continuous_dump: 196 print("FATAL: Specifying continuous dump not supported in OOM mode", 197 file=sys.stderr) 198 return None 199 200 if args.dump_smaps: 201 print("FATAL: Dumping smaps not supported in OOM mode", 202 file=sys.stderr) 203 return None 204 205 process_cfg = '' 206 for name in args.name.split(','): 207 process_cfg += '{}process_cmdline: "{}"\n'.format(CFG_INDENT, name) 208 209 return OOM_CFG.format( 210 size_kb=convert_size_to_kb(args.buffer_size), 211 wait_duration_ms=args.oom_wait_seconds * 1000, 212 process_cfg=process_cfg) 213 214 215def main(argv): 216 parser = argparse.ArgumentParser() 217 parser.add_argument( 218 "-o", 219 "--output", 220 help="Filename to save profile to.", 221 metavar="FILE", 222 default=None) 223 parser.add_argument( 224 "-p", 225 "--pid", 226 help="Comma-separated list of PIDs to " 227 "profile.", 228 metavar="PIDS") 229 parser.add_argument( 230 "-n", 231 "--name", 232 help="Comma-separated list of process " 233 "names to profile.", 234 metavar="NAMES") 235 parser.add_argument( 236 "-b", 237 "--buffer-size", 238 help="Buffer size in memory that store the whole java heap graph. N(kb|mb|gb)", 239 type=str, 240 default="256mb") 241 parser.add_argument( 242 "-c", 243 "--continuous-dump", 244 help="Dump interval in ms. 0 to disable continuous dump. When continuous " 245 "dump is enabled, use CTRL+C to stop", 246 type=int, 247 default=0) 248 parser.add_argument( 249 "--no-versions", 250 action="store_true", 251 help="Do not get version information about APKs.") 252 parser.add_argument( 253 "--dump-smaps", 254 action="store_true", 255 help="Get information about /proc/$PID/smaps of target.") 256 parser.add_argument( 257 "--print-config", 258 action="store_true", 259 help="Print config instead of running. For debugging.") 260 parser.add_argument( 261 "--stop-when-done", 262 action="store_true", 263 default=None, 264 help="Use a new method to stop the profile when the dump is done. " 265 "Previously, we would hardcode a duration. Available and default on S.") 266 parser.add_argument( 267 "--no-stop-when-done", 268 action="store_false", 269 dest='stop_when_done', 270 help="Do not use a new method to stop the profile when the dump is done.") 271 parser.add_argument( 272 "--wait-for-oom", 273 action="store_true", 274 dest='wait_for_oom', 275 help="Starts a tracing session waiting for an OutOfMemoryError to be " 276 "thrown. Available on U.") 277 parser.add_argument( 278 "--oom-wait-seconds", 279 type=int, 280 default=60, 281 help="Seconds to wait for an OutOfMemoryError to be thrown. " 282 "Defaults to 60.") 283 284 args = parser.parse_args() 285 286 if args.stop_when_done is None: 287 args.stop_when_done = release_or_newer('S') 288 289 cfg = None 290 if args.wait_for_oom: 291 cfg = generate_oom_config(args) 292 else: 293 cfg = generate_heap_dump_config(args) 294 295 if not cfg: 296 parser.print_help() 297 return 1 298 299 if not args.no_versions: 300 cfg += PACKAGES_LIST_CFG 301 302 if args.print_config: 303 print(cfg) 304 return 0 305 306 output_file = args.output 307 if output_file is None: 308 fd, name = tempfile.mkstemp('profile') 309 os.close(fd) 310 output_file = name 311 312 user = subprocess.check_output(['adb', 'shell', 313 'whoami']).strip().decode('utf8') 314 perfetto_pid = subprocess.check_output( 315 ['adb', 'exec-out', 316 PERFETTO_CMD.format(cfg=cfg, user=user)]).strip().decode('utf8') 317 try: 318 int(perfetto_pid.strip()) 319 except ValueError: 320 print("Failed to invoke perfetto: {}".format(perfetto_pid), file=sys.stderr) 321 return 1 322 323 if args.wait_for_oom: 324 print("Waiting for OutOfMemoryError") 325 else: 326 print("Dumping Java Heap.") 327 328 exists = True 329 ctrl_c_count = 0 330 # Wait for perfetto cmd to return. 331 while exists: 332 try: 333 exists = subprocess.call( 334 ['adb', 'shell', '[ -d /proc/{} ]'.format(perfetto_pid)]) == 0 335 time.sleep(1) 336 except KeyboardInterrupt as e: 337 ctrl_c_count += 1 338 subprocess.check_call( 339 ['adb', 'shell', 'kill -TERM {}'.format(perfetto_pid)]) 340 if ctrl_c_count == 1: 341 print("Stopping perfetto and waiting for data...") 342 else: 343 raise e 344 345 subprocess.check_call(['adb', 'pull', PROFILE_PATH, output_file], stdout=NULL) 346 347 subprocess.check_call(['adb', 'shell', 'rm', '-f', PROFILE_PATH], stdout=NULL) 348 349 print("Wrote profile to {}".format(output_file)) 350 print("This can be viewed using https://ui.perfetto.dev.") 351 352 353if __name__ == '__main__': 354 sys.exit(main(sys.argv)) 355