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