tools: python amalgamation for tools/tracebox etc

Before this change:
the business logic for tools/{tracebox, record_android_trace, ...}
was in the file itself and the manifest was replaced in-place
when invoking tools/roll-prebuilts.
This still made it impossible to share code between tools
without copy/pasting.

With this change:
- Move the business logic to python/xxx
- Add an amalgamator that follows includes. Only the form
  'from perfetto.xxx import yyy' is supported.
- Keep the amalgamated files in tools/traceconv

No code sharing / major refactorings are made by this change.
They can happen as a follow-up though

Change-Id: I7420387881e6ef1e109abae6380dde7c06ac1b27
diff --git a/PRESUBMIT.py b/PRESUBMIT.py
index 901f812..5307e5a 100644
--- a/PRESUBMIT.py
+++ b/PRESUBMIT.py
@@ -76,6 +76,7 @@
   results += RunAndReportIfLong(CheckBannedCpp, input, output)
   results += RunAndReportIfLong(CheckSqlMetrics, input, output)
   results += RunAndReportIfLong(CheckTestData, input, output)
+  results += RunAndReportIfLong(CheckAmalgamatedPythonTools, input, output)
   return results
 
 
@@ -291,3 +292,21 @@
         )
     ]
   return []
+
+
+def CheckAmalgamatedPythonTools(input_api, output_api):
+  tool = 'tools/gen_amalgamated_python_tools'
+
+  # If no GN files were modified, bail out.
+  def build_file_filter(x):
+    return input_api.FilterSourceFile(x, files_to_check=('python/.*$', tool))
+
+  if not input_api.AffectedSourceFiles(build_file_filter):
+    return []
+  if subprocess.call([tool, '--check-only']):
+    return [
+        output_api.PresubmitError(
+            'amalgamated python tools/ are out of date. ' + 'Run ' + tool +
+            ' to update them.')
+    ]
+  return []
diff --git a/python/perfetto/common/repo_utils.py b/python/perfetto/common/repo_utils.py
new file mode 100644
index 0000000..0a84030
--- /dev/null
+++ b/python/perfetto/common/repo_utils.py
@@ -0,0 +1,35 @@
+# 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 os
+
+
+def repo_root():
+  """ Finds the repo root by traversing up the hierarchy
+
+  This is for use in scripts that get amalgamated, where _file_ can be either
+  python/perfetto/... or tools/amalgamated_tool.
+  """
+  path = os.path.dirname(os.path.abspath(__file__))  # amalgamator:nocheck
+  last_dir = ''
+  while path and path != last_dir:
+    if os.path.exists(os.path.join(path, 'perfetto.rc')):
+      return path
+    last_dir = path
+    path = os.path.dirname(path)
+  return None
+
+
+def repo_dir(rel_path):
+  return os.path.join(repo_root() or '', rel_path)
diff --git a/python/perfetto/prebuilts/manifests/trace_processor_shell.py b/python/perfetto/prebuilts/manifests/trace_processor_shell.py
new file mode 100755
index 0000000..1c2ebd4
--- /dev/null
+++ b/python/perfetto/prebuilts/manifests/trace_processor_shell.py
@@ -0,0 +1,130 @@
+# This file has been generated by: /s/perfetto/tools/roll-prebuilts v28.0
+TRACE_PROCESSOR_SHELL_MANIFEST = [{
+    'arch':
+        'mac-amd64',
+    'file_name':
+        'trace_processor_shell',
+    'file_size':
+        7991568,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/mac-amd64/trace_processor_shell',
+    'sha256':
+        '4704eb8c51946ae288b74ad9f4386019a2b146c310c7717aeeaada73d6eef8fc',
+    'platform':
+        'darwin',
+    'machine': ['x86_64']
+}, {
+    'arch':
+        'mac-arm64',
+    'file_name':
+        'trace_processor_shell',
+    'file_size':
+        6757608,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/mac-arm64/trace_processor_shell',
+    'sha256':
+        '2aa7ddebedb6d5cf6300b3dc3e6ea8e294f645f67c2dbd727d497ab4a17d6599',
+    'platform':
+        'darwin',
+    'machine': ['arm64']
+}, {
+    'arch':
+        'linux-amd64',
+    'file_name':
+        'trace_processor_shell',
+    'file_size':
+        8568248,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/linux-amd64/trace_processor_shell',
+    'sha256':
+        '67deabb7e215cc66d3694c0e0367f441cd48a3456d2dd5877bab6eb07117887f',
+    'platform':
+        'linux',
+    'machine': ['x86_64']
+}, {
+    'arch':
+        'linux-arm',
+    'file_name':
+        'trace_processor_shell',
+    'file_size':
+        6279736,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/linux-arm/trace_processor_shell',
+    'sha256':
+        '172ac15b010b1e9a28e1efb8991e2e9195dc732969b33e04ebd5b5f801a99218',
+    'platform':
+        'linux',
+    'machine': ['armv6l', 'armv7l', 'armv8l']
+}, {
+    'arch':
+        'linux-arm64',
+    'file_name':
+        'trace_processor_shell',
+    'file_size':
+        7703256,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/linux-arm64/trace_processor_shell',
+    'sha256':
+        'd70d621ca9aedff0b29a24984ff896e7069cadee607ada1ccfca58634e1774a1',
+    'platform':
+        'linux',
+    'machine': ['aarch64']
+}, {
+    'arch':
+        'android-arm',
+    'file_name':
+        'trace_processor_shell',
+    'file_size':
+        5357880,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/android-arm/trace_processor_shell',
+    'sha256':
+        '75285ac8964e93890b936bfacd2679ee67ae4cd029be74e675253f92746f9904'
+}, {
+    'arch':
+        'android-arm64',
+    'file_name':
+        'trace_processor_shell',
+    'file_size':
+        6940832,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/android-arm64/trace_processor_shell',
+    'sha256':
+        '75118859c9a1b2ed2bcffc664eb98884dab26a9d2ad7788d05d90ef22b24a8d5'
+}, {
+    'arch':
+        'android-x86',
+    'file_name':
+        'trace_processor_shell',
+    'file_size':
+        7856396,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/android-x86/trace_processor_shell',
+    'sha256':
+        'c3d4da7aa12a2eedd59df98b5c8143658f39ebd9a4ba3c8180be33ab3484e824'
+}, {
+    'arch':
+        'android-x64',
+    'file_name':
+        'trace_processor_shell',
+    'file_size':
+        8194240,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/android-x64/trace_processor_shell',
+    'sha256':
+        '105c70a3908bf6997b428ee7f74bf7ee8a48907b860480c87e62a62cf92e1a8a'
+}, {
+    'arch':
+        'windows-amd64',
+    'file_name':
+        'trace_processor_shell.exe',
+    'file_size':
+        7857152,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/windows-amd64/trace_processor_shell.exe',
+    'sha256':
+        'eda1e38cb7490a99ba31285a977a3a564c53f25eae2b301152b2578de62fcc5d',
+    'platform':
+        'win32',
+    'machine': ['amd64']
+}]
diff --git a/python/perfetto/prebuilts/manifests/tracebox.py b/python/perfetto/prebuilts/manifests/tracebox.py
new file mode 100755
index 0000000..46779cb
--- /dev/null
+++ b/python/perfetto/prebuilts/manifests/tracebox.py
@@ -0,0 +1,116 @@
+# This file has been generated by: /s/perfetto/tools/roll-prebuilts v28.0
+TRACEBOX_MANIFEST = [{
+    'arch':
+        'mac-amd64',
+    'file_name':
+        'tracebox',
+    'file_size':
+        1399080,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/mac-amd64/tracebox',
+    'sha256':
+        '05674f9872a14bf92d4215a180c883ab54c58376d7e08b95e3f83d03efdeba21',
+    'platform':
+        'darwin',
+    'machine': ['x86_64']
+}, {
+    'arch':
+        'mac-arm64',
+    'file_name':
+        'tracebox',
+    'file_size':
+        1292504,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/mac-arm64/tracebox',
+    'sha256':
+        '48008e6aeb7680c4f31b1147d5c26a504368a696c793aec1895bb5fb1f597f64',
+    'platform':
+        'darwin',
+    'machine': ['arm64']
+}, {
+    'arch':
+        'linux-amd64',
+    'file_name':
+        'tracebox',
+    'file_size':
+        2243632,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/linux-amd64/tracebox',
+    'sha256':
+        '12c87ba1d03e39d4c07792e09f0be9e240677cec2d9bc1638b0a676bc664d724',
+    'platform':
+        'linux',
+    'machine': ['x86_64']
+}, {
+    'arch':
+        'linux-arm',
+    'file_name':
+        'tracebox',
+    'file_size':
+        1315252,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/linux-arm/tracebox',
+    'sha256':
+        '3d026fe982e90fb5bc410528f210458f9bcafea30d3e1b9728772fd32148ee9a',
+    'platform':
+        'linux',
+    'machine': ['armv6l', 'armv7l', 'armv8l']
+}, {
+    'arch':
+        'linux-arm64',
+    'file_name':
+        'tracebox',
+    'file_size':
+        2089160,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/linux-arm64/tracebox',
+    'sha256':
+        '9f524f943a1c12dcb6b273e78d5c46952d4a7528514639cd2537686d5c530c89',
+    'platform':
+        'linux',
+    'machine': ['aarch64']
+}, {
+    'arch':
+        'android-arm',
+    'file_name':
+        'tracebox',
+    'file_size':
+        1099732,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/android-arm/tracebox',
+    'sha256':
+        '53a1a65e2e409a552cd75f003c755b5903075e37e06841b3a514889306fb9616'
+}, {
+    'arch':
+        'android-arm64',
+    'file_name':
+        'tracebox',
+    'file_size':
+        1653416,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/android-arm64/tracebox',
+    'sha256':
+        'df911aa2e5d80d1fdda5860e1d18dca7ba13c835aada090bb07e8f253562129a'
+}, {
+    'arch':
+        'android-x86',
+    'file_name':
+        'tracebox',
+    'file_size':
+        1677228,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/android-x86/tracebox',
+    'sha256':
+        '8d1a296a084f488f826dac837530a40f2ca38af76c70a10966e11f9fec91a2f3'
+}, {
+    'arch':
+        'android-x64',
+    'file_name':
+        'tracebox',
+    'file_size':
+        1911464,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/android-x64/tracebox',
+    'sha256':
+        '8f844f4d8263d0ff904d120b5858a02d7a24efd87f49c3fa4fd8682d534c738f'
+}]
diff --git a/python/perfetto/prebuilts/manifests/traceconv.py b/python/perfetto/prebuilts/manifests/traceconv.py
new file mode 100755
index 0000000..3b7334a
--- /dev/null
+++ b/python/perfetto/prebuilts/manifests/traceconv.py
@@ -0,0 +1,130 @@
+# This file has been generated by: /s/perfetto/tools/roll-prebuilts v28.0
+TRACECONV_MANIFEST = [{
+    'arch':
+        'mac-amd64',
+    'file_name':
+        'traceconv',
+    'file_size':
+        7181712,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/mac-amd64/traceconv',
+    'sha256':
+        '6b398ad9539ddf8208536c0412db198d4627daa97efc7e0850f3e7ec0e115510',
+    'platform':
+        'darwin',
+    'machine': ['x86_64']
+}, {
+    'arch':
+        'mac-arm64',
+    'file_name':
+        'traceconv',
+    'file_size':
+        6025176,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/mac-arm64/traceconv',
+    'sha256':
+        '407e2988e795a593158304061c547093ad74419f826dd03c2a66911b5a29d065',
+    'platform':
+        'darwin',
+    'machine': ['arm64']
+}, {
+    'arch':
+        'linux-amd64',
+    'file_name':
+        'traceconv',
+    'file_size':
+        7668600,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/linux-amd64/traceconv',
+    'sha256':
+        '1bebc0dd7b2b18fd4abeeb5f811d6d4c7f431d212efd5469c7e5d8b18b19e0c7',
+    'platform':
+        'linux',
+    'machine': ['x86_64']
+}, {
+    'arch':
+        'linux-arm',
+    'file_name':
+        'traceconv',
+    'file_size':
+        5827680,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/linux-arm/traceconv',
+    'sha256':
+        'a19780145f965838e334a57a52230bc67b0db207365746360314fbbbe4e1d12f',
+    'platform':
+        'linux',
+    'machine': ['armv6l', 'armv7l', 'armv8l']
+}, {
+    'arch':
+        'linux-arm64',
+    'file_name':
+        'traceconv',
+    'file_size':
+        6876384,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/linux-arm64/traceconv',
+    'sha256':
+        '2a5e5fcf340070ed6a30204c79b7e76787c5f26181bc8377587547f3eb5df685',
+    'platform':
+        'linux',
+    'machine': ['aarch64']
+}, {
+    'arch':
+        'android-arm',
+    'file_name':
+        'traceconv',
+    'file_size':
+        4881820,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/android-arm/traceconv',
+    'sha256':
+        '73827b82d941a9650580fbd48c3d4ff2323eb8d4ff9d3fffd3e0cac1bc853f34'
+}, {
+    'arch':
+        'android-arm64',
+    'file_name':
+        'traceconv',
+    'file_size':
+        6222592,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/android-arm64/traceconv',
+    'sha256':
+        '72d46258645d486f40ee463052b609d1fd7c4cc64f70c0ba2ef811a9924be98e'
+}, {
+    'arch':
+        'android-x86',
+    'file_name':
+        'traceconv',
+    'file_size':
+        7089524,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/android-x86/traceconv',
+    'sha256':
+        '689d0b48f91624585285b3833362cdcfdf0de1ff5dedcb97bb9851c729b4a15e'
+}, {
+    'arch':
+        'android-x64',
+    'file_name':
+        'traceconv',
+    'file_size':
+        7316248,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/android-x64/traceconv',
+    'sha256':
+        '785ec3f0da302ed52521febc5ed5e2cef57ae8840ff241037c51b8d94464f6a2'
+}, {
+    'arch':
+        'windows-amd64',
+    'file_name':
+        'traceconv.exe',
+    'file_size':
+        6850048,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/windows-amd64/traceconv.exe',
+    'sha256':
+        '19cdec3824d369be3bb053b40b3cfe9f62c2e57e71a5e2ee17ca15b6e7463683',
+    'platform':
+        'win32',
+    'machine': ['amd64']
+}]
diff --git a/python/perfetto/prebuilts/perfetto_prebuilts.py b/python/perfetto/prebuilts/perfetto_prebuilts.py
new file mode 100644
index 0000000..723e796
--- /dev/null
+++ b/python/perfetto/prebuilts/perfetto_prebuilts.py
@@ -0,0 +1,123 @@
+# 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.
+"""
+Functions to fetch pre-pinned Perfetto prebuilts.
+
+This function is used in different places:
+- Into the //tools/{trace_processor, traceconv} scripts, which are just plain
+  wrappers around executables.
+- Into the //tools/{heap_profiler, record_android_trace} scripts, which contain
+  some other hand-written python code.
+
+The manifest argument looks as follows:
+TRACECONV_MANIFEST = [
+  {
+    'arch': 'mac-amd64',
+    'file_name': 'traceconv',
+    'file_size': 7087080,
+    'url': https://commondatastorage.googleapis.com/.../trace_to_text',
+    'sha256': 7d957c005b0dc130f5bd855d6cec27e060d38841b320d04840afc569f9087490',
+    'platform': 'darwin',
+    'machine': 'x86_64'
+  },
+  ...
+]
+
+The intended usage is:
+
+  from perfetto.prebuilts.manifests.traceconv import TRACECONV_MANIFEST
+  bin_path = get_perfetto_prebuilt(TRACECONV_MANIFEST)
+  subprocess.call(bin_path, ...)
+"""
+
+import hashlib
+import os
+import platform
+import subprocess
+import sys
+
+
+def download_or_get_cached(file_name, url, sha256):
+  """ Downloads a prebuilt or returns a cached version
+
+  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.
+  """
+  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
+
+
+def get_perfetto_prebuilt(manifest, soft_fail=False, arch=None):
+  """ Downloads the prebuilt, if necessary, and returns its path on disk. """
+  plat = sys.platform.lower()
+  machine = platform.machine().lower()
+  manifest_entry = None
+  for entry in manifest:
+    # If the caller overrides the arch, just match that (for Android prebuilts).
+    if arch:
+      if entry.get('arch') == arch:
+        manifest_entry = entry
+        break
+      continue
+    # Otherwise guess the local machine arch.
+    if 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'])
+
+
+def run_perfetto_prebuilt(manifest):
+  bin_path = get_perfetto_prebuilt(manifest)
+  if sys.platform.lower() == 'win32':
+    sys.exit(subprocess.check_call([bin_path, *sys.argv[1:]]))
+  os.execv(bin_path, [bin_path] + sys.argv[1:])
diff --git a/python/tools/cpu_profile.py b/python/tools/cpu_profile.py
new file mode 100644
index 0000000..4c33f2b
--- /dev/null
+++ b/python/tools/cpu_profile.py
@@ -0,0 +1,454 @@
+#!/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 textwrap
+import time
+import uuid
+
+from perfetto.prebuilts.manifests.traceconv import *
+from perfetto.prebuilts.perfetto_prebuilts import *
+
+# 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(
+      "--print-config",
+      action="store_true",
+      help="Print config instead of running. For debugging.")
+  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 = textwrap.dedent('''\
+  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
+  ''')
+
+  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.")
+
+  target_config = "\n".join(
+      [f'{CONFIG_INDENT}target_cmdline: "{p}"' for p in matching_processes])
+
+  if not args.print_config:
+    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_traceconv():
+  """Sets up and returns the path to `traceconv`."""
+  try:
+    traceconv = get_perfetto_prebuilt(TRACECONV_MANIFEST, soft_fail=True)
+  except Exception as error:
+    exit_with_bug_report(error)
+  if traceconv is None:
+    exit_with_bug_report(
+        "Unable to download `traceconv` for symbolizing profiles.")
+
+  return traceconv
+
+
+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(traceconv, profile_target):
+  """Attempts symbolization of the recorded trace/profile, if symbols are available.
+
+  Args:
+    traceconv: The path to the `traceconv` 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([traceconv, '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(traceconv, trace_file):
+  """Generates pprof profiles from the recorded trace.
+
+  Args:
+    traceconv: The path to the `traceconv` 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:
+    traceconv_output = subprocess.check_output(
+        [traceconv, '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 traceconv_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)
+  trace_config = get_perfetto_config(args)
+  if args.print_config:
+    print(trace_config)
+    return 0
+  record_trace(trace_config, profile_target)
+  traceconv = get_traceconv()
+  trace_file = symbolize_trace(traceconv, profile_target)
+  copy_profiles_to_destination(profile_target,
+                               generate_pprof_profiles(traceconv, trace_file))
+  return 0
+
+
+if __name__ == '__main__':
+  sys.exit(main(sys.argv))
diff --git a/python/tools/heap_profile.py b/python/tools/heap_profile.py
new file mode 100644
index 0000000..38325de
--- /dev/null
+++ b/python/tools/heap_profile.py
@@ -0,0 +1,578 @@
+#!/usr/bin/env python3
+# Copyright (C) 2017 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.
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+import argparse
+import atexit
+import os
+import shutil
+import signal
+import subprocess
+import sys
+import tempfile
+import time
+import uuid
+
+from perfetto.prebuilts.manifests.traceconv import *
+from perfetto.prebuilts.perfetto_prebuilts import *
+
+NULL = open(os.devnull)
+NOOUT = {
+    'stdout': NULL,
+    'stderr': NULL,
+}
+
+UUID = str(uuid.uuid4())[-6:]
+
+PACKAGES_LIST_CFG = '''data_sources {
+  config {
+    name: "android.packages_list"
+  }
+}
+'''
+
+CFG_INDENT = '      '
+CFG = '''buffers {{
+  size_kb: 63488
+}}
+
+data_sources {{
+  config {{
+    name: "android.heapprofd"
+    heapprofd_config {{
+      shmem_size_bytes: {shmem_size}
+      sampling_interval_bytes: {interval}
+{target_cfg}
+    }}
+  }}
+}}
+
+duration_ms: {duration}
+write_into_file: true
+flush_timeout_ms: 30000
+flush_period_ms: 604800000
+'''
+
+# flush_period_ms of 1 week to suppress trace_processor_shell warning.
+
+CONTINUOUS_DUMP = """
+      continuous_dump_config {{
+        dump_phase_ms: 0
+        dump_interval_ms: {dump_interval}
+      }}
+"""
+
+PROFILE_LOCAL_PATH = os.path.join(tempfile.gettempdir(), UUID)
+
+IS_INTERRUPTED = False
+
+
+def sigint_handler(sig, frame):
+  global IS_INTERRUPTED
+  IS_INTERRUPTED = True
+
+
+def print_no_profile_error():
+  print("No profiles generated", file=sys.stderr)
+  print(
+      "If this is unexpected, check "
+      "https://perfetto.dev/docs/data-sources/native-heap-profiler#troubleshooting.",
+      file=sys.stderr)
+
+
+def known_issues_url(number):
+  return ('https://perfetto.dev/docs/data-sources/native-heap-profiler'
+          '#known-issues-android{}'.format(number))
+
+
+KNOWN_ISSUES = {
+    '10': known_issues_url(10),
+    'Q': known_issues_url(10),
+    '11': known_issues_url(11),
+    'R': known_issues_url(11),
+}
+
+
+def maybe_known_issues():
+  release_or_codename = subprocess.check_output(
+      ['adb', 'shell', 'getprop',
+       'ro.build.version.release_or_codename']).decode('utf-8').strip()
+  return KNOWN_ISSUES.get(release_or_codename, None)
+
+
+SDK = {
+    'R': 30,
+}
+
+
+def release_or_newer(release):
+  sdk = int(
+      subprocess.check_output(
+          ['adb', 'shell', 'getprop',
+           'ro.system.build.version.sdk']).decode('utf-8').strip())
+  if sdk >= SDK[release]:
+    return True
+  codename = subprocess.check_output(
+      ['adb', 'shell', 'getprop',
+       'ro.build.version.codename']).decode('utf-8').strip()
+  return codename == release
+
+
+ORDER = ['-n', '-p', '-i', '-o']
+
+
+def arg_order(action):
+  result = len(ORDER)
+  for opt in action.option_strings:
+    if opt in ORDER:
+      result = min(ORDER.index(opt), result)
+  return result, action.option_strings[0].strip('-')
+
+
+def print_options(parser):
+  for action in sorted(parser._actions, key=arg_order):
+    if action.help is argparse.SUPPRESS:
+      continue
+    opts = ', '.join('`' + x + '`' for x in action.option_strings)
+    metavar = '' if action.metavar is None else ' _' + action.metavar + '_'
+    print('{}{}'.format(opts, metavar))
+    print(':    {}'.format(action.help))
+    print()
+
+
+def main(argv):
+  parser = argparse.ArgumentParser()
+  parser.add_argument(
+      "-i",
+      "--interval",
+      help="Sampling interval. "
+      "Default 4096 (4KiB)",
+      type=int,
+      default=4096)
+  parser.add_argument(
+      "-d",
+      "--duration",
+      help="Duration of profile (ms). 0 to run until interrupted. "
+      "Default: until interrupted by user.",
+      type=int,
+      default=0)
+  # This flag is a no-op now. We never start heapprofd explicitly using system
+  # properties.
+  parser.add_argument(
+      "--no-start", help="Do not start heapprofd.", action='store_true')
+  parser.add_argument(
+      "-p",
+      "--pid",
+      help="Comma-separated list of PIDs to "
+      "profile.",
+      metavar="PIDS")
+  parser.add_argument(
+      "-n",
+      "--name",
+      help="Comma-separated list of process "
+      "names to profile.",
+      metavar="NAMES")
+  parser.add_argument(
+      "-c",
+      "--continuous-dump",
+      help="Dump interval in ms. 0 to disable continuous dump.",
+      type=int,
+      default=0)
+  parser.add_argument(
+      "--heaps",
+      help="Comma-separated list of heaps to collect, e.g: malloc,art. "
+      "Requires Android 12.",
+      metavar="HEAPS")
+  parser.add_argument(
+      "--all-heaps",
+      action="store_true",
+      help="Collect allocations from all heaps registered by target.")
+  parser.add_argument(
+      "--no-android-tree-symbolization",
+      action="store_true",
+      help="Do not symbolize using currently lunched target in the "
+      "Android tree.")
+  parser.add_argument(
+      "--disable-selinux",
+      action="store_true",
+      help="Disable SELinux enforcement for duration of "
+      "profile.")
+  parser.add_argument(
+      "--no-versions",
+      action="store_true",
+      help="Do not get version information about APKs.")
+  parser.add_argument(
+      "--no-running",
+      action="store_true",
+      help="Do not target already running processes. Requires Android 11.")
+  parser.add_argument(
+      "--no-startup",
+      action="store_true",
+      help="Do not target processes that start during "
+      "the profile. Requires Android 11.")
+  parser.add_argument(
+      "--shmem-size",
+      help="Size of buffer between client and "
+      "heapprofd. Default 8MiB. Needs to be a power of two "
+      "multiple of 4096, at least 8192.",
+      type=int,
+      default=8 * 1048576)
+  parser.add_argument(
+      "--block-client",
+      help="When buffer is full, block the "
+      "client to wait for buffer space. Use with caution as "
+      "this can significantly slow down the client. "
+      "This is the default",
+      action="store_true")
+  parser.add_argument(
+      "--block-client-timeout",
+      help="If --block-client is given, do not block any allocation for "
+      "longer than this timeout (us).",
+      type=int)
+  parser.add_argument(
+      "--no-block-client",
+      help="When buffer is full, stop the "
+      "profile early.",
+      action="store_true")
+  parser.add_argument(
+      "--idle-allocations",
+      help="Keep track of how many "
+      "bytes were unused since the last dump, per "
+      "callstack",
+      action="store_true")
+  parser.add_argument(
+      "--dump-at-max",
+      help="Dump the maximum memory usage "
+      "rather than at the time of the dump.",
+      action="store_true")
+  parser.add_argument(
+      "--disable-fork-teardown",
+      help="Do not tear down client in forks. This can be useful for programs "
+      "that use vfork. Android 11+ only.",
+      action="store_true")
+  parser.add_argument(
+      "--simpleperf",
+      action="store_true",
+      help="Get simpleperf profile of heapprofd. This is "
+      "only for heapprofd development.")
+  parser.add_argument(
+      "--traceconv-binary", help="Path to local trace to text. For debugging.")
+  parser.add_argument(
+      "--print-config",
+      action="store_true",
+      help="Print config instead of running. For debugging.")
+  parser.add_argument(
+      "-o",
+      "--output",
+      help="Output directory.",
+      metavar="DIRECTORY",
+      default=None)
+  parser.add_argument(
+      "--print-options", action="store_true", help=argparse.SUPPRESS)
+
+  args = parser.parse_args()
+  if args.print_options:
+    print_options(parser)
+    return 0
+  fail = False
+  if args.block_client and args.no_block_client:
+    print(
+        "FATAL: Both block-client and no-block-client given.", file=sys.stderr)
+    fail = True
+  if args.pid is None and args.name is None:
+    print("FATAL: Neither PID nor NAME given.", file=sys.stderr)
+    fail = True
+  if args.duration is None:
+    print("FATAL: No duration given.", file=sys.stderr)
+    fail = True
+  if args.interval is None:
+    print("FATAL: No interval given.", file=sys.stderr)
+    fail = True
+  if args.shmem_size % 4096:
+    print("FATAL: shmem-size is not a multiple of 4096.", file=sys.stderr)
+    fail = True
+  if args.shmem_size < 8192:
+    print("FATAL: shmem-size is less than 8192.", file=sys.stderr)
+    fail = True
+  if args.shmem_size & (args.shmem_size - 1):
+    print("FATAL: shmem-size is not a power of two.", file=sys.stderr)
+    fail = True
+
+  target_cfg = ""
+  if not args.no_block_client:
+    target_cfg += CFG_INDENT + "block_client: true\n"
+  if args.block_client_timeout:
+    target_cfg += (
+        CFG_INDENT +
+        "block_client_timeout_us: %s\n" % args.block_client_timeout)
+  if args.no_startup:
+    target_cfg += CFG_INDENT + "no_startup: true\n"
+  if args.no_running:
+    target_cfg += CFG_INDENT + "no_running: true\n"
+  if args.dump_at_max:
+    target_cfg += CFG_INDENT + "dump_at_max: true\n"
+  if args.disable_fork_teardown:
+    target_cfg += CFG_INDENT + "disable_fork_teardown: true\n"
+  if args.all_heaps:
+    target_cfg += CFG_INDENT + "all_heaps: true\n"
+  if args.pid:
+    for pid in args.pid.split(','):
+      try:
+        pid = int(pid)
+      except ValueError:
+        print("FATAL: invalid PID %s" % pid, file=sys.stderr)
+        fail = True
+      target_cfg += CFG_INDENT + 'pid: {}\n'.format(pid)
+  if args.name:
+    for name in args.name.split(','):
+      target_cfg += CFG_INDENT + 'process_cmdline: "{}"\n'.format(name)
+  if args.heaps:
+    for heap in args.heaps.split(','):
+      target_cfg += CFG_INDENT + 'heaps: "{}"\n'.format(heap)
+
+  if fail:
+    parser.print_help()
+    return 1
+
+  traceconv_binary = args.traceconv_binary
+
+  if args.continuous_dump:
+    target_cfg += CONTINUOUS_DUMP.format(dump_interval=args.continuous_dump)
+  cfg = CFG.format(
+      interval=args.interval,
+      duration=args.duration,
+      target_cfg=target_cfg,
+      shmem_size=args.shmem_size)
+  if not args.no_versions:
+    cfg += PACKAGES_LIST_CFG
+
+  if args.print_config:
+    print(cfg)
+    return 0
+
+  # Do this AFTER print_config so we do not download traceconv only to
+  # print out the config.
+  if traceconv_binary is None:
+    traceconv_binary = get_perfetto_prebuilt(TRACECONV_MANIFEST, soft_fail=True)
+
+  known_issues = maybe_known_issues()
+  if known_issues:
+    print('If you are experiencing problems, please see the known issues for '
+          'your release: {}.'.format(known_issues))
+
+  # TODO(fmayer): Maybe feature detect whether we can remove traces instead of
+  # this.
+  uuid_trace = release_or_newer('R')
+  if uuid_trace:
+    profile_device_path = '/data/misc/perfetto-traces/profile-' + UUID
+  else:
+    user = subprocess.check_output(['adb', 'shell',
+                                    'whoami']).decode('utf-8').strip()
+    profile_device_path = '/data/misc/perfetto-traces/profile-' + user
+
+  perfetto_cmd = ('CFG=\'{cfg}\'; echo ${{CFG}} | '
+                  'perfetto --txt -c - -o ' + profile_device_path + ' -d')
+
+  if args.disable_selinux:
+    enforcing = subprocess.check_output(['adb', 'shell', 'getenforce'])
+    atexit.register(
+        subprocess.check_call,
+        ['adb', 'shell', 'su root setenforce %s' % enforcing])
+    subprocess.check_call(['adb', 'shell', 'su root setenforce 0'])
+
+  if args.simpleperf:
+    subprocess.check_call([
+        'adb', 'shell', 'mkdir -p /data/local/tmp/heapprofd_profile && '
+        'cd /data/local/tmp/heapprofd_profile &&'
+        '(nohup simpleperf record -g -p $(pidof heapprofd) 2>&1 &) '
+        '> /dev/null'
+    ])
+
+  profile_target = PROFILE_LOCAL_PATH
+  if args.output is not None:
+    profile_target = args.output
+  else:
+    os.mkdir(profile_target)
+
+  if not os.path.isdir(profile_target):
+    print(
+        "Output directory {} not found".format(profile_target), file=sys.stderr)
+    return 1
+
+  if os.listdir(profile_target):
+    print(
+        "Output directory {} not empty".format(profile_target), file=sys.stderr)
+    return 1
+
+  perfetto_pid = subprocess.check_output(
+      ['adb', 'exec-out', perfetto_cmd.format(cfg=cfg)]).strip()
+  try:
+    perfetto_pid = int(perfetto_pid.strip())
+  except ValueError:
+    print("Failed to invoke perfetto: {}".format(perfetto_pid), file=sys.stderr)
+    return 1
+
+  old_handler = signal.signal(signal.SIGINT, sigint_handler)
+  print("Profiling active. Press Ctrl+C to terminate.")
+  print("You may disconnect your device.")
+  print()
+  exists = True
+  device_connected = True
+  while not device_connected or (exists and not IS_INTERRUPTED):
+    exists = subprocess.call(
+        ['adb', 'shell', '[ -d /proc/{} ]'.format(perfetto_pid)], **NOOUT) == 0
+    device_connected = subprocess.call(['adb', 'shell', 'true'], **NOOUT) == 0
+    time.sleep(1)
+  print("Waiting for profiler shutdown...")
+  signal.signal(signal.SIGINT, old_handler)
+  if IS_INTERRUPTED:
+    # Not check_call because it could have existed in the meantime.
+    subprocess.call(['adb', 'shell', 'kill', '-INT', str(perfetto_pid)])
+  if args.simpleperf:
+    subprocess.check_call(['adb', 'shell', 'killall', '-INT', 'simpleperf'])
+    print("Waiting for simpleperf to exit.")
+    while subprocess.call(
+        ['adb', 'shell', '[ -f /proc/$(pidof simpleperf)/exe ]'], **NOOUT) == 0:
+      time.sleep(1)
+    subprocess.check_call(
+        ['adb', 'pull', '/data/local/tmp/heapprofd_profile', profile_target])
+    print("Pulled simpleperf profile to " + profile_target +
+          "/heapprofd_profile")
+
+  # Wait for perfetto cmd to return.
+  while exists:
+    exists = subprocess.call(
+        ['adb', 'shell', '[ -d /proc/{} ]'.format(perfetto_pid)]) == 0
+    time.sleep(1)
+
+  profile_host_path = os.path.join(profile_target, 'raw-trace')
+  subprocess.check_call(['adb', 'pull', profile_device_path, profile_host_path],
+                        stdout=NULL)
+  if uuid_trace:
+    subprocess.check_call(['adb', 'shell', 'rm', profile_device_path],
+                          stdout=NULL)
+
+  if traceconv_binary is None:
+    print('Wrote profile to {}'.format(profile_host_path))
+    print(
+        'This file can be opened using the Perfetto UI, https://ui.perfetto.dev'
+    )
+    return 0
+
+  binary_path = os.getenv('PERFETTO_BINARY_PATH')
+  if not args.no_android_tree_symbolization:
+    product_out = os.getenv('ANDROID_PRODUCT_OUT')
+    if product_out:
+      product_out_symbols = product_out + '/symbols'
+    else:
+      product_out_symbols = None
+
+    if binary_path is None:
+      binary_path = product_out_symbols
+    elif product_out_symbols is not None:
+      binary_path += ":" + product_out_symbols
+
+  trace_file = os.path.join(profile_target, 'raw-trace')
+  concat_files = [trace_file]
+
+  if binary_path is not None:
+    with open(os.path.join(profile_target, 'symbols'), 'w') as fd:
+      ret = subprocess.call([
+          traceconv_binary, 'symbolize',
+          os.path.join(profile_target, 'raw-trace')
+      ],
+                            env=dict(
+                                os.environ, PERFETTO_BINARY_PATH=binary_path),
+                            stdout=fd)
+    if ret == 0:
+      concat_files.append(os.path.join(profile_target, 'symbols'))
+    else:
+      print("Failed to symbolize. Continuing without symbols.", file=sys.stderr)
+
+  proguard_map = os.getenv('PERFETTO_PROGUARD_MAP')
+  if proguard_map is not None:
+    with open(os.path.join(profile_target, 'deobfuscation-packets'), 'w') as fd:
+      ret = subprocess.call([
+          traceconv_binary, 'deobfuscate',
+          os.path.join(profile_target, 'raw-trace')
+      ],
+                            env=dict(
+                                os.environ, PERFETTO_PROGUARD_MAP=proguard_map),
+                            stdout=fd)
+    if ret == 0:
+      concat_files.append(os.path.join(profile_target, 'deobfuscation-packets'))
+    else:
+      print(
+          "Failed to deobfuscate. Continuing without deobfuscated.",
+          file=sys.stderr)
+
+  if len(concat_files) > 1:
+    with open(os.path.join(profile_target, 'symbolized-trace'), 'wb') as out:
+      for fn in concat_files:
+        with open(fn, 'rb') as inp:
+          while True:
+            buf = inp.read(4096)
+            if not buf:
+              break
+            out.write(buf)
+    trace_file = os.path.join(profile_target, 'symbolized-trace')
+
+  traceconv_output = subprocess.check_output(
+      [traceconv_binary, 'profile', trace_file])
+  profile_path = None
+  for word in traceconv_output.decode('utf-8').split():
+    if 'heap_profile-' in word:
+      profile_path = word
+  if profile_path is None:
+    print_no_profile_error()
+    return 1
+
+  profile_files = os.listdir(profile_path)
+  if not profile_files:
+    print_no_profile_error()
+    return 1
+
+  for profile_file in profile_files:
+    shutil.copy(os.path.join(profile_path, profile_file), profile_target)
+
+  symlink_path = None
+  if not sys.platform.startswith('win'):
+    subprocess.check_call(
+        ['gzip'] + [os.path.join(profile_target, x) for x in profile_files])
+    if args.output is None:
+      symlink_path = os.path.join(
+          os.path.dirname(profile_target), "heap_profile-latest")
+      if os.path.lexists(symlink_path):
+        os.unlink(symlink_path)
+      os.symlink(profile_target, symlink_path)
+
+  if symlink_path is not None:
+    print("Wrote profiles to {} (symlink {})".format(profile_target,
+                                                     symlink_path))
+  else:
+    print("Wrote profiles to {}".format(profile_target))
+
+  print("The raw-trace file can be viewed using https://ui.perfetto.dev.")
+  print("The heap_dump.* files can be viewed using pprof/ (Googlers only) " +
+        "or https://www.speedscope.app/.")
+  print("The two above are equivalent. The raw-trace contains the union of " +
+        "all the heap dumps.")
+
+
+if __name__ == '__main__':
+  sys.exit(main(sys.argv))
diff --git a/python/tools/record_android_trace.py b/python/tools/record_android_trace.py
new file mode 100644
index 0000000..ed62c7e
--- /dev/null
+++ b/python/tools/record_android_trace.py
@@ -0,0 +1,409 @@
+#!/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 time
+import webbrowser
+
+from perfetto.prebuilts.manifests.tracebox import *
+from perfetto.prebuilts.perfetto_prebuilts import *
+from perfetto.common.repo_utils import *
+
+# 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 = repo_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)
+
+  help = 'Specify the ADB device serial'
+  parser.add_argument('--serial', '-s', default=None, 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)
+
+  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_MANIFEST, 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
+
+
+if __name__ == '__main__':
+  sys.exit(main())
diff --git a/python/tools/trace_processor.py b/python/tools/trace_processor.py
new file mode 100644
index 0000000..12d63e4
--- /dev/null
+++ b/python/tools/trace_processor.py
@@ -0,0 +1,33 @@
+#!/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.
+
+# This file should do the same thing when being invoked in any of these ways:
+# ./trace_processor
+# python trace_processor
+# bash trace_processor
+# cat ./trace_processor | bash
+# cat ./trace_processor | python -
+
+BASH_FALLBACK=""" "
+exec python3 - "$@" <<'#'EOF
+#"""  # yapf: disable
+
+from perfetto.prebuilts.manifests.trace_processor_shell import *
+from perfetto.prebuilts.perfetto_prebuilts import *
+
+if __name__ == '__main__':
+  run_perfetto_prebuilt(TRACE_PROCESSOR_SHELL_MANIFEST)
+
+#EOF
diff --git a/python/tools/tracebox.py b/python/tools/tracebox.py
new file mode 100644
index 0000000..5ca2898
--- /dev/null
+++ b/python/tools/tracebox.py
@@ -0,0 +1,33 @@
+#!/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.
+
+# This file should do the same thing when being invoked in any of these ways:
+# ./tracebox
+# python tracebox
+# bash tracebox
+# cat ./tracebox | bash
+# cat ./tracebox | python -
+
+BASH_FALLBACK=""" "
+exec python3 - "$@" <<'#'EOF
+#"""  # yapf: disable
+
+from perfetto.prebuilts.manifests.tracebox import *
+from perfetto.prebuilts.perfetto_prebuilts import *
+
+if __name__ == '__main__':
+  run_perfetto_prebuilt(TRACEBOX_MANIFEST)
+
+#EOF
diff --git a/python/tools/traceconv.py b/python/tools/traceconv.py
new file mode 100644
index 0000000..4a42baa
--- /dev/null
+++ b/python/tools/traceconv.py
@@ -0,0 +1,33 @@
+#!/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.
+
+# This file should do the same thing when being invoked in any of these ways:
+# ./traceconv
+# python traceconv
+# bash traceconv
+# cat ./traceconv | bash
+# cat ./traceconv | python -
+
+BASH_FALLBACK=""" "
+exec python3 - "$@" <<'#'EOF
+#"""  # yapf: disable
+
+from perfetto.prebuilts.manifests.traceconv import *
+from perfetto.prebuilts.perfetto_prebuilts import *
+
+if __name__ == '__main__':
+  run_perfetto_prebuilt(TRACECONV_MANIFEST)
+
+#EOF
diff --git a/tools/cpu_profile b/tools/cpu_profile
index 38fb15f..46cedbd 100755
--- a/tools/cpu_profile
+++ b/tools/cpu_profile
@@ -1,5 +1,4 @@
 #!/usr/bin/env python3
-
 # Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -15,6 +14,10 @@
 # limitations under the License.
 """Runs tracing with CPU profiling enabled, and symbolizes traces if requested.
 
+# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+# DO NOT EDIT. Auto-generated by tools/gen_amalgamated_python_tools
+# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
 For usage instructions, please see:
 https://perfetto.dev/docs/quickstart/callstack-sampling
 
@@ -32,6 +35,268 @@
 import time
 import uuid
 
+
+# ----- Amalgamator: begin of python/perfetto/prebuilts/manifests/traceconv.py
+# This file has been generated by: /s/perfetto/tools/roll-prebuilts v28.0
+TRACECONV_MANIFEST = [{
+    'arch':
+        'mac-amd64',
+    'file_name':
+        'traceconv',
+    'file_size':
+        7181712,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/mac-amd64/traceconv',
+    'sha256':
+        '6b398ad9539ddf8208536c0412db198d4627daa97efc7e0850f3e7ec0e115510',
+    'platform':
+        'darwin',
+    'machine': ['x86_64']
+}, {
+    'arch':
+        'mac-arm64',
+    'file_name':
+        'traceconv',
+    'file_size':
+        6025176,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/mac-arm64/traceconv',
+    'sha256':
+        '407e2988e795a593158304061c547093ad74419f826dd03c2a66911b5a29d065',
+    'platform':
+        'darwin',
+    'machine': ['arm64']
+}, {
+    'arch':
+        'linux-amd64',
+    'file_name':
+        'traceconv',
+    'file_size':
+        7668600,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/linux-amd64/traceconv',
+    'sha256':
+        '1bebc0dd7b2b18fd4abeeb5f811d6d4c7f431d212efd5469c7e5d8b18b19e0c7',
+    'platform':
+        'linux',
+    'machine': ['x86_64']
+}, {
+    'arch':
+        'linux-arm',
+    'file_name':
+        'traceconv',
+    'file_size':
+        5827680,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/linux-arm/traceconv',
+    'sha256':
+        'a19780145f965838e334a57a52230bc67b0db207365746360314fbbbe4e1d12f',
+    'platform':
+        'linux',
+    'machine': ['armv6l', 'armv7l', 'armv8l']
+}, {
+    'arch':
+        'linux-arm64',
+    'file_name':
+        'traceconv',
+    'file_size':
+        6876384,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/linux-arm64/traceconv',
+    'sha256':
+        '2a5e5fcf340070ed6a30204c79b7e76787c5f26181bc8377587547f3eb5df685',
+    'platform':
+        'linux',
+    'machine': ['aarch64']
+}, {
+    'arch':
+        'android-arm',
+    'file_name':
+        'traceconv',
+    'file_size':
+        4881820,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/android-arm/traceconv',
+    'sha256':
+        '73827b82d941a9650580fbd48c3d4ff2323eb8d4ff9d3fffd3e0cac1bc853f34'
+}, {
+    'arch':
+        'android-arm64',
+    'file_name':
+        'traceconv',
+    'file_size':
+        6222592,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/android-arm64/traceconv',
+    'sha256':
+        '72d46258645d486f40ee463052b609d1fd7c4cc64f70c0ba2ef811a9924be98e'
+}, {
+    'arch':
+        'android-x86',
+    'file_name':
+        'traceconv',
+    'file_size':
+        7089524,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/android-x86/traceconv',
+    'sha256':
+        '689d0b48f91624585285b3833362cdcfdf0de1ff5dedcb97bb9851c729b4a15e'
+}, {
+    'arch':
+        'android-x64',
+    'file_name':
+        'traceconv',
+    'file_size':
+        7316248,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/android-x64/traceconv',
+    'sha256':
+        '785ec3f0da302ed52521febc5ed5e2cef57ae8840ff241037c51b8d94464f6a2'
+}, {
+    'arch':
+        'windows-amd64',
+    'file_name':
+        'traceconv.exe',
+    'file_size':
+        6850048,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/windows-amd64/traceconv.exe',
+    'sha256':
+        '19cdec3824d369be3bb053b40b3cfe9f62c2e57e71a5e2ee17ca15b6e7463683',
+    'platform':
+        'win32',
+    'machine': ['amd64']
+}]
+
+# ----- Amalgamator: end of python/perfetto/prebuilts/manifests/traceconv.py
+
+# ----- Amalgamator: begin of python/perfetto/prebuilts/perfetto_prebuilts.py
+# 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.
+"""
+Functions to fetch pre-pinned Perfetto prebuilts.
+
+This function is used in different places:
+- Into the //tools/{trace_processor, traceconv} scripts, which are just plain
+  wrappers around executables.
+- Into the //tools/{heap_profiler, record_android_trace} scripts, which contain
+  some other hand-written python code.
+
+The manifest argument looks as follows:
+TRACECONV_MANIFEST = [
+  {
+    'arch': 'mac-amd64',
+    'file_name': 'traceconv',
+    'file_size': 7087080,
+    'url': https://commondatastorage.googleapis.com/.../trace_to_text',
+    'sha256': 7d957c005b0dc130f5bd855d6cec27e060d38841b320d04840afc569f9087490',
+    'platform': 'darwin',
+    'machine': 'x86_64'
+  },
+  ...
+]
+
+The intended usage is:
+
+  from perfetto.prebuilts.manifests.traceconv import TRACECONV_MANIFEST
+  bin_path = get_perfetto_prebuilt(TRACECONV_MANIFEST)
+  subprocess.call(bin_path, ...)
+"""
+
+import hashlib
+import os
+import platform
+import subprocess
+import sys
+
+
+def download_or_get_cached(file_name, url, sha256):
+  """ Downloads a prebuilt or returns a cached version
+
+  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.
+  """
+  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
+
+
+def get_perfetto_prebuilt(manifest, soft_fail=False, arch=None):
+  """ Downloads the prebuilt, if necessary, and returns its path on disk. """
+  plat = sys.platform.lower()
+  machine = platform.machine().lower()
+  manifest_entry = None
+  for entry in manifest:
+    # If the caller overrides the arch, just match that (for Android prebuilts).
+    if arch:
+      if entry.get('arch') == arch:
+        manifest_entry = entry
+        break
+      continue
+    # Otherwise guess the local machine arch.
+    if 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'])
+
+
+def run_perfetto_prebuilt(manifest):
+  bin_path = get_perfetto_prebuilt(manifest)
+  if sys.platform.lower() == 'win32':
+    sys.exit(subprocess.check_call([bin_path, *sys.argv[1:]]))
+  os.execv(bin_path, [bin_path] + sys.argv[1:])
+
+# ----- Amalgamator: end of python/perfetto/prebuilts/perfetto_prebuilts.py
+
 # Used for creating directories, etc.
 UUID = str(uuid.uuid4())[-6:]
 
@@ -329,7 +594,7 @@
 def get_traceconv():
   """Sets up and returns the path to `traceconv`."""
   try:
-    traceconv = get_perfetto_prebuilt('traceconv', soft_fail=True)
+    traceconv = get_perfetto_prebuilt(TRACECONV_MANIFEST, soft_fail=True)
   except Exception as error:
     exit_with_bug_report(error)
   if traceconv is None:
@@ -448,233 +713,5 @@
   return 0
 
 
-# BEGIN_SECTION_GENERATED_BY(roll-prebuilts)
-# Revision: v28.0
-PERFETTO_PREBUILT_MANIFEST = [{
-    'tool':
-        'traceconv',
-    'arch':
-        'mac-amd64',
-    'file_name':
-        'traceconv',
-    'file_size':
-        7181712,
-    'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/mac-amd64/traceconv',
-    'sha256':
-        '6b398ad9539ddf8208536c0412db198d4627daa97efc7e0850f3e7ec0e115510',
-    'platform':
-        'darwin',
-    'machine': ['x86_64']
-}, {
-    'tool':
-        'traceconv',
-    'arch':
-        'mac-arm64',
-    'file_name':
-        'traceconv',
-    'file_size':
-        6025176,
-    'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/mac-arm64/traceconv',
-    'sha256':
-        '407e2988e795a593158304061c547093ad74419f826dd03c2a66911b5a29d065',
-    'platform':
-        'darwin',
-    'machine': ['arm64']
-}, {
-    'tool':
-        'traceconv',
-    'arch':
-        'linux-amd64',
-    'file_name':
-        'traceconv',
-    'file_size':
-        7668600,
-    'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/linux-amd64/traceconv',
-    'sha256':
-        '1bebc0dd7b2b18fd4abeeb5f811d6d4c7f431d212efd5469c7e5d8b18b19e0c7',
-    'platform':
-        'linux',
-    'machine': ['x86_64']
-}, {
-    'tool':
-        'traceconv',
-    'arch':
-        'linux-arm',
-    'file_name':
-        'traceconv',
-    'file_size':
-        5827680,
-    'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/linux-arm/traceconv',
-    'sha256':
-        'a19780145f965838e334a57a52230bc67b0db207365746360314fbbbe4e1d12f',
-    'platform':
-        'linux',
-    'machine': ['armv6l', 'armv7l', 'armv8l']
-}, {
-    'tool':
-        'traceconv',
-    'arch':
-        'linux-arm64',
-    'file_name':
-        'traceconv',
-    'file_size':
-        6876384,
-    'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/linux-arm64/traceconv',
-    'sha256':
-        '2a5e5fcf340070ed6a30204c79b7e76787c5f26181bc8377587547f3eb5df685',
-    'platform':
-        'linux',
-    'machine': ['aarch64']
-}, {
-    'tool':
-        'traceconv',
-    'arch':
-        'android-arm',
-    'file_name':
-        'traceconv',
-    'file_size':
-        4881820,
-    'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/android-arm/traceconv',
-    'sha256':
-        '73827b82d941a9650580fbd48c3d4ff2323eb8d4ff9d3fffd3e0cac1bc853f34'
-}, {
-    'tool':
-        'traceconv',
-    'arch':
-        'android-arm64',
-    'file_name':
-        'traceconv',
-    'file_size':
-        6222592,
-    'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/android-arm64/traceconv',
-    'sha256':
-        '72d46258645d486f40ee463052b609d1fd7c4cc64f70c0ba2ef811a9924be98e'
-}, {
-    'tool':
-        'traceconv',
-    'arch':
-        'android-x86',
-    'file_name':
-        'traceconv',
-    'file_size':
-        7089524,
-    'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/android-x86/traceconv',
-    'sha256':
-        '689d0b48f91624585285b3833362cdcfdf0de1ff5dedcb97bb9851c729b4a15e'
-}, {
-    'tool':
-        'traceconv',
-    'arch':
-        'android-x64',
-    'file_name':
-        'traceconv',
-    'file_size':
-        7316248,
-    'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/android-x64/traceconv',
-    'sha256':
-        '785ec3f0da302ed52521febc5ed5e2cef57ae8840ff241037c51b8d94464f6a2'
-}, {
-    'tool':
-        'traceconv',
-    'arch':
-        'windows-amd64',
-    'file_name':
-        'traceconv.exe',
-    'file_size':
-        6850048,
-    'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/windows-amd64/traceconv.exe',
-    'sha256':
-        '19cdec3824d369be3bb053b40b3cfe9f62c2e57e71a5e2ee17ca15b6e7463683',
-    'platform':
-        'win32',
-    'machine': ['amd64']
-}]
-
-
-# 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:
-      if entry.get('arch') == arch:
-        manifest_entry = entry
-        break
-      continue
-    # 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))
diff --git a/tools/gen_all b/tools/gen_all
index bcf6bd3..ad2884f 100755
--- a/tools/gen_all
+++ b/tools/gen_all
@@ -58,6 +58,7 @@
       call('gen_bazel', *check_only)
       call('gen_android_bp', *check_only)
     call('gen_merged_protos', *check_only)
+    call('gen_amalgamated_python_tools', *check_only)
     call('ninja', '-C', out, 'protoc')
     call('gen_binary_descriptors', '--protoc', protoc_path(out), *check_only)
 
diff --git a/tools/gen_amalgamated_python_tools b/tools/gen_amalgamated_python_tools
new file mode 100755
index 0000000..4b21069
--- /dev/null
+++ b/tools/gen_amalgamated_python_tools
@@ -0,0 +1,110 @@
+#!/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.
+
+import os
+import sys
+import logging
+import re
+
+ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+AMALGAMATION_MAP = {
+    'python/tools/record_android_trace.py': 'tools/record_android_trace',
+    'python/tools/tracebox.py': 'tools/tracebox',
+    'python/tools/traceconv.py': 'tools/traceconv',
+    'python/tools/trace_processor.py': 'tools/trace_processor',
+    'python/tools/cpu_profile.py': 'tools/cpu_profile',
+    'python/tools/heap_profile.py': 'tools/heap_profile',
+}
+
+
+def amalgamate_file(fname, stack=None, done=None, in_progress=None):
+  stack = [] if stack is None else stack
+  done = set() if done is None else done
+  in_progress = set() if in_progress is None else in_progress
+  if fname in in_progress:
+    cycle = ' > '.join(stack + [fname])
+    logging.fatal('Cycle detected in %s', cycle)
+    sys.exit(1)
+  if fname in done:
+    return []
+  logging.debug('Processing %s', fname)
+  done.add(fname)
+  in_progress.add(fname)
+  with open(fname, encoding='utf-8') as f:
+    lines = f.readlines()
+  outlines = []
+  for line in lines:
+    if line.startswith('from perfetto') or line.startswith('import perfetto'):
+      if not re.match('from perfetto[.][.\w]+\s+import\s+[*]$', line):
+        logging.fatal('Error in %s on line \"%s\"', fname, line.rstrip())
+        logging.fatal('Only "from perfetto.foo import *" is supported in '
+                      'sources that are used in //tools and get amalgamated')
+        sys.exit(1)
+      pkg = line.split()[1]
+      fpath = os.path.join('python', pkg.replace('.', os.sep) + '.py')
+      outlines.append('\n# ----- Amalgamator: begin of %s\n' % fpath)
+      outlines += amalgamate_file(fpath, stack + [fname], done, in_progress)
+      outlines.append('\n# ----- Amalgamator: end of %s\n' % fpath)
+    elif '__file__' in line and not 'amalgamator:nocheck' in line:
+      logging.fatal('__file__ is not allowed in sources that get amalgamated.'
+                    'In %s on line \"%s\"', fname, line.rstrip())
+      sys.exit(1)
+
+    else:
+      outlines.append(line)
+  in_progress.remove(fname)
+  logging.debug('%s: %d lines', fname, len(outlines))
+  return outlines
+
+
+def amalgamate(src, dst, check_only=False):
+  lines = amalgamate_file(src)
+  banner = '''
+# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+# DO NOT EDIT. Auto-generated by %s
+# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+'''
+  lines.insert(lines.index('\n'), banner % os.path.relpath(__file__, ROOT_DIR))
+  new_content = ''.join(lines)
+
+  if check_only:
+    if not os.path.exists(dst):
+      return False
+    with open(dst, encoding='utf-8') as f:
+      return f.read() == new_content
+
+  logging.info('Amalgamating %s -> %s', src, dst)
+  with open(dst + '.tmp', 'w', encoding='utf-8') as f:
+    f.write(new_content)
+  os.chmod(dst + '.tmp', 0o755)
+  os.rename(dst + '.tmp', dst)
+  return True
+
+
+def main():
+  check_only = '--check-only' in sys.argv
+  logging.basicConfig(
+    format='%(levelname)-8s: %(message)s',
+    level=logging.DEBUG if '-v' in sys.argv else logging.INFO)
+  os.chdir(ROOT_DIR)  # Make the execution cwd-independent.
+  success = True
+  for src, dst in AMALGAMATION_MAP.items():
+    success = success and amalgamate(src, dst, check_only)
+  return 0 if success else 1
+
+
+if __name__ == '__main__':
+  sys.exit(main())
diff --git a/tools/get_perfetto_prebuilt.py b/tools/get_perfetto_prebuilt.py
deleted file mode 100644
index ad9248b..0000000
--- a/tools/get_perfetto_prebuilt.py
+++ /dev/null
@@ -1,124 +0,0 @@
-# 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.
-"""
-This source defines a self-contained function to fetch a perfetto prebuilt.
-
-This function is copy/pasted by //tools/roll-prebuilts in different places:
-- Into the //tools/{trace_processor, traceconv} scripts, which are just plain
-  wrappers around executables.
-- Into the //tools/{heap_profiler, record_android_trace} scripts, which contain
-  some other hand-written python code.
-In both cases toll-prebuilts copies this source (together with a manifest) into
-a section annotated with "BEGIN_SECTION_GENERATED_BY(roll-prebuilts)" / END... .
-The automated-copy-paste is to keep those script hermetic, so people can just
-download and run them without checking out the repo.
-
-The manifest argument looks as follows in the generated files:
-PERFETTO_PREBUILT_MANIFEST = [{
-    'tool': 'traceconv',
-    'arch': 'mac-amd64',
-    'file_name': 'traceconv',
-    'file_size': 7087080,
-    'url': https://commondatastorage.googleapis.com/.../trace_to_text',
-    'sha256': 7d957c005b0dc130f5bd855d6cec27e060d38841b320d04840afc569f9087490',
-    'platform': 'darwin',
-    'machine': 'x86_64'
-  },
-  ...
-]
-
-The intended usage is:
-
-bin_path = get_perfetto_prebuilt('trace_processor_shell')
-subprocess.call(bin_path, ...)
-"""
-
-from logging import exception
-
-PERFETTO_PREBUILT_MANIFEST = []
-
-# COPIED_SECTION_START_MARKER
-
-
-# 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:
-      if entry.get('arch') == arch:
-        manifest_entry = entry
-        break
-      continue
-    # 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'])
diff --git a/tools/heap_profile b/tools/heap_profile
index e2c5a0e..c952191 100755
--- a/tools/heap_profile
+++ b/tools/heap_profile
@@ -1,5 +1,4 @@
 #!/usr/bin/env python3
-
 # Copyright (C) 2017 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,13 +13,16 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+# DO NOT EDIT. Auto-generated by tools/gen_amalgamated_python_tools
+# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
 from __future__ import absolute_import
 from __future__ import division
 from __future__ import print_function
 
 import argparse
 import atexit
-import hashlib
 import os
 import shutil
 import signal
@@ -30,6 +32,268 @@
 import time
 import uuid
 
+
+# ----- Amalgamator: begin of python/perfetto/prebuilts/manifests/traceconv.py
+# This file has been generated by: /s/perfetto/tools/roll-prebuilts v28.0
+TRACECONV_MANIFEST = [{
+    'arch':
+        'mac-amd64',
+    'file_name':
+        'traceconv',
+    'file_size':
+        7181712,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/mac-amd64/traceconv',
+    'sha256':
+        '6b398ad9539ddf8208536c0412db198d4627daa97efc7e0850f3e7ec0e115510',
+    'platform':
+        'darwin',
+    'machine': ['x86_64']
+}, {
+    'arch':
+        'mac-arm64',
+    'file_name':
+        'traceconv',
+    'file_size':
+        6025176,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/mac-arm64/traceconv',
+    'sha256':
+        '407e2988e795a593158304061c547093ad74419f826dd03c2a66911b5a29d065',
+    'platform':
+        'darwin',
+    'machine': ['arm64']
+}, {
+    'arch':
+        'linux-amd64',
+    'file_name':
+        'traceconv',
+    'file_size':
+        7668600,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/linux-amd64/traceconv',
+    'sha256':
+        '1bebc0dd7b2b18fd4abeeb5f811d6d4c7f431d212efd5469c7e5d8b18b19e0c7',
+    'platform':
+        'linux',
+    'machine': ['x86_64']
+}, {
+    'arch':
+        'linux-arm',
+    'file_name':
+        'traceconv',
+    'file_size':
+        5827680,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/linux-arm/traceconv',
+    'sha256':
+        'a19780145f965838e334a57a52230bc67b0db207365746360314fbbbe4e1d12f',
+    'platform':
+        'linux',
+    'machine': ['armv6l', 'armv7l', 'armv8l']
+}, {
+    'arch':
+        'linux-arm64',
+    'file_name':
+        'traceconv',
+    'file_size':
+        6876384,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/linux-arm64/traceconv',
+    'sha256':
+        '2a5e5fcf340070ed6a30204c79b7e76787c5f26181bc8377587547f3eb5df685',
+    'platform':
+        'linux',
+    'machine': ['aarch64']
+}, {
+    'arch':
+        'android-arm',
+    'file_name':
+        'traceconv',
+    'file_size':
+        4881820,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/android-arm/traceconv',
+    'sha256':
+        '73827b82d941a9650580fbd48c3d4ff2323eb8d4ff9d3fffd3e0cac1bc853f34'
+}, {
+    'arch':
+        'android-arm64',
+    'file_name':
+        'traceconv',
+    'file_size':
+        6222592,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/android-arm64/traceconv',
+    'sha256':
+        '72d46258645d486f40ee463052b609d1fd7c4cc64f70c0ba2ef811a9924be98e'
+}, {
+    'arch':
+        'android-x86',
+    'file_name':
+        'traceconv',
+    'file_size':
+        7089524,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/android-x86/traceconv',
+    'sha256':
+        '689d0b48f91624585285b3833362cdcfdf0de1ff5dedcb97bb9851c729b4a15e'
+}, {
+    'arch':
+        'android-x64',
+    'file_name':
+        'traceconv',
+    'file_size':
+        7316248,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/android-x64/traceconv',
+    'sha256':
+        '785ec3f0da302ed52521febc5ed5e2cef57ae8840ff241037c51b8d94464f6a2'
+}, {
+    'arch':
+        'windows-amd64',
+    'file_name':
+        'traceconv.exe',
+    'file_size':
+        6850048,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/windows-amd64/traceconv.exe',
+    'sha256':
+        '19cdec3824d369be3bb053b40b3cfe9f62c2e57e71a5e2ee17ca15b6e7463683',
+    'platform':
+        'win32',
+    'machine': ['amd64']
+}]
+
+# ----- Amalgamator: end of python/perfetto/prebuilts/manifests/traceconv.py
+
+# ----- Amalgamator: begin of python/perfetto/prebuilts/perfetto_prebuilts.py
+# 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.
+"""
+Functions to fetch pre-pinned Perfetto prebuilts.
+
+This function is used in different places:
+- Into the //tools/{trace_processor, traceconv} scripts, which are just plain
+  wrappers around executables.
+- Into the //tools/{heap_profiler, record_android_trace} scripts, which contain
+  some other hand-written python code.
+
+The manifest argument looks as follows:
+TRACECONV_MANIFEST = [
+  {
+    'arch': 'mac-amd64',
+    'file_name': 'traceconv',
+    'file_size': 7087080,
+    'url': https://commondatastorage.googleapis.com/.../trace_to_text',
+    'sha256': 7d957c005b0dc130f5bd855d6cec27e060d38841b320d04840afc569f9087490',
+    'platform': 'darwin',
+    'machine': 'x86_64'
+  },
+  ...
+]
+
+The intended usage is:
+
+  from perfetto.prebuilts.manifests.traceconv import TRACECONV_MANIFEST
+  bin_path = get_perfetto_prebuilt(TRACECONV_MANIFEST)
+  subprocess.call(bin_path, ...)
+"""
+
+import hashlib
+import os
+import platform
+import subprocess
+import sys
+
+
+def download_or_get_cached(file_name, url, sha256):
+  """ Downloads a prebuilt or returns a cached version
+
+  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.
+  """
+  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
+
+
+def get_perfetto_prebuilt(manifest, soft_fail=False, arch=None):
+  """ Downloads the prebuilt, if necessary, and returns its path on disk. """
+  plat = sys.platform.lower()
+  machine = platform.machine().lower()
+  manifest_entry = None
+  for entry in manifest:
+    # If the caller overrides the arch, just match that (for Android prebuilts).
+    if arch:
+      if entry.get('arch') == arch:
+        manifest_entry = entry
+        break
+      continue
+    # Otherwise guess the local machine arch.
+    if 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'])
+
+
+def run_perfetto_prebuilt(manifest):
+  bin_path = get_perfetto_prebuilt(manifest)
+  if sys.platform.lower() == 'win32':
+    sys.exit(subprocess.check_call([bin_path, *sys.argv[1:]]))
+  os.execv(bin_path, [bin_path] + sys.argv[1:])
+
+# ----- Amalgamator: end of python/perfetto/prebuilts/perfetto_prebuilts.py
+
 NULL = open(os.devnull)
 NOOUT = {
     'stdout': NULL,
@@ -367,7 +631,7 @@
   # Do this AFTER print_config so we do not download traceconv only to
   # print out the config.
   if traceconv_binary is None:
-    traceconv_binary = get_perfetto_prebuilt('traceconv', soft_fail=True)
+    traceconv_binary = get_perfetto_prebuilt(TRACECONV_MANIFEST, soft_fail=True)
 
   known_issues = maybe_known_issues()
   if known_issues:
@@ -573,233 +837,5 @@
         "all the heap dumps.")
 
 
-# BEGIN_SECTION_GENERATED_BY(roll-prebuilts)
-# Revision: v28.0
-PERFETTO_PREBUILT_MANIFEST = [{
-    'tool':
-        'traceconv',
-    'arch':
-        'mac-amd64',
-    'file_name':
-        'traceconv',
-    'file_size':
-        7181712,
-    'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/mac-amd64/traceconv',
-    'sha256':
-        '6b398ad9539ddf8208536c0412db198d4627daa97efc7e0850f3e7ec0e115510',
-    'platform':
-        'darwin',
-    'machine': ['x86_64']
-}, {
-    'tool':
-        'traceconv',
-    'arch':
-        'mac-arm64',
-    'file_name':
-        'traceconv',
-    'file_size':
-        6025176,
-    'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/mac-arm64/traceconv',
-    'sha256':
-        '407e2988e795a593158304061c547093ad74419f826dd03c2a66911b5a29d065',
-    'platform':
-        'darwin',
-    'machine': ['arm64']
-}, {
-    'tool':
-        'traceconv',
-    'arch':
-        'linux-amd64',
-    'file_name':
-        'traceconv',
-    'file_size':
-        7668600,
-    'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/linux-amd64/traceconv',
-    'sha256':
-        '1bebc0dd7b2b18fd4abeeb5f811d6d4c7f431d212efd5469c7e5d8b18b19e0c7',
-    'platform':
-        'linux',
-    'machine': ['x86_64']
-}, {
-    'tool':
-        'traceconv',
-    'arch':
-        'linux-arm',
-    'file_name':
-        'traceconv',
-    'file_size':
-        5827680,
-    'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/linux-arm/traceconv',
-    'sha256':
-        'a19780145f965838e334a57a52230bc67b0db207365746360314fbbbe4e1d12f',
-    'platform':
-        'linux',
-    'machine': ['armv6l', 'armv7l', 'armv8l']
-}, {
-    'tool':
-        'traceconv',
-    'arch':
-        'linux-arm64',
-    'file_name':
-        'traceconv',
-    'file_size':
-        6876384,
-    'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/linux-arm64/traceconv',
-    'sha256':
-        '2a5e5fcf340070ed6a30204c79b7e76787c5f26181bc8377587547f3eb5df685',
-    'platform':
-        'linux',
-    'machine': ['aarch64']
-}, {
-    'tool':
-        'traceconv',
-    'arch':
-        'android-arm',
-    'file_name':
-        'traceconv',
-    'file_size':
-        4881820,
-    'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/android-arm/traceconv',
-    'sha256':
-        '73827b82d941a9650580fbd48c3d4ff2323eb8d4ff9d3fffd3e0cac1bc853f34'
-}, {
-    'tool':
-        'traceconv',
-    'arch':
-        'android-arm64',
-    'file_name':
-        'traceconv',
-    'file_size':
-        6222592,
-    'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/android-arm64/traceconv',
-    'sha256':
-        '72d46258645d486f40ee463052b609d1fd7c4cc64f70c0ba2ef811a9924be98e'
-}, {
-    'tool':
-        'traceconv',
-    'arch':
-        'android-x86',
-    'file_name':
-        'traceconv',
-    'file_size':
-        7089524,
-    'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/android-x86/traceconv',
-    'sha256':
-        '689d0b48f91624585285b3833362cdcfdf0de1ff5dedcb97bb9851c729b4a15e'
-}, {
-    'tool':
-        'traceconv',
-    'arch':
-        'android-x64',
-    'file_name':
-        'traceconv',
-    'file_size':
-        7316248,
-    'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/android-x64/traceconv',
-    'sha256':
-        '785ec3f0da302ed52521febc5ed5e2cef57ae8840ff241037c51b8d94464f6a2'
-}, {
-    'tool':
-        'traceconv',
-    'arch':
-        'windows-amd64',
-    'file_name':
-        'traceconv.exe',
-    'file_size':
-        6850048,
-    'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/windows-amd64/traceconv.exe',
-    'sha256':
-        '19cdec3824d369be3bb053b40b3cfe9f62c2e57e71a5e2ee17ca15b6e7463683',
-    'platform':
-        'win32',
-    'machine': ['amd64']
-}]
-
-
-# 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:
-      if entry.get('arch') == arch:
-        manifest_entry = entry
-        break
-      continue
-    # 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))
diff --git a/tools/record_android_trace b/tools/record_android_trace
index 35915c0..c507a2a 100755
--- a/tools/record_android_trace
+++ b/tools/record_android_trace
@@ -13,6 +13,10 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+# DO NOT EDIT. Auto-generated by tools/gen_amalgamated_python_tools
+# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
 import atexit
 import argparse
 import datetime
@@ -24,15 +28,299 @@
 import socketserver
 import subprocess
 import sys
-import tempfile
 import time
 import webbrowser
 
-ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+# ----- Amalgamator: begin of python/perfetto/prebuilts/manifests/tracebox.py
+# This file has been generated by: /s/perfetto/tools/roll-prebuilts v28.0
+TRACEBOX_MANIFEST = [{
+    'arch':
+        'mac-amd64',
+    'file_name':
+        'tracebox',
+    'file_size':
+        1399080,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/mac-amd64/tracebox',
+    'sha256':
+        '05674f9872a14bf92d4215a180c883ab54c58376d7e08b95e3f83d03efdeba21',
+    'platform':
+        'darwin',
+    'machine': ['x86_64']
+}, {
+    'arch':
+        'mac-arm64',
+    'file_name':
+        'tracebox',
+    'file_size':
+        1292504,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/mac-arm64/tracebox',
+    'sha256':
+        '48008e6aeb7680c4f31b1147d5c26a504368a696c793aec1895bb5fb1f597f64',
+    'platform':
+        'darwin',
+    'machine': ['arm64']
+}, {
+    'arch':
+        'linux-amd64',
+    'file_name':
+        'tracebox',
+    'file_size':
+        2243632,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/linux-amd64/tracebox',
+    'sha256':
+        '12c87ba1d03e39d4c07792e09f0be9e240677cec2d9bc1638b0a676bc664d724',
+    'platform':
+        'linux',
+    'machine': ['x86_64']
+}, {
+    'arch':
+        'linux-arm',
+    'file_name':
+        'tracebox',
+    'file_size':
+        1315252,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/linux-arm/tracebox',
+    'sha256':
+        '3d026fe982e90fb5bc410528f210458f9bcafea30d3e1b9728772fd32148ee9a',
+    'platform':
+        'linux',
+    'machine': ['armv6l', 'armv7l', 'armv8l']
+}, {
+    'arch':
+        'linux-arm64',
+    'file_name':
+        'tracebox',
+    'file_size':
+        2089160,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/linux-arm64/tracebox',
+    'sha256':
+        '9f524f943a1c12dcb6b273e78d5c46952d4a7528514639cd2537686d5c530c89',
+    'platform':
+        'linux',
+    'machine': ['aarch64']
+}, {
+    'arch':
+        'android-arm',
+    'file_name':
+        'tracebox',
+    'file_size':
+        1099732,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/android-arm/tracebox',
+    'sha256':
+        '53a1a65e2e409a552cd75f003c755b5903075e37e06841b3a514889306fb9616'
+}, {
+    'arch':
+        'android-arm64',
+    'file_name':
+        'tracebox',
+    'file_size':
+        1653416,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/android-arm64/tracebox',
+    'sha256':
+        'df911aa2e5d80d1fdda5860e1d18dca7ba13c835aada090bb07e8f253562129a'
+}, {
+    'arch':
+        'android-x86',
+    'file_name':
+        'tracebox',
+    'file_size':
+        1677228,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/android-x86/tracebox',
+    'sha256':
+        '8d1a296a084f488f826dac837530a40f2ca38af76c70a10966e11f9fec91a2f3'
+}, {
+    'arch':
+        'android-x64',
+    'file_name':
+        'tracebox',
+    'file_size':
+        1911464,
+    'url':
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/android-x64/tracebox',
+    'sha256':
+        '8f844f4d8263d0ff904d120b5858a02d7a24efd87f49c3fa4fd8682d534c738f'
+}]
+
+# ----- Amalgamator: end of python/perfetto/prebuilts/manifests/tracebox.py
+
+# ----- Amalgamator: begin of python/perfetto/prebuilts/perfetto_prebuilts.py
+# 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.
+"""
+Functions to fetch pre-pinned Perfetto prebuilts.
+
+This function is used in different places:
+- Into the //tools/{trace_processor, traceconv} scripts, which are just plain
+  wrappers around executables.
+- Into the //tools/{heap_profiler, record_android_trace} scripts, which contain
+  some other hand-written python code.
+
+The manifest argument looks as follows:
+TRACECONV_MANIFEST = [
+  {
+    'arch': 'mac-amd64',
+    'file_name': 'traceconv',
+    'file_size': 7087080,
+    'url': https://commondatastorage.googleapis.com/.../trace_to_text',
+    'sha256': 7d957c005b0dc130f5bd855d6cec27e060d38841b320d04840afc569f9087490',
+    'platform': 'darwin',
+    'machine': 'x86_64'
+  },
+  ...
+]
+
+The intended usage is:
+
+  from perfetto.prebuilts.manifests.traceconv import TRACECONV_MANIFEST
+  bin_path = get_perfetto_prebuilt(TRACECONV_MANIFEST)
+  subprocess.call(bin_path, ...)
+"""
+
+import hashlib
+import os
+import platform
+import subprocess
+import sys
+
+
+def download_or_get_cached(file_name, url, sha256):
+  """ Downloads a prebuilt or returns a cached version
+
+  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.
+  """
+  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
+
+
+def get_perfetto_prebuilt(manifest, soft_fail=False, arch=None):
+  """ Downloads the prebuilt, if necessary, and returns its path on disk. """
+  plat = sys.platform.lower()
+  machine = platform.machine().lower()
+  manifest_entry = None
+  for entry in manifest:
+    # If the caller overrides the arch, just match that (for Android prebuilts).
+    if arch:
+      if entry.get('arch') == arch:
+        manifest_entry = entry
+        break
+      continue
+    # Otherwise guess the local machine arch.
+    if 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'])
+
+
+def run_perfetto_prebuilt(manifest):
+  bin_path = get_perfetto_prebuilt(manifest)
+  if sys.platform.lower() == 'win32':
+    sys.exit(subprocess.check_call([bin_path, *sys.argv[1:]]))
+  os.execv(bin_path, [bin_path] + sys.argv[1:])
+
+# ----- Amalgamator: end of python/perfetto/prebuilts/perfetto_prebuilts.py
+
+# ----- Amalgamator: begin of python/perfetto/common/repo_utils.py
+# 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 os
+
+
+def repo_root():
+  """ Finds the repo root by traversing up the hierarchy
+
+  This is for use in scripts that get amalgamated, where _file_ can be either
+  python/perfetto/... or tools/amalgamated_tool.
+  """
+  path = os.path.dirname(os.path.abspath(__file__))  # amalgamator:nocheck
+  last_dir = ''
+  while path and path != last_dir:
+    if os.path.exists(os.path.join(path, 'perfetto.rc')):
+      return path
+    last_dir = path
+    path = os.path.dirname(path)
+  return None
+
+
+def repo_dir(rel_path):
+  return os.path.join(repo_root() or '', rel_path)
+
+# ----- Amalgamator: end of python/perfetto/common/repo_utils.py
 
 # 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'
+HERMETIC_ADB_PATH = repo_dir('/buildtools/android_sdk/platform-tools/adb')
 
 # Translates the Android ro.product.cpu.abi into the GN's target_cpu.
 ABI_TO_ARCH = {
@@ -200,7 +488,8 @@
   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)
+      tracebox_bin = get_perfetto_prebuilt(
+          TRACEBOX_MANIFEST, 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()
@@ -403,217 +692,5 @@
     return file_hash == sha_value
 
 
-# BEGIN_SECTION_GENERATED_BY(roll-prebuilts)
-# Revision: v28.0
-PERFETTO_PREBUILT_MANIFEST = [{
-    'tool':
-        'tracebox',
-    'arch':
-        'mac-amd64',
-    'file_name':
-        'tracebox',
-    'file_size':
-        1399080,
-    'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/mac-amd64/tracebox',
-    'sha256':
-        '05674f9872a14bf92d4215a180c883ab54c58376d7e08b95e3f83d03efdeba21',
-    'platform':
-        'darwin',
-    'machine': ['x86_64']
-}, {
-    'tool':
-        'tracebox',
-    'arch':
-        'mac-arm64',
-    'file_name':
-        'tracebox',
-    'file_size':
-        1292504,
-    'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/mac-arm64/tracebox',
-    'sha256':
-        '48008e6aeb7680c4f31b1147d5c26a504368a696c793aec1895bb5fb1f597f64',
-    'platform':
-        'darwin',
-    'machine': ['arm64']
-}, {
-    'tool':
-        'tracebox',
-    'arch':
-        'linux-amd64',
-    'file_name':
-        'tracebox',
-    'file_size':
-        2243632,
-    'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/linux-amd64/tracebox',
-    'sha256':
-        '12c87ba1d03e39d4c07792e09f0be9e240677cec2d9bc1638b0a676bc664d724',
-    'platform':
-        'linux',
-    'machine': ['x86_64']
-}, {
-    'tool':
-        'tracebox',
-    'arch':
-        'linux-arm',
-    'file_name':
-        'tracebox',
-    'file_size':
-        1315252,
-    'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/linux-arm/tracebox',
-    'sha256':
-        '3d026fe982e90fb5bc410528f210458f9bcafea30d3e1b9728772fd32148ee9a',
-    'platform':
-        'linux',
-    'machine': ['armv6l', 'armv7l', 'armv8l']
-}, {
-    'tool':
-        'tracebox',
-    'arch':
-        'linux-arm64',
-    'file_name':
-        'tracebox',
-    'file_size':
-        2089160,
-    'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/linux-arm64/tracebox',
-    'sha256':
-        '9f524f943a1c12dcb6b273e78d5c46952d4a7528514639cd2537686d5c530c89',
-    'platform':
-        'linux',
-    'machine': ['aarch64']
-}, {
-    'tool':
-        'tracebox',
-    'arch':
-        'android-arm',
-    'file_name':
-        'tracebox',
-    'file_size':
-        1099732,
-    'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/android-arm/tracebox',
-    'sha256':
-        '53a1a65e2e409a552cd75f003c755b5903075e37e06841b3a514889306fb9616'
-}, {
-    'tool':
-        'tracebox',
-    'arch':
-        'android-arm64',
-    'file_name':
-        'tracebox',
-    'file_size':
-        1653416,
-    'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/android-arm64/tracebox',
-    'sha256':
-        'df911aa2e5d80d1fdda5860e1d18dca7ba13c835aada090bb07e8f253562129a'
-}, {
-    'tool':
-        'tracebox',
-    'arch':
-        'android-x86',
-    'file_name':
-        'tracebox',
-    'file_size':
-        1677228,
-    'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/android-x86/tracebox',
-    'sha256':
-        '8d1a296a084f488f826dac837530a40f2ca38af76c70a10966e11f9fec91a2f3'
-}, {
-    'tool':
-        'tracebox',
-    'arch':
-        'android-x64',
-    'file_name':
-        'tracebox',
-    'file_size':
-        1911464,
-    'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v28.0/android-x64/tracebox',
-    'sha256':
-        '8f844f4d8263d0ff904d120b5858a02d7a24efd87f49c3fa4fd8682d534c738f'
-}]
-
-
-# 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:
-      if entry.get('arch') == arch:
-        manifest_entry = entry
-        break
-      continue
-    # 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())
diff --git a/tools/roll-prebuilts b/tools/roll-prebuilts
index 50ef65d..08666ad 100755
--- a/tools/roll-prebuilts
+++ b/tools/roll-prebuilts
@@ -12,9 +12,9 @@
 # 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.
-"""Updates the python scripts in tools/{trace_processor, traceconv, tracebox}
+"""Updates the python scripts in python/perfetto/prebuilts/manifests
 
-This script does the following, for each entry in SCRIPTS_TO_UPDATE:
+This script does the following, for each entry in MANIFESTS_TO_UPDATE:
   - Downloads the artifact by the LUCI infrastructure, one for each arch.
   - Computes the SHA-256 of each artifact.
   - Generates a manifest with URL, SHA-256 and other details.
@@ -36,7 +36,7 @@
 GCS_URL = 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts'
 
 ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
-TOOLS_DIR = os.path.join(ROOT_DIR, 'tools')
+MANIFESTS_DIR = os.path.join(ROOT_DIR, 'python/perfetto/prebuilts/manifests')
 
 UNIX_ARCHS = [
     'mac-amd64',
@@ -51,39 +51,16 @@
 ]
 ALL_ARCHS = UNIX_ARCHS + ['windows-amd64']
 
-SCRIPTS_TO_UPDATE = [
-    # Scripts using trace_processor_shell.
+MANIFESTS_TO_UPDATE = [
     {
-        'script': 'trace_processor',
         'tool': 'trace_processor_shell',
         'archs': ALL_ARCHS
     },
-
-    # Scripts using traceconv.
     {
-        'script': 'traceconv',
         'tool': 'traceconv',
         'archs': ALL_ARCHS
     },
     {
-        'script': 'heap_profile',
-        'tool': 'traceconv',
-        'archs': ALL_ARCHS
-    },
-    {
-        'script': 'cpu_profile',
-        'tool': 'traceconv',
-        'archs': ALL_ARCHS
-    },
-
-    # Scripts using tracebox.
-    {
-        'script': 'tracebox',
-        'tool': 'tracebox',
-        'archs': UNIX_ARCHS
-    },
-    {
-        'script': 'record_android_trace',
         'tool': 'tracebox',
         'archs': UNIX_ARCHS
     },
@@ -127,7 +104,6 @@
   logging.info('Downloading %s', url)
   data = subprocess.check_output(['curl', '-fsL', '-o', '-', url])
   manifest = {
-      'tool': tool,
       'arch': arch,
       'file_name': file_name,
       'file_size': len(data),
@@ -138,41 +114,23 @@
   return manifest
 
 
-# Returns the section of get_perfetto_prebuilt.py which should be copy/pasted
-# in the various scripts.
-def read_get_perfetto_prebuilt_script():
-  in_file = os.path.join(TOOLS_DIR, 'get_perfetto_prebuilt.py')
-  with open(in_file, 'r') as f:
-    contents = f.read()
-  return contents.split('COPIED_SECTION_START_MARKER')[1]
-
-
-def update_script(git_revision, tool_name, script_name, archs):
+def update_manifest(git_revision, tool_name, archs):
   with ThreadPoolExecutor(max_workers=8) as executor:
     manifests = list(
         executor.map(lambda arch: make_manifest(git_revision, tool_name, arch),
                      archs))
-  out_file = os.path.join(TOOLS_DIR, script_name)
-  with open(out_file) as f:
-    script = f.read()
+  out_file = os.path.join(MANIFESTS_DIR, tool_name + '.py')
 
-  begin_marker = '\n# BEGIN_SECTION_GENERATED_BY(roll-prebuilts)\n'
-  end_marker = '\n# END_SECTION_GENERATED_BY(roll-prebuilts)\n'
-  before = script.partition(begin_marker)[0]
-  after = script.partition(end_marker)[2]
-
-  content = '# Revision: {git_revision}\n'
-  content += 'PERFETTO_PREBUILT_MANIFEST = {manifests}\n'
-  content += '{fn_body}\n'
+  content = '# This file has been generated by: {script} {git_revision}\n'
+  content += '{tool_uppercase}_MANIFEST = {manifests}\n'
   content = content.format(
+      script=__file__,
+      tool_uppercase=tool_name.upper(),
       git_revision=git_revision,
-      manifests=str(manifests),
-      fn_body=read_get_perfetto_prebuilt_script())
-
-  script = before + begin_marker + content + end_marker + after
+      manifests=str(manifests))
 
   with open(out_file + '.tmp', 'w') as f:
-    f.write(script)
+    f.write(content)
   subprocess.check_call(['yapf', '-i', out_file + '.tmp'])
   os.rename(out_file + '.tmp', out_file)
   os.chmod(out_file, 0o755)
@@ -188,9 +146,9 @@
   args = parser.parse_args()
 
   git_revision = args.version
-  for spec in SCRIPTS_TO_UPDATE:
-    logging.info('Updating %s', spec['script'])
-    update_script(git_revision, spec['tool'], spec['script'], spec['archs'])
+  for spec in MANIFESTS_TO_UPDATE:
+    logging.info('Updating %s', spec['tool'])
+    update_manifest(git_revision, spec['tool'], spec['archs'])
 
 
 if __name__ == '__main__':
diff --git a/tools/trace_processor b/tools/trace_processor
index 48d13b1..18354b2 100755
--- a/tools/trace_processor
+++ b/tools/trace_processor
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright (C) 2019 The Android Open Source Project
+# 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.
@@ -13,6 +13,10 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+# DO NOT EDIT. Auto-generated by tools/gen_amalgamated_python_tools
+# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
 # This file should do the same thing when being invoked in any of these ways:
 # ./trace_processor
 # python trace_processor
@@ -20,17 +24,14 @@
 # cat ./trace_processor | bash
 # cat ./trace_processor | python -
 
-BASH_FALLBACK = """ "
+BASH_FALLBACK=""" "
 exec python3 - "$@" <<'#'EOF
-#"""
+#"""  # yapf: disable
 
-TOOL_NAME = 'trace_processor_shell'
 
-# BEGIN_SECTION_GENERATED_BY(roll-prebuilts)
-# Revision: v28.0
-PERFETTO_PREBUILT_MANIFEST = [{
-    'tool':
-        'trace_processor_shell',
+# ----- Amalgamator: begin of python/perfetto/prebuilts/manifests/trace_processor_shell.py
+# This file has been generated by: /s/perfetto/tools/roll-prebuilts v28.0
+TRACE_PROCESSOR_SHELL_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
@@ -45,8 +46,6 @@
         'darwin',
     'machine': ['x86_64']
 }, {
-    'tool':
-        'trace_processor_shell',
     'arch':
         'mac-arm64',
     'file_name':
@@ -61,8 +60,6 @@
         'darwin',
     'machine': ['arm64']
 }, {
-    'tool':
-        'trace_processor_shell',
     'arch':
         'linux-amd64',
     'file_name':
@@ -77,8 +74,6 @@
         'linux',
     'machine': ['x86_64']
 }, {
-    'tool':
-        'trace_processor_shell',
     'arch':
         'linux-arm',
     'file_name':
@@ -93,8 +88,6 @@
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
 }, {
-    'tool':
-        'trace_processor_shell',
     'arch':
         'linux-arm64',
     'file_name':
@@ -109,8 +102,6 @@
         'linux',
     'machine': ['aarch64']
 }, {
-    'tool':
-        'trace_processor_shell',
     'arch':
         'android-arm',
     'file_name':
@@ -122,8 +113,6 @@
     'sha256':
         '75285ac8964e93890b936bfacd2679ee67ae4cd029be74e675253f92746f9904'
 }, {
-    'tool':
-        'trace_processor_shell',
     'arch':
         'android-arm64',
     'file_name':
@@ -135,8 +124,6 @@
     'sha256':
         '75118859c9a1b2ed2bcffc664eb98884dab26a9d2ad7788d05d90ef22b24a8d5'
 }, {
-    'tool':
-        'trace_processor_shell',
     'arch':
         'android-x86',
     'file_name':
@@ -148,8 +135,6 @@
     'sha256':
         'c3d4da7aa12a2eedd59df98b5c8143658f39ebd9a4ba3c8180be33ab3484e824'
 }, {
-    'tool':
-        'trace_processor_shell',
     'arch':
         'android-x64',
     'file_name':
@@ -161,8 +146,6 @@
     'sha256':
         '105c70a3908bf6997b428ee7f74bf7ee8a48907b860480c87e62a62cf92e1a8a'
 }, {
-    'tool':
-        'trace_processor_shell',
     'arch':
         'windows-amd64',
     'file_name':
@@ -178,56 +161,104 @@
     'machine': ['amd64']
 }]
 
+# ----- Amalgamator: end of python/perfetto/prebuilts/manifests/trace_processor_shell.py
 
-# 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):
+# ----- Amalgamator: begin of python/perfetto/prebuilts/perfetto_prebuilts.py
+# 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.
+"""
+Functions to fetch pre-pinned Perfetto prebuilts.
+
+This function is used in different places:
+- Into the //tools/{trace_processor, traceconv} scripts, which are just plain
+  wrappers around executables.
+- Into the //tools/{heap_profiler, record_android_trace} scripts, which contain
+  some other hand-written python code.
+
+The manifest argument looks as follows:
+TRACECONV_MANIFEST = [
+  {
+    'arch': 'mac-amd64',
+    'file_name': 'traceconv',
+    'file_size': 7087080,
+    'url': https://commondatastorage.googleapis.com/.../trace_to_text',
+    'sha256': 7d957c005b0dc130f5bd855d6cec27e060d38841b320d04840afc569f9087490',
+    'platform': 'darwin',
+    'machine': 'x86_64'
+  },
+  ...
+]
+
+The intended usage is:
+
+  from perfetto.prebuilts.manifests.traceconv import TRACECONV_MANIFEST
+  bin_path = get_perfetto_prebuilt(TRACECONV_MANIFEST)
+  subprocess.call(bin_path, ...)
+"""
+
+import hashlib
+import os
+import platform
+import subprocess
+import sys
+
+
+def download_or_get_cached(file_name, url, sha256):
+  """ Downloads a prebuilt or returns a cached version
+
+  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.
+  """
+  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
+
+
+def get_perfetto_prebuilt(manifest, 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:
+  for entry in manifest:
     # If the caller overrides the arch, just match that (for Android prebuilts).
     if arch:
       if entry.get('arch') == arch:
@@ -235,8 +266,7 @@
         break
       continue
     # Otherwise guess the local machine arch.
-    if entry.get('tool') == tool_name and entry.get(
-        'platform') == plat and machine in entry.get('machine', []):
+    if entry.get('platform') == plat and machine in entry.get('machine', []):
       manifest_entry = entry
       break
   if manifest_entry is None:
@@ -252,13 +282,15 @@
       sha256=manifest_entry['sha256'])
 
 
-# END_SECTION_GENERATED_BY(roll-prebuilts)
+def run_perfetto_prebuilt(manifest):
+  bin_path = get_perfetto_prebuilt(manifest)
+  if sys.platform.lower() == 'win32':
+    sys.exit(subprocess.check_call([bin_path, *sys.argv[1:]]))
+  os.execv(bin_path, [bin_path] + sys.argv[1:])
+
+# ----- Amalgamator: end of python/perfetto/prebuilts/perfetto_prebuilts.py
 
 if __name__ == '__main__':
-  import sys, subprocess, os
-  bin_path = get_perfetto_prebuilt(TOOL_NAME)
-  if sys.platform.lower() == 'win32':
-    exit(subprocess.check_call([bin_path, *sys.argv[1:]]))
-  os.execv(bin_path, [bin_path] + sys.argv[1:])
+  run_perfetto_prebuilt(TRACE_PROCESSOR_SHELL_MANIFEST)
 
 #EOF
diff --git a/tools/tracebox b/tools/tracebox
index 1d834cf..89eebea 100755
--- a/tools/tracebox
+++ b/tools/tracebox
@@ -13,6 +13,10 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+# DO NOT EDIT. Auto-generated by tools/gen_amalgamated_python_tools
+# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
 # This file should do the same thing when being invoked in any of these ways:
 # ./tracebox
 # python tracebox
@@ -20,17 +24,14 @@
 # cat ./tracebox | bash
 # cat ./tracebox | python -
 
-BASH_FALLBACK = """ "
+BASH_FALLBACK=""" "
 exec python3 - "$@" <<'#'EOF
-#"""
+#"""  # yapf: disable
 
-TOOL_NAME = 'tracebox'
 
-# BEGIN_SECTION_GENERATED_BY(roll-prebuilts)
-# Revision: v28.0
-PERFETTO_PREBUILT_MANIFEST = [{
-    'tool':
-        'tracebox',
+# ----- Amalgamator: begin of python/perfetto/prebuilts/manifests/tracebox.py
+# This file has been generated by: /s/perfetto/tools/roll-prebuilts v28.0
+TRACEBOX_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
@@ -45,8 +46,6 @@
         'darwin',
     'machine': ['x86_64']
 }, {
-    'tool':
-        'tracebox',
     'arch':
         'mac-arm64',
     'file_name':
@@ -61,8 +60,6 @@
         'darwin',
     'machine': ['arm64']
 }, {
-    'tool':
-        'tracebox',
     'arch':
         'linux-amd64',
     'file_name':
@@ -77,8 +74,6 @@
         'linux',
     'machine': ['x86_64']
 }, {
-    'tool':
-        'tracebox',
     'arch':
         'linux-arm',
     'file_name':
@@ -93,8 +88,6 @@
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
 }, {
-    'tool':
-        'tracebox',
     'arch':
         'linux-arm64',
     'file_name':
@@ -109,8 +102,6 @@
         'linux',
     'machine': ['aarch64']
 }, {
-    'tool':
-        'tracebox',
     'arch':
         'android-arm',
     'file_name':
@@ -122,8 +113,6 @@
     'sha256':
         '53a1a65e2e409a552cd75f003c755b5903075e37e06841b3a514889306fb9616'
 }, {
-    'tool':
-        'tracebox',
     'arch':
         'android-arm64',
     'file_name':
@@ -135,8 +124,6 @@
     'sha256':
         'df911aa2e5d80d1fdda5860e1d18dca7ba13c835aada090bb07e8f253562129a'
 }, {
-    'tool':
-        'tracebox',
     'arch':
         'android-x86',
     'file_name':
@@ -148,8 +135,6 @@
     'sha256':
         '8d1a296a084f488f826dac837530a40f2ca38af76c70a10966e11f9fec91a2f3'
 }, {
-    'tool':
-        'tracebox',
     'arch':
         'android-x64',
     'file_name':
@@ -162,56 +147,104 @@
         '8f844f4d8263d0ff904d120b5858a02d7a24efd87f49c3fa4fd8682d534c738f'
 }]
 
+# ----- Amalgamator: end of python/perfetto/prebuilts/manifests/tracebox.py
 
-# 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):
+# ----- Amalgamator: begin of python/perfetto/prebuilts/perfetto_prebuilts.py
+# 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.
+"""
+Functions to fetch pre-pinned Perfetto prebuilts.
+
+This function is used in different places:
+- Into the //tools/{trace_processor, traceconv} scripts, which are just plain
+  wrappers around executables.
+- Into the //tools/{heap_profiler, record_android_trace} scripts, which contain
+  some other hand-written python code.
+
+The manifest argument looks as follows:
+TRACECONV_MANIFEST = [
+  {
+    'arch': 'mac-amd64',
+    'file_name': 'traceconv',
+    'file_size': 7087080,
+    'url': https://commondatastorage.googleapis.com/.../trace_to_text',
+    'sha256': 7d957c005b0dc130f5bd855d6cec27e060d38841b320d04840afc569f9087490',
+    'platform': 'darwin',
+    'machine': 'x86_64'
+  },
+  ...
+]
+
+The intended usage is:
+
+  from perfetto.prebuilts.manifests.traceconv import TRACECONV_MANIFEST
+  bin_path = get_perfetto_prebuilt(TRACECONV_MANIFEST)
+  subprocess.call(bin_path, ...)
+"""
+
+import hashlib
+import os
+import platform
+import subprocess
+import sys
+
+
+def download_or_get_cached(file_name, url, sha256):
+  """ Downloads a prebuilt or returns a cached version
+
+  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.
+  """
+  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
+
+
+def get_perfetto_prebuilt(manifest, 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:
+  for entry in manifest:
     # If the caller overrides the arch, just match that (for Android prebuilts).
     if arch:
       if entry.get('arch') == arch:
@@ -219,8 +252,7 @@
         break
       continue
     # Otherwise guess the local machine arch.
-    if entry.get('tool') == tool_name and entry.get(
-        'platform') == plat and machine in entry.get('machine', []):
+    if entry.get('platform') == plat and machine in entry.get('machine', []):
       manifest_entry = entry
       break
   if manifest_entry is None:
@@ -236,13 +268,15 @@
       sha256=manifest_entry['sha256'])
 
 
-# END_SECTION_GENERATED_BY(roll-prebuilts)
+def run_perfetto_prebuilt(manifest):
+  bin_path = get_perfetto_prebuilt(manifest)
+  if sys.platform.lower() == 'win32':
+    sys.exit(subprocess.check_call([bin_path, *sys.argv[1:]]))
+  os.execv(bin_path, [bin_path] + sys.argv[1:])
+
+# ----- Amalgamator: end of python/perfetto/prebuilts/perfetto_prebuilts.py
 
 if __name__ == '__main__':
-  import sys, subprocess, os
-  bin_path = get_perfetto_prebuilt(TOOL_NAME)
-  if sys.platform.lower() == 'win32':
-    exit(subprocess.check_call([bin_path, *sys.argv[1:]]))
-  os.execv(bin_path, [bin_path] + sys.argv[1:])
+  run_perfetto_prebuilt(TRACEBOX_MANIFEST)
 
 #EOF
diff --git a/tools/traceconv b/tools/traceconv
index 0572002..db6699b 100755
--- a/tools/traceconv
+++ b/tools/traceconv
@@ -13,6 +13,10 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+# DO NOT EDIT. Auto-generated by tools/gen_amalgamated_python_tools
+# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
 # This file should do the same thing when being invoked in any of these ways:
 # ./traceconv
 # python traceconv
@@ -20,17 +24,14 @@
 # cat ./traceconv | bash
 # cat ./traceconv | python -
 
-BASH_FALLBACK = """ "
+BASH_FALLBACK=""" "
 exec python3 - "$@" <<'#'EOF
-#"""
+#"""  # yapf: disable
 
-TOOL_NAME = 'traceconv'
 
-# BEGIN_SECTION_GENERATED_BY(roll-prebuilts)
-# Revision: v28.0
-PERFETTO_PREBUILT_MANIFEST = [{
-    'tool':
-        'traceconv',
+# ----- Amalgamator: begin of python/perfetto/prebuilts/manifests/traceconv.py
+# This file has been generated by: /s/perfetto/tools/roll-prebuilts v28.0
+TRACECONV_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
@@ -45,8 +46,6 @@
         'darwin',
     'machine': ['x86_64']
 }, {
-    'tool':
-        'traceconv',
     'arch':
         'mac-arm64',
     'file_name':
@@ -61,8 +60,6 @@
         'darwin',
     'machine': ['arm64']
 }, {
-    'tool':
-        'traceconv',
     'arch':
         'linux-amd64',
     'file_name':
@@ -77,8 +74,6 @@
         'linux',
     'machine': ['x86_64']
 }, {
-    'tool':
-        'traceconv',
     'arch':
         'linux-arm',
     'file_name':
@@ -93,8 +88,6 @@
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
 }, {
-    'tool':
-        'traceconv',
     'arch':
         'linux-arm64',
     'file_name':
@@ -109,8 +102,6 @@
         'linux',
     'machine': ['aarch64']
 }, {
-    'tool':
-        'traceconv',
     'arch':
         'android-arm',
     'file_name':
@@ -122,8 +113,6 @@
     'sha256':
         '73827b82d941a9650580fbd48c3d4ff2323eb8d4ff9d3fffd3e0cac1bc853f34'
 }, {
-    'tool':
-        'traceconv',
     'arch':
         'android-arm64',
     'file_name':
@@ -135,8 +124,6 @@
     'sha256':
         '72d46258645d486f40ee463052b609d1fd7c4cc64f70c0ba2ef811a9924be98e'
 }, {
-    'tool':
-        'traceconv',
     'arch':
         'android-x86',
     'file_name':
@@ -148,8 +135,6 @@
     'sha256':
         '689d0b48f91624585285b3833362cdcfdf0de1ff5dedcb97bb9851c729b4a15e'
 }, {
-    'tool':
-        'traceconv',
     'arch':
         'android-x64',
     'file_name':
@@ -161,8 +146,6 @@
     'sha256':
         '785ec3f0da302ed52521febc5ed5e2cef57ae8840ff241037c51b8d94464f6a2'
 }, {
-    'tool':
-        'traceconv',
     'arch':
         'windows-amd64',
     'file_name':
@@ -178,56 +161,104 @@
     'machine': ['amd64']
 }]
 
+# ----- Amalgamator: end of python/perfetto/prebuilts/manifests/traceconv.py
 
-# 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):
+# ----- Amalgamator: begin of python/perfetto/prebuilts/perfetto_prebuilts.py
+# 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.
+"""
+Functions to fetch pre-pinned Perfetto prebuilts.
+
+This function is used in different places:
+- Into the //tools/{trace_processor, traceconv} scripts, which are just plain
+  wrappers around executables.
+- Into the //tools/{heap_profiler, record_android_trace} scripts, which contain
+  some other hand-written python code.
+
+The manifest argument looks as follows:
+TRACECONV_MANIFEST = [
+  {
+    'arch': 'mac-amd64',
+    'file_name': 'traceconv',
+    'file_size': 7087080,
+    'url': https://commondatastorage.googleapis.com/.../trace_to_text',
+    'sha256': 7d957c005b0dc130f5bd855d6cec27e060d38841b320d04840afc569f9087490',
+    'platform': 'darwin',
+    'machine': 'x86_64'
+  },
+  ...
+]
+
+The intended usage is:
+
+  from perfetto.prebuilts.manifests.traceconv import TRACECONV_MANIFEST
+  bin_path = get_perfetto_prebuilt(TRACECONV_MANIFEST)
+  subprocess.call(bin_path, ...)
+"""
+
+import hashlib
+import os
+import platform
+import subprocess
+import sys
+
+
+def download_or_get_cached(file_name, url, sha256):
+  """ Downloads a prebuilt or returns a cached version
+
+  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.
+  """
+  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
+
+
+def get_perfetto_prebuilt(manifest, 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:
+  for entry in manifest:
     # If the caller overrides the arch, just match that (for Android prebuilts).
     if arch:
       if entry.get('arch') == arch:
@@ -235,8 +266,7 @@
         break
       continue
     # Otherwise guess the local machine arch.
-    if entry.get('tool') == tool_name and entry.get(
-        'platform') == plat and machine in entry.get('machine', []):
+    if entry.get('platform') == plat and machine in entry.get('machine', []):
       manifest_entry = entry
       break
   if manifest_entry is None:
@@ -252,13 +282,15 @@
       sha256=manifest_entry['sha256'])
 
 
-# END_SECTION_GENERATED_BY(roll-prebuilts)
+def run_perfetto_prebuilt(manifest):
+  bin_path = get_perfetto_prebuilt(manifest)
+  if sys.platform.lower() == 'win32':
+    sys.exit(subprocess.check_call([bin_path, *sys.argv[1:]]))
+  os.execv(bin_path, [bin_path] + sys.argv[1:])
+
+# ----- Amalgamator: end of python/perfetto/prebuilts/perfetto_prebuilts.py
 
 if __name__ == '__main__':
-  import sys, subprocess, os
-  bin_path = get_perfetto_prebuilt(TOOL_NAME)
-  if sys.platform.lower() == 'win32':
-    exit(subprocess.check_call([bin_path, *sys.argv[1:]]))
-  os.execv(bin_path, [bin_path] + sys.argv[1:])
+  run_perfetto_prebuilt(TRACECONV_MANIFEST)
 
 #EOF