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', self.server.allow_origin) 68 self.send_header('Cache-Control', 'no-cache') 69 super().end_headers() 70 71 def do_GET(self): 72 if self.path != '/' + self.server.expected_fname: 73 self.send_error(404, "File not found") 74 return 75 76 self.server.fname_get_completed = True 77 super().do_GET() 78 79 def do_POST(self): 80 self.send_error(404, "File not found") 81 82 83def main(): 84 atexit.register(kill_all_subprocs_on_exit) 85 default_out_dir_str = '~/traces/' 86 default_out_dir = os.path.expanduser(default_out_dir_str) 87 88 examples = '\n'.join([ 89 ANSI.BOLD + 'Examples' + ANSI.END, ' -t 10s -b 32mb sched gfx wm -a*', 90 ' -t 5s sched/sched_switch raw_syscalls/sys_enter raw_syscalls/sys_exit', 91 ' -c /path/to/full-textual-trace.config', '', 92 ANSI.BOLD + 'Long traces' + ANSI.END, 93 'If you want to record a hours long trace and stream it into a file ', 94 'you need to pass a full trace config and set write_into_file = true.', 95 'See https://perfetto.dev/docs/concepts/config#long-traces .' 96 ]) 97 parser = argparse.ArgumentParser( 98 epilog=examples, formatter_class=argparse.RawTextHelpFormatter) 99 100 help = 'Output file or directory (default: %s)' % default_out_dir_str 101 parser.add_argument('-o', '--out', default=default_out_dir, help=help) 102 103 help = 'Don\'t open or serve the trace' 104 parser.add_argument('-n', '--no-open', action='store_true', help=help) 105 106 help = 'Don\'t open in browser, but still serve trace (good for remote use)' 107 parser.add_argument('--no-open-browser', action='store_true', help=help) 108 109 help = 'The web address used to open trace files' 110 parser.add_argument('--origin', default='https://ui.perfetto.dev', help=help) 111 112 help = 'Force the use of the sideloaded binaries rather than system daemons' 113 parser.add_argument('--sideload', action='store_true', help=help) 114 115 help = ('Sideload the given binary rather than downloading it. ' + 116 'Implies --sideload') 117 parser.add_argument('--sideload-path', default=None, help=help) 118 119 help = 'Ignores any tracing guardrails which might be used' 120 parser.add_argument('--no-guardrails', action='store_true', help=help) 121 122 help = 'Don\'t run `adb root` run as user (only when sideloading)' 123 parser.add_argument('-u', '--user', action='store_true', help=help) 124 125 help = 'Specify the ADB device serial' 126 parser.add_argument('--serial', '-s', default=None, help=help) 127 128 grp = parser.add_argument_group( 129 'Short options: (only when not using -c/--config)') 130 131 help = 'Trace duration N[s,m,h] (default: trace until stopped)' 132 grp.add_argument('-t', '--time', default='0s', help=help) 133 134 help = 'Ring buffer size N[mb,gb] (default: 32mb)' 135 grp.add_argument('-b', '--buffer', default='32mb', help=help) 136 137 help = ('Android (atrace) app names. Can be specified multiple times.\n-a*' + 138 'for all apps (without space between a and * or bash will expand it)') 139 grp.add_argument( 140 '-a', 141 '--app', 142 metavar='com.myapp', 143 action='append', 144 default=[], 145 help=help) 146 147 help = 'sched, gfx, am, wm (see --list)' 148 grp.add_argument('events', metavar='Atrace events', nargs='*', help=help) 149 150 help = 'sched/sched_switch kmem/kmem (see --list-ftrace)' 151 grp.add_argument('_', metavar='Ftrace events', nargs='*', help=help) 152 153 help = 'Lists all the categories available' 154 grp.add_argument('--list', action='store_true', help=help) 155 156 help = 'Lists all the ftrace events available' 157 grp.add_argument('--list-ftrace', action='store_true', help=help) 158 159 section = ('Full trace config (only when not using short options)') 160 grp = parser.add_argument_group(section) 161 162 help = 'Can be generated with https://ui.perfetto.dev/#!/record' 163 grp.add_argument('-c', '--config', default=None, help=help) 164 165 help = 'Parse input from --config as binary proto (default: parse as text)' 166 grp.add_argument('--bin', action='store_true', help=help) 167 168 help = ('Pass the trace through the trace reporter API. Only works when ' 169 'using the full trace config (-c) with the reporter package name ' 170 "'android.perfetto.cts.reporter' and the reporter class name " 171 "'android.perfetto.cts.reporter.PerfettoReportService' with the " 172 'reporter installed on the device (see ' 173 'tools/install_test_reporter_app.py).') 174 grp.add_argument('--reporter-api', action='store_true', help=help) 175 176 args = parser.parse_args() 177 args.sideload = args.sideload or args.sideload_path is not None 178 179 if args.serial: 180 os.environ["ANDROID_SERIAL"] = args.serial 181 182 find_adb() 183 184 if args.list: 185 adb('shell', 'atrace', '--list_categories').wait() 186 sys.exit(0) 187 188 if args.list_ftrace: 189 adb('shell', 'cat /d/tracing/available_events | tr : /').wait() 190 sys.exit(0) 191 192 if args.config is not None and not os.path.exists(args.config): 193 prt('Config file not found: %s' % args.config, ANSI.RED) 194 sys.exit(1) 195 196 if len(args.events) == 0 and args.config is None: 197 prt('Must either pass short options (e.g. -t 10s sched) or a --config file', 198 ANSI.RED) 199 parser.print_help() 200 sys.exit(1) 201 202 if args.config is None and args.events and os.path.exists(args.events[0]): 203 prt(('The passed event name "%s" is a local file. ' % args.events[0] + 204 'Did you mean to pass -c / --config ?'), ANSI.RED) 205 sys.exit(1) 206 207 if args.reporter_api and not args.config: 208 prt('Must pass --config when using --reporter-api', ANSI.RED) 209 parser.print_help() 210 sys.exit(1) 211 212 perfetto_cmd = 'perfetto' 213 device_dir = '/data/misc/perfetto-traces/' 214 215 # Check the version of android. If too old (< Q) sideload tracebox. Also use 216 # use /data/local/tmp as /data/misc/perfetto-traces was introduced only later. 217 probe_cmd = 'getprop ro.build.version.sdk; getprop ro.product.cpu.abi; whoami' 218 probe = adb('shell', probe_cmd, stdout=subprocess.PIPE) 219 lines = probe.communicate()[0].decode().strip().split('\n') 220 lines = [x.strip() for x in lines] # To strip \r(s) on Windows. 221 if probe.returncode != 0: 222 prt('ADB connection failed', ANSI.RED) 223 sys.exit(1) 224 api_level = int(lines[0]) 225 abi = lines[1] 226 arch = ABI_TO_ARCH.get(abi) 227 if arch is None: 228 prt('Unsupported ABI: ' + abi) 229 sys.exit(1) 230 shell_user = lines[2] 231 if api_level < 29 or args.sideload: # 29: Android Q. 232 tracebox_bin = args.sideload_path 233 if tracebox_bin is None: 234 tracebox_bin = get_perfetto_prebuilt( 235 TRACEBOX_MANIFEST, arch='android-' + arch) 236 perfetto_cmd = '/data/local/tmp/tracebox' 237 exit_code = adb('push', '--sync', tracebox_bin, perfetto_cmd).wait() 238 exit_code |= adb('shell', 'chmod 755 ' + perfetto_cmd).wait() 239 if exit_code != 0: 240 prt('ADB push failed', ANSI.RED) 241 sys.exit(1) 242 device_dir = '/data/local/tmp/' 243 if shell_user != 'root' and not args.user: 244 # Run as root if possible as that will give access to more tracing 245 # capabilities. Non-root still works, but some ftrace events might not be 246 # available. 247 adb('root').wait() 248 249 tstamp = datetime.datetime.now().strftime('%Y-%m-%d_%H-%M') 250 fname = '%s-%s.pftrace' % (tstamp, os.urandom(3).hex()) 251 device_file = device_dir + fname 252 253 cmd = [perfetto_cmd, '--background'] 254 if not args.bin: 255 cmd.append('--txt') 256 257 if args.no_guardrails: 258 cmd.append('--no-guardrails') 259 260 if args.reporter_api: 261 # Remove all old reporter files to avoid polluting the file we will extract 262 # later. 263 adb('shell', 264 'rm /sdcard/Android/data/android.perfetto.cts.reporter/files/*').wait() 265 cmd.append('--upload') 266 else: 267 cmd.extend(['-o', device_file]) 268 269 on_device_config = None 270 on_host_config = None 271 if args.config is not None: 272 cmd += ['-c', '-'] 273 if api_level < 24: 274 # adb shell does not redirect stdin. Push the config on a temporary file 275 # on the device. 276 mktmp = adb( 277 'shell', 278 'mktemp', 279 '--tmpdir', 280 '/data/local/tmp', 281 stdout=subprocess.PIPE) 282 on_device_config = mktmp.communicate()[0].decode().strip().strip() 283 if mktmp.returncode != 0: 284 prt('Failed to create config on device', ANSI.RED) 285 sys.exit(1) 286 exit_code = adb('push', '--sync', args.config, on_device_config).wait() 287 if exit_code != 0: 288 prt('Failed to push config on device', ANSI.RED) 289 sys.exit(1) 290 cmd = ['cat', on_device_config, '|'] + cmd 291 else: 292 on_host_config = args.config 293 else: 294 cmd += ['-t', args.time, '-b', args.buffer] 295 for app in args.app: 296 cmd += ['--app', '\'' + app + '\''] 297 cmd += args.events 298 299 # Work out the output file or directory. 300 if args.out.endswith('/') or os.path.isdir(args.out): 301 host_dir = args.out 302 host_file = os.path.join(args.out, fname) 303 else: 304 host_file = args.out 305 host_dir = os.path.dirname(host_file) 306 if host_dir == '': 307 host_dir = '.' 308 host_file = './' + host_file 309 if not os.path.exists(host_dir): 310 shutil.os.makedirs(host_dir) 311 312 with open(on_host_config or os.devnull, 'rb') as f: 313 print('Running ' + ' '.join(cmd)) 314 proc = adb('shell', *cmd, stdin=f, stdout=subprocess.PIPE) 315 proc_out = proc.communicate()[0].decode().strip() 316 if on_device_config is not None: 317 adb('shell', 'rm', on_device_config).wait() 318 # On older versions of Android (x86_64 emulator running API 22) the output 319 # looks like: 320 # WARNING: linker: /data/local/tmp/tracebox: unused DT entry: ... 321 # WARNING: ... (other 2 WARNING: linker: lines) 322 # 1234 <-- The actual pid we want. 323 match = re.search(r'^(\d+)$', proc_out, re.M) 324 if match is None: 325 prt('Failed to read the pid from perfetto --background', ANSI.RED) 326 prt(proc_out) 327 sys.exit(1) 328 bg_pid = match.group(1) 329 exit_code = proc.wait() 330 331 if exit_code != 0: 332 prt('Perfetto invocation failed', ANSI.RED) 333 sys.exit(1) 334 335 prt('Trace started. Press CTRL+C to stop', ANSI.BLACK + ANSI.BG_BLUE) 336 logcat = adb('logcat', '-v', 'brief', '-s', 'perfetto', '-b', 'main', '-T', 337 '1') 338 339 ctrl_c_count = 0 340 adb_failure_count = 0 341 while ctrl_c_count < 2: 342 try: 343 # On older Android devices adbd doesn't propagate the exit code. Hence 344 # the RUN/TERM parts. 345 poll = adb( 346 'shell', 347 'test -d /proc/%s && echo RUN || echo TERM' % bg_pid, 348 stdout=subprocess.PIPE) 349 poll_res = poll.communicate()[0].decode().strip() 350 if poll_res == 'TERM': 351 break # Process terminated 352 if poll_res == 'RUN': 353 # The 'perfetto' cmdline client is still running. If previously we had 354 # an ADB error, tell the user now it's all right again. 355 if adb_failure_count > 0: 356 adb_failure_count = 0 357 prt('ADB connection re-established, the trace is still ongoing', 358 ANSI.BLUE) 359 time.sleep(0.5) 360 continue 361 # Some ADB error happened. This can happen when tracing soon after boot, 362 # before logging in, when adb gets restarted. 363 adb_failure_count += 1 364 if adb_failure_count >= MAX_ADB_FAILURES: 365 prt('Too many unrecoverable ADB failures, bailing out', ANSI.RED) 366 sys.exit(1) 367 time.sleep(2) 368 except KeyboardInterrupt: 369 sig = 'TERM' if ctrl_c_count == 0 else 'KILL' 370 ctrl_c_count += 1 371 prt('Stopping the trace (SIG%s)' % sig, ANSI.BLACK + ANSI.BG_YELLOW) 372 adb('shell', 'kill -%s %s' % (sig, bg_pid)).wait() 373 374 logcat.kill() 375 logcat.wait() 376 377 if args.reporter_api: 378 prt('Waiting a few seconds to allow reporter to copy trace') 379 time.sleep(5) 380 381 ret = adb( 382 'shell', 383 'cp /sdcard/Android/data/android.perfetto.cts.reporter/files/* ' + 384 device_file).wait() 385 if ret != 0: 386 prt('Failed to extract reporter trace', ANSI.RED) 387 sys.exit(1) 388 389 prt('\n') 390 prt('Pulling into %s' % host_file, ANSI.BOLD) 391 adb('pull', device_file, host_file).wait() 392 adb('shell', 'rm -f ' + device_file).wait() 393 394 if not args.no_open: 395 prt('\n') 396 prt('Opening the trace (%s) in the browser' % host_file) 397 open_browser = not args.no_open_browser 398 open_trace_in_browser(host_file, open_browser, args.origin) 399 400 401def prt(msg, colors=ANSI.END): 402 print(colors + msg + ANSI.END) 403 404 405def find_adb(): 406 """ Locate the "right" adb path 407 408 If adb is in the PATH use that (likely what the user wants) otherwise use the 409 hermetic one in our SDK copy. 410 """ 411 global adb_path 412 for path in ['adb', HERMETIC_ADB_PATH]: 413 try: 414 subprocess.call([path, '--version'], stdout=devnull, stderr=devnull) 415 adb_path = path 416 break 417 except OSError: 418 continue 419 if adb_path is None: 420 sdk_url = 'https://developer.android.com/studio/releases/platform-tools' 421 prt('Could not find a suitable adb binary in the PATH. ', ANSI.RED) 422 prt('You can download adb from %s' % sdk_url, ANSI.RED) 423 sys.exit(1) 424 425 426def open_trace_in_browser(path, open_browser, origin): 427 # We reuse the HTTP+RPC port because it's the only one allowed by the CSP. 428 PORT = 9001 429 path = os.path.abspath(path) 430 os.chdir(os.path.dirname(path)) 431 fname = os.path.basename(path) 432 socketserver.TCPServer.allow_reuse_address = True 433 with socketserver.TCPServer(('127.0.0.1', PORT), HttpHandler) as httpd: 434 address = f'{origin}/#!/?url=http://127.0.0.1:{PORT}/{fname}&referrer=record_android_trace' 435 if open_browser: 436 webbrowser.open_new_tab(address) 437 else: 438 print(f'Open URL in browser: {address}') 439 440 httpd.expected_fname = fname 441 httpd.fname_get_completed = None 442 httpd.allow_origin = origin 443 while httpd.fname_get_completed is None: 444 httpd.handle_request() 445 446 447def adb(*args, stdin=devnull, stdout=None): 448 cmd = [adb_path, *args] 449 setpgrp = None 450 if os.name != 'nt': 451 # On Linux/Mac, start a new process group so all child processes are killed 452 # on exit. Unsupported on Windows. 453 setpgrp = lambda: os.setpgrp() 454 proc = subprocess.Popen(cmd, stdin=stdin, stdout=stdout, preexec_fn=setpgrp) 455 procs.append(proc) 456 return proc 457 458 459def kill_all_subprocs_on_exit(): 460 for p in [p for p in procs if p.poll() is None]: 461 p.kill() 462 463 464def check_hash(file_name, sha_value): 465 with open(file_name, 'rb') as fd: 466 file_hash = hashlib.sha1(fd.read()).hexdigest() 467 return file_hash == sha_value 468 469 470if __name__ == '__main__': 471 sys.exit(main()) 472