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 17from __future__ import absolute_import 18from __future__ import division 19from __future__ import print_function 20 21import argparse 22import atexit 23import hashlib 24import os 25import signal 26import subprocess 27import sys 28import tempfile 29import time 30import urllib 31 32TRACE_TO_TEXT_SHAS = { 33 'linux': 'a8171d85c5964ccafe457142dbb7df68ca8da543', 34 'mac': '268c2fc096039566979d16c1a7a99eabef0d9682', 35} 36TRACE_TO_TEXT_PATH = tempfile.gettempdir() 37TRACE_TO_TEXT_BASE_URL = ( 38 'https://storage.googleapis.com/perfetto/') 39 40NULL = open(os.devnull) 41NOOUT = { 42 'stdout': NULL, 43 'stderr': NULL, 44} 45 46 47def 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 54def 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 74CFG_IDENT = ' ' 75CFG='''buffers {{ 76 size_kb: 32768 77}} 78 79data_sources {{ 80 config {{ 81 name: "android.packages_list" 82 }} 83}} 84 85data_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 98duration_ms: {duration} 99flush_timeout_ms: 30000 100''' 101 102CONTINUOUS_DUMP = """ 103 continuous_dump_config {{ 104 dump_phase_ms: 0 105 dump_interval_ms: {dump_interval} 106 }} 107""" 108 109PERFETTO_CMD=('CFG=\'{cfg}\'; echo ${{CFG}} | ' 110 'perfetto --txt -c - -o ' 111 '/data/misc/perfetto-traces/profile-{user} -d') 112IS_INTERRUPTED = False 113def sigint_handler(sig, frame): 114 global IS_INTERRUPTED 115 IS_INTERRUPTED = True 116 117 118def 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 311if __name__ == '__main__': 312 sys.exit(main(sys.argv)) 313