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