#!/usr/bin/env python3 import atexit import argparse import datetime import http.server import os import shutil import socketserver import subprocess import sys import time import webbrowser ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # This is not required. It's only used as a fallback if no adb is found on the # PATH. It's fine if it doesn't exist so this script can be copied elsewhere. HERMETIC_ADB_PATH = ROOT_DIR + '/buildtools/android_sdk/platform-tools/adb' devnull = open(os.devnull, 'rb') adb_path = None procs = [] class ANSI: END = '\033[0m' BOLD = '\033[1m' RED = '\033[91m' BLACK = '\033[30m' BLUE = '\033[94m' BG_YELLOW = '\033[43m' BG_BLUE = '\033[44m' # HTTP Server used to open the trace in the browser. class HttpHandler(http.server.SimpleHTTPRequestHandler): def end_headers(self): self.send_header('Access-Control-Allow-Origin', '*') return super().end_headers() def do_GET(self): self.server.last_request = self.path return super().do_GET() def do_POST(self): self.send_error(404, "File not found") def main(): atexit.register(kill_all_subprocs_on_exit) default_out_dir_str = '~/traces/' default_out_dir = os.path.expanduser(default_out_dir_str) examples = '\n'.join([ ANSI.BOLD + 'Examples' + ANSI.END, ' -t 10s -b 32mb sched gfx wm', ' -t 5s sched/sched_switch raw_syscalls/sys_enter raw_syscalls/sys_exit', ' -c /path/to/full-textual-trace.config', '', ANSI.BOLD + 'Long traces' + ANSI.END, 'If you want to record a hours long trace and stream it into a file ', 'you need to pass a full trace config and set write_into_file = true.', 'See https://perfetto.dev/docs/concepts/config#long-traces .' ]) parser = argparse.ArgumentParser( epilog=examples, formatter_class=argparse.RawTextHelpFormatter) help = 'Output file or directory (default: %s)' % default_out_dir_str parser.add_argument('-o', '--out', default=default_out_dir, help=help) help = 'Don\'t open in the browser' parser.add_argument('-n', '--no-open', action='store_true', help=help) grp = parser.add_argument_group( 'Short options: (only when not using -c/--config)') help = 'Trace duration N[s,m,h] (default: trace until stopped)' grp.add_argument('-t', '--time', default='0s', help=help) help = 'Ring buffer size N[mb,gb] (default: 32mb)' grp.add_argument('-b', '--buffer', default='32mb', help=help) help = 'Android (atrace) app names (can be specified multiple times)' grp.add_argument( '-a', '--app', metavar='Atrace apps', action='append', default=[], help=help) help = 'sched, gfx, am, wm (see --list)' grp.add_argument('events', metavar='Atrace events', nargs='*', help=help) help = 'sched/sched_switch kmem/kmem (see --list-ftrace)' grp.add_argument('_', metavar='Ftrace events', nargs='*', help=help) help = 'Lists all the categories available' grp.add_argument('--list', action='store_true', help=help) help = 'Lists all the ftrace events available' grp.add_argument('--list-ftrace', action='store_true', help=help) section = ('Full trace config (only when not using short options)') grp = parser.add_argument_group(section) help = 'Can be generated with https://ui.perfetto.dev/#!/record' grp.add_argument('-c', '--config', default=None, help=help) args = parser.parse_args() tstamp = datetime.datetime.now().strftime('%Y-%m-%d_%H-%M') fname = '%s.pftrace' % tstamp device_file = '/data/misc/perfetto-traces/' + fname find_adb() if args.list: adb('shell', 'atrace', '--list_categories').wait() sys.exit(0) if args.list_ftrace: adb('shell', 'cat /d/tracing/available_events | tr : /').wait() sys.exit(0) if args.config is not None and not os.path.exists(args.config): prt('Config file not found: %s' % args.config, ANSI.RED) sys.exit(1) if len(args.events) == 0 and args.config is None: prt('Must either pass short options (e.g. -t 10s sched) or a --config file', ANSI.RED) parser.print_help() sys.exit(1) if args.config is None and args.events and os.path.exists(args.events[0]): prt(('The passed event name "%s" is a local file. ' % args.events[0] + 'Did you mean to pass -c / --config ?'), ANSI.RED) sys.exit(1) cmd = ['perfetto', '--background', '--txt', '-o', device_file] if args.config is not None: cmd += ['-c', '-'] else: cmd += ['-t', args.time, '-b', args.buffer] for app in args.app: cmd += ['--app', app] cmd += args.events # Perfetto will error out with a proper message if both a config file and # short options are specified. No need to replicate that logic. # Work out the output file or directory. if args.out.endswith('/') or os.path.isdir(args.out): host_dir = args.out host_file = os.path.join(args.out, fname) else: host_file = args.out host_dir = os.path.dirname(host_file) if host_dir == '': host_dir = '.' host_file = './' + host_file if not os.path.exists(host_dir): shutil.os.makedirs(host_dir) with open(args.config or os.devnull, 'rb') as f: print('Running ' + ' '.join(cmd)) proc = adb('shell', *cmd, stdin=f, stdout=subprocess.PIPE) bg_pid = proc.communicate()[0].decode().strip() exit_code = proc.wait() if exit_code != 0: prt('Perfetto invocation failed', ANSI.RED) sys.exit(1) prt('Trace started. Press CTRL+C to stop', ANSI.BLACK + ANSI.BG_BLUE) logcat = adb('logcat', '-v', 'brief', '-s', 'perfetto', '-b', 'main', '-T', '1') ctrl_c_count = 0 while ctrl_c_count < 2: try: poll = adb('shell', 'test -d /proc/' + bg_pid) if poll.wait() != 0: break time.sleep(0.5) except KeyboardInterrupt: sig = 'TERM' if ctrl_c_count == 0 else 'KILL' ctrl_c_count += 1 prt('Stopping the trace (SIG%s)' % sig, ANSI.BLACK + ANSI.BG_YELLOW) res = adb('shell', 'kill -%s %s' % (sig, bg_pid)).wait() logcat.kill() logcat.wait() prt('\n') prt('Pulling into %s' % host_file, ANSI.BOLD) adb('pull', device_file, host_file).wait() if not args.no_open: prt('\n') prt('Opening the trace (%s) in the browser' % host_file) open_trace_in_browser(host_file) def prt(msg, colors=ANSI.END): print(colors + msg + ANSI.END) def find_adb(): """ Locate the "right" adb path If adb is in the PATH use that (likely what the user wants) otherwise use the hermetic one in our SDK copy. """ global adb_path for path in ['adb', HERMETIC_ADB_PATH]: try: subprocess.call([path, '--version'], stdout=devnull, stderr=devnull) adb_path = path break except OSError: continue if adb_path is None: sdk_url = 'https://developer.android.com/studio/releases/platform-tools' prt('Could not find a suitable adb binary in the PATH. ', ANSI.RED) prt('You can download adb from %s' % sdk_url, ANSI.RED) sys.exit(1) def open_trace_in_browser(path): # We reuse the HTTP+RPC port because it's the only one allowed by the CSP. PORT = 9001 os.chdir(os.path.dirname(path)) fname = os.path.basename(path) socketserver.TCPServer.allow_reuse_address = True with socketserver.TCPServer(('127.0.0.1', PORT), HttpHandler) as httpd: webbrowser.open_new_tab( 'https://ui.perfetto.dev/#!/?url=http://127.0.0.1:%d/%s' % (PORT, fname)) while httpd.__dict__.get('last_request') != '/' + fname: httpd.handle_request() def adb(*args, stdin=devnull, stdout=None): cmd = [adb_path, *args] setpgrp = None if os.name != 'nt': # On Linux/Mac, start a new process group so all child processes are killed # on exit. Unsupported on Windows. setpgrp = lambda: os.setpgrp() proc = subprocess.Popen(cmd, stdin=stdin, stdout=stdout, preexec_fn=setpgrp) procs.append(proc) return proc def kill_all_subprocs_on_exit(): for p in [p for p in procs if p.poll() is None]: p.kill() if __name__ == '__main__': sys.exit(main())