• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2
3# Copyright (C) 2022 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#      http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16"""Runs tracing with CPU profiling enabled, and symbolizes traces if requested.
17
18For usage instructions, please see:
19https://perfetto.dev/docs/quickstart/callstack-sampling
20
21Adapted in large part from `heap_profile`.
22"""
23
24import argparse
25import os
26import shutil
27import signal
28import subprocess
29import sys
30import tempfile
31import time
32import uuid
33
34# Used for creating directories, etc.
35UUID = str(uuid.uuid4())[-6:]
36
37# See `sigint_handler` below.
38IS_INTERRUPTED = False
39
40
41def sigint_handler(signal, frame):
42  """Useful for cleanly interrupting tracing."""
43  global IS_INTERRUPTED
44  IS_INTERRUPTED = True
45
46
47def exit_with_no_profile():
48  sys.exit("No profiles generated.")
49
50
51def exit_with_bug_report(error):
52  sys.exit(
53      "{}\n\n If this is unexpected, please consider filing a bug at: \n"
54      "https://perfetto.dev/docs/contributing/getting-started#bugs.".format(
55          error))
56
57
58def adb_check_output(command):
59  """Runs an `adb` command and returns its output."""
60  try:
61    return subprocess.check_output(command).decode('utf-8')
62  except FileNotFoundError:
63    sys.exit("`adb` not found: Is it installed or on PATH?")
64  except subprocess.CalledProcessError as error:
65    sys.exit("`adb` error: Are any (or multiple) devices connected?\n"
66             "If multiple devices are connected, please select one by "
67             "setting `ANDROID_SERIAL=device_id`.\n"
68             "{}".format(error))
69  except Exception as error:
70    exit_with_bug_report(error)
71
72
73def parse_and_validate_args():
74  """Parses, validates, and returns command-line arguments for this script."""
75  DESCRIPTION = """Runs tracing with CPU profiling enabled, and symbolizes
76  traces if requested.
77
78  For usage instructions, please see:
79  https://perfetto.dev/docs/quickstart/cpu-profiling
80  """
81  parser = argparse.ArgumentParser(description=DESCRIPTION)
82  parser.add_argument(
83      "-f",
84      "--frequency",
85      help="Sampling frequency (Hz). "
86      "Default: 100 Hz.",
87      metavar="FREQUENCY",
88      type=int,
89      default=100)
90  parser.add_argument(
91      "-d",
92      "--duration",
93      help="Duration of profile (ms). 0 to run until interrupted. "
94      "Default: until interrupted by user.",
95      metavar="DURATION",
96      type=int,
97      default=0)
98  parser.add_argument(
99      "-n",
100      "--name",
101      help="Comma-separated list of names of processes to be profiled.",
102      metavar="NAMES",
103      default=None)
104  parser.add_argument(
105      "-p",
106      "--partial-matching",
107      help="If set, enables \"partial matching\" on the strings in --names/-n."
108      "Processes that are already running when profiling is started, and whose "
109      "names include any of the values in --names/-n as substrings will be profiled.",
110      action="store_true")
111  parser.add_argument(
112      "-c",
113      "--config",
114      help="A custom configuration file, if any, to be used for profiling. "
115      "If provided, --frequency/-f, --duration/-d, and --name/-n are not used.",
116      metavar="CONFIG",
117      default=None)
118  parser.add_argument(
119      "-o",
120      "--output",
121      help="Output directory for recorded trace.",
122      metavar="DIRECTORY",
123      default=None)
124
125  args = parser.parse_args()
126  if args.config is not None and args.name is not None:
127    sys.exit("--name/-n should not be provided when --config/-c is provided.")
128  elif args.config is None and args.name is None:
129    sys.exit("One of --names/-n or --config/-c is required.")
130
131  return args
132
133
134def get_matching_processes(args, names_to_match):
135  """Returns a list of currently-running processes whose names match `names_to_match`.
136
137  Args:
138    args: The command-line arguments provided to this script.
139    names_to_match: The list of process names provided by the user.
140  """
141  # Returns names as they are.
142  if not args.partial_matching:
143    return names_to_match
144
145  # Attempt to match names to names of currently running processes.
146  PS_PROCESS_OFFSET = 8
147  matching_processes = []
148  for line in adb_check_output(['adb', 'shell', 'ps', '-A']).splitlines():
149    line_split = line.split()
150    if len(line_split) <= PS_PROCESS_OFFSET:
151      continue
152    process = line_split[PS_PROCESS_OFFSET]
153    for name in names_to_match:
154      if name in process:
155        matching_processes.append(process)
156        break
157
158  return matching_processes
159
160
161def get_perfetto_config(args):
162  """Returns a Perfetto config with CPU profiling enabled for the selected processes.
163
164  Args:
165    args: The command-line arguments provided to this script.
166  """
167  if args.config is not None:
168    try:
169      with open(args.config, 'r') as config_file:
170        return config_file.read()
171    except IOError as error:
172      sys.exit("Unable to read config file: {}".format(error))
173
174  CONFIG_INDENT = '      '
175  CONFIG = '''buffers {{
176    size_kb: 2048
177  }}
178
179  buffers {{
180    size_kb: 63488
181  }}
182
183  data_sources {{
184    config {{
185      name: "linux.process_stats"
186      target_buffer: 0
187      process_stats_config {{
188        proc_stats_poll_ms: 100
189      }}
190    }}
191  }}
192
193  data_sources {{
194    config {{
195      name: "linux.perf"
196      target_buffer: 1
197      perf_event_config {{
198        all_cpus: true
199        sampling_frequency: {frequency}
200{target_config}
201      }}
202    }}
203  }}
204
205  duration_ms: {duration}
206  write_into_file: true
207  flush_timeout_ms: 30000
208  flush_period_ms: 604800000
209  '''
210
211  target_config = ""
212  matching_processes = []
213  if args.name is not None:
214    names_to_match = [name.strip() for name in args.name.split(',')]
215    matching_processes = get_matching_processes(args, names_to_match)
216
217  if not matching_processes:
218    sys.exit("No running processes matched for profiling.")
219
220  for process in matching_processes:
221    target_config += CONFIG_INDENT + 'target_cmdline: "{}"\n'.format(process)
222
223  print("Configured profiling for these processes:\n")
224  for matching_process in matching_processes:
225    print(matching_process)
226  print()
227
228  config = CONFIG.format(
229      frequency=args.frequency,
230      duration=args.duration,
231      target_config=target_config)
232
233  return config
234
235
236def release_or_newer(release):
237  """Returns whether a new enough Android release is being used."""
238  SDK = {'R': 30}
239  sdk = int(
240      adb_check_output(
241          ['adb', 'shell', 'getprop', 'ro.system.build.version.sdk']).strip())
242  if sdk >= SDK[release]:
243    return True
244
245  codename = adb_check_output(
246      ['adb', 'shell', 'getprop', 'ro.build.version.codename']).strip()
247  return codename == release
248
249
250def get_and_prepare_profile_target(args):
251  """Returns the target where the trace/profile will be output. Creates a new directory if necessary.
252
253  Args:
254    args: The command-line arguments provided to this script.
255  """
256  profile_target = os.path.join(tempfile.gettempdir(), UUID)
257  if args.output is not None:
258    profile_target = args.output
259  else:
260    os.makedirs(profile_target, exist_ok=True)
261  if not os.path.isdir(profile_target):
262    sys.exit("Output directory {} not found.".format(profile_target))
263  if os.listdir(profile_target):
264    sys.exit("Output directory {} not empty.".format(profile_target))
265
266  return profile_target
267
268
269def record_trace(config, profile_target):
270  """Runs Perfetto with the provided configuration to record a trace.
271
272  Args:
273    config: The Perfetto config to be used for tracing/profiling.
274    profile_target: The directory where the recorded trace is output.
275  """
276  NULL = open(os.devnull)
277  NO_OUT = {
278      'stdout': NULL,
279      'stderr': NULL,
280  }
281  if not release_or_newer('R'):
282    sys.exit("This tool requires Android R+ to run.")
283  profile_device_path = '/data/misc/perfetto-traces/profile-' + UUID
284  perfetto_command = ('CONFIG=\'{}\'; echo ${{CONFIG}} | '
285                      'perfetto --txt -c - -o {} -d')
286  try:
287    perfetto_pid = int(
288        adb_check_output([
289            'adb', 'exec-out',
290            perfetto_command.format(config, profile_device_path)
291        ]).strip())
292  except ValueError as error:
293    sys.exit("Unable to start profiling: {}".format(error))
294
295  print("Profiling active. Press Ctrl+C to terminate.")
296
297  old_handler = signal.signal(signal.SIGINT, sigint_handler)
298
299  perfetto_alive = True
300  while perfetto_alive and not IS_INTERRUPTED:
301    perfetto_alive = subprocess.call(
302        ['adb', 'shell', '[ -d /proc/{} ]'.format(perfetto_pid)], **NO_OUT) == 0
303    time.sleep(0.25)
304
305  print("Finishing profiling and symbolization...")
306
307  if IS_INTERRUPTED:
308    adb_check_output(['adb', 'shell', 'kill', '-INT', str(perfetto_pid)])
309
310  # Restore old handler.
311  signal.signal(signal.SIGINT, old_handler)
312
313  while perfetto_alive:
314    perfetto_alive = subprocess.call(
315        ['adb', 'shell', '[ -d /proc/{} ]'.format(perfetto_pid)]) == 0
316    time.sleep(0.25)
317
318  profile_host_path = os.path.join(profile_target, 'raw-trace')
319  adb_check_output(['adb', 'pull', profile_device_path, profile_host_path])
320  adb_check_output(['adb', 'shell', 'rm', profile_device_path])
321
322
323def get_trace_to_text():
324  """Sets up and returns the path to `trace_to_text`."""
325  try:
326    trace_to_text = get_perfetto_prebuilt('trace_to_text', soft_fail=True)
327  except Exception as error:
328    exit_with_bug_report(error)
329  if trace_to_text is None:
330    exit_with_bug_report(
331        "Unable to download `trace_to_text` for symbolizing profiles.")
332
333  return trace_to_text
334
335
336def concatenate_files(files_to_concatenate, output_file):
337  """Concatenates files.
338
339  Args:
340    files_to_concatenate: Paths for input files to concatenate.
341    output_file: Path to the resultant output file.
342  """
343  with open(output_file, 'wb') as output:
344    for file in files_to_concatenate:
345      with open(file, 'rb') as input:
346        shutil.copyfileobj(input, output)
347
348
349def symbolize_trace(trace_to_text, profile_target):
350  """Attempts symbolization of the recorded trace/profile, if symbols are available.
351
352  Args:
353    trace_to_text: The path to the `trace_to_text` binary used for symbolization.
354    profile_target: The directory where the recorded trace was output.
355
356  Returns:
357    The path to the symbolized trace file if symbolization was completed,
358    and the original trace file, if it was not.
359  """
360  binary_path = os.getenv('PERFETTO_BINARY_PATH')
361  trace_file = os.path.join(profile_target, 'raw-trace')
362  files_to_concatenate = [trace_file]
363
364  if binary_path is not None:
365    try:
366      with open(os.path.join(profile_target, 'symbols'), 'w') as symbols_file:
367        return_code = subprocess.call([trace_to_text, 'symbolize', trace_file],
368                                      env=dict(
369                                          os.environ,
370                                          PERFETTO_BINARY_PATH=binary_path),
371                                      stdout=symbols_file)
372    except IOError as error:
373      sys.exit("Unable to write symbols to disk: {}".format(error))
374    if return_code == 0:
375      files_to_concatenate.append(os.path.join(profile_target, 'symbols'))
376    else:
377      print("Failed to symbolize. Continuing without symbols.", file=sys.stderr)
378
379  if len(files_to_concatenate) > 1:
380    trace_file = os.path.join(profile_target, 'symbolized-trace')
381    try:
382      concatenate_files(files_to_concatenate, trace_file)
383    except Exception as error:
384      sys.exit("Unable to write symbolized profile to disk: {}".format(error))
385
386  return trace_file
387
388
389def generate_pprof_profiles(trace_to_text, trace_file):
390  """Generates pprof profiles from the recorded trace.
391
392  Args:
393    trace_to_text: The path to the `trace_to_text` binary used for generating profiles.
394    trace_file: The oath to the recorded and potentially symbolized trace file.
395
396  Returns:
397    The directory where pprof profiles are output.
398  """
399  try:
400    trace_to_text_output = subprocess.check_output(
401        [trace_to_text, 'profile', '--perf', trace_file])
402  except Exception as error:
403    exit_with_bug_report(
404        "Unable to extract profiles from trace: {}".format(error))
405
406  profiles_output_directory = None
407  for word in trace_to_text_output.decode('utf-8').split():
408    if 'perf_profile-' in word:
409      profiles_output_directory = word
410  if profiles_output_directory is None:
411    exit_with_no_profile()
412  return profiles_output_directory
413
414
415def copy_profiles_to_destination(profile_target, profile_path):
416  """Copies recorded profiles to `profile_target` from `profile_path`."""
417  profile_files = os.listdir(profile_path)
418  if not profile_files:
419    exit_with_no_profile()
420
421  try:
422    for profile_file in profile_files:
423      shutil.copy(os.path.join(profile_path, profile_file), profile_target)
424  except Exception as error:
425    sys.exit("Unable to copy profiles to {}: {}".format(profile_target, error))
426
427  print("Wrote profiles to {}".format(profile_target))
428
429
430def main(argv):
431  args = parse_and_validate_args()
432  profile_target = get_and_prepare_profile_target(args)
433  record_trace(get_perfetto_config(args), profile_target)
434  trace_to_text = get_trace_to_text()
435  trace_file = symbolize_trace(trace_to_text, profile_target)
436  copy_profiles_to_destination(
437      profile_target, generate_pprof_profiles(trace_to_text, trace_file))
438  return 0
439
440
441# BEGIN_SECTION_GENERATED_BY(roll-prebuilts)
442# Revision: v25.0
443PERFETTO_PREBUILT_MANIFEST = [{
444    'tool':
445        'trace_to_text',
446    'arch':
447        'mac-amd64',
448    'file_name':
449        'trace_to_text',
450    'file_size':
451        6525752,
452    'url':
453        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v25.0/mac-amd64/trace_to_text',
454    'sha256':
455        '64ccf6bac87825145691c6533412e514891f82300d68ff7ce69e8d2ca69aaf62',
456    'platform':
457        'darwin',
458    'machine': ['x86_64']
459}, {
460    'tool':
461        'trace_to_text',
462    'arch':
463        'windows-amd64',
464    'file_name':
465        'trace_to_text.exe',
466    'file_size':
467        5925888,
468    'url':
469        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v25.0/windows-amd64/trace_to_text.exe',
470    'sha256':
471        '29e50ec4d8e28c7c322ba13273afcce80c63fe7d9f182b83af0e2077b4d2b952',
472    'platform':
473        'win32',
474    'machine': ['amd64']
475}, {
476    'tool':
477        'trace_to_text',
478    'arch':
479        'linux-amd64',
480    'file_name':
481        'trace_to_text',
482    'file_size':
483        6939560,
484    'url':
485        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v25.0/linux-amd64/trace_to_text',
486    'sha256':
487        '109f4ff3bbd47633b0c08a338f1230e69d529ddf1584656ed45d8a59acaaabeb',
488    'platform':
489        'linux',
490    'machine': ['x86_64']
491}]
492
493
494# DO NOT EDIT. If you wish to make edits to this code, you need to change only
495# //tools/get_perfetto_prebuilt.py and run /tools/roll-prebuilts to regenerate
496# all the others scripts this is embedded into.
497def get_perfetto_prebuilt(tool_name, soft_fail=False, arch=None):
498  """ Downloads the prebuilt, if necessary, and returns its path on disk. """
499
500  # The first time this is invoked, it downloads the |url| and caches it into
501  # ~/.perfetto/prebuilts/$tool_name. On subsequent invocations it just runs the
502  # cached version.
503  def download_or_get_cached(file_name, url, sha256):
504    import os, hashlib, subprocess
505    dir = os.path.join(
506        os.path.expanduser('~'), '.local', 'share', 'perfetto', 'prebuilts')
507    os.makedirs(dir, exist_ok=True)
508    bin_path = os.path.join(dir, file_name)
509    sha256_path = os.path.join(dir, file_name + '.sha256')
510    needs_download = True
511
512    # Avoid recomputing the SHA-256 on each invocation. The SHA-256 of the last
513    # download is cached into file_name.sha256, just check if that matches.
514    if os.path.exists(bin_path) and os.path.exists(sha256_path):
515      with open(sha256_path, 'rb') as f:
516        digest = f.read().decode()
517        if digest == sha256:
518          needs_download = False
519
520    if needs_download:
521      # Either the filed doesn't exist or the SHA256 doesn't match.
522      tmp_path = bin_path + '.tmp'
523      print('Downloading ' + url)
524      subprocess.check_call(['curl', '-f', '-L', '-#', '-o', tmp_path, url])
525      with open(tmp_path, 'rb') as fd:
526        actual_sha256 = hashlib.sha256(fd.read()).hexdigest()
527      if actual_sha256 != sha256:
528        raise Exception('Checksum mismatch for %s (actual: %s, expected: %s)' %
529                        (url, actual_sha256, sha256))
530      os.chmod(tmp_path, 0o755)
531      os.rename(tmp_path, bin_path)
532      with open(sha256_path, 'w') as f:
533        f.write(sha256)
534    return bin_path
535    # --- end of download_or_get_cached() ---
536
537  # --- get_perfetto_prebuilt() function starts here. ---
538  import os, platform, sys
539  plat = sys.platform.lower()
540  machine = platform.machine().lower()
541  manifest_entry = None
542  for entry in PERFETTO_PREBUILT_MANIFEST:
543    # If the caller overrides the arch, just match that (for Android prebuilts).
544    if arch and entry.get('arch') == arch:
545      manifest_entry = entry
546      break
547    # Otherwise guess the local machine arch.
548    if entry.get('tool') == tool_name and entry.get(
549        'platform') == plat and machine in entry.get('machine', []):
550      manifest_entry = entry
551      break
552  if manifest_entry is None:
553    if soft_fail:
554      return None
555    raise Exception(
556        ('No prebuilts available for %s-%s\n' % (plat, machine)) +
557        'See https://perfetto.dev/docs/contributing/build-instructions')
558
559  return download_or_get_cached(
560      file_name=manifest_entry['file_name'],
561      url=manifest_entry['url'],
562      sha256=manifest_entry['sha256'])
563
564
565# END_SECTION_GENERATED_BY(roll-prebuilts)
566
567if __name__ == '__main__':
568  sys.exit(main(sys.argv))
569