• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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