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