| #!/usr/bin/env python3 | 
 |  | 
 | # Copyright (C) 2020 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 os | 
 | import subprocess | 
 | import sys | 
 | import tempfile | 
 | import time | 
 | import uuid | 
 |  | 
 | NULL = open(os.devnull) | 
 |  | 
 | PACKAGES_LIST_CFG = '''data_sources { | 
 |   config { | 
 |     name: "android.packages_list" | 
 |   } | 
 | } | 
 | ''' | 
 |  | 
 | CFG_IDENT = '      ' | 
 | CFG = '''buffers {{ | 
 |   size_kb: 100024 | 
 |   fill_policy: RING_BUFFER | 
 | }} | 
 |  | 
 | data_sources {{ | 
 |   config {{ | 
 |     name: "android.java_hprof" | 
 |     java_hprof_config {{ | 
 | {target_cfg} | 
 | {continuous_dump_config} | 
 |     }} | 
 |   }} | 
 | }} | 
 |  | 
 | data_source_stop_timeout_ms: {data_source_stop_timeout_ms} | 
 | duration_ms: {duration_ms} | 
 | ''' | 
 |  | 
 | CONTINUOUS_DUMP = """ | 
 |       continuous_dump_config {{ | 
 |         dump_phase_ms: 0 | 
 |         dump_interval_ms: {dump_interval} | 
 |       }} | 
 | """ | 
 |  | 
 | UUID = str(uuid.uuid4())[-6:] | 
 | PROFILE_PATH = '/data/misc/perfetto-traces/java-profile-' +  UUID | 
 |  | 
 | PERFETTO_CMD = ('CFG=\'{cfg}\'; echo ${{CFG}} | ' | 
 |                 'perfetto --txt -c - -o ' + PROFILE_PATH + ' -d') | 
 |  | 
 | SDK = { | 
 |     'S': 31, | 
 | } | 
 |  | 
 | 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 | 
 |  | 
 | def main(argv): | 
 |   parser = argparse.ArgumentParser() | 
 |   parser.add_argument( | 
 |       "-o", | 
 |       "--output", | 
 |       help="Filename to save profile to.", | 
 |       metavar="FILE", | 
 |       default=None) | 
 |   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( | 
 |       "--no-versions", | 
 |       action="store_true", | 
 |       help="Do not get version information about APKs.") | 
 |   parser.add_argument( | 
 |       "--dump-smaps", | 
 |       action="store_true", | 
 |       help="Get information about /proc/$PID/smaps of target.") | 
 |   parser.add_argument( | 
 |       "--print-config", | 
 |       action="store_true", | 
 |       help="Print config instead of running. For debugging.") | 
 |   parser.add_argument( | 
 |       "--stop-when-done", | 
 |       action="store_true", | 
 |       default=None, | 
 |       help="Use a new method to stop the profile when the dump is done. " | 
 |       "Previously, we would hardcode a duration. Available and default on S.") | 
 |   parser.add_argument( | 
 |       "--no-stop-when-done", | 
 |       action="store_false", | 
 |       dest='stop_when_done', | 
 |       help="Do not use a new method to stop the profile when the dump is done.") | 
 |  | 
 |   args = parser.parse_args() | 
 |  | 
 |   if args.stop_when_done is None: | 
 |     args.stop_when_done = release_or_newer('S') | 
 |  | 
 |   fail = False | 
 |   if args.pid is None and args.name is None: | 
 |     print("FATAL: Neither PID nor NAME given.", file=sys.stderr) | 
 |     fail = True | 
 |  | 
 |   target_cfg = "" | 
 |   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 += '{}pid: {}\n'.format(CFG_IDENT, pid) | 
 |   if args.name: | 
 |     for name in args.name.split(','): | 
 |       target_cfg += '{}process_cmdline: "{}"\n'.format(CFG_IDENT, name) | 
 |   if args.dump_smaps: | 
 |     target_cfg += '{}dump_smaps: true\n'.format(CFG_IDENT) | 
 |  | 
 |   if fail: | 
 |     parser.print_help() | 
 |     return 1 | 
 |  | 
 |   output_file = args.output | 
 |   if output_file is None: | 
 |     fd, name = tempfile.mkstemp('profile') | 
 |     os.close(fd) | 
 |     output_file = name | 
 |  | 
 |   continuous_dump_cfg = "" | 
 |   if args.continuous_dump: | 
 |     continuous_dump_cfg = CONTINUOUS_DUMP.format( | 
 |         dump_interval=args.continuous_dump) | 
 |  | 
 |   if args.stop_when_done: | 
 |     duration_ms = 1000 | 
 |     data_source_stop_timeout_ms = 100000 | 
 |   else: | 
 |     duration_ms = 20000 | 
 |     data_source_stop_timeout_ms = 0 | 
 |  | 
 |   cfg = CFG.format( | 
 |       target_cfg=target_cfg, | 
 |       continuous_dump_config=continuous_dump_cfg, | 
 |       duration_ms=duration_ms, | 
 |       data_source_stop_timeout_ms=data_source_stop_timeout_ms) | 
 |   if not args.no_versions: | 
 |     cfg += PACKAGES_LIST_CFG | 
 |  | 
 |   if args.print_config: | 
 |     print(cfg) | 
 |     return 0 | 
 |  | 
 |   user = subprocess.check_output( | 
 |     ['adb', 'shell', 'whoami']).strip().decode('utf8') | 
 |   perfetto_pid = subprocess.check_output( | 
 |       ['adb', 'exec-out', | 
 |        PERFETTO_CMD.format(cfg=cfg, user=user)]).strip().decode('utf8') | 
 |   try: | 
 |     int(perfetto_pid.strip()) | 
 |   except ValueError: | 
 |     print("Failed to invoke perfetto: {}".format(perfetto_pid), file=sys.stderr) | 
 |     return 1 | 
 |  | 
 |   print("Dumping Java Heap.") | 
 |   exists = True | 
 |  | 
 |   # Wait for perfetto cmd to return. | 
 |   while exists: | 
 |     exists = subprocess.call( | 
 |         ['adb', 'shell', '[ -d /proc/{} ]'.format(perfetto_pid)]) == 0 | 
 |     time.sleep(1) | 
 |  | 
 |   subprocess.check_call( | 
 |     ['adb', 'pull', PROFILE_PATH, output_file], stdout=NULL) | 
 |  | 
 |   subprocess.check_call( | 
 |         ['adb', 'shell', 'rm', PROFILE_PATH], stdout=NULL) | 
 |  | 
 |   print("Wrote profile to {}".format(output_file)) | 
 |   print("This can be viewed using https://ui.perfetto.dev.") | 
 |  | 
 |  | 
 | if __name__ == '__main__': | 
 |   sys.exit(main(sys.argv)) |