1#!/usr/bin/env python3 2# Copyright (C) 2021 The Android Open Source Project 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16import atexit 17import argparse 18import datetime 19import hashlib 20import http.server 21import os 22import re 23import shutil 24import socketserver 25import subprocess 26import sys 27import time 28import webbrowser 29 30from perfetto.prebuilts.manifests.tracebox import * 31from perfetto.prebuilts.perfetto_prebuilts import * 32from perfetto.common.repo_utils import * 33 34# This is not required. It's only used as a fallback if no adb is found on the 35# PATH. It's fine if it doesn't exist so this script can be copied elsewhere. 36HERMETIC_ADB_PATH = repo_dir('buildtools/android_sdk/platform-tools/adb') 37 38# Translates the Android ro.product.cpu.abi into the GN's target_cpu. 39ABI_TO_ARCH = { 40 'armeabi-v7a': 'arm', 41 'arm64-v8a': 'arm64', 42 'x86': 'x86', 43 'x86_64': 'x64', 44} 45 46MAX_ADB_FAILURES = 15 # 2 seconds between retries, 30 seconds total. 47 48devnull = open(os.devnull, 'rb') 49adb_path = None 50procs = [] 51 52 53class ANSI: 54 END = '\033[0m' 55 BOLD = '\033[1m' 56 RED = '\033[91m' 57 BLACK = '\033[30m' 58 BLUE = '\033[94m' 59 BG_YELLOW = '\033[43m' 60 BG_BLUE = '\033[44m' 61 62 63# HTTP Server used to open the trace in the browser. 64class HttpHandler(http.server.SimpleHTTPRequestHandler): 65 66 def end_headers(self): 67 self.send_header('Access-Control-Allow-Origin', '*') 68 return super().end_headers() 69 70 def do_GET(self): 71 self.server.last_request = self.path 72 return super().do_GET() 73 74 def do_POST(self): 75 self.send_error(404, "File not found") 76 77 78def main(): 79 atexit.register(kill_all_subprocs_on_exit) 80 default_out_dir_str = '~/traces/' 81 default_out_dir = os.path.expanduser(default_out_dir_str) 82 83 examples = '\n'.join([ 84 ANSI.BOLD + 'Examples' + ANSI.END, ' -t 10s -b 32mb sched gfx wm -a*', 85 ' -t 5s sched/sched_switch raw_syscalls/sys_enter raw_syscalls/sys_exit', 86 ' -c /path/to/full-textual-trace.config', '', 87 ANSI.BOLD + 'Long traces' + ANSI.END, 88 'If you want to record a hours long trace and stream it into a file ', 89 'you need to pass a full trace config and set write_into_file = true.', 90 'See https://perfetto.dev/docs/concepts/config#long-traces .' 91 ]) 92 parser = argparse.ArgumentParser( 93 epilog=examples, formatter_class=argparse.RawTextHelpFormatter) 94 95 help = 'Output file or directory (default: %s)' % default_out_dir_str 96 parser.add_argument('-o', '--out', default=default_out_dir, help=help) 97 98 help = 'Don\'t open in the browser' 99 parser.add_argument('-n', '--no-open', action='store_true', help=help) 100 101 help = 'Force the use of the sideloaded binaries rather than system daemons' 102 parser.add_argument('--sideload', action='store_true', help=help) 103 104 help = ('Sideload the given binary rather than downloading it. ' + 105 'Implies --sideload') 106 parser.add_argument('--sideload-path', default=None, help=help) 107 108 help = 'Don\'t run `adb root` run as user (only when sideloading)' 109 parser.add_argument('-u', '--user', action='store_true', help=help) 110 111 help = 'Specify the ADB device serial' 112 parser.add_argument('--serial', '-s', default=None, help=help) 113 114 grp = parser.add_argument_group( 115 'Short options: (only when not using -c/--config)') 116 117 help = 'Trace duration N[s,m,h] (default: trace until stopped)' 118 grp.add_argument('-t', '--time', default='0s', help=help) 119 120 help = 'Ring buffer size N[mb,gb] (default: 32mb)' 121 grp.add_argument('-b', '--buffer', default='32mb', help=help) 122 123 help = ('Android (atrace) app names. Can be specified multiple times.\n-a*' + 124 'for all apps (without space between a and * or bash will expand it)') 125 grp.add_argument( 126 '-a', 127 '--app', 128 metavar='com.myapp', 129 action='append', 130 default=[], 131 help=help) 132 133 help = 'sched, gfx, am, wm (see --list)' 134 grp.add_argument('events', metavar='Atrace events', nargs='*', help=help) 135 136 help = 'sched/sched_switch kmem/kmem (see --list-ftrace)' 137 grp.add_argument('_', metavar='Ftrace events', nargs='*', help=help) 138 139 help = 'Lists all the categories available' 140 grp.add_argument('--list', action='store_true', help=help) 141 142 help = 'Lists all the ftrace events available' 143 grp.add_argument('--list-ftrace', action='store_true', help=help) 144 145 section = ('Full trace config (only when not using short options)') 146 grp = parser.add_argument_group(section) 147 148 help = 'Can be generated with https://ui.perfetto.dev/#!/record' 149 grp.add_argument('-c', '--config', default=None, help=help) 150 151 args = parser.parse_args() 152 args.sideload = args.sideload or args.sideload_path is not None 153 154 if args.serial: 155 os.environ["ANDROID_SERIAL"] = args.serial 156 157 find_adb() 158 159 if args.list: 160 adb('shell', 'atrace', '--list_categories').wait() 161 sys.exit(0) 162 163 if args.list_ftrace: 164 adb('shell', 'cat /d/tracing/available_events | tr : /').wait() 165 sys.exit(0) 166 167 if args.config is not None and not os.path.exists(args.config): 168 prt('Config file not found: %s' % args.config, ANSI.RED) 169 sys.exit(1) 170 171 if len(args.events) == 0 and args.config is None: 172 prt('Must either pass short options (e.g. -t 10s sched) or a --config file', 173 ANSI.RED) 174 parser.print_help() 175 sys.exit(1) 176 177 if args.config is None and args.events and os.path.exists(args.events[0]): 178 prt(('The passed event name "%s" is a local file. ' % args.events[0] + 179 'Did you mean to pass -c / --config ?'), ANSI.RED) 180 sys.exit(1) 181 182 perfetto_cmd = 'perfetto' 183 device_dir = '/data/misc/perfetto-traces/' 184 185 # Check the version of android. If too old (< Q) sideload tracebox. Also use 186 # use /data/local/tmp as /data/misc/perfetto-traces was introduced only later. 187 probe_cmd = 'getprop ro.build.version.sdk; getprop ro.product.cpu.abi; whoami' 188 probe = adb('shell', probe_cmd, stdout=subprocess.PIPE) 189 lines = probe.communicate()[0].decode().strip().split('\n') 190 lines = [x.strip() for x in lines] # To strip \r(s) on Windows. 191 if probe.returncode != 0: 192 prt('ADB connection failed', ANSI.RED) 193 sys.exit(1) 194 api_level = int(lines[0]) 195 abi = lines[1] 196 arch = ABI_TO_ARCH.get(abi) 197 if arch is None: 198 prt('Unsupported ABI: ' + abi) 199 sys.exit(1) 200 shell_user = lines[2] 201 if api_level < 29 or args.sideload: # 29: Android Q. 202 tracebox_bin = args.sideload_path 203 if tracebox_bin is None: 204 tracebox_bin = get_perfetto_prebuilt( 205 TRACEBOX_MANIFEST, arch='android-' + arch) 206 perfetto_cmd = '/data/local/tmp/tracebox' 207 exit_code = adb('push', '--sync', tracebox_bin, perfetto_cmd).wait() 208 exit_code |= adb('shell', 'chmod 755 ' + perfetto_cmd).wait() 209 if exit_code != 0: 210 prt('ADB push failed', ANSI.RED) 211 sys.exit(1) 212 device_dir = '/data/local/tmp/' 213 if shell_user != 'root' and not args.user: 214 # Run as root if possible as that will give access to more tracing 215 # capabilities. Non-root still works, but some ftrace events might not be 216 # available. 217 adb('root').wait() 218 219 tstamp = datetime.datetime.now().strftime('%Y-%m-%d_%H-%M') 220 fname = '%s-%s.pftrace' % (tstamp, os.urandom(3).hex()) 221 device_file = device_dir + fname 222 223 cmd = [perfetto_cmd, '--background', '--txt', '-o', device_file] 224 on_device_config = None 225 on_host_config = None 226 if args.config is not None: 227 cmd += ['-c', '-'] 228 if api_level < 24: 229 # adb shell does not redirect stdin. Push the config on a temporary file 230 # on the device. 231 mktmp = adb( 232 'shell', 233 'mktemp', 234 '--tmpdir', 235 '/data/local/tmp', 236 stdout=subprocess.PIPE) 237 on_device_config = mktmp.communicate()[0].decode().strip().strip() 238 if mktmp.returncode != 0: 239 prt('Failed to create config on device', ANSI.RED) 240 sys.exit(1) 241 exit_code = adb('push', '--sync', args.config, on_device_config).wait() 242 if exit_code != 0: 243 prt('Failed to push config on device', ANSI.RED) 244 sys.exit(1) 245 cmd = ['cat', on_device_config, '|'] + cmd 246 else: 247 on_host_config = args.config 248 else: 249 cmd += ['-t', args.time, '-b', args.buffer] 250 for app in args.app: 251 cmd += ['--app', '\'' + app + '\''] 252 cmd += args.events 253 254 # Perfetto will error out with a proper message if both a config file and 255 # short options are specified. No need to replicate that logic. 256 257 # Work out the output file or directory. 258 if args.out.endswith('/') or os.path.isdir(args.out): 259 host_dir = args.out 260 host_file = os.path.join(args.out, fname) 261 else: 262 host_file = args.out 263 host_dir = os.path.dirname(host_file) 264 if host_dir == '': 265 host_dir = '.' 266 host_file = './' + host_file 267 if not os.path.exists(host_dir): 268 shutil.os.makedirs(host_dir) 269 270 with open(on_host_config or os.devnull, 'rb') as f: 271 print('Running ' + ' '.join(cmd)) 272 proc = adb('shell', *cmd, stdin=f, stdout=subprocess.PIPE) 273 proc_out = proc.communicate()[0].decode().strip() 274 if on_device_config is not None: 275 adb('shell', 'rm', on_device_config).wait() 276 # On older versions of Android (x86_64 emulator running API 22) the output 277 # looks like: 278 # WARNING: linker: /data/local/tmp/tracebox: unused DT entry: ... 279 # WARNING: ... (other 2 WARNING: linker: lines) 280 # 1234 <-- The actual pid we want. 281 match = re.search(r'^(\d+)$', proc_out, re.M) 282 if match is None: 283 prt('Failed to read the pid from perfetto --background', ANSI.RED) 284 prt(proc_out) 285 sys.exit(1) 286 bg_pid = match.group(1) 287 exit_code = proc.wait() 288 289 if exit_code != 0: 290 prt('Perfetto invocation failed', ANSI.RED) 291 sys.exit(1) 292 293 prt('Trace started. Press CTRL+C to stop', ANSI.BLACK + ANSI.BG_BLUE) 294 logcat = adb('logcat', '-v', 'brief', '-s', 'perfetto', '-b', 'main', '-T', 295 '1') 296 297 ctrl_c_count = 0 298 adb_failure_count = 0 299 while ctrl_c_count < 2: 300 try: 301 # On older Android devices adbd doesn't propagate the exit code. Hence 302 # the RUN/TERM parts. 303 poll = adb( 304 'shell', 305 'test -d /proc/%s && echo RUN || echo TERM' % bg_pid, 306 stdout=subprocess.PIPE) 307 poll_res = poll.communicate()[0].decode().strip() 308 if poll_res == 'TERM': 309 break # Process terminated 310 if poll_res == 'RUN': 311 # The 'perfetto' cmdline client is still running. If previously we had 312 # an ADB error, tell the user now it's all right again. 313 if adb_failure_count > 0: 314 adb_failure_count = 0 315 prt('ADB connection re-established, the trace is still ongoing', 316 ANSI.BLUE) 317 time.sleep(0.5) 318 continue 319 # Some ADB error happened. This can happen when tracing soon after boot, 320 # before logging in, when adb gets restarted. 321 adb_failure_count += 1 322 if adb_failure_count >= MAX_ADB_FAILURES: 323 prt('Too many unrecoverable ADB failures, bailing out', ANSI.RED) 324 sys.exit(1) 325 time.sleep(2) 326 except KeyboardInterrupt: 327 sig = 'TERM' if ctrl_c_count == 0 else 'KILL' 328 ctrl_c_count += 1 329 prt('Stopping the trace (SIG%s)' % sig, ANSI.BLACK + ANSI.BG_YELLOW) 330 adb('shell', 'kill -%s %s' % (sig, bg_pid)).wait() 331 332 logcat.kill() 333 logcat.wait() 334 335 prt('\n') 336 prt('Pulling into %s' % host_file, ANSI.BOLD) 337 adb('pull', device_file, host_file).wait() 338 adb('shell', 'rm -f ' + device_file).wait() 339 340 if not args.no_open: 341 prt('\n') 342 prt('Opening the trace (%s) in the browser' % host_file) 343 open_trace_in_browser(host_file) 344 345 346def prt(msg, colors=ANSI.END): 347 print(colors + msg + ANSI.END) 348 349 350def find_adb(): 351 """ Locate the "right" adb path 352 353 If adb is in the PATH use that (likely what the user wants) otherwise use the 354 hermetic one in our SDK copy. 355 """ 356 global adb_path 357 for path in ['adb', HERMETIC_ADB_PATH]: 358 try: 359 subprocess.call([path, '--version'], stdout=devnull, stderr=devnull) 360 adb_path = path 361 break 362 except OSError: 363 continue 364 if adb_path is None: 365 sdk_url = 'https://developer.android.com/studio/releases/platform-tools' 366 prt('Could not find a suitable adb binary in the PATH. ', ANSI.RED) 367 prt('You can download adb from %s' % sdk_url, ANSI.RED) 368 sys.exit(1) 369 370 371def open_trace_in_browser(path): 372 # We reuse the HTTP+RPC port because it's the only one allowed by the CSP. 373 PORT = 9001 374 os.chdir(os.path.dirname(path)) 375 fname = os.path.basename(path) 376 socketserver.TCPServer.allow_reuse_address = True 377 with socketserver.TCPServer(('127.0.0.1', PORT), HttpHandler) as httpd: 378 webbrowser.open_new_tab( 379 'https://ui.perfetto.dev/#!/?url=http://127.0.0.1:%d/%s' % 380 (PORT, fname)) 381 while httpd.__dict__.get('last_request') != '/' + fname: 382 httpd.handle_request() 383 384 385def adb(*args, stdin=devnull, stdout=None): 386 cmd = [adb_path, *args] 387 setpgrp = None 388 if os.name != 'nt': 389 # On Linux/Mac, start a new process group so all child processes are killed 390 # on exit. Unsupported on Windows. 391 setpgrp = lambda: os.setpgrp() 392 proc = subprocess.Popen(cmd, stdin=stdin, stdout=stdout, preexec_fn=setpgrp) 393 procs.append(proc) 394 return proc 395 396 397def kill_all_subprocs_on_exit(): 398 for p in [p for p in procs if p.poll() is None]: 399 p.kill() 400 401 402def check_hash(file_name, sha_value): 403 with open(file_name, 'rb') as fd: 404 file_hash = hashlib.sha1(fd.read()).hexdigest() 405 return file_hash == sha_value 406 407 408if __name__ == '__main__': 409 sys.exit(main()) 410