569 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
			
		
		
	
	
			569 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
| #!/usr/bin/env python3
 | |
| 
 | |
| # Copyright (C) 2022 The Android Open Source Project
 | |
| #
 | |
| # Licensed under the Apache License, Version 2.0 (the "License");
 | |
| # you may not use this file except in compliance with the License.
 | |
| # You may obtain a copy of the License at
 | |
| #
 | |
| #      http://www.apache.org/licenses/LICENSE-2.0
 | |
| #
 | |
| # Unless required by applicable law or agreed to in writing, software
 | |
| # distributed under the License is distributed on an "AS IS" BASIS,
 | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
| # See the License for the specific language governing permissions and
 | |
| # limitations under the License.
 | |
| """Runs tracing with CPU profiling enabled, and symbolizes traces if requested.
 | |
| 
 | |
| For usage instructions, please see:
 | |
| https://perfetto.dev/docs/quickstart/callstack-sampling
 | |
| 
 | |
| Adapted in large part from `heap_profile`.
 | |
| """
 | |
| 
 | |
| import argparse
 | |
| import os
 | |
| import shutil
 | |
| import signal
 | |
| import subprocess
 | |
| import sys
 | |
| import tempfile
 | |
| import time
 | |
| import uuid
 | |
| 
 | |
| # Used for creating directories, etc.
 | |
| UUID = str(uuid.uuid4())[-6:]
 | |
| 
 | |
| # See `sigint_handler` below.
 | |
| IS_INTERRUPTED = False
 | |
| 
 | |
| 
 | |
| def sigint_handler(signal, frame):
 | |
|   """Useful for cleanly interrupting tracing."""
 | |
|   global IS_INTERRUPTED
 | |
|   IS_INTERRUPTED = True
 | |
| 
 | |
| 
 | |
| def exit_with_no_profile():
 | |
|   sys.exit("No profiles generated.")
 | |
| 
 | |
| 
 | |
| def exit_with_bug_report(error):
 | |
|   sys.exit(
 | |
|       "{}\n\n If this is unexpected, please consider filing a bug at: \n"
 | |
|       "https://perfetto.dev/docs/contributing/getting-started#bugs.".format(
 | |
|           error))
 | |
| 
 | |
| 
 | |
| def adb_check_output(command):
 | |
|   """Runs an `adb` command and returns its output."""
 | |
|   try:
 | |
|     return subprocess.check_output(command).decode('utf-8')
 | |
|   except FileNotFoundError:
 | |
|     sys.exit("`adb` not found: Is it installed or on PATH?")
 | |
|   except subprocess.CalledProcessError as error:
 | |
|     sys.exit("`adb` error: Are any (or multiple) devices connected?\n"
 | |
|              "If multiple devices are connected, please select one by "
 | |
|              "setting `ANDROID_SERIAL=device_id`.\n"
 | |
|              "{}".format(error))
 | |
|   except Exception as error:
 | |
|     exit_with_bug_report(error)
 | |
| 
 | |
| 
 | |
| def parse_and_validate_args():
 | |
|   """Parses, validates, and returns command-line arguments for this script."""
 | |
|   DESCRIPTION = """Runs tracing with CPU profiling enabled, and symbolizes
 | |
|   traces if requested.
 | |
| 
 | |
|   For usage instructions, please see:
 | |
|   https://perfetto.dev/docs/quickstart/cpu-profiling
 | |
|   """
 | |
|   parser = argparse.ArgumentParser(description=DESCRIPTION)
 | |
|   parser.add_argument(
 | |
|       "-f",
 | |
|       "--frequency",
 | |
|       help="Sampling frequency (Hz). "
 | |
|       "Default: 100 Hz.",
 | |
|       metavar="FREQUENCY",
 | |
|       type=int,
 | |
|       default=100)
 | |
|   parser.add_argument(
 | |
|       "-d",
 | |
|       "--duration",
 | |
|       help="Duration of profile (ms). 0 to run until interrupted. "
 | |
|       "Default: until interrupted by user.",
 | |
|       metavar="DURATION",
 | |
|       type=int,
 | |
|       default=0)
 | |
|   parser.add_argument(
 | |
|       "-n",
 | |
|       "--name",
 | |
|       help="Comma-separated list of names of processes to be profiled.",
 | |
|       metavar="NAMES",
 | |
|       default=None)
 | |
|   parser.add_argument(
 | |
|       "-p",
 | |
|       "--partial-matching",
 | |
|       help="If set, enables \"partial matching\" on the strings in --names/-n."
 | |
|       "Processes that are already running when profiling is started, and whose "
 | |
|       "names include any of the values in --names/-n as substrings will be profiled.",
 | |
|       action="store_true")
 | |
|   parser.add_argument(
 | |
|       "-c",
 | |
|       "--config",
 | |
|       help="A custom configuration file, if any, to be used for profiling. "
 | |
|       "If provided, --frequency/-f, --duration/-d, and --name/-n are not used.",
 | |
|       metavar="CONFIG",
 | |
|       default=None)
 | |
|   parser.add_argument(
 | |
|       "-o",
 | |
|       "--output",
 | |
|       help="Output directory for recorded trace.",
 | |
|       metavar="DIRECTORY",
 | |
|       default=None)
 | |
| 
 | |
|   args = parser.parse_args()
 | |
|   if args.config is not None and args.name is not None:
 | |
|     sys.exit("--name/-n should not be provided when --config/-c is provided.")
 | |
|   elif args.config is None and args.name is None:
 | |
|     sys.exit("One of --names/-n or --config/-c is required.")
 | |
| 
 | |
|   return args
 | |
| 
 | |
| 
 | |
| def get_matching_processes(args, names_to_match):
 | |
|   """Returns a list of currently-running processes whose names match `names_to_match`.
 | |
| 
 | |
|   Args:
 | |
|     args: The command-line arguments provided to this script.
 | |
|     names_to_match: The list of process names provided by the user.
 | |
|   """
 | |
|   # Returns names as they are.
 | |
|   if not args.partial_matching:
 | |
|     return names_to_match
 | |
| 
 | |
|   # Attempt to match names to names of currently running processes.
 | |
|   PS_PROCESS_OFFSET = 8
 | |
|   matching_processes = []
 | |
|   for line in adb_check_output(['adb', 'shell', 'ps', '-A']).splitlines():
 | |
|     line_split = line.split()
 | |
|     if len(line_split) <= PS_PROCESS_OFFSET:
 | |
|       continue
 | |
|     process = line_split[PS_PROCESS_OFFSET]
 | |
|     for name in names_to_match:
 | |
|       if name in process:
 | |
|         matching_processes.append(process)
 | |
|         break
 | |
| 
 | |
|   return matching_processes
 | |
| 
 | |
| 
 | |
| def get_perfetto_config(args):
 | |
|   """Returns a Perfetto config with CPU profiling enabled for the selected processes.
 | |
| 
 | |
|   Args:
 | |
|     args: The command-line arguments provided to this script.
 | |
|   """
 | |
|   if args.config is not None:
 | |
|     try:
 | |
|       with open(args.config, 'r') as config_file:
 | |
|         return config_file.read()
 | |
|     except IOError as error:
 | |
|       sys.exit("Unable to read config file: {}".format(error))
 | |
| 
 | |
|   CONFIG_INDENT = '      '
 | |
|   CONFIG = '''buffers {{
 | |
|     size_kb: 2048
 | |
|   }}
 | |
| 
 | |
|   buffers {{
 | |
|     size_kb: 63488
 | |
|   }}
 | |
| 
 | |
|   data_sources {{
 | |
|     config {{
 | |
|       name: "linux.process_stats"
 | |
|       target_buffer: 0
 | |
|       process_stats_config {{
 | |
|         proc_stats_poll_ms: 100
 | |
|       }}
 | |
|     }}
 | |
|   }}
 | |
| 
 | |
|   data_sources {{
 | |
|     config {{
 | |
|       name: "linux.perf"
 | |
|       target_buffer: 1
 | |
|       perf_event_config {{
 | |
|         all_cpus: true
 | |
|         sampling_frequency: {frequency}
 | |
| {target_config}
 | |
|       }}
 | |
|     }}
 | |
|   }}
 | |
| 
 | |
|   duration_ms: {duration}
 | |
|   write_into_file: true
 | |
|   flush_timeout_ms: 30000
 | |
|   flush_period_ms: 604800000
 | |
|   '''
 | |
| 
 | |
|   target_config = ""
 | |
|   matching_processes = []
 | |
|   if args.name is not None:
 | |
|     names_to_match = [name.strip() for name in args.name.split(',')]
 | |
|     matching_processes = get_matching_processes(args, names_to_match)
 | |
| 
 | |
|   if not matching_processes:
 | |
|     sys.exit("No running processes matched for profiling.")
 | |
| 
 | |
|   for process in matching_processes:
 | |
|     target_config += CONFIG_INDENT + 'target_cmdline: "{}"\n'.format(process)
 | |
| 
 | |
|   print("Configured profiling for these processes:\n")
 | |
|   for matching_process in matching_processes:
 | |
|     print(matching_process)
 | |
|   print()
 | |
| 
 | |
|   config = CONFIG.format(
 | |
|       frequency=args.frequency,
 | |
|       duration=args.duration,
 | |
|       target_config=target_config)
 | |
| 
 | |
|   return config
 | |
| 
 | |
| 
 | |
| def release_or_newer(release):
 | |
|   """Returns whether a new enough Android release is being used."""
 | |
|   SDK = {'R': 30}
 | |
|   sdk = int(
 | |
|       adb_check_output(
 | |
|           ['adb', 'shell', 'getprop', 'ro.system.build.version.sdk']).strip())
 | |
|   if sdk >= SDK[release]:
 | |
|     return True
 | |
| 
 | |
|   codename = adb_check_output(
 | |
|       ['adb', 'shell', 'getprop', 'ro.build.version.codename']).strip()
 | |
|   return codename == release
 | |
| 
 | |
| 
 | |
| def get_and_prepare_profile_target(args):
 | |
|   """Returns the target where the trace/profile will be output. Creates a new directory if necessary.
 | |
| 
 | |
|   Args:
 | |
|     args: The command-line arguments provided to this script.
 | |
|   """
 | |
|   profile_target = os.path.join(tempfile.gettempdir(), UUID)
 | |
|   if args.output is not None:
 | |
|     profile_target = args.output
 | |
|   else:
 | |
|     os.makedirs(profile_target, exist_ok=True)
 | |
|   if not os.path.isdir(profile_target):
 | |
|     sys.exit("Output directory {} not found.".format(profile_target))
 | |
|   if os.listdir(profile_target):
 | |
|     sys.exit("Output directory {} not empty.".format(profile_target))
 | |
| 
 | |
|   return profile_target
 | |
| 
 | |
| 
 | |
| def record_trace(config, profile_target):
 | |
|   """Runs Perfetto with the provided configuration to record a trace.
 | |
| 
 | |
|   Args:
 | |
|     config: The Perfetto config to be used for tracing/profiling.
 | |
|     profile_target: The directory where the recorded trace is output.
 | |
|   """
 | |
|   NULL = open(os.devnull)
 | |
|   NO_OUT = {
 | |
|       'stdout': NULL,
 | |
|       'stderr': NULL,
 | |
|   }
 | |
|   if not release_or_newer('R'):
 | |
|     sys.exit("This tool requires Android R+ to run.")
 | |
|   profile_device_path = '/data/misc/perfetto-traces/profile-' + UUID
 | |
|   perfetto_command = ('CONFIG=\'{}\'; echo ${{CONFIG}} | '
 | |
|                       'perfetto --txt -c - -o {} -d')
 | |
|   try:
 | |
|     perfetto_pid = int(
 | |
|         adb_check_output([
 | |
|             'adb', 'exec-out',
 | |
|             perfetto_command.format(config, profile_device_path)
 | |
|         ]).strip())
 | |
|   except ValueError as error:
 | |
|     sys.exit("Unable to start profiling: {}".format(error))
 | |
| 
 | |
|   print("Profiling active. Press Ctrl+C to terminate.")
 | |
| 
 | |
|   old_handler = signal.signal(signal.SIGINT, sigint_handler)
 | |
| 
 | |
|   perfetto_alive = True
 | |
|   while perfetto_alive and not IS_INTERRUPTED:
 | |
|     perfetto_alive = subprocess.call(
 | |
|         ['adb', 'shell', '[ -d /proc/{} ]'.format(perfetto_pid)], **NO_OUT) == 0
 | |
|     time.sleep(0.25)
 | |
| 
 | |
|   print("Finishing profiling and symbolization...")
 | |
| 
 | |
|   if IS_INTERRUPTED:
 | |
|     adb_check_output(['adb', 'shell', 'kill', '-INT', str(perfetto_pid)])
 | |
| 
 | |
|   # Restore old handler.
 | |
|   signal.signal(signal.SIGINT, old_handler)
 | |
| 
 | |
|   while perfetto_alive:
 | |
|     perfetto_alive = subprocess.call(
 | |
|         ['adb', 'shell', '[ -d /proc/{} ]'.format(perfetto_pid)]) == 0
 | |
|     time.sleep(0.25)
 | |
| 
 | |
|   profile_host_path = os.path.join(profile_target, 'raw-trace')
 | |
|   adb_check_output(['adb', 'pull', profile_device_path, profile_host_path])
 | |
|   adb_check_output(['adb', 'shell', 'rm', profile_device_path])
 | |
| 
 | |
| 
 | |
| def get_trace_to_text():
 | |
|   """Sets up and returns the path to `trace_to_text`."""
 | |
|   try:
 | |
|     trace_to_text = get_perfetto_prebuilt('trace_to_text', soft_fail=True)
 | |
|   except Exception as error:
 | |
|     exit_with_bug_report(error)
 | |
|   if trace_to_text is None:
 | |
|     exit_with_bug_report(
 | |
|         "Unable to download `trace_to_text` for symbolizing profiles.")
 | |
| 
 | |
|   return trace_to_text
 | |
| 
 | |
| 
 | |
| def concatenate_files(files_to_concatenate, output_file):
 | |
|   """Concatenates files.
 | |
| 
 | |
|   Args:
 | |
|     files_to_concatenate: Paths for input files to concatenate.
 | |
|     output_file: Path to the resultant output file.
 | |
|   """
 | |
|   with open(output_file, 'wb') as output:
 | |
|     for file in files_to_concatenate:
 | |
|       with open(file, 'rb') as input:
 | |
|         shutil.copyfileobj(input, output)
 | |
| 
 | |
| 
 | |
| def symbolize_trace(trace_to_text, profile_target):
 | |
|   """Attempts symbolization of the recorded trace/profile, if symbols are available.
 | |
| 
 | |
|   Args:
 | |
|     trace_to_text: The path to the `trace_to_text` binary used for symbolization.
 | |
|     profile_target: The directory where the recorded trace was output.
 | |
| 
 | |
|   Returns:
 | |
|     The path to the symbolized trace file if symbolization was completed,
 | |
|     and the original trace file, if it was not.
 | |
|   """
 | |
|   binary_path = os.getenv('PERFETTO_BINARY_PATH')
 | |
|   trace_file = os.path.join(profile_target, 'raw-trace')
 | |
|   files_to_concatenate = [trace_file]
 | |
| 
 | |
|   if binary_path is not None:
 | |
|     try:
 | |
|       with open(os.path.join(profile_target, 'symbols'), 'w') as symbols_file:
 | |
|         return_code = subprocess.call([trace_to_text, 'symbolize', trace_file],
 | |
|                                       env=dict(
 | |
|                                           os.environ,
 | |
|                                           PERFETTO_BINARY_PATH=binary_path),
 | |
|                                       stdout=symbols_file)
 | |
|     except IOError as error:
 | |
|       sys.exit("Unable to write symbols to disk: {}".format(error))
 | |
|     if return_code == 0:
 | |
|       files_to_concatenate.append(os.path.join(profile_target, 'symbols'))
 | |
|     else:
 | |
|       print("Failed to symbolize. Continuing without symbols.", file=sys.stderr)
 | |
| 
 | |
|   if len(files_to_concatenate) > 1:
 | |
|     trace_file = os.path.join(profile_target, 'symbolized-trace')
 | |
|     try:
 | |
|       concatenate_files(files_to_concatenate, trace_file)
 | |
|     except Exception as error:
 | |
|       sys.exit("Unable to write symbolized profile to disk: {}".format(error))
 | |
| 
 | |
|   return trace_file
 | |
| 
 | |
| 
 | |
| def generate_pprof_profiles(trace_to_text, trace_file):
 | |
|   """Generates pprof profiles from the recorded trace.
 | |
| 
 | |
|   Args:
 | |
|     trace_to_text: The path to the `trace_to_text` binary used for generating profiles.
 | |
|     trace_file: The oath to the recorded and potentially symbolized trace file.
 | |
| 
 | |
|   Returns:
 | |
|     The directory where pprof profiles are output.
 | |
|   """
 | |
|   try:
 | |
|     trace_to_text_output = subprocess.check_output(
 | |
|         [trace_to_text, 'profile', '--perf', trace_file])
 | |
|   except Exception as error:
 | |
|     exit_with_bug_report(
 | |
|         "Unable to extract profiles from trace: {}".format(error))
 | |
| 
 | |
|   profiles_output_directory = None
 | |
|   for word in trace_to_text_output.decode('utf-8').split():
 | |
|     if 'perf_profile-' in word:
 | |
|       profiles_output_directory = word
 | |
|   if profiles_output_directory is None:
 | |
|     exit_with_no_profile()
 | |
|   return profiles_output_directory
 | |
| 
 | |
| 
 | |
| def copy_profiles_to_destination(profile_target, profile_path):
 | |
|   """Copies recorded profiles to `profile_target` from `profile_path`."""
 | |
|   profile_files = os.listdir(profile_path)
 | |
|   if not profile_files:
 | |
|     exit_with_no_profile()
 | |
| 
 | |
|   try:
 | |
|     for profile_file in profile_files:
 | |
|       shutil.copy(os.path.join(profile_path, profile_file), profile_target)
 | |
|   except Exception as error:
 | |
|     sys.exit("Unable to copy profiles to {}: {}".format(profile_target, error))
 | |
| 
 | |
|   print("Wrote profiles to {}".format(profile_target))
 | |
| 
 | |
| 
 | |
| def main(argv):
 | |
|   args = parse_and_validate_args()
 | |
|   profile_target = get_and_prepare_profile_target(args)
 | |
|   record_trace(get_perfetto_config(args), profile_target)
 | |
|   trace_to_text = get_trace_to_text()
 | |
|   trace_file = symbolize_trace(trace_to_text, profile_target)
 | |
|   copy_profiles_to_destination(
 | |
|       profile_target, generate_pprof_profiles(trace_to_text, trace_file))
 | |
|   return 0
 | |
| 
 | |
| 
 | |
| # BEGIN_SECTION_GENERATED_BY(roll-prebuilts)
 | |
| # Revision: v25.0
 | |
| PERFETTO_PREBUILT_MANIFEST = [{
 | |
|     'tool':
 | |
|         'trace_to_text',
 | |
|     'arch':
 | |
|         'mac-amd64',
 | |
|     'file_name':
 | |
|         'trace_to_text',
 | |
|     'file_size':
 | |
|         6525752,
 | |
|     'url':
 | |
|         'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v25.0/mac-amd64/trace_to_text',
 | |
|     'sha256':
 | |
|         '64ccf6bac87825145691c6533412e514891f82300d68ff7ce69e8d2ca69aaf62',
 | |
|     'platform':
 | |
|         'darwin',
 | |
|     'machine': ['x86_64']
 | |
| }, {
 | |
|     'tool':
 | |
|         'trace_to_text',
 | |
|     'arch':
 | |
|         'windows-amd64',
 | |
|     'file_name':
 | |
|         'trace_to_text.exe',
 | |
|     'file_size':
 | |
|         5925888,
 | |
|     'url':
 | |
|         'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v25.0/windows-amd64/trace_to_text.exe',
 | |
|     'sha256':
 | |
|         '29e50ec4d8e28c7c322ba13273afcce80c63fe7d9f182b83af0e2077b4d2b952',
 | |
|     'platform':
 | |
|         'win32',
 | |
|     'machine': ['amd64']
 | |
| }, {
 | |
|     'tool':
 | |
|         'trace_to_text',
 | |
|     'arch':
 | |
|         'linux-amd64',
 | |
|     'file_name':
 | |
|         'trace_to_text',
 | |
|     'file_size':
 | |
|         6939560,
 | |
|     'url':
 | |
|         'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v25.0/linux-amd64/trace_to_text',
 | |
|     'sha256':
 | |
|         '109f4ff3bbd47633b0c08a338f1230e69d529ddf1584656ed45d8a59acaaabeb',
 | |
|     'platform':
 | |
|         'linux',
 | |
|     'machine': ['x86_64']
 | |
| }]
 | |
| 
 | |
| 
 | |
| # DO NOT EDIT. If you wish to make edits to this code, you need to change only
 | |
| # //tools/get_perfetto_prebuilt.py and run /tools/roll-prebuilts to regenerate
 | |
| # all the others scripts this is embedded into.
 | |
| def get_perfetto_prebuilt(tool_name, soft_fail=False, arch=None):
 | |
|   """ Downloads the prebuilt, if necessary, and returns its path on disk. """
 | |
| 
 | |
|   # The first time this is invoked, it downloads the |url| and caches it into
 | |
|   # ~/.perfetto/prebuilts/$tool_name. On subsequent invocations it just runs the
 | |
|   # cached version.
 | |
|   def download_or_get_cached(file_name, url, sha256):
 | |
|     import os, hashlib, subprocess
 | |
|     dir = os.path.join(
 | |
|         os.path.expanduser('~'), '.local', 'share', 'perfetto', 'prebuilts')
 | |
|     os.makedirs(dir, exist_ok=True)
 | |
|     bin_path = os.path.join(dir, file_name)
 | |
|     sha256_path = os.path.join(dir, file_name + '.sha256')
 | |
|     needs_download = True
 | |
| 
 | |
|     # Avoid recomputing the SHA-256 on each invocation. The SHA-256 of the last
 | |
|     # download is cached into file_name.sha256, just check if that matches.
 | |
|     if os.path.exists(bin_path) and os.path.exists(sha256_path):
 | |
|       with open(sha256_path, 'rb') as f:
 | |
|         digest = f.read().decode()
 | |
|         if digest == sha256:
 | |
|           needs_download = False
 | |
| 
 | |
|     if needs_download:
 | |
|       # Either the filed doesn't exist or the SHA256 doesn't match.
 | |
|       tmp_path = bin_path + '.tmp'
 | |
|       print('Downloading ' + url)
 | |
|       subprocess.check_call(['curl', '-f', '-L', '-#', '-o', tmp_path, url])
 | |
|       with open(tmp_path, 'rb') as fd:
 | |
|         actual_sha256 = hashlib.sha256(fd.read()).hexdigest()
 | |
|       if actual_sha256 != sha256:
 | |
|         raise Exception('Checksum mismatch for %s (actual: %s, expected: %s)' %
 | |
|                         (url, actual_sha256, sha256))
 | |
|       os.chmod(tmp_path, 0o755)
 | |
|       os.rename(tmp_path, bin_path)
 | |
|       with open(sha256_path, 'w') as f:
 | |
|         f.write(sha256)
 | |
|     return bin_path
 | |
|     # --- end of download_or_get_cached() ---
 | |
| 
 | |
|   # --- get_perfetto_prebuilt() function starts here. ---
 | |
|   import os, platform, sys
 | |
|   plat = sys.platform.lower()
 | |
|   machine = platform.machine().lower()
 | |
|   manifest_entry = None
 | |
|   for entry in PERFETTO_PREBUILT_MANIFEST:
 | |
|     # If the caller overrides the arch, just match that (for Android prebuilts).
 | |
|     if arch and entry.get('arch') == arch:
 | |
|       manifest_entry = entry
 | |
|       break
 | |
|     # Otherwise guess the local machine arch.
 | |
|     if entry.get('tool') == tool_name and entry.get(
 | |
|         'platform') == plat and machine in entry.get('machine', []):
 | |
|       manifest_entry = entry
 | |
|       break
 | |
|   if manifest_entry is None:
 | |
|     if soft_fail:
 | |
|       return None
 | |
|     raise Exception(
 | |
|         ('No prebuilts available for %s-%s\n' % (plat, machine)) +
 | |
|         'See https://perfetto.dev/docs/contributing/build-instructions')
 | |
| 
 | |
|   return download_or_get_cached(
 | |
|       file_name=manifest_entry['file_name'],
 | |
|       url=manifest_entry['url'],
 | |
|       sha256=manifest_entry['sha256'])
 | |
| 
 | |
| 
 | |
| # END_SECTION_GENERATED_BY(roll-prebuilts)
 | |
| 
 | |
| if __name__ == '__main__':
 | |
|   sys.exit(main(sys.argv))
 |