xref: /aosp_15_r20/external/perfetto/tools/cpu_profile (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1#!/usr/bin/env python3
2# Copyright (C) 2022 The Android Open Source Project
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#      http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15"""Runs tracing with CPU profiling enabled, and symbolizes traces if requested.
16
17# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
18# DO NOT EDIT. Auto-generated by tools/gen_amalgamated_python_tools
19# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
20
21For usage instructions, please see:
22https://perfetto.dev/docs/quickstart/callstack-sampling
23
24Adapted in large part from `heap_profile`.
25"""
26
27import argparse
28import os
29import shutil
30import signal
31import subprocess
32import sys
33import tempfile
34import textwrap
35import time
36import uuid
37
38
39# ----- Amalgamator: begin of python/perfetto/prebuilts/manifests/traceconv.py
40# This file has been generated by: tools/roll-prebuilts v48.1
41TRACECONV_MANIFEST = [{
42    'arch':
43        'mac-amd64',
44    'file_name':
45        'traceconv',
46    'file_size':
47        9041560,
48    'url':
49        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-amd64/traceconv',
50    'sha256':
51        'cec2da5cb771a4812d0b2d15604d5023954d28e0af12e87313da2ab70d26b970',
52    'platform':
53        'darwin',
54    'machine': ['x86_64']
55}, {
56    'arch':
57        'mac-arm64',
58    'file_name':
59        'traceconv',
60    'file_size':
61        8375512,
62    'url':
63        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-arm64/traceconv',
64    'sha256':
65        '64e200a58ea9c9f366e1071dd274d0023d1fd14043f75dbba3fe0cc138ff5fc7',
66    'platform':
67        'darwin',
68    'machine': ['arm64']
69}, {
70    'arch':
71        'linux-amd64',
72    'file_name':
73        'traceconv',
74    'file_size':
75        9134136,
76    'url':
77        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-amd64/traceconv',
78    'sha256':
79        '87b87e1778367c1e3b99fc77439a28b4911125d2751f9909fd1b51f6bd60b6f4',
80    'platform':
81        'linux',
82    'machine': ['x86_64']
83}, {
84    'arch':
85        'linux-arm',
86    'file_name':
87        'traceconv',
88    'file_size':
89        6753020,
90    'url':
91        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm/traceconv',
92    'sha256':
93        '804c4e13aca5798731056952d9cb0c6ee58795c03477c69514ccd39703060812',
94    'platform':
95        'linux',
96    'machine': ['armv6l', 'armv7l', 'armv8l']
97}, {
98    'arch':
99        'linux-arm64',
100    'file_name':
101        'traceconv',
102    'file_size':
103        8740064,
104    'url':
105        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm64/traceconv',
106    'sha256':
107        '0d781886531d11e1d573a1ec5e06376ef139bb479eec38c16c8735821c35b895',
108    'platform':
109        'linux',
110    'machine': ['aarch64']
111}, {
112    'arch':
113        'android-arm',
114    'file_name':
115        'traceconv',
116    'file_size':
117        6792280,
118    'url':
119        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm/traceconv',
120    'sha256':
121        '7d91e4133184a3722a25488edd3692c5a195148eba56621014311d3f85d3fc15'
122}, {
123    'arch':
124        'android-arm64',
125    'file_name':
126        'traceconv',
127    'file_size':
128        8677992,
129    'url':
130        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm64/traceconv',
131    'sha256':
132        'c03c4a901ed23f1e20a12c98ce4556353a62bddcd260fb4d797cd29ff6c49a05'
133}, {
134    'arch':
135        'android-x86',
136    'file_name':
137        'traceconv',
138    'file_size':
139        9503704,
140    'url':
141        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x86/traceconv',
142    'sha256':
143        '704e58a7249de56aadec64d4c0d83bab0821d2c4fd77114a9b71705ff4224539'
144}, {
145    'arch':
146        'android-x64',
147    'file_name':
148        'traceconv',
149    'file_size':
150        8964488,
151    'url':
152        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x64/traceconv',
153    'sha256':
154        'e4f07836fc2a5fb7cd997a9acc4183af7a06997d1e73aac71021af5114b921bc'
155}, {
156    'arch':
157        'windows-amd64',
158    'file_name':
159        'traceconv.exe',
160    'file_size':
161        8763904,
162    'url':
163        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/windows-amd64/traceconv.exe',
164    'sha256':
165        '084670ac28ed59a9642782a30e051735c1b7474b8cd569b9bc94c305af68290e',
166    'platform':
167        'win32',
168    'machine': ['amd64']
169}]
170
171# ----- Amalgamator: end of python/perfetto/prebuilts/manifests/traceconv.py
172
173# ----- Amalgamator: begin of python/perfetto/prebuilts/perfetto_prebuilts.py
174# Copyright (C) 2021 The Android Open Source Project
175#
176# Licensed under the Apache License, Version 2.0 (the "License");
177# you may not use this file except in compliance with the License.
178# You may obtain a copy of the License at
179#
180#      http://www.apache.org/licenses/LICENSE-2.0
181#
182# Unless required by applicable law or agreed to in writing, software
183# distributed under the License is distributed on an "AS IS" BASIS,
184# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
185# See the License for the specific language governing permissions and
186# limitations under the License.
187"""
188Functions to fetch pre-pinned Perfetto prebuilts.
189
190This function is used in different places:
191- Into the //tools/{trace_processor, traceconv} scripts, which are just plain
192  wrappers around executables.
193- Into the //tools/{heap_profiler, record_android_trace} scripts, which contain
194  some other hand-written python code.
195
196The manifest argument looks as follows:
197TRACECONV_MANIFEST = [
198  {
199    'arch': 'mac-amd64',
200    'file_name': 'traceconv',
201    'file_size': 7087080,
202    'url': https://commondatastorage.googleapis.com/.../trace_to_text',
203    'sha256': 7d957c005b0dc130f5bd855d6cec27e060d38841b320d04840afc569f9087490',
204    'platform': 'darwin',
205    'machine': 'x86_64'
206  },
207  ...
208]
209
210The intended usage is:
211
212  from perfetto.prebuilts.manifests.traceconv import TRACECONV_MANIFEST
213  bin_path = get_perfetto_prebuilt(TRACECONV_MANIFEST)
214  subprocess.call(bin_path, ...)
215"""
216
217import hashlib
218import os
219import platform
220import random
221import subprocess
222import sys
223
224
225def download_or_get_cached(file_name, url, sha256):
226  """ Downloads a prebuilt or returns a cached version
227
228  The first time this is invoked, it downloads the |url| and caches it into
229  ~/.local/share/perfetto/prebuilts/$tool_name. On subsequent invocations it
230  just runs the cached version.
231  """
232  dir = os.path.join(
233      os.path.expanduser('~'), '.local', 'share', 'perfetto', 'prebuilts')
234  os.makedirs(dir, exist_ok=True)
235  bin_path = os.path.join(dir, file_name)
236  sha256_path = os.path.join(dir, file_name + '.sha256')
237  needs_download = True
238
239  # Avoid recomputing the SHA-256 on each invocation. The SHA-256 of the last
240  # download is cached into file_name.sha256, just check if that matches.
241  if os.path.exists(bin_path) and os.path.exists(sha256_path):
242    with open(sha256_path, 'rb') as f:
243      digest = f.read().decode()
244      if digest == sha256:
245        needs_download = False
246
247  if needs_download:  # The file doesn't exist or the SHA256 doesn't match.
248    # Use a unique random file to guard against concurrent executions.
249    # See https://github.com/google/perfetto/issues/786 .
250    tmp_path = '%s.%d.tmp' % (bin_path, random.randint(0, 100000))
251    print('Downloading ' + url)
252    subprocess.check_call(['curl', '-f', '-L', '-#', '-o', tmp_path, url])
253    with open(tmp_path, 'rb') as fd:
254      actual_sha256 = hashlib.sha256(fd.read()).hexdigest()
255    if actual_sha256 != sha256:
256      raise Exception('Checksum mismatch for %s (actual: %s, expected: %s)' %
257                      (url, actual_sha256, sha256))
258    os.chmod(tmp_path, 0o755)
259    os.replace(tmp_path, bin_path)
260    with open(tmp_path, 'w') as f:
261      f.write(sha256)
262    os.replace(tmp_path, sha256_path)
263  return bin_path
264
265
266def get_perfetto_prebuilt(manifest, soft_fail=False, arch=None):
267  """ Downloads the prebuilt, if necessary, and returns its path on disk. """
268  plat = sys.platform.lower()
269  machine = platform.machine().lower()
270  manifest_entry = None
271  for entry in manifest:
272    # If the caller overrides the arch, just match that (for Android prebuilts).
273    if arch:
274      if entry.get('arch') == arch:
275        manifest_entry = entry
276        break
277      continue
278    # Otherwise guess the local machine arch.
279    if entry.get('platform') == plat and machine in entry.get('machine', []):
280      manifest_entry = entry
281      break
282  if manifest_entry is None:
283    if soft_fail:
284      return None
285    raise Exception(
286        ('No prebuilts available for %s-%s\n' % (plat, machine)) +
287        'See https://perfetto.dev/docs/contributing/build-instructions')
288
289  return download_or_get_cached(
290      file_name=manifest_entry['file_name'],
291      url=manifest_entry['url'],
292      sha256=manifest_entry['sha256'])
293
294
295def run_perfetto_prebuilt(manifest):
296  bin_path = get_perfetto_prebuilt(manifest)
297  if sys.platform.lower() == 'win32':
298    sys.exit(subprocess.check_call([bin_path, *sys.argv[1:]]))
299  os.execv(bin_path, [bin_path] + sys.argv[1:])
300
301# ----- Amalgamator: end of python/perfetto/prebuilts/perfetto_prebuilts.py
302
303# Used for creating directories, etc.
304UUID = str(uuid.uuid4())[-6:]
305
306# See `sigint_handler` below.
307IS_INTERRUPTED = False
308
309
310def sigint_handler(signal, frame):
311  """Useful for cleanly interrupting tracing."""
312  global IS_INTERRUPTED
313  IS_INTERRUPTED = True
314
315
316def exit_with_no_profile():
317  sys.exit("No profiles generated.")
318
319
320def exit_with_bug_report(error):
321  sys.exit(
322      "{}\n\n If this is unexpected, please consider filing a bug at: \n"
323      "https://perfetto.dev/docs/contributing/getting-started#bugs.".format(
324          error))
325
326
327def adb_check_output(command):
328  """Runs an `adb` command and returns its output."""
329  try:
330    return subprocess.check_output(command).decode('utf-8')
331  except FileNotFoundError:
332    sys.exit("`adb` not found: Is it installed or on PATH?")
333  except subprocess.CalledProcessError as error:
334    sys.exit("`adb` error: Are any (or multiple) devices connected?\n"
335             "If multiple devices are connected, please select one by "
336             "setting `ANDROID_SERIAL=device_id`.\n"
337             "{}".format(error))
338  except Exception as error:
339    exit_with_bug_report(error)
340
341
342def parse_and_validate_args():
343  """Parses, validates, and returns command-line arguments for this script."""
344  DESCRIPTION = """Runs tracing with CPU profiling enabled, and symbolizes
345  traces if requested.
346
347  For usage instructions, please see:
348  https://perfetto.dev/docs/quickstart/callstack-sampling
349  """
350  parser = argparse.ArgumentParser(description=DESCRIPTION)
351  parser.add_argument(
352      "-f",
353      "--frequency",
354      help="Sampling frequency (Hz). "
355      "Default: 100 Hz.",
356      metavar="FREQUENCY",
357      type=int,
358      default=100)
359  parser.add_argument(
360      "-d",
361      "--duration",
362      help="Duration of profile (ms). 0 to run until interrupted. "
363      "Default: until interrupted by user.",
364      metavar="DURATION",
365      type=int,
366      default=0)
367  # Profiling using hardware counters.
368  parser.add_argument(
369      "-e",
370      "--event",
371      help="Use the specified hardware counter event for sampling.",
372      metavar="EVENT",
373      action="append",
374      # See: '//perfetto/protos/perfetto/trace/perfetto_trace.proto'.
375      choices=['HW_CPU_CYCLES', 'HW_INSTRUCTIONS', 'HW_CACHE_REFERENCES',
376               'HW_CACHE_MISSES', 'HW_BRANCH_INSTRUCTIONS', 'HW_BRANCH_MISSES',
377               'HW_BUS_CYCLES', 'HW_STALLED_CYCLES_FRONTEND',
378               'HW_STALLED_CYCLES_BACKEND'],
379      default=[])
380  parser.add_argument(
381      "-k",
382      "--kernel-frames",
383      help="Collect kernel frames.  Default: false.",
384      action="store_true",
385      default=False)
386  parser.add_argument(
387      "-n",
388      "--name",
389      help="Comma-separated list of names of processes to be profiled.",
390      metavar="NAMES",
391      default=None)
392  parser.add_argument(
393      "-p",
394      "--partial-matching",
395      help="If set, enables \"partial matching\" on the strings in --names/-n."
396      "Processes that are already running when profiling is started, and whose "
397      "names include any of the values in --names/-n as substrings will be "
398      "profiled.",
399      action="store_true")
400  parser.add_argument(
401      "-c",
402      "--config",
403      help="A custom configuration file, if any, to be used for profiling. "
404      "If provided, --frequency/-f, --duration/-d, and --name/-n are not used.",
405      metavar="CONFIG",
406      default=None)
407  parser.add_argument(
408      "--no-annotations",
409      help="Do not suffix the pprof function names with Android ART mode "
410      "annotations such as [jit].",
411      action="store_true")
412  parser.add_argument(
413      "--print-config",
414      action="store_true",
415      help="Print config instead of running. For debugging.")
416  parser.add_argument(
417      "-o",
418      "--output",
419      help="Output directory for recorded trace.",
420      metavar="DIRECTORY",
421      default=None)
422
423  args = parser.parse_args()
424  if args.config is not None:
425    if args.name is not None:
426      sys.exit("--name/-n should not be specified with --config/-c.")
427    elif args.event:
428      sys.exit("-e/--event should not be specified with --config/-c.")
429  elif args.config is None and args.name is None:
430    sys.exit("One of --names/-n or --config/-c is required.")
431
432  return args
433
434
435def get_matching_processes(args, names_to_match):
436  """Returns a list of currently-running processes whose names match
437  `names_to_match`.
438
439  Args:
440    args: The command-line arguments provided to this script.
441    names_to_match: The list of process names provided by the user.
442  """
443  # Returns names as they are.
444  if not args.partial_matching:
445    return names_to_match
446
447  # Attempt to match names to names of currently running processes.
448  PS_PROCESS_OFFSET = 8
449  matching_processes = []
450  for line in adb_check_output(['adb', 'shell', 'ps', '-A']).splitlines():
451    line_split = line.split()
452    if len(line_split) <= PS_PROCESS_OFFSET:
453      continue
454    process = line_split[PS_PROCESS_OFFSET]
455    for name in names_to_match:
456      if name in process:
457        matching_processes.append(process)
458        break
459
460  return matching_processes
461
462
463def get_perfetto_config(args):
464  """Returns a Perfetto config with CPU profiling enabled for the selected
465  processes.
466
467  Args:
468    args: The command-line arguments provided to this script.
469  """
470  if args.config is not None:
471    try:
472      with open(args.config, 'r') as config_file:
473        return config_file.read()
474    except IOError as error:
475      sys.exit("Unable to read config file: {}".format(error))
476
477  CONFIG_INDENT = '          '
478  CONFIG = textwrap.dedent('''\
479  buffers {{
480    size_kb: 2048
481  }}
482
483  buffers {{
484    size_kb: 63488
485  }}
486
487  data_sources {{
488    config {{
489      name: "linux.process_stats"
490      target_buffer: 0
491      process_stats_config {{
492        proc_stats_poll_ms: 100
493      }}
494    }}
495  }}
496
497  duration_ms: {duration}
498  write_into_file: true
499  flush_timeout_ms: 30000
500  flush_period_ms: 604800000
501  ''')
502
503  matching_processes = []
504  if args.name is not None:
505    names_to_match = [name.strip() for name in args.name.split(',')]
506    matching_processes = get_matching_processes(args, names_to_match)
507
508  if not matching_processes:
509    sys.exit("No running processes matched for profiling.")
510
511  target_config = "\n".join(
512      [f'{CONFIG_INDENT}target_cmdline: "{p}"' for p in matching_processes])
513
514  events = args.event or ['SW_CPU_CLOCK']
515  for event in events:
516    CONFIG += (textwrap.dedent('''
517    data_sources {{
518      config {{
519        name: "linux.perf"
520        target_buffer: 1
521        perf_event_config {{
522          timebase {{
523            counter: %s
524            frequency: {frequency}
525            timestamp_clock: PERF_CLOCK_MONOTONIC
526          }}
527          callstack_sampling {{
528            scope {{
529    {target_config}
530            }}
531            kernel_frames: {kernel_config}
532          }}
533        }}
534      }}
535    }}
536    ''') % (event))
537
538  if args.kernel_frames:
539    kernel_config = "true"
540  else:
541    kernel_config = "false"
542
543  if not args.print_config:
544    print("Configured profiling for these processes:\n")
545    for matching_process in matching_processes:
546      print(matching_process)
547    print()
548
549  config = CONFIG.format(
550      frequency=args.frequency,
551      duration=args.duration,
552      target_config=target_config,
553      kernel_config=kernel_config)
554
555  return config
556
557
558def release_or_newer(release):
559  """Returns whether a new enough Android release is being used."""
560  SDK = {'T': 33}
561  sdk = int(
562      adb_check_output(
563          ['adb', 'shell', 'getprop', 'ro.system.build.version.sdk']).strip())
564  if sdk >= SDK[release]:
565    return True
566
567  codename = adb_check_output(
568      ['adb', 'shell', 'getprop', 'ro.build.version.codename']).strip()
569  return codename == release
570
571
572def get_and_prepare_profile_target(args):
573  """Returns the target where the trace/profile will be output.  Creates a
574  new directory if necessary.
575
576  Args:
577    args: The command-line arguments provided to this script.
578  """
579  profile_target = os.path.join(tempfile.gettempdir(), UUID)
580  if args.output is not None:
581    profile_target = args.output
582  else:
583    os.makedirs(profile_target, exist_ok=True)
584  if not os.path.isdir(profile_target):
585    sys.exit("Output directory {} not found.".format(profile_target))
586  if os.listdir(profile_target):
587    sys.exit("Output directory {} not empty.".format(profile_target))
588
589  return profile_target
590
591
592def record_trace(config, profile_target):
593  """Runs Perfetto with the provided configuration to record a trace.
594
595  Args:
596    config: The Perfetto config to be used for tracing/profiling.
597    profile_target: The directory where the recorded trace is output.
598  """
599  NULL = open(os.devnull)
600  NO_OUT = {
601      'stdout': NULL,
602      'stderr': NULL,
603  }
604  if not release_or_newer('T'):
605    sys.exit("This tool requires Android T+ to run.")
606
607  # Push configuration to the device.
608  tf = tempfile.NamedTemporaryFile()
609  tf.file.write(config.encode('utf-8'))
610  tf.file.flush()
611  profile_config_path = '/data/misc/perfetto-configs/config-' + UUID
612  adb_check_output(['adb', 'push', tf.name, profile_config_path])
613  tf.close()
614
615
616  profile_device_path = '/data/misc/perfetto-traces/profile-' + UUID
617  perfetto_command = ('perfetto --txt -c {} -o {} -d')
618  try:
619    perfetto_pid = int(
620        adb_check_output([
621            'adb', 'exec-out',
622            perfetto_command.format(profile_config_path, profile_device_path)
623        ]).strip())
624  except ValueError as error:
625    sys.exit("Unable to start profiling: {}".format(error))
626
627  print("Profiling active. Press Ctrl+C to terminate.")
628
629  old_handler = signal.signal(signal.SIGINT, sigint_handler)
630
631  perfetto_alive = True
632  while perfetto_alive and not IS_INTERRUPTED:
633    perfetto_alive = subprocess.call(
634        ['adb', 'shell', '[ -d /proc/{} ]'.format(perfetto_pid)], **NO_OUT) == 0
635    time.sleep(0.25)
636
637  print("Finishing profiling and symbolization...")
638
639  if IS_INTERRUPTED:
640    adb_check_output(['adb', 'shell', 'kill', '-INT', str(perfetto_pid)])
641
642  # Restore old handler.
643  signal.signal(signal.SIGINT, old_handler)
644
645  while perfetto_alive:
646    perfetto_alive = subprocess.call(
647        ['adb', 'shell', '[ -d /proc/{} ]'.format(perfetto_pid)]) == 0
648    time.sleep(0.25)
649
650  profile_host_path = os.path.join(profile_target, 'raw-trace')
651  adb_check_output(['adb', 'pull', profile_device_path, profile_host_path])
652  adb_check_output(['adb', 'shell', 'rm', profile_config_path])
653  adb_check_output(['adb', 'shell', 'rm', profile_device_path])
654
655
656def get_traceconv():
657  """Sets up and returns the path to `traceconv`."""
658  try:
659    traceconv = get_perfetto_prebuilt(TRACECONV_MANIFEST, soft_fail=True)
660  except Exception as error:
661    exit_with_bug_report(error)
662  if traceconv is None:
663    exit_with_bug_report(
664        "Unable to download `traceconv` for symbolizing profiles.")
665
666  return traceconv
667
668
669def concatenate_files(files_to_concatenate, output_file):
670  """Concatenates files.
671
672  Args:
673    files_to_concatenate: Paths for input files to concatenate.
674    output_file: Path to the resultant output file.
675  """
676  with open(output_file, 'wb') as output:
677    for file in files_to_concatenate:
678      with open(file, 'rb') as input:
679        shutil.copyfileobj(input, output)
680
681
682def symbolize_trace(traceconv, profile_target):
683  """Attempts symbolization of the recorded trace/profile, if symbols are
684  available.
685
686  Args:
687    traceconv: The path to the `traceconv` binary used for symbolization.
688    profile_target: The directory where the recorded trace was output.
689
690  Returns:
691    The path to the symbolized trace file if symbolization was completed,
692    and the original trace file, if it was not.
693  """
694  binary_path = os.getenv('PERFETTO_BINARY_PATH')
695  trace_file = os.path.join(profile_target, 'raw-trace')
696  files_to_concatenate = [trace_file]
697
698  if binary_path is not None:
699    try:
700      with open(os.path.join(profile_target, 'symbols'), 'w') as symbols_file:
701        return_code = subprocess.call([traceconv, 'symbolize', trace_file],
702                                      env=dict(
703                                          os.environ,
704                                          PERFETTO_BINARY_PATH=binary_path),
705                                      stdout=symbols_file)
706    except IOError as error:
707      sys.exit("Unable to write symbols to disk: {}".format(error))
708    if return_code == 0:
709      files_to_concatenate.append(os.path.join(profile_target, 'symbols'))
710    else:
711      print("Failed to symbolize. Continuing without symbols.", file=sys.stderr)
712
713  if len(files_to_concatenate) > 1:
714    trace_file = os.path.join(profile_target, 'symbolized-trace')
715    try:
716      concatenate_files(files_to_concatenate, trace_file)
717    except Exception as error:
718      sys.exit("Unable to write symbolized profile to disk: {}".format(error))
719
720  return trace_file
721
722
723def generate_pprof_profiles(traceconv, trace_file, args):
724  """Generates pprof profiles from the recorded trace.
725
726  Args:
727    traceconv: The path to the `traceconv` binary used for generating profiles.
728    trace_file: The oath to the recorded and potentially symbolized trace file.
729
730  Returns:
731    The directory where pprof profiles are output.
732  """
733  try:
734    conversion_args = [traceconv, 'profile', '--perf'] + (
735        ['--no-annotations'] if args.no_annotations else []) + [trace_file]
736    traceconv_output = subprocess.check_output(conversion_args)
737  except Exception as error:
738    exit_with_bug_report(
739        "Unable to extract profiles from trace: {}".format(error))
740
741  profiles_output_directory = None
742  for word in traceconv_output.decode('utf-8').split():
743    if 'perf_profile-' in word:
744      profiles_output_directory = word
745  if profiles_output_directory is None:
746    exit_with_no_profile()
747  return profiles_output_directory
748
749
750def copy_profiles_to_destination(profile_target, profile_path):
751  """Copies recorded profiles to `profile_target` from `profile_path`."""
752  profile_files = os.listdir(profile_path)
753  if not profile_files:
754    exit_with_no_profile()
755
756  try:
757    for profile_file in profile_files:
758      shutil.copy(os.path.join(profile_path, profile_file), profile_target)
759  except Exception as error:
760    sys.exit("Unable to copy profiles to {}: {}".format(profile_target, error))
761
762  print("Wrote profiles to {}".format(profile_target))
763
764
765def main(argv):
766  args = parse_and_validate_args()
767  profile_target = get_and_prepare_profile_target(args)
768  trace_config = get_perfetto_config(args)
769  if args.print_config:
770    print(trace_config)
771    return 0
772  record_trace(trace_config, profile_target)
773  traceconv = get_traceconv()
774  trace_file = symbolize_trace(traceconv, profile_target)
775  copy_profiles_to_destination(
776      profile_target, generate_pprof_profiles(traceconv, trace_file, args))
777  return 0
778
779
780if __name__ == '__main__':
781  sys.exit(main(sys.argv))
782