#!/usr/bin/env python # Copyright (C) 2017 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import absolute_import from __future__ import division from __future__ import print_function import argparse import atexit import hashlib import os import signal import subprocess import sys import tempfile import time import urllib TRACE_TO_TEXT_SHAS = { 'linux': 'a8171d85c5964ccafe457142dbb7df68ca8da543', 'mac': '268c2fc096039566979d16c1a7a99eabef0d9682', } TRACE_TO_TEXT_PATH = tempfile.gettempdir() TRACE_TO_TEXT_BASE_URL = ( 'https://storage.googleapis.com/perfetto/') NULL = open(os.devnull) NOOUT = { 'stdout': NULL, 'stderr': NULL, } def check_hash(file_name, sha_value): with open(file_name, 'rb') as fd: # TODO(fmayer): Chunking. file_hash = hashlib.sha1(fd.read()).hexdigest() return file_hash == sha_value def load_trace_to_text(platform): sha_value = TRACE_TO_TEXT_SHAS[platform] file_name = 'trace_to_text-' + platform + '-' + sha_value local_file = os.path.join(TRACE_TO_TEXT_PATH, file_name) if os.path.exists(local_file): if not check_hash(local_file, sha_value): os.remove(local_file) else: return local_file url = TRACE_TO_TEXT_BASE_URL + file_name urllib.urlretrieve(url, local_file) if not check_hash(local_file, sha_value): os.remove(local_file) raise ValueError("Invalid signature.") os.chmod(local_file, 0o755) return local_file CFG_IDENT = ' ' CFG='''buffers {{ size_kb: 32768 }} data_sources {{ config {{ name: "android.packages_list" }} }} data_sources {{ config {{ name: "android.heapprofd" heapprofd_config {{ shmem_size_bytes: {shmem_size} sampling_interval_bytes: {interval} {target_cfg} {continuous_dump_cfg} }} }} }} duration_ms: {duration} flush_timeout_ms: 30000 ''' CONTINUOUS_DUMP = """ continuous_dump_config {{ dump_phase_ms: 0 dump_interval_ms: {dump_interval} }} """ PERFETTO_CMD=('CFG=\'{cfg}\'; echo ${{CFG}} | ' 'perfetto --txt -c - -o ' '/data/misc/perfetto-traces/profile-{user} -d') IS_INTERRUPTED = False def sigint_handler(sig, frame): global IS_INTERRUPTED IS_INTERRUPTED = True def main(argv): parser = argparse.ArgumentParser() parser.add_argument("-i", "--interval", help="Sampling interval. " "Default 4096 (4KiB)", type=int, default=4096) parser.add_argument("-d", "--duration", help="Duration of profile (ms). " "Default 7 days.", type=int, default=604800000) parser.add_argument("--no-start", help="Do not start heapprofd.", action='store_true') parser.add_argument("-p", "--pid", help="Comma-separated list of PIDs to " "profile.", metavar="PIDS") parser.add_argument("-n", "--name", help="Comma-separated list of process " "names to profile.", metavar="NAMES") parser.add_argument("-c", "--continuous-dump", help="Dump interval in ms. 0 to disable continuous dump.", type=int, default=0) parser.add_argument("--disable-selinux", action="store_true", help="Disable SELinux enforcement for duration of " "profile.") parser.add_argument("--shmem-size", help="Size of buffer between client and " "heapprofd. Default 8MiB. Needs to be a power of two " "multiple of 4096, at least 8192.", type=int, default=8 * 1048576) parser.add_argument("--block-client", help="When buffer is full, block the " "client to wait for buffer space. Use with caution as " "this can significantly slow down the client.", action="store_true") parser.add_argument("--simpleperf", action="store_true", help="Get simpleperf profile of heapprofd. This is " "only for heapprofd development.") parser.add_argument("--trace-to-text-binary", help="Path to local trace to text. For debugging.") args = parser.parse_args() fail = False if args.pid is None and args.name is None: print("FATAL: Neither PID nor NAME given.", file=sys.stderr) fail = True if args.duration is None: print("FATAL: No duration given.", file=sys.stderr) fail = True if args.interval is None: print("FATAL: No interval given.", file=sys.stderr) fail = True if args.shmem_size % 4096: print("FATAL: shmem-size is not a multiple of 4096.", file=sys.stderr) fail = True if args.shmem_size < 8192: print("FATAL: shmem-size is less than 8192.", file=sys.stderr) fail = True if args.shmem_size & (args.shmem_size - 1): print("FATAL: shmem-size is not a power of two.", file=sys.stderr) fail = True target_cfg = "" if args.block_client: target_cfg += "block_client: true\n" if args.pid: for pid in args.pid.split(','): try: pid = int(pid) except ValueError: print("FATAL: invalid PID %s" % pid, file=sys.stderr) fail = True target_cfg += '{}pid: {}\n'.format(CFG_IDENT, pid) if args.name: for name in args.name.split(','): target_cfg += '{}process_cmdline: "{}"\n'.format(CFG_IDENT, name) if fail: parser.print_help() return 1 trace_to_text_binary = args.trace_to_text_binary if trace_to_text_binary is None: platform = None if sys.platform.startswith('linux'): platform = 'linux' elif sys.platform.startswith('darwin'): platform = 'mac' else: print("Invalid platform: {}".format(sys.platform), file=sys.stderr) return 1 trace_to_text_binary = load_trace_to_text(platform) continuous_dump_cfg = "" if args.continuous_dump: continuous_dump_cfg = CONTINUOUS_DUMP.format( dump_interval=args.continuous_dump) cfg = CFG.format(interval=args.interval, duration=args.duration, target_cfg=target_cfg, continuous_dump_cfg=continuous_dump_cfg, shmem_size=args.shmem_size) if args.disable_selinux: enforcing = subprocess.check_output(['adb', 'shell', 'getenforce']) atexit.register(subprocess.check_call, ['adb', 'shell', 'su root setenforce %s' % enforcing]) subprocess.check_call(['adb', 'shell', 'su root setenforce 0']) if not args.no_start: heapprofd_prop = subprocess.check_output( ['adb', 'shell', 'getprop persist.heapprofd.enable']) if heapprofd_prop.strip() != '1': subprocess.check_call( ['adb', 'shell', 'setprop persist.heapprofd.enable 1']) atexit.register(subprocess.check_call, ['adb', 'shell', 'setprop persist.heapprofd.enable 0']) user = subprocess.check_output(['adb', 'shell', 'whoami']).strip() if args.simpleperf: subprocess.check_call( ['adb', 'shell', 'mkdir -p /data/local/tmp/heapprofd_profile && ' 'cd /data/local/tmp/heapprofd_profile &&' '(nohup simpleperf record -g -p $(pgrep heapprofd) 2>&1 &) ' '> /dev/null']) perfetto_pid = subprocess.check_output( ['adb', 'exec-out', PERFETTO_CMD.format(cfg=cfg, user=user)]).strip() try: int(perfetto_pid.strip()) except ValueError: print("Failed to invoke perfetto: {}".format(perfetto_pid), file=sys.stderr) return 1 old_handler = signal.signal(signal.SIGINT, sigint_handler) print("Profiling active. Press Ctrl+C to terminate.") print("You may disconnect your device.") exists = True device_connected = True while not device_connected or (exists and not IS_INTERRUPTED): exists = subprocess.call( ['adb', 'shell', '[ -d /proc/{} ]'.format(perfetto_pid)], **NOOUT) == 0 device_connected = subprocess.call(['adb', 'shell', 'true'], **NOOUT) == 0 time.sleep(1) signal.signal(signal.SIGINT, old_handler) if IS_INTERRUPTED: # Not check_call because it could have existed in the meantime. subprocess.call(['adb', 'shell', 'kill', '-INT', perfetto_pid]) if args.simpleperf: subprocess.check_call(['adb', 'shell', 'killall', '-INT', 'simpleperf']) print("Waiting for simpleperf to exit.") while subprocess.call( ['adb', 'shell', '[ -f /proc/$(pgrep simpleperf)/exe ]'], **NOOUT) == 0: time.sleep(1) subprocess.check_call(['adb', 'pull', '/data/local/tmp/heapprofd_profile', '/tmp']) print("Pulled simpleperf profile to /tmp/heapprofd_profile") # Wait for perfetto cmd to return. while exists: exists = subprocess.call( ['adb', 'shell', '[ -d /proc/{} ]'.format(perfetto_pid)]) == 0 time.sleep(1) subprocess.check_call(['adb', 'pull', '/data/misc/perfetto-traces/profile-{}'.format(user), '/tmp/profile'], stdout=NULL) trace_to_text_output = subprocess.check_output( [trace_to_text_binary, 'profile', '/tmp/profile']) profile_path = None for word in trace_to_text_output.split(): if 'heap_profile-' in word: profile_path = word if profile_path is None: print("Could not find trace_to_text output path.", file=sys.stderr) return 1 profile_files = os.listdir(profile_path) if not profile_files: print("No profiles generated", file=sys.stderr) return 1 subprocess.check_call(['gzip'] + [os.path.join(profile_path, x) for x in os.listdir(profile_path)]) symlink_path = os.path.join(os.path.dirname(profile_path), "heap_profile-latest") if os.path.lexists(symlink_path): os.unlink(symlink_path) os.symlink(profile_path, symlink_path) print("Wrote profiles to {} (symlink {})".format(profile_path, symlink_path)) print("These can be viewed using pprof. Googlers: head to pprof/ and " "upload them.") if __name__ == '__main__': sys.exit(main(sys.argv))