1#!/usr/bin/env python3 2 3# Copyright (C) 2017 The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17from __future__ import absolute_import 18from __future__ import division 19from __future__ import print_function 20 21import argparse 22import atexit 23import hashlib 24import os 25import shutil 26import signal 27import subprocess 28import sys 29import tempfile 30import time 31import uuid 32 33NULL = open(os.devnull) 34NOOUT = { 35 'stdout': NULL, 36 'stderr': NULL, 37} 38 39UUID = str(uuid.uuid4())[-6:] 40 41PACKAGES_LIST_CFG = '''data_sources { 42 config { 43 name: "android.packages_list" 44 } 45} 46''' 47 48CFG_INDENT = ' ' 49CFG = '''buffers {{ 50 size_kb: 63488 51}} 52 53data_sources {{ 54 config {{ 55 name: "android.heapprofd" 56 heapprofd_config {{ 57 shmem_size_bytes: {shmem_size} 58 sampling_interval_bytes: {interval} 59{target_cfg} 60 }} 61 }} 62}} 63 64duration_ms: {duration} 65write_into_file: true 66flush_timeout_ms: 30000 67flush_period_ms: 604800000 68''' 69 70# flush_period_ms of 1 week to suppress trace_processor_shell warning. 71 72CONTINUOUS_DUMP = """ 73 continuous_dump_config {{ 74 dump_phase_ms: 0 75 dump_interval_ms: {dump_interval} 76 }} 77""" 78 79PROFILE_LOCAL_PATH = os.path.join(tempfile.gettempdir(), UUID) 80 81IS_INTERRUPTED = False 82 83 84def sigint_handler(sig, frame): 85 global IS_INTERRUPTED 86 IS_INTERRUPTED = True 87 88 89def print_no_profile_error(): 90 print("No profiles generated", file=sys.stderr) 91 print( 92 "If this is unexpected, check " 93 "https://perfetto.dev/docs/data-sources/native-heap-profiler#troubleshooting.", 94 file=sys.stderr) 95 96 97def known_issues_url(number): 98 return ('https://perfetto.dev/docs/data-sources/native-heap-profiler' 99 '#known-issues-android{}'.format(number)) 100 101 102KNOWN_ISSUES = { 103 '10': known_issues_url(10), 104 'Q': known_issues_url(10), 105 '11': known_issues_url(11), 106 'R': known_issues_url(11), 107} 108 109 110def maybe_known_issues(): 111 release_or_codename = subprocess.check_output( 112 ['adb', 'shell', 'getprop', 113 'ro.build.version.release_or_codename']).decode('utf-8').strip() 114 return KNOWN_ISSUES.get(release_or_codename, None) 115 116 117SDK = { 118 'R': 30, 119} 120 121 122def release_or_newer(release): 123 sdk = int( 124 subprocess.check_output( 125 ['adb', 'shell', 'getprop', 126 'ro.system.build.version.sdk']).decode('utf-8').strip()) 127 if sdk >= SDK[release]: 128 return True 129 codename = subprocess.check_output( 130 ['adb', 'shell', 'getprop', 131 'ro.build.version.codename']).decode('utf-8').strip() 132 return codename == release 133 134 135ORDER = ['-n', '-p', '-i', '-o'] 136 137 138def arg_order(action): 139 result = len(ORDER) 140 for opt in action.option_strings: 141 if opt in ORDER: 142 result = min(ORDER.index(opt), result) 143 return result, action.option_strings[0].strip('-') 144 145 146def print_options(parser): 147 for action in sorted(parser._actions, key=arg_order): 148 if action.help is argparse.SUPPRESS: 149 continue 150 opts = ', '.join('`' + x + '`' for x in action.option_strings) 151 metavar = '' if action.metavar is None else ' _' + action.metavar + '_' 152 print('{}{}'.format(opts, metavar)) 153 print(': {}'.format(action.help)) 154 print() 155 156 157def main(argv): 158 parser = argparse.ArgumentParser() 159 parser.add_argument( 160 "-i", 161 "--interval", 162 help="Sampling interval. " 163 "Default 4096 (4KiB)", 164 type=int, 165 default=4096) 166 parser.add_argument( 167 "-d", 168 "--duration", 169 help="Duration of profile (ms). 0 to run until interrupted. " 170 "Default: until interrupted by user.", 171 type=int, 172 default=0) 173 # This flag is a no-op now. We never start heapprofd explicitly using system 174 # properties. 175 parser.add_argument( 176 "--no-start", help="Do not start heapprofd.", action='store_true') 177 parser.add_argument( 178 "-p", 179 "--pid", 180 help="Comma-separated list of PIDs to " 181 "profile.", 182 metavar="PIDS") 183 parser.add_argument( 184 "-n", 185 "--name", 186 help="Comma-separated list of process " 187 "names to profile.", 188 metavar="NAMES") 189 parser.add_argument( 190 "-c", 191 "--continuous-dump", 192 help="Dump interval in ms. 0 to disable continuous dump.", 193 type=int, 194 default=0) 195 parser.add_argument( 196 "--heaps", 197 help="Comma-separated list of heaps to collect, e.g: malloc,art. " 198 "Requires Android 12.", 199 metavar="HEAPS") 200 parser.add_argument( 201 "--all-heaps", 202 action="store_true", 203 help="Collect allocations from all heaps registered by target.") 204 parser.add_argument( 205 "--no-android-tree-symbolization", 206 action="store_true", 207 help="Do not symbolize using currently lunched target in the " 208 "Android tree.") 209 parser.add_argument( 210 "--disable-selinux", 211 action="store_true", 212 help="Disable SELinux enforcement for duration of " 213 "profile.") 214 parser.add_argument( 215 "--no-versions", 216 action="store_true", 217 help="Do not get version information about APKs.") 218 parser.add_argument( 219 "--no-running", 220 action="store_true", 221 help="Do not target already running processes. Requires Android 11.") 222 parser.add_argument( 223 "--no-startup", 224 action="store_true", 225 help="Do not target processes that start during " 226 "the profile. Requires Android 11.") 227 parser.add_argument( 228 "--shmem-size", 229 help="Size of buffer between client and " 230 "heapprofd. Default 8MiB. Needs to be a power of two " 231 "multiple of 4096, at least 8192.", 232 type=int, 233 default=8 * 1048576) 234 parser.add_argument( 235 "--block-client", 236 help="When buffer is full, block the " 237 "client to wait for buffer space. Use with caution as " 238 "this can significantly slow down the client. " 239 "This is the default", 240 action="store_true") 241 parser.add_argument( 242 "--block-client-timeout", 243 help="If --block-client is given, do not block any allocation for " 244 "longer than this timeout (us).", 245 type=int) 246 parser.add_argument( 247 "--no-block-client", 248 help="When buffer is full, stop the " 249 "profile early.", 250 action="store_true") 251 parser.add_argument( 252 "--idle-allocations", 253 help="Keep track of how many " 254 "bytes were unused since the last dump, per " 255 "callstack", 256 action="store_true") 257 parser.add_argument( 258 "--dump-at-max", 259 help="Dump the maximum memory usage " 260 "rather than at the time of the dump.", 261 action="store_true") 262 parser.add_argument( 263 "--disable-fork-teardown", 264 help="Do not tear down client in forks. This can be useful for programs " 265 "that use vfork. Android 11+ only.", 266 action="store_true") 267 parser.add_argument( 268 "--simpleperf", 269 action="store_true", 270 help="Get simpleperf profile of heapprofd. This is " 271 "only for heapprofd development.") 272 parser.add_argument( 273 "--trace-to-text-binary", 274 help="Path to local trace to text. For debugging.") 275 parser.add_argument( 276 "--print-config", 277 action="store_true", 278 help="Print config instead of running. For debugging.") 279 parser.add_argument( 280 "-o", 281 "--output", 282 help="Output directory.", 283 metavar="DIRECTORY", 284 default=None) 285 parser.add_argument( 286 "--print-options", action="store_true", help=argparse.SUPPRESS) 287 288 args = parser.parse_args() 289 if args.print_options: 290 print_options(parser) 291 return 0 292 fail = False 293 if args.block_client and args.no_block_client: 294 print( 295 "FATAL: Both block-client and no-block-client given.", file=sys.stderr) 296 fail = True 297 if args.pid is None and args.name is None: 298 print("FATAL: Neither PID nor NAME given.", file=sys.stderr) 299 fail = True 300 if args.duration is None: 301 print("FATAL: No duration given.", file=sys.stderr) 302 fail = True 303 if args.interval is None: 304 print("FATAL: No interval given.", file=sys.stderr) 305 fail = True 306 if args.shmem_size % 4096: 307 print("FATAL: shmem-size is not a multiple of 4096.", file=sys.stderr) 308 fail = True 309 if args.shmem_size < 8192: 310 print("FATAL: shmem-size is less than 8192.", file=sys.stderr) 311 fail = True 312 if args.shmem_size & (args.shmem_size - 1): 313 print("FATAL: shmem-size is not a power of two.", file=sys.stderr) 314 fail = True 315 316 target_cfg = "" 317 if not args.no_block_client: 318 target_cfg += CFG_INDENT + "block_client: true\n" 319 if args.block_client_timeout: 320 target_cfg += ( 321 CFG_INDENT + 322 "block_client_timeout_us: %s\n" % args.block_client_timeout) 323 if args.no_startup: 324 target_cfg += CFG_INDENT + "no_startup: true\n" 325 if args.no_running: 326 target_cfg += CFG_INDENT + "no_running: true\n" 327 if args.dump_at_max: 328 target_cfg += CFG_INDENT + "dump_at_max: true\n" 329 if args.disable_fork_teardown: 330 target_cfg += CFG_INDENT + "disable_fork_teardown: true\n" 331 if args.all_heaps: 332 target_cfg += CFG_INDENT + "all_heaps: true\n" 333 if args.pid: 334 for pid in args.pid.split(','): 335 try: 336 pid = int(pid) 337 except ValueError: 338 print("FATAL: invalid PID %s" % pid, file=sys.stderr) 339 fail = True 340 target_cfg += CFG_INDENT + 'pid: {}\n'.format(pid) 341 if args.name: 342 for name in args.name.split(','): 343 target_cfg += CFG_INDENT + 'process_cmdline: "{}"\n'.format(name) 344 if args.heaps: 345 for heap in args.heaps.split(','): 346 target_cfg += CFG_INDENT + 'heaps: "{}"\n'.format(heap) 347 348 if fail: 349 parser.print_help() 350 return 1 351 352 trace_to_text_binary = args.trace_to_text_binary 353 354 if args.continuous_dump: 355 target_cfg += CONTINUOUS_DUMP.format(dump_interval=args.continuous_dump) 356 cfg = CFG.format( 357 interval=args.interval, 358 duration=args.duration, 359 target_cfg=target_cfg, 360 shmem_size=args.shmem_size) 361 if not args.no_versions: 362 cfg += PACKAGES_LIST_CFG 363 364 if args.print_config: 365 print(cfg) 366 return 0 367 368 # Do this AFTER print_config so we do not download trace_to_text only to 369 # print out the config. 370 if trace_to_text_binary is None: 371 trace_to_text_binary = get_perfetto_prebuilt( 372 'trace_to_text', soft_fail=True) 373 374 known_issues = maybe_known_issues() 375 if known_issues: 376 print('If you are experiencing problems, please see the known issues for ' 377 'your release: {}.'.format(known_issues)) 378 379 # TODO(fmayer): Maybe feature detect whether we can remove traces instead of 380 # this. 381 uuid_trace = release_or_newer('R') 382 if uuid_trace: 383 profile_device_path = '/data/misc/perfetto-traces/profile-' + UUID 384 else: 385 user = subprocess.check_output(['adb', 'shell', 386 'whoami']).decode('utf-8').strip() 387 profile_device_path = '/data/misc/perfetto-traces/profile-' + user 388 389 perfetto_cmd = ('CFG=\'{cfg}\'; echo ${{CFG}} | ' 390 'perfetto --txt -c - -o ' + profile_device_path + ' -d') 391 392 if args.disable_selinux: 393 enforcing = subprocess.check_output(['adb', 'shell', 'getenforce']) 394 atexit.register( 395 subprocess.check_call, 396 ['adb', 'shell', 'su root setenforce %s' % enforcing]) 397 subprocess.check_call(['adb', 'shell', 'su root setenforce 0']) 398 399 if args.simpleperf: 400 subprocess.check_call([ 401 'adb', 'shell', 'mkdir -p /data/local/tmp/heapprofd_profile && ' 402 'cd /data/local/tmp/heapprofd_profile &&' 403 '(nohup simpleperf record -g -p $(pidof heapprofd) 2>&1 &) ' 404 '> /dev/null' 405 ]) 406 407 profile_target = PROFILE_LOCAL_PATH 408 if args.output is not None: 409 profile_target = args.output 410 else: 411 os.mkdir(profile_target) 412 413 if not os.path.isdir(profile_target): 414 print( 415 "Output directory {} not found".format(profile_target), file=sys.stderr) 416 return 1 417 418 if os.listdir(profile_target): 419 print( 420 "Output directory {} not empty".format(profile_target), file=sys.stderr) 421 return 1 422 423 perfetto_pid = subprocess.check_output( 424 ['adb', 'exec-out', perfetto_cmd.format(cfg=cfg)]).strip() 425 try: 426 perfetto_pid = int(perfetto_pid.strip()) 427 except ValueError: 428 print("Failed to invoke perfetto: {}".format(perfetto_pid), file=sys.stderr) 429 return 1 430 431 old_handler = signal.signal(signal.SIGINT, sigint_handler) 432 print("Profiling active. Press Ctrl+C to terminate.") 433 print("You may disconnect your device.") 434 print() 435 exists = True 436 device_connected = True 437 while not device_connected or (exists and not IS_INTERRUPTED): 438 exists = subprocess.call( 439 ['adb', 'shell', '[ -d /proc/{} ]'.format(perfetto_pid)], **NOOUT) == 0 440 device_connected = subprocess.call(['adb', 'shell', 'true'], **NOOUT) == 0 441 time.sleep(1) 442 print("Waiting for profiler shutdown...") 443 signal.signal(signal.SIGINT, old_handler) 444 if IS_INTERRUPTED: 445 # Not check_call because it could have existed in the meantime. 446 subprocess.call(['adb', 'shell', 'kill', '-INT', str(perfetto_pid)]) 447 if args.simpleperf: 448 subprocess.check_call(['adb', 'shell', 'killall', '-INT', 'simpleperf']) 449 print("Waiting for simpleperf to exit.") 450 while subprocess.call( 451 ['adb', 'shell', '[ -f /proc/$(pidof simpleperf)/exe ]'], **NOOUT) == 0: 452 time.sleep(1) 453 subprocess.check_call( 454 ['adb', 'pull', '/data/local/tmp/heapprofd_profile', profile_target]) 455 print("Pulled simpleperf profile to " + profile_target + 456 "/heapprofd_profile") 457 458 # Wait for perfetto cmd to return. 459 while exists: 460 exists = subprocess.call( 461 ['adb', 'shell', '[ -d /proc/{} ]'.format(perfetto_pid)]) == 0 462 time.sleep(1) 463 464 profile_host_path = os.path.join(profile_target, 'raw-trace') 465 subprocess.check_call(['adb', 'pull', profile_device_path, profile_host_path], 466 stdout=NULL) 467 if uuid_trace: 468 subprocess.check_call(['adb', 'shell', 'rm', profile_device_path], 469 stdout=NULL) 470 471 if trace_to_text_binary is None: 472 print('Wrote profile to {}'.format(profile_host_path)) 473 print( 474 'This file can be opened using the Perfetto UI, https://ui.perfetto.dev' 475 ) 476 return 0 477 478 binary_path = os.getenv('PERFETTO_BINARY_PATH') 479 if not args.no_android_tree_symbolization: 480 product_out = os.getenv('ANDROID_PRODUCT_OUT') 481 if product_out: 482 product_out_symbols = product_out + '/symbols' 483 else: 484 product_out_symbols = None 485 486 if binary_path is None: 487 binary_path = product_out_symbols 488 elif product_out_symbols is not None: 489 binary_path += ":" + product_out_symbols 490 491 trace_file = os.path.join(profile_target, 'raw-trace') 492 concat_files = [trace_file] 493 494 if binary_path is not None: 495 with open(os.path.join(profile_target, 'symbols'), 'w') as fd: 496 ret = subprocess.call([ 497 trace_to_text_binary, 'symbolize', 498 os.path.join(profile_target, 'raw-trace') 499 ], 500 env=dict( 501 os.environ, PERFETTO_BINARY_PATH=binary_path), 502 stdout=fd) 503 if ret == 0: 504 concat_files.append(os.path.join(profile_target, 'symbols')) 505 else: 506 print("Failed to symbolize. Continuing without symbols.", file=sys.stderr) 507 508 proguard_map = os.getenv('PERFETTO_PROGUARD_MAP') 509 if proguard_map is not None: 510 with open(os.path.join(profile_target, 'deobfuscation-packets'), 'w') as fd: 511 ret = subprocess.call([ 512 trace_to_text_binary, 'deobfuscate', 513 os.path.join(profile_target, 'raw-trace') 514 ], 515 env=dict( 516 os.environ, PERFETTO_PROGUARD_MAP=proguard_map), 517 stdout=fd) 518 if ret == 0: 519 concat_files.append(os.path.join(profile_target, 'deobfuscation-packets')) 520 else: 521 print( 522 "Failed to deobfuscate. Continuing without deobfuscated.", 523 file=sys.stderr) 524 525 if len(concat_files) > 1: 526 with open(os.path.join(profile_target, 'symbolized-trace'), 'wb') as out: 527 for fn in concat_files: 528 with open(fn, 'rb') as inp: 529 while True: 530 buf = inp.read(4096) 531 if not buf: 532 break 533 out.write(buf) 534 trace_file = os.path.join(profile_target, 'symbolized-trace') 535 536 trace_to_text_output = subprocess.check_output( 537 [trace_to_text_binary, 'profile', trace_file]) 538 profile_path = None 539 for word in trace_to_text_output.decode('utf-8').split(): 540 if 'heap_profile-' in word: 541 profile_path = word 542 if profile_path is None: 543 print_no_profile_error() 544 return 1 545 546 profile_files = os.listdir(profile_path) 547 if not profile_files: 548 print_no_profile_error() 549 return 1 550 551 for profile_file in profile_files: 552 shutil.copy(os.path.join(profile_path, profile_file), profile_target) 553 554 symlink_path = None 555 if not sys.platform.startswith('win'): 556 subprocess.check_call( 557 ['gzip'] + [os.path.join(profile_target, x) for x in profile_files]) 558 if args.output is None: 559 symlink_path = os.path.join( 560 os.path.dirname(profile_target), "heap_profile-latest") 561 if os.path.lexists(symlink_path): 562 os.unlink(symlink_path) 563 os.symlink(profile_target, symlink_path) 564 565 if symlink_path is not None: 566 print("Wrote profiles to {} (symlink {})".format(profile_target, 567 symlink_path)) 568 else: 569 print("Wrote profiles to {}".format(profile_target)) 570 571 print("The raw-trace file can be viewed using https://ui.perfetto.dev.") 572 print("The heap_dump.* files can be viewed using pprof/ (Googlers only) " + 573 "or https://www.speedscope.app/.") 574 print("The two above are equivalent. The raw-trace contains the union of " + 575 "all the heap dumps.") 576 577 578# BEGIN_SECTION_GENERATED_BY(roll-prebuilts) 579# Revision: v25.0 580PERFETTO_PREBUILT_MANIFEST = [{ 581 'tool': 582 'trace_to_text', 583 'arch': 584 'mac-amd64', 585 'file_name': 586 'trace_to_text', 587 'file_size': 588 6525752, 589 'url': 590 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v25.0/mac-amd64/trace_to_text', 591 'sha256': 592 '64ccf6bac87825145691c6533412e514891f82300d68ff7ce69e8d2ca69aaf62', 593 'platform': 594 'darwin', 595 'machine': ['x86_64'] 596}, { 597 'tool': 598 'trace_to_text', 599 'arch': 600 'mac-arm64', 601 'file_name': 602 'trace_to_text', 603 'file_size': 604 5661424, 605 'url': 606 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v25.0/mac-arm64/trace_to_text', 607 'sha256': 608 'fbb6d256c96cdc296f2faec2965d16a1527c4900e66a33aca979ff1c336a9f2f', 609 'platform': 610 'darwin', 611 'machine': ['arm64'] 612}, { 613 'tool': 614 'trace_to_text', 615 'arch': 616 'windows-amd64', 617 'file_name': 618 'trace_to_text.exe', 619 'file_size': 620 5925888, 621 'url': 622 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v25.0/windows-amd64/trace_to_text.exe', 623 'sha256': 624 '29e50ec4d8e28c7c322ba13273afcce80c63fe7d9f182b83af0e2077b4d2b952', 625 'platform': 626 'win32', 627 'machine': ['amd64'] 628}, { 629 'tool': 630 'trace_to_text', 631 'arch': 632 'linux-amd64', 633 'file_name': 634 'trace_to_text', 635 'file_size': 636 6939560, 637 'url': 638 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v25.0/linux-amd64/trace_to_text', 639 'sha256': 640 '109f4ff3bbd47633b0c08a338f1230e69d529ddf1584656ed45d8a59acaaabeb', 641 'platform': 642 'linux', 643 'machine': ['x86_64'] 644}] 645 646 647# DO NOT EDIT. If you wish to make edits to this code, you need to change only 648# //tools/get_perfetto_prebuilt.py and run /tools/roll-prebuilts to regenerate 649# all the others scripts this is embedded into. 650def get_perfetto_prebuilt(tool_name, soft_fail=False, arch=None): 651 """ Downloads the prebuilt, if necessary, and returns its path on disk. """ 652 653 # The first time this is invoked, it downloads the |url| and caches it into 654 # ~/.perfetto/prebuilts/$tool_name. On subsequent invocations it just runs the 655 # cached version. 656 def download_or_get_cached(file_name, url, sha256): 657 import os, hashlib, subprocess 658 dir = os.path.join( 659 os.path.expanduser('~'), '.local', 'share', 'perfetto', 'prebuilts') 660 os.makedirs(dir, exist_ok=True) 661 bin_path = os.path.join(dir, file_name) 662 sha256_path = os.path.join(dir, file_name + '.sha256') 663 needs_download = True 664 665 # Avoid recomputing the SHA-256 on each invocation. The SHA-256 of the last 666 # download is cached into file_name.sha256, just check if that matches. 667 if os.path.exists(bin_path) and os.path.exists(sha256_path): 668 with open(sha256_path, 'rb') as f: 669 digest = f.read().decode() 670 if digest == sha256: 671 needs_download = False 672 673 if needs_download: 674 # Either the filed doesn't exist or the SHA256 doesn't match. 675 tmp_path = bin_path + '.tmp' 676 print('Downloading ' + url) 677 subprocess.check_call(['curl', '-f', '-L', '-#', '-o', tmp_path, url]) 678 with open(tmp_path, 'rb') as fd: 679 actual_sha256 = hashlib.sha256(fd.read()).hexdigest() 680 if actual_sha256 != sha256: 681 raise Exception('Checksum mismatch for %s (actual: %s, expected: %s)' % 682 (url, actual_sha256, sha256)) 683 os.chmod(tmp_path, 0o755) 684 os.rename(tmp_path, bin_path) 685 with open(sha256_path, 'w') as f: 686 f.write(sha256) 687 return bin_path 688 # --- end of download_or_get_cached() --- 689 690 # --- get_perfetto_prebuilt() function starts here. --- 691 import os, platform, sys 692 plat = sys.platform.lower() 693 machine = platform.machine().lower() 694 manifest_entry = None 695 for entry in PERFETTO_PREBUILT_MANIFEST: 696 # If the caller overrides the arch, just match that (for Android prebuilts). 697 if arch and entry.get('arch') == arch: 698 manifest_entry = entry 699 break 700 # Otherwise guess the local machine arch. 701 if entry.get('tool') == tool_name and entry.get( 702 'platform') == plat and machine in entry.get('machine', []): 703 manifest_entry = entry 704 break 705 if manifest_entry is None: 706 if soft_fail: 707 return None 708 raise Exception( 709 ('No prebuilts available for %s-%s\n' % (plat, machine)) + 710 'See https://perfetto.dev/docs/contributing/build-instructions') 711 712 return download_or_get_cached( 713 file_name=manifest_entry['file_name'], 714 url=manifest_entry['url'], 715 sha256=manifest_entry['sha256']) 716 717 718# END_SECTION_GENERATED_BY(roll-prebuilts) 719 720if __name__ == '__main__': 721 sys.exit(main(sys.argv)) 722