538 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
			
		
		
	
	
			538 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
#!/usr/bin/env python3
 | 
						|
# Copyright (C) 2021 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.
 | 
						|
 | 
						|
import atexit
 | 
						|
import argparse
 | 
						|
import datetime
 | 
						|
import hashlib
 | 
						|
import http.server
 | 
						|
import os
 | 
						|
import re
 | 
						|
import shutil
 | 
						|
import socketserver
 | 
						|
import subprocess
 | 
						|
import sys
 | 
						|
import tempfile
 | 
						|
import time
 | 
						|
import webbrowser
 | 
						|
 | 
						|
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 | 
						|
 | 
						|
# This is not required. It's only used as a fallback if no adb is found on the
 | 
						|
# PATH. It's fine if it doesn't exist so this script can be copied elsewhere.
 | 
						|
HERMETIC_ADB_PATH = ROOT_DIR + '/buildtools/android_sdk/platform-tools/adb'
 | 
						|
 | 
						|
# Translates the Android ro.product.cpu.abi into the GN's target_cpu.
 | 
						|
ABI_TO_ARCH = {
 | 
						|
    'armeabi-v7a': 'arm',
 | 
						|
    'arm64-v8a': 'arm64',
 | 
						|
    'x86': 'x86',
 | 
						|
    'x86_64': 'x64',
 | 
						|
}
 | 
						|
 | 
						|
MAX_ADB_FAILURES = 15  # 2 seconds between retries, 30 seconds total.
 | 
						|
 | 
						|
devnull = open(os.devnull, 'rb')
 | 
						|
adb_path = None
 | 
						|
procs = []
 | 
						|
 | 
						|
 | 
						|
class ANSI:
 | 
						|
  END = '\033[0m'
 | 
						|
  BOLD = '\033[1m'
 | 
						|
  RED = '\033[91m'
 | 
						|
  BLACK = '\033[30m'
 | 
						|
  BLUE = '\033[94m'
 | 
						|
  BG_YELLOW = '\033[43m'
 | 
						|
  BG_BLUE = '\033[44m'
 | 
						|
 | 
						|
 | 
						|
# HTTP Server used to open the trace in the browser.
 | 
						|
class HttpHandler(http.server.SimpleHTTPRequestHandler):
 | 
						|
 | 
						|
  def end_headers(self):
 | 
						|
    self.send_header('Access-Control-Allow-Origin', '*')
 | 
						|
    return super().end_headers()
 | 
						|
 | 
						|
  def do_GET(self):
 | 
						|
    self.server.last_request = self.path
 | 
						|
    return super().do_GET()
 | 
						|
 | 
						|
  def do_POST(self):
 | 
						|
    self.send_error(404, "File not found")
 | 
						|
 | 
						|
 | 
						|
def main():
 | 
						|
  atexit.register(kill_all_subprocs_on_exit)
 | 
						|
  default_out_dir_str = '~/traces/'
 | 
						|
  default_out_dir = os.path.expanduser(default_out_dir_str)
 | 
						|
 | 
						|
  examples = '\n'.join([
 | 
						|
      ANSI.BOLD + 'Examples' + ANSI.END, '  -t 10s -b 32mb sched gfx wm -a*',
 | 
						|
      '  -t 5s sched/sched_switch raw_syscalls/sys_enter raw_syscalls/sys_exit',
 | 
						|
      '  -c /path/to/full-textual-trace.config', '',
 | 
						|
      ANSI.BOLD + 'Long traces' + ANSI.END,
 | 
						|
      'If you want to record a hours long trace and stream it into a file ',
 | 
						|
      'you need to pass a full trace config and set write_into_file = true.',
 | 
						|
      'See https://perfetto.dev/docs/concepts/config#long-traces .'
 | 
						|
  ])
 | 
						|
  parser = argparse.ArgumentParser(
 | 
						|
      epilog=examples, formatter_class=argparse.RawTextHelpFormatter)
 | 
						|
 | 
						|
  help = 'Output file or directory (default: %s)' % default_out_dir_str
 | 
						|
  parser.add_argument('-o', '--out', default=default_out_dir, help=help)
 | 
						|
 | 
						|
  help = 'Don\'t open in the browser'
 | 
						|
  parser.add_argument('-n', '--no-open', action='store_true', help=help)
 | 
						|
 | 
						|
  help = 'Force the use of the sideloaded binaries rather than system daemons'
 | 
						|
  parser.add_argument('--sideload', action='store_true', help=help)
 | 
						|
 | 
						|
  help = ('Sideload the given binary rather than downloading it. ' +
 | 
						|
          'Implies --sideload')
 | 
						|
  parser.add_argument('--sideload-path', default=None, help=help)
 | 
						|
 | 
						|
  help = 'Don\'t run `adb root` run as user (only when sideloading)'
 | 
						|
  parser.add_argument('-u', '--user', action='store_true', help=help)
 | 
						|
 | 
						|
  grp = parser.add_argument_group(
 | 
						|
      'Short options: (only when not using -c/--config)')
 | 
						|
 | 
						|
  help = 'Trace duration N[s,m,h] (default: trace until stopped)'
 | 
						|
  grp.add_argument('-t', '--time', default='0s', help=help)
 | 
						|
 | 
						|
  help = 'Ring buffer size N[mb,gb] (default: 32mb)'
 | 
						|
  grp.add_argument('-b', '--buffer', default='32mb', help=help)
 | 
						|
 | 
						|
  help = ('Android (atrace) app names. Can be specified multiple times.\n-a*' +
 | 
						|
          'for all apps (without space between a and * or bash will expand it)')
 | 
						|
  grp.add_argument(
 | 
						|
      '-a',
 | 
						|
      '--app',
 | 
						|
      metavar='com.myapp',
 | 
						|
      action='append',
 | 
						|
      default=[],
 | 
						|
      help=help)
 | 
						|
 | 
						|
  help = 'sched, gfx, am, wm (see --list)'
 | 
						|
  grp.add_argument('events', metavar='Atrace events', nargs='*', help=help)
 | 
						|
 | 
						|
  help = 'sched/sched_switch kmem/kmem (see --list-ftrace)'
 | 
						|
  grp.add_argument('_', metavar='Ftrace events', nargs='*', help=help)
 | 
						|
 | 
						|
  help = 'Lists all the categories available'
 | 
						|
  grp.add_argument('--list', action='store_true', help=help)
 | 
						|
 | 
						|
  help = 'Lists all the ftrace events available'
 | 
						|
  grp.add_argument('--list-ftrace', action='store_true', help=help)
 | 
						|
 | 
						|
  section = ('Full trace config (only when not using short options)')
 | 
						|
  grp = parser.add_argument_group(section)
 | 
						|
 | 
						|
  help = 'Can be generated with https://ui.perfetto.dev/#!/record'
 | 
						|
  grp.add_argument('-c', '--config', default=None, help=help)
 | 
						|
 | 
						|
  help = 'Specify the ADB device serial'
 | 
						|
  grp.add_argument('--serial', '-s', default=None, help=help)
 | 
						|
 | 
						|
  args = parser.parse_args()
 | 
						|
  args.sideload = args.sideload or args.sideload_path is not None
 | 
						|
 | 
						|
  if args.serial:
 | 
						|
    os.environ["ANDROID_SERIAL"] = args.serial
 | 
						|
 | 
						|
  find_adb()
 | 
						|
 | 
						|
  if args.list:
 | 
						|
    adb('shell', 'atrace', '--list_categories').wait()
 | 
						|
    sys.exit(0)
 | 
						|
 | 
						|
  if args.list_ftrace:
 | 
						|
    adb('shell', 'cat /d/tracing/available_events | tr : /').wait()
 | 
						|
    sys.exit(0)
 | 
						|
 | 
						|
  if args.config is not None and not os.path.exists(args.config):
 | 
						|
    prt('Config file not found: %s' % args.config, ANSI.RED)
 | 
						|
    sys.exit(1)
 | 
						|
 | 
						|
  if len(args.events) == 0 and args.config is None:
 | 
						|
    prt('Must either pass short options (e.g. -t 10s sched) or a --config file',
 | 
						|
        ANSI.RED)
 | 
						|
    parser.print_help()
 | 
						|
    sys.exit(1)
 | 
						|
 | 
						|
  if args.config is None and args.events and os.path.exists(args.events[0]):
 | 
						|
    prt(('The passed event name "%s" is a local file. ' % args.events[0] +
 | 
						|
         'Did you mean to pass -c / --config ?'), ANSI.RED)
 | 
						|
    sys.exit(1)
 | 
						|
 | 
						|
  perfetto_cmd = 'perfetto'
 | 
						|
  device_dir = '/data/misc/perfetto-traces/'
 | 
						|
 | 
						|
  # Check the version of android. If too old (< Q) sideload tracebox. Also use
 | 
						|
  # use /data/local/tmp as /data/misc/perfetto-traces was introduced only later.
 | 
						|
  probe_cmd = 'getprop ro.build.version.sdk; getprop ro.product.cpu.abi; whoami'
 | 
						|
  probe = adb('shell', probe_cmd, stdout=subprocess.PIPE)
 | 
						|
  lines = probe.communicate()[0].decode().strip().split('\n')
 | 
						|
  lines = [x.strip() for x in lines]  # To strip \r(s) on Windows.
 | 
						|
  if probe.returncode != 0:
 | 
						|
    prt('ADB connection failed', ANSI.RED)
 | 
						|
    sys.exit(1)
 | 
						|
  api_level = int(lines[0])
 | 
						|
  abi = lines[1]
 | 
						|
  arch = ABI_TO_ARCH.get(abi)
 | 
						|
  if arch is None:
 | 
						|
    prt('Unsupported ABI: ' + abi)
 | 
						|
    sys.exit(1)
 | 
						|
  shell_user = lines[2]
 | 
						|
  if api_level < 29 or args.sideload:  # 29: Android Q.
 | 
						|
    tracebox_bin = args.sideload_path
 | 
						|
    if tracebox_bin is None:
 | 
						|
      tracebox_bin = get_perfetto_prebuilt('tracebox', arch='android-' + arch)
 | 
						|
    perfetto_cmd = '/data/local/tmp/tracebox'
 | 
						|
    exit_code = adb('push', '--sync', tracebox_bin, perfetto_cmd).wait()
 | 
						|
    exit_code |= adb('shell', 'chmod 755 ' + perfetto_cmd).wait()
 | 
						|
    if exit_code != 0:
 | 
						|
      prt('ADB push failed', ANSI.RED)
 | 
						|
      sys.exit(1)
 | 
						|
    device_dir = '/data/local/tmp/'
 | 
						|
    if shell_user != 'root' and not args.user:
 | 
						|
      # Run as root if possible as that will give access to more tracing
 | 
						|
      # capabilities. Non-root still works, but some ftrace events might not be
 | 
						|
      # available.
 | 
						|
      adb('root').wait()
 | 
						|
 | 
						|
  tstamp = datetime.datetime.now().strftime('%Y-%m-%d_%H-%M')
 | 
						|
  fname = '%s-%s.pftrace' % (tstamp, os.urandom(3).hex())
 | 
						|
  device_file = device_dir + fname
 | 
						|
 | 
						|
  cmd = [perfetto_cmd, '--background', '--txt', '-o', device_file]
 | 
						|
  on_device_config = None
 | 
						|
  on_host_config = None
 | 
						|
  if args.config is not None:
 | 
						|
    cmd += ['-c', '-']
 | 
						|
    if api_level < 24:
 | 
						|
      # adb shell does not redirect stdin. Push the config on a temporary file
 | 
						|
      # on the device.
 | 
						|
      mktmp = adb(
 | 
						|
          'shell',
 | 
						|
          'mktemp',
 | 
						|
          '--tmpdir',
 | 
						|
          '/data/local/tmp',
 | 
						|
          stdout=subprocess.PIPE)
 | 
						|
      on_device_config = mktmp.communicate()[0].decode().strip().strip()
 | 
						|
      if mktmp.returncode != 0:
 | 
						|
        prt('Failed to create config on device', ANSI.RED)
 | 
						|
        sys.exit(1)
 | 
						|
      exit_code = adb('push', '--sync', args.config, on_device_config).wait()
 | 
						|
      if exit_code != 0:
 | 
						|
        prt('Failed to push config on device', ANSI.RED)
 | 
						|
        sys.exit(1)
 | 
						|
      cmd = ['cat', on_device_config, '|'] + cmd
 | 
						|
    else:
 | 
						|
      on_host_config = args.config
 | 
						|
  else:
 | 
						|
    cmd += ['-t', args.time, '-b', args.buffer]
 | 
						|
    for app in args.app:
 | 
						|
      cmd += ['--app', '\'' + app + '\'']
 | 
						|
    cmd += args.events
 | 
						|
 | 
						|
  # Perfetto will error out with a proper message if both a config file and
 | 
						|
  # short options are specified. No need to replicate that logic.
 | 
						|
 | 
						|
  # Work out the output file or directory.
 | 
						|
  if args.out.endswith('/') or os.path.isdir(args.out):
 | 
						|
    host_dir = args.out
 | 
						|
    host_file = os.path.join(args.out, fname)
 | 
						|
  else:
 | 
						|
    host_file = args.out
 | 
						|
    host_dir = os.path.dirname(host_file)
 | 
						|
    if host_dir == '':
 | 
						|
      host_dir = '.'
 | 
						|
      host_file = './' + host_file
 | 
						|
  if not os.path.exists(host_dir):
 | 
						|
    shutil.os.makedirs(host_dir)
 | 
						|
 | 
						|
  with open(on_host_config or os.devnull, 'rb') as f:
 | 
						|
    print('Running ' + ' '.join(cmd))
 | 
						|
    proc = adb('shell', *cmd, stdin=f, stdout=subprocess.PIPE)
 | 
						|
    proc_out = proc.communicate()[0].decode().strip()
 | 
						|
    if on_device_config is not None:
 | 
						|
      adb('shell', 'rm', on_device_config).wait()
 | 
						|
    # On older versions of Android (x86_64 emulator running API 22) the output
 | 
						|
    # looks like:
 | 
						|
    #   WARNING: linker: /data/local/tmp/tracebox: unused DT entry: ...
 | 
						|
    #   WARNING: ... (other 2 WARNING: linker: lines)
 | 
						|
    #   1234  <-- The actual pid we want.
 | 
						|
    match = re.search(r'^(\d+)$', proc_out, re.M)
 | 
						|
    if match is None:
 | 
						|
      prt('Failed to read the pid from perfetto --background', ANSI.RED)
 | 
						|
      prt(proc_out)
 | 
						|
      sys.exit(1)
 | 
						|
    bg_pid = match.group(1)
 | 
						|
    exit_code = proc.wait()
 | 
						|
 | 
						|
  if exit_code != 0:
 | 
						|
    prt('Perfetto invocation failed', ANSI.RED)
 | 
						|
    sys.exit(1)
 | 
						|
 | 
						|
  prt('Trace started. Press CTRL+C to stop', ANSI.BLACK + ANSI.BG_BLUE)
 | 
						|
  logcat = adb('logcat', '-v', 'brief', '-s', 'perfetto', '-b', 'main', '-T',
 | 
						|
               '1')
 | 
						|
 | 
						|
  ctrl_c_count = 0
 | 
						|
  adb_failure_count = 0
 | 
						|
  while ctrl_c_count < 2:
 | 
						|
    try:
 | 
						|
      # On older Android devices adbd doesn't propagate the exit code. Hence
 | 
						|
      # the RUN/TERM parts.
 | 
						|
      poll = adb(
 | 
						|
          'shell',
 | 
						|
          'test -d /proc/%s && echo RUN || echo TERM' % bg_pid,
 | 
						|
          stdout=subprocess.PIPE)
 | 
						|
      poll_res = poll.communicate()[0].decode().strip()
 | 
						|
      if poll_res == 'TERM':
 | 
						|
        break  # Process terminated
 | 
						|
      if poll_res == 'RUN':
 | 
						|
        # The 'perfetto' cmdline client is still running. If previously we had
 | 
						|
        # an ADB error, tell the user now it's all right again.
 | 
						|
        if adb_failure_count > 0:
 | 
						|
          adb_failure_count = 0
 | 
						|
          prt('ADB connection re-established, the trace is still ongoing',
 | 
						|
              ANSI.BLUE)
 | 
						|
        time.sleep(0.5)
 | 
						|
        continue
 | 
						|
      # Some ADB error happened. This can happen when tracing soon after boot,
 | 
						|
      # before logging in, when adb gets restarted.
 | 
						|
      adb_failure_count += 1
 | 
						|
      if adb_failure_count >= MAX_ADB_FAILURES:
 | 
						|
        prt('Too many unrecoverable ADB failures, bailing out', ANSI.RED)
 | 
						|
        sys.exit(1)
 | 
						|
      time.sleep(2)
 | 
						|
    except KeyboardInterrupt:
 | 
						|
      sig = 'TERM' if ctrl_c_count == 0 else 'KILL'
 | 
						|
      ctrl_c_count += 1
 | 
						|
      prt('Stopping the trace (SIG%s)' % sig, ANSI.BLACK + ANSI.BG_YELLOW)
 | 
						|
      adb('shell', 'kill -%s %s' % (sig, bg_pid)).wait()
 | 
						|
 | 
						|
  logcat.kill()
 | 
						|
  logcat.wait()
 | 
						|
 | 
						|
  prt('\n')
 | 
						|
  prt('Pulling into %s' % host_file, ANSI.BOLD)
 | 
						|
  adb('pull', device_file, host_file).wait()
 | 
						|
  adb('shell', 'rm -f ' + device_file).wait()
 | 
						|
 | 
						|
  if not args.no_open:
 | 
						|
    prt('\n')
 | 
						|
    prt('Opening the trace (%s) in the browser' % host_file)
 | 
						|
    open_trace_in_browser(host_file)
 | 
						|
 | 
						|
 | 
						|
def prt(msg, colors=ANSI.END):
 | 
						|
  print(colors + msg + ANSI.END)
 | 
						|
 | 
						|
 | 
						|
def find_adb():
 | 
						|
  """ Locate the "right" adb path
 | 
						|
 | 
						|
  If adb is in the PATH use that (likely what the user wants) otherwise use the
 | 
						|
  hermetic one in our SDK copy.
 | 
						|
  """
 | 
						|
  global adb_path
 | 
						|
  for path in ['adb', HERMETIC_ADB_PATH]:
 | 
						|
    try:
 | 
						|
      subprocess.call([path, '--version'], stdout=devnull, stderr=devnull)
 | 
						|
      adb_path = path
 | 
						|
      break
 | 
						|
    except OSError:
 | 
						|
      continue
 | 
						|
  if adb_path is None:
 | 
						|
    sdk_url = 'https://developer.android.com/studio/releases/platform-tools'
 | 
						|
    prt('Could not find a suitable adb binary in the PATH. ', ANSI.RED)
 | 
						|
    prt('You can download adb from %s' % sdk_url, ANSI.RED)
 | 
						|
    sys.exit(1)
 | 
						|
 | 
						|
 | 
						|
def open_trace_in_browser(path):
 | 
						|
  # We reuse the HTTP+RPC port because it's the only one allowed by the CSP.
 | 
						|
  PORT = 9001
 | 
						|
  os.chdir(os.path.dirname(path))
 | 
						|
  fname = os.path.basename(path)
 | 
						|
  socketserver.TCPServer.allow_reuse_address = True
 | 
						|
  with socketserver.TCPServer(('127.0.0.1', PORT), HttpHandler) as httpd:
 | 
						|
    webbrowser.open_new_tab(
 | 
						|
        'https://ui.perfetto.dev/#!/?url=http://127.0.0.1:%d/%s' %
 | 
						|
        (PORT, fname))
 | 
						|
    while httpd.__dict__.get('last_request') != '/' + fname:
 | 
						|
      httpd.handle_request()
 | 
						|
 | 
						|
 | 
						|
def adb(*args, stdin=devnull, stdout=None):
 | 
						|
  cmd = [adb_path, *args]
 | 
						|
  setpgrp = None
 | 
						|
  if os.name != 'nt':
 | 
						|
    # On Linux/Mac, start a new process group so all child processes are killed
 | 
						|
    # on exit. Unsupported on Windows.
 | 
						|
    setpgrp = lambda: os.setpgrp()
 | 
						|
  proc = subprocess.Popen(cmd, stdin=stdin, stdout=stdout, preexec_fn=setpgrp)
 | 
						|
  procs.append(proc)
 | 
						|
  return proc
 | 
						|
 | 
						|
 | 
						|
def kill_all_subprocs_on_exit():
 | 
						|
  for p in [p for p in procs if p.poll() is None]:
 | 
						|
    p.kill()
 | 
						|
 | 
						|
 | 
						|
def check_hash(file_name, sha_value):
 | 
						|
  with open(file_name, 'rb') as fd:
 | 
						|
    file_hash = hashlib.sha1(fd.read()).hexdigest()
 | 
						|
    return file_hash == sha_value
 | 
						|
 | 
						|
 | 
						|
# BEGIN_SECTION_GENERATED_BY(roll-prebuilts)
 | 
						|
# Revision: v25.0
 | 
						|
PERFETTO_PREBUILT_MANIFEST = [{
 | 
						|
    'tool':
 | 
						|
        'tracebox',
 | 
						|
    'arch':
 | 
						|
        'android-arm',
 | 
						|
    'file_name':
 | 
						|
        'tracebox',
 | 
						|
    'file_size':
 | 
						|
        1067004,
 | 
						|
    'url':
 | 
						|
        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v25.0/android-arm/tracebox',
 | 
						|
    'sha256':
 | 
						|
        '6fff40fc02d154b187187fe70069cc93bde00d3be5b3582ba6120b0cfa83d379'
 | 
						|
}, {
 | 
						|
    'tool':
 | 
						|
        'tracebox',
 | 
						|
    'arch':
 | 
						|
        'android-arm64',
 | 
						|
    'file_name':
 | 
						|
        'tracebox',
 | 
						|
    'file_size':
 | 
						|
        1620704,
 | 
						|
    'url':
 | 
						|
        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v25.0/android-arm64/tracebox',
 | 
						|
    'sha256':
 | 
						|
        'd482c86eccd7dc0ff4f57bd5d841d90e97ca5e31b881fcea442d4ad03ecfd4f4'
 | 
						|
}, {
 | 
						|
    'tool':
 | 
						|
        'tracebox',
 | 
						|
    'arch':
 | 
						|
        'android-x86',
 | 
						|
    'file_name':
 | 
						|
        'tracebox',
 | 
						|
    'file_size':
 | 
						|
        1644500,
 | 
						|
    'url':
 | 
						|
        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v25.0/android-x86/tracebox',
 | 
						|
    'sha256':
 | 
						|
        '48de1d10959b6130d2d6d68ae3ba0d8bb3f3aa6fe526d77ea9abad058a631d8f'
 | 
						|
}, {
 | 
						|
    'tool':
 | 
						|
        'tracebox',
 | 
						|
    'arch':
 | 
						|
        'android-x64',
 | 
						|
    'file_name':
 | 
						|
        'tracebox',
 | 
						|
    'file_size':
 | 
						|
        1870560,
 | 
						|
    'url':
 | 
						|
        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v25.0/android-x64/tracebox',
 | 
						|
    'sha256':
 | 
						|
        '333c17912a143300ca6afa3b27d93497821615028e612e5d6132889427c10599'
 | 
						|
}]
 | 
						|
 | 
						|
 | 
						|
# 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())
 |