xref: /aosp_15_r20/external/perfetto/python/tools/heap_profile.py (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1#!/usr/bin/env python3
2# Copyright (C) 2017 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
16from __future__ import absolute_import
17from __future__ import division
18from __future__ import print_function
19
20import argparse
21import atexit
22import os
23import shutil
24import signal
25import subprocess
26import sys
27import tempfile
28import time
29import uuid
30
31from perfetto.prebuilts.manifests.traceconv import *
32from perfetto.prebuilts.perfetto_prebuilts import *
33
34NULL = open(os.devnull)
35NOOUT = {
36    'stdout': NULL,
37    'stderr': NULL,
38}
39
40UUID = str(uuid.uuid4())[-6:]
41
42PACKAGES_LIST_CFG = '''data_sources {
43  config {
44    name: "android.packages_list"
45  }
46}
47'''
48
49CFG_INDENT = '      '
50CFG = '''buffers {{
51  size_kb: 63488
52}}
53
54data_sources {{
55  config {{
56    name: "android.heapprofd"
57    heapprofd_config {{
58      shmem_size_bytes: {shmem_size}
59      sampling_interval_bytes: {interval}
60{target_cfg}
61    }}
62  }}
63}}
64
65duration_ms: {duration}
66write_into_file: true
67flush_timeout_ms: 30000
68flush_period_ms: 604800000
69'''
70
71# flush_period_ms of 1 week to suppress trace_processor_shell warning.
72
73CONTINUOUS_DUMP = """
74      continuous_dump_config {{
75        dump_phase_ms: 0
76        dump_interval_ms: {dump_interval}
77      }}
78"""
79
80PROFILE_LOCAL_PATH = os.path.join(tempfile.gettempdir(), UUID)
81
82IS_INTERRUPTED = False
83
84
85def sigint_handler(sig, frame):
86  global IS_INTERRUPTED
87  IS_INTERRUPTED = True
88
89
90def print_no_profile_error():
91  print("No profiles generated", file=sys.stderr)
92  print(
93      "If this is unexpected, check "
94      "https://perfetto.dev/docs/data-sources/native-heap-profiler#troubleshooting.",
95      file=sys.stderr)
96
97
98def known_issues_url(number):
99  return ('https://perfetto.dev/docs/data-sources/native-heap-profiler'
100          '#known-issues-android{}'.format(number))
101
102
103KNOWN_ISSUES = {
104    '10': known_issues_url(10),
105    'Q': known_issues_url(10),
106    '11': known_issues_url(11),
107    'R': known_issues_url(11),
108}
109
110
111def maybe_known_issues():
112  release_or_codename = subprocess.check_output(
113      ['adb', 'shell', 'getprop',
114       'ro.build.version.release_or_codename']).decode('utf-8').strip()
115  return KNOWN_ISSUES.get(release_or_codename, None)
116
117
118SDK = {
119    'R': 30,
120}
121
122
123def release_or_newer(release):
124  sdk = int(
125      subprocess.check_output(
126          ['adb', 'shell', 'getprop',
127           'ro.system.build.version.sdk']).decode('utf-8').strip())
128  if sdk >= SDK[release]:
129    return True
130  codename = subprocess.check_output(
131      ['adb', 'shell', 'getprop',
132       'ro.build.version.codename']).decode('utf-8').strip()
133  return codename == release
134
135
136ORDER = ['-n', '-p', '-i', '-o']
137
138
139def arg_order(action):
140  result = len(ORDER)
141  for opt in action.option_strings:
142    if opt in ORDER:
143      result = min(ORDER.index(opt), result)
144  return result, action.option_strings[0].strip('-')
145
146
147def print_options(parser):
148  for action in sorted(parser._actions, key=arg_order):
149    if action.help is argparse.SUPPRESS:
150      continue
151    opts = ', '.join('`' + x + '`' for x in action.option_strings)
152    metavar = '' if action.metavar is None else ' _' + action.metavar + '_'
153    print('{}{}'.format(opts, metavar))
154    print(':    {}'.format(action.help))
155    print()
156
157
158def main(argv):
159  parser = argparse.ArgumentParser(description="""Collect a heap profile
160
161  The PERFETTO_PROGUARD_MAP=packagename=map_filename.txt[:packagename=map_filename.txt...] environment variable can be used to pass proguard deobfuscation maps for different packages""", formatter_class=argparse.RawDescriptionHelpFormatter)
162
163  parser.add_argument(
164      "-i",
165      "--interval",
166      help="Sampling interval. "
167      "Default 4096 (4KiB)",
168      type=int,
169      default=4096)
170  parser.add_argument(
171      "-d",
172      "--duration",
173      help="Duration of profile (ms). 0 to run until interrupted. "
174      "Default: until interrupted by user.",
175      type=int,
176      default=0)
177  # This flag is a no-op now. We never start heapprofd explicitly using system
178  # properties.
179  parser.add_argument(
180      "--no-start", help="Do not start heapprofd.", action='store_true')
181  parser.add_argument(
182      "-p",
183      "--pid",
184      help="Comma-separated list of PIDs to "
185      "profile.",
186      metavar="PIDS")
187  parser.add_argument(
188      "-n",
189      "--name",
190      help="Comma-separated list of process "
191      "names to profile.",
192      metavar="NAMES")
193  parser.add_argument(
194      "-c",
195      "--continuous-dump",
196      help="Dump interval in ms. 0 to disable continuous dump.",
197      type=int,
198      default=0)
199  parser.add_argument(
200      "--heaps",
201      help="Comma-separated list of heaps to collect, e.g: malloc,art. "
202      "Requires Android 12.",
203      metavar="HEAPS")
204  parser.add_argument(
205      "--all-heaps",
206      action="store_true",
207      help="Collect allocations from all heaps registered by target.")
208  parser.add_argument(
209      "--no-android-tree-symbolization",
210      action="store_true",
211      help="Do not symbolize using currently lunched target in the "
212      "Android tree.")
213  parser.add_argument(
214      "--disable-selinux",
215      action="store_true",
216      help="Disable SELinux enforcement for duration of "
217      "profile.")
218  parser.add_argument(
219      "--no-versions",
220      action="store_true",
221      help="Do not get version information about APKs.")
222  parser.add_argument(
223      "--no-running",
224      action="store_true",
225      help="Do not target already running processes. Requires Android 11.")
226  parser.add_argument(
227      "--no-startup",
228      action="store_true",
229      help="Do not target processes that start during "
230      "the profile. Requires Android 11.")
231  parser.add_argument(
232      "--shmem-size",
233      help="Size of buffer between client and "
234      "heapprofd. Default 8MiB. Needs to be a power of two "
235      "multiple of 4096, at least 8192.",
236      type=int,
237      default=8 * 1048576)
238  parser.add_argument(
239      "--block-client",
240      help="When buffer is full, block the "
241      "client to wait for buffer space. Use with caution as "
242      "this can significantly slow down the client. "
243      "This is the default",
244      action="store_true")
245  parser.add_argument(
246      "--block-client-timeout",
247      help="If --block-client is given, do not block any allocation for "
248      "longer than this timeout (us).",
249      type=int)
250  parser.add_argument(
251      "--no-block-client",
252      help="When buffer is full, stop the "
253      "profile early.",
254      action="store_true")
255  parser.add_argument(
256      "--idle-allocations",
257      help="Keep track of how many "
258      "bytes were unused since the last dump, per "
259      "callstack",
260      action="store_true")
261  parser.add_argument(
262      "--dump-at-max",
263      help="Dump the maximum memory usage "
264      "rather than at the time of the dump.",
265      action="store_true")
266  parser.add_argument(
267      "--disable-fork-teardown",
268      help="Do not tear down client in forks. This can be useful for programs "
269      "that use vfork. Android 11+ only.",
270      action="store_true")
271  parser.add_argument(
272      "--simpleperf",
273      action="store_true",
274      help="Get simpleperf profile of heapprofd. This is "
275      "only for heapprofd development.")
276  parser.add_argument(
277      "--traceconv-binary", help="Path to local trace to text. For debugging.")
278  parser.add_argument(
279      "--no-annotations",
280      help="Do not suffix the pprof function names with Android ART mode "
281      "annotations such as [jit].",
282      action="store_true")
283  parser.add_argument(
284      "--print-config",
285      action="store_true",
286      help="Print config instead of running. For debugging.")
287  parser.add_argument(
288      "-o",
289      "--output",
290      help="Output directory.",
291      metavar="DIRECTORY",
292      default=None)
293  parser.add_argument(
294      "--print-options", action="store_true", help=argparse.SUPPRESS)
295
296  args = parser.parse_args()
297  if args.print_options:
298    print_options(parser)
299    return 0
300  fail = False
301  if args.block_client and args.no_block_client:
302    print(
303        "FATAL: Both block-client and no-block-client given.", file=sys.stderr)
304    fail = True
305  if args.pid is None and args.name is None:
306    print("FATAL: Neither PID nor NAME given.", file=sys.stderr)
307    fail = True
308  if args.duration is None:
309    print("FATAL: No duration given.", file=sys.stderr)
310    fail = True
311  if args.interval is None:
312    print("FATAL: No interval given.", file=sys.stderr)
313    fail = True
314  if args.shmem_size % 4096:
315    print("FATAL: shmem-size is not a multiple of 4096.", file=sys.stderr)
316    fail = True
317  if args.shmem_size < 8192:
318    print("FATAL: shmem-size is less than 8192.", file=sys.stderr)
319    fail = True
320  if args.shmem_size & (args.shmem_size - 1):
321    print("FATAL: shmem-size is not a power of two.", file=sys.stderr)
322    fail = True
323
324  target_cfg = ""
325  if not args.no_block_client:
326    target_cfg += CFG_INDENT + "block_client: true\n"
327  if args.block_client_timeout:
328    target_cfg += (
329        CFG_INDENT +
330        "block_client_timeout_us: %s\n" % args.block_client_timeout)
331  if args.no_startup:
332    target_cfg += CFG_INDENT + "no_startup: true\n"
333  if args.no_running:
334    target_cfg += CFG_INDENT + "no_running: true\n"
335  if args.dump_at_max:
336    target_cfg += CFG_INDENT + "dump_at_max: true\n"
337  if args.disable_fork_teardown:
338    target_cfg += CFG_INDENT + "disable_fork_teardown: true\n"
339  if args.all_heaps:
340    target_cfg += CFG_INDENT + "all_heaps: true\n"
341  if args.pid:
342    for pid in args.pid.split(','):
343      try:
344        pid = int(pid)
345      except ValueError:
346        print("FATAL: invalid PID %s" % pid, file=sys.stderr)
347        fail = True
348      target_cfg += CFG_INDENT + 'pid: {}\n'.format(pid)
349  if args.name:
350    for name in args.name.split(','):
351      target_cfg += CFG_INDENT + 'process_cmdline: "{}"\n'.format(name)
352  if args.heaps:
353    for heap in args.heaps.split(','):
354      target_cfg += CFG_INDENT + 'heaps: "{}"\n'.format(heap)
355
356  if fail:
357    parser.print_help()
358    return 1
359
360  traceconv_binary = args.traceconv_binary
361
362  if args.continuous_dump:
363    target_cfg += CONTINUOUS_DUMP.format(dump_interval=args.continuous_dump)
364  cfg = CFG.format(
365      interval=args.interval,
366      duration=args.duration,
367      target_cfg=target_cfg,
368      shmem_size=args.shmem_size)
369  if not args.no_versions:
370    cfg += PACKAGES_LIST_CFG
371
372  if args.print_config:
373    print(cfg)
374    return 0
375
376  # Do this AFTER print_config so we do not download traceconv only to
377  # print out the config.
378  if traceconv_binary is None:
379    traceconv_binary = get_perfetto_prebuilt(TRACECONV_MANIFEST, soft_fail=True)
380
381  known_issues = maybe_known_issues()
382  if known_issues:
383    print('If you are experiencing problems, please see the known issues for '
384          'your release: {}.'.format(known_issues))
385
386  # TODO(fmayer): Maybe feature detect whether we can remove traces instead of
387  # this.
388  uuid_trace = release_or_newer('R')
389  if uuid_trace:
390    profile_device_path = '/data/misc/perfetto-traces/profile-' + UUID
391  else:
392    user = subprocess.check_output(['adb', 'shell',
393                                    'whoami']).decode('utf-8').strip()
394    profile_device_path = '/data/misc/perfetto-traces/profile-' + user
395
396  perfetto_cmd = ('CFG=\'{cfg}\'; echo ${{CFG}} | '
397                  'perfetto --txt -c - -o ' + profile_device_path + ' -d')
398
399  if args.disable_selinux:
400    enforcing = subprocess.check_output(['adb', 'shell',
401                                         'getenforce']).decode('utf-8').strip()
402    atexit.register(
403        subprocess.check_call,
404        ['adb', 'shell', 'su root setenforce %s' % enforcing])
405    subprocess.check_call(['adb', 'shell', 'su root setenforce 0'])
406
407  if args.simpleperf:
408    subprocess.check_call([
409        'adb', 'shell', 'mkdir -p /data/local/tmp/heapprofd_profile && '
410        'cd /data/local/tmp/heapprofd_profile &&'
411        '(nohup simpleperf record -g -p $(pidof heapprofd) 2>&1 &) '
412        '> /dev/null'
413    ])
414
415  profile_target = PROFILE_LOCAL_PATH
416  if args.output is not None:
417    profile_target = args.output
418  else:
419    os.mkdir(profile_target)
420
421  if not os.path.isdir(profile_target):
422    print(
423        "Output directory {} not found".format(profile_target), file=sys.stderr)
424    return 1
425
426  if os.listdir(profile_target):
427    print(
428        "Output directory {} not empty".format(profile_target), file=sys.stderr)
429    return 1
430
431  perfetto_pid = subprocess.check_output(
432      ['adb', 'exec-out', perfetto_cmd.format(cfg=cfg)]).strip()
433  try:
434    perfetto_pid = int(perfetto_pid.strip())
435  except ValueError:
436    print("Failed to invoke perfetto: {}".format(perfetto_pid), file=sys.stderr)
437    return 1
438
439  old_handler = signal.signal(signal.SIGINT, sigint_handler)
440  print("Profiling active. Press Ctrl+C to terminate.")
441  print("You may disconnect your device.")
442  print()
443  exists = True
444  device_connected = True
445  while not device_connected or (exists and not IS_INTERRUPTED):
446    exists = subprocess.call(
447        ['adb', 'shell', '[ -d /proc/{} ]'.format(perfetto_pid)], **NOOUT) == 0
448    device_connected = subprocess.call(['adb', 'shell', 'true'], **NOOUT) == 0
449    time.sleep(1)
450  print("Waiting for profiler shutdown...")
451  signal.signal(signal.SIGINT, old_handler)
452  if IS_INTERRUPTED:
453    # Not check_call because it could have existed in the meantime.
454    subprocess.call(['adb', 'shell', 'kill', '-INT', str(perfetto_pid)])
455  if args.simpleperf:
456    subprocess.check_call(['adb', 'shell', 'killall', '-INT', 'simpleperf'])
457    print("Waiting for simpleperf to exit.")
458    while subprocess.call(
459        ['adb', 'shell', '[ -f /proc/$(pidof simpleperf)/exe ]'], **NOOUT) == 0:
460      time.sleep(1)
461    subprocess.check_call(
462        ['adb', 'pull', '/data/local/tmp/heapprofd_profile', profile_target])
463    print("Pulled simpleperf profile to " + profile_target +
464          "/heapprofd_profile")
465
466  # Wait for perfetto cmd to return.
467  while exists:
468    exists = subprocess.call(
469        ['adb', 'shell', '[ -d /proc/{} ]'.format(perfetto_pid)]) == 0
470    time.sleep(1)
471
472  profile_host_path = os.path.join(profile_target, 'raw-trace')
473  subprocess.check_call(['adb', 'pull', profile_device_path, profile_host_path],
474                        stdout=NULL)
475  if uuid_trace:
476    subprocess.check_call(['adb', 'shell', 'rm', profile_device_path],
477                          stdout=NULL)
478
479  if traceconv_binary is None:
480    print('Wrote profile to {}'.format(profile_host_path))
481    print(
482        'This file can be opened using the Perfetto UI, https://ui.perfetto.dev'
483    )
484    return 0
485
486  binary_path = os.getenv('PERFETTO_BINARY_PATH')
487  if not args.no_android_tree_symbolization:
488    product_out = os.getenv('ANDROID_PRODUCT_OUT')
489    if product_out:
490      product_out_symbols = product_out + '/symbols'
491    else:
492      product_out_symbols = None
493
494    if binary_path is None:
495      binary_path = product_out_symbols
496    elif product_out_symbols is not None:
497      binary_path += os.pathsep + product_out_symbols
498
499  trace_file = os.path.join(profile_target, 'raw-trace')
500  concat_files = [trace_file]
501
502  if binary_path is not None:
503    with open(os.path.join(profile_target, 'symbols'), 'w') as fd:
504      ret = subprocess.call([
505          traceconv_binary, 'symbolize',
506          os.path.join(profile_target, 'raw-trace')
507      ],
508                            env=dict(
509                                os.environ, PERFETTO_BINARY_PATH=binary_path),
510                            stdout=fd)
511    if ret == 0:
512      concat_files.append(os.path.join(profile_target, 'symbols'))
513    else:
514      print("Failed to symbolize. Continuing without symbols.", file=sys.stderr)
515
516  proguard_map = os.getenv('PERFETTO_PROGUARD_MAP')
517  if proguard_map is not None:
518    with open(os.path.join(profile_target, 'deobfuscation-packets'), 'w') as fd:
519      ret = subprocess.call([
520          traceconv_binary, 'deobfuscate',
521          os.path.join(profile_target, 'raw-trace')
522      ],
523                            env=dict(
524                                os.environ, PERFETTO_PROGUARD_MAP=proguard_map),
525                            stdout=fd)
526    if ret == 0:
527      concat_files.append(os.path.join(profile_target, 'deobfuscation-packets'))
528    else:
529      print(
530          "Failed to deobfuscate. Continuing without deobfuscated.",
531          file=sys.stderr)
532
533  if len(concat_files) > 1:
534    with open(os.path.join(profile_target, 'symbolized-trace'), 'wb') as out:
535      for fn in concat_files:
536        with open(fn, 'rb') as inp:
537          while True:
538            buf = inp.read(4096)
539            if not buf:
540              break
541            out.write(buf)
542    trace_file = os.path.join(profile_target, 'symbolized-trace')
543
544  conversion_args = [traceconv_binary, 'profile'] + (
545      ['--no-annotations'] if args.no_annotations else []) + [trace_file]
546  traceconv_output = subprocess.check_output(conversion_args)
547  profile_path = None
548  for word in traceconv_output.decode('utf-8').split():
549    if 'heap_profile-' in word:
550      profile_path = word
551  if profile_path is None:
552    print_no_profile_error()
553    return 1
554
555  profile_files = os.listdir(profile_path)
556  if not profile_files:
557    print_no_profile_error()
558    return 1
559
560  for profile_file in profile_files:
561    shutil.copy(os.path.join(profile_path, profile_file), profile_target)
562
563  symlink_path = None
564  if not sys.platform.startswith('win'):
565    subprocess.check_call(
566        ['gzip'] + [os.path.join(profile_target, x) for x in profile_files])
567    if args.output is None:
568      symlink_path = os.path.join(
569          os.path.dirname(profile_target), "heap_profile-latest")
570      if os.path.lexists(symlink_path):
571        os.unlink(symlink_path)
572      os.symlink(profile_target, symlink_path)
573
574  if symlink_path is not None:
575    print("Wrote profiles to {} (symlink {})".format(profile_target,
576                                                     symlink_path))
577  else:
578    print("Wrote profiles to {}".format(profile_target))
579
580  print("The raw-trace file can be viewed using https://ui.perfetto.dev.")
581  print("The heap_dump.* files can be viewed using pprof/ (Googlers only) " +
582        "or https://www.speedscope.app/.")
583  print("The two above are equivalent. The raw-trace contains the union of " +
584        "all the heap dumps.")
585
586
587if __name__ == '__main__':
588  sys.exit(main(sys.argv))
589