1 #!/usr/bin/env python 2 3 # Copyright (C) 2017 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 17 from __future__ import absolute_import 18 from __future__ import division 19 from __future__ import print_function 20 21 import argparse 22 import atexit 23 import hashlib 24 import os 25 import signal 26 import subprocess 27 import sys 28 import tempfile 29 import time 30 import urllib 31 32 TRACE_TO_TEXT_SHAS = { 33 'linux': 'a8171d85c5964ccafe457142dbb7df68ca8da543', 34 'mac': '268c2fc096039566979d16c1a7a99eabef0d9682', 35 } 36 TRACE_TO_TEXT_PATH = tempfile.gettempdir() 37 TRACE_TO_TEXT_BASE_URL = ( 38 'https://storage.googleapis.com/perfetto/') 39 40 NULL = open(os.devnull) 41 NOOUT = { 42 'stdout': NULL, 43 'stderr': NULL, 44 } 45 46 47 def check_hash(file_name, sha_value): 48 with open(file_name, 'rb') as fd: 49 # TODO(fmayer): Chunking. 50 file_hash = hashlib.sha1(fd.read()).hexdigest() 51 return file_hash == sha_value 52 53 54 def load_trace_to_text(platform): 55 sha_value = TRACE_TO_TEXT_SHAS[platform] 56 file_name = 'trace_to_text-' + platform + '-' + sha_value 57 local_file = os.path.join(TRACE_TO_TEXT_PATH, file_name) 58 59 if os.path.exists(local_file): 60 if not check_hash(local_file, sha_value): 61 os.remove(local_file) 62 else: 63 return local_file 64 65 url = TRACE_TO_TEXT_BASE_URL + file_name 66 urllib.urlretrieve(url, local_file) 67 if not check_hash(local_file, sha_value): 68 os.remove(local_file) 69 raise ValueError("Invalid signature.") 70 os.chmod(local_file, 0o755) 71 return local_file 72 73 74 CFG_IDENT = ' ' 75 CFG='''buffers {{ 76 size_kb: 32768 77 }} 78 79 data_sources {{ 80 config {{ 81 name: "android.packages_list" 82 }} 83 }} 84 85 data_sources {{ 86 config {{ 87 name: "android.heapprofd" 88 heapprofd_config {{ 89 90 shmem_size_bytes: {shmem_size} 91 sampling_interval_bytes: {interval} 92 {target_cfg} 93 {continuous_dump_cfg} 94 }} 95 }} 96 }} 97 98 duration_ms: {duration} 99 flush_timeout_ms: 30000 100 ''' 101 102 CONTINUOUS_DUMP = """ 103 continuous_dump_config {{ 104 dump_phase_ms: 0 105 dump_interval_ms: {dump_interval} 106 }} 107 """ 108 109 PERFETTO_CMD=('CFG=\'{cfg}\'; echo ${{CFG}} | ' 110 'perfetto --txt -c - -o ' 111 '/data/misc/perfetto-traces/profile-{user} -d') 112 IS_INTERRUPTED = False 113 def sigint_handler(sig, frame): 114 global IS_INTERRUPTED 115 IS_INTERRUPTED = True 116 117 118 def main(argv): 119 parser = argparse.ArgumentParser() 120 parser.add_argument("-i", "--interval", help="Sampling interval. " 121 "Default 4096 (4KiB)", type=int, default=4096) 122 parser.add_argument("-d", "--duration", help="Duration of profile (ms). " 123 "Default 7 days.", type=int, default=604800000) 124 parser.add_argument("--no-start", help="Do not start heapprofd.", 125 action='store_true') 126 parser.add_argument("-p", "--pid", help="Comma-separated list of PIDs to " 127 "profile.", metavar="PIDS") 128 parser.add_argument("-n", "--name", help="Comma-separated list of process " 129 "names to profile.", metavar="NAMES") 130 parser.add_argument("-c", "--continuous-dump", 131 help="Dump interval in ms. 0 to disable continuous dump.", 132 type=int, default=0) 133 parser.add_argument("--disable-selinux", action="store_true", 134 help="Disable SELinux enforcement for duration of " 135 "profile.") 136 parser.add_argument("--shmem-size", help="Size of buffer between client and " 137 "heapprofd. Default 8MiB. Needs to be a power of two " 138 "multiple of 4096, at least 8192.", type=int, 139 default=8 * 1048576) 140 parser.add_argument("--block-client", help="When buffer is full, block the " 141 "client to wait for buffer space. Use with caution as " 142 "this can significantly slow down the client.", 143 action="store_true") 144 parser.add_argument("--simpleperf", action="store_true", 145 help="Get simpleperf profile of heapprofd. This is " 146 "only for heapprofd development.") 147 parser.add_argument("--trace-to-text-binary", 148 help="Path to local trace to text. For debugging.") 149 150 args = parser.parse_args() 151 152 fail = False 153 if args.pid is None and args.name is None: 154 print("FATAL: Neither PID nor NAME given.", file=sys.stderr) 155 fail = True 156 if args.duration is None: 157 print("FATAL: No duration given.", file=sys.stderr) 158 fail = True 159 if args.interval is None: 160 print("FATAL: No interval given.", file=sys.stderr) 161 fail = True 162 if args.shmem_size % 4096: 163 print("FATAL: shmem-size is not a multiple of 4096.", file=sys.stderr) 164 fail = True 165 if args.shmem_size < 8192: 166 print("FATAL: shmem-size is less than 8192.", file=sys.stderr) 167 fail = True 168 if args.shmem_size & (args.shmem_size - 1): 169 print("FATAL: shmem-size is not a power of two.", file=sys.stderr) 170 fail = True 171 172 target_cfg = "" 173 if args.block_client: 174 target_cfg += "block_client: true\n" 175 if args.pid: 176 for pid in args.pid.split(','): 177 try: 178 pid = int(pid) 179 except ValueError: 180 print("FATAL: invalid PID %s" % pid, file=sys.stderr) 181 fail = True 182 target_cfg += '{}pid: {}\n'.format(CFG_IDENT, pid) 183 if args.name: 184 for name in args.name.split(','): 185 target_cfg += '{}process_cmdline: "{}"\n'.format(CFG_IDENT, name) 186 187 if fail: 188 parser.print_help() 189 return 1 190 191 trace_to_text_binary = args.trace_to_text_binary 192 if trace_to_text_binary is None: 193 platform = None 194 if sys.platform.startswith('linux'): 195 platform = 'linux' 196 elif sys.platform.startswith('darwin'): 197 platform = 'mac' 198 else: 199 print("Invalid platform: {}".format(sys.platform), file=sys.stderr) 200 return 1 201 202 trace_to_text_binary = load_trace_to_text(platform) 203 204 continuous_dump_cfg = "" 205 if args.continuous_dump: 206 continuous_dump_cfg = CONTINUOUS_DUMP.format( 207 dump_interval=args.continuous_dump) 208 cfg = CFG.format(interval=args.interval, 209 duration=args.duration, target_cfg=target_cfg, 210 continuous_dump_cfg=continuous_dump_cfg, 211 shmem_size=args.shmem_size) 212 213 if args.disable_selinux: 214 enforcing = subprocess.check_output(['adb', 'shell', 'getenforce']) 215 atexit.register(subprocess.check_call, 216 ['adb', 'shell', 'su root setenforce %s' % enforcing]) 217 subprocess.check_call(['adb', 'shell', 'su root setenforce 0']) 218 219 if not args.no_start: 220 heapprofd_prop = subprocess.check_output( 221 ['adb', 'shell', 'getprop persist.heapprofd.enable']) 222 if heapprofd_prop.strip() != '1': 223 subprocess.check_call( 224 ['adb', 'shell', 'setprop persist.heapprofd.enable 1']) 225 atexit.register(subprocess.check_call, 226 ['adb', 'shell', 'setprop persist.heapprofd.enable 0']) 227 228 user = subprocess.check_output(['adb', 'shell', 'whoami']).strip() 229 230 if args.simpleperf: 231 subprocess.check_call( 232 ['adb', 'shell', 233 'mkdir -p /data/local/tmp/heapprofd_profile && ' 234 'cd /data/local/tmp/heapprofd_profile &&' 235 '(nohup simpleperf record -g -p $(pgrep heapprofd) 2>&1 &) ' 236 '> /dev/null']) 237 238 perfetto_pid = subprocess.check_output( 239 ['adb', 'exec-out', PERFETTO_CMD.format(cfg=cfg, user=user)]).strip() 240 try: 241 int(perfetto_pid.strip()) 242 except ValueError: 243 print("Failed to invoke perfetto: {}".format(perfetto_pid), 244 file=sys.stderr) 245 return 1 246 247 old_handler = signal.signal(signal.SIGINT, sigint_handler) 248 print("Profiling active. Press Ctrl+C to terminate.") 249 print("You may disconnect your device.") 250 exists = True 251 device_connected = True 252 while not device_connected or (exists and not IS_INTERRUPTED): 253 exists = subprocess.call( 254 ['adb', 'shell', '[ -d /proc/{} ]'.format(perfetto_pid)], 255 **NOOUT) == 0 256 device_connected = subprocess.call(['adb', 'shell', 'true'], **NOOUT) == 0 257 time.sleep(1) 258 signal.signal(signal.SIGINT, old_handler) 259 if IS_INTERRUPTED: 260 # Not check_call because it could have existed in the meantime. 261 subprocess.call(['adb', 'shell', 'kill', '-INT', perfetto_pid]) 262 if args.simpleperf: 263 subprocess.check_call(['adb', 'shell', 'killall', '-INT', 'simpleperf']) 264 print("Waiting for simpleperf to exit.") 265 while subprocess.call( 266 ['adb', 'shell', '[ -f /proc/$(pgrep simpleperf)/exe ]'], 267 **NOOUT) == 0: 268 time.sleep(1) 269 subprocess.check_call(['adb', 'pull', '/data/local/tmp/heapprofd_profile', 270 '/tmp']) 271 print("Pulled simpleperf profile to /tmp/heapprofd_profile") 272 273 # Wait for perfetto cmd to return. 274 while exists: 275 exists = subprocess.call( 276 ['adb', 'shell', '[ -d /proc/{} ]'.format(perfetto_pid)]) == 0 277 time.sleep(1) 278 279 subprocess.check_call(['adb', 'pull', 280 '/data/misc/perfetto-traces/profile-{}'.format(user), 281 '/tmp/profile'], stdout=NULL) 282 trace_to_text_output = subprocess.check_output( 283 [trace_to_text_binary, 'profile', '/tmp/profile']) 284 profile_path = None 285 for word in trace_to_text_output.split(): 286 if 'heap_profile-' in word: 287 profile_path = word 288 if profile_path is None: 289 print("Could not find trace_to_text output path.", file=sys.stderr) 290 return 1 291 292 profile_files = os.listdir(profile_path) 293 if not profile_files: 294 print("No profiles generated", file=sys.stderr) 295 return 1 296 297 subprocess.check_call(['gzip'] + [os.path.join(profile_path, x) for x in 298 os.listdir(profile_path)]) 299 300 symlink_path = os.path.join(os.path.dirname(profile_path), 301 "heap_profile-latest") 302 if os.path.lexists(symlink_path): 303 os.unlink(symlink_path) 304 os.symlink(profile_path, symlink_path) 305 306 print("Wrote profiles to {} (symlink {})".format(profile_path, symlink_path)) 307 print("These can be viewed using pprof. Googlers: head to pprof/ and " 308 "upload them.") 309 310 311 if __name__ == '__main__': 312 sys.exit(main(sys.argv)) 313