Home | History | Annotate | Download | only in tools
      1 #!/usr/bin/env python
      2 
      3 # Copyright (C) 2017 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 
     17 from __future__ import absolute_import
     18 from __future__ import division
     19 from __future__ import print_function
     20 
     21 import argparse
     22 import atexit
     23 import hashlib
     24 import os
     25 import signal
     26 import subprocess
     27 import sys
     28 import tempfile
     29 import time
     30 import urllib
     31 
     32 TRACE_TO_TEXT_SHAS = {
     33   'linux': 'a8171d85c5964ccafe457142dbb7df68ca8da543',
     34   'mac': '268c2fc096039566979d16c1a7a99eabef0d9682',
     35 }
     36 TRACE_TO_TEXT_PATH = tempfile.gettempdir()
     37 TRACE_TO_TEXT_BASE_URL = (
     38     'https://storage.googleapis.com/perfetto/')
     39 
     40 NULL = open(os.devnull)
     41 NOOUT = {
     42   'stdout': NULL,
     43   'stderr': NULL,
     44 }
     45 
     46 
     47 def check_hash(file_name, sha_value):
     48   with open(file_name, 'rb') as fd:
     49     # TODO(fmayer): Chunking.
     50     file_hash = hashlib.sha1(fd.read()).hexdigest()
     51     return file_hash == sha_value
     52 
     53 
     54 def load_trace_to_text(platform):
     55   sha_value = TRACE_TO_TEXT_SHAS[platform]
     56   file_name = 'trace_to_text-' + platform + '-' + sha_value
     57   local_file = os.path.join(TRACE_TO_TEXT_PATH, file_name)
     58 
     59   if os.path.exists(local_file):
     60     if not check_hash(local_file, sha_value):
     61       os.remove(local_file)
     62     else:
     63       return local_file
     64 
     65   url = TRACE_TO_TEXT_BASE_URL + file_name
     66   urllib.urlretrieve(url, local_file)
     67   if not check_hash(local_file, sha_value):
     68     os.remove(local_file)
     69     raise ValueError("Invalid signature.")
     70   os.chmod(local_file, 0o755)
     71   return local_file
     72 
     73 
     74 CFG_IDENT = '      '
     75 CFG='''buffers {{
     76   size_kb: 32768
     77 }}
     78 
     79 data_sources {{
     80   config {{
     81     name: "android.packages_list"
     82   }}
     83 }}
     84 
     85 data_sources {{
     86   config {{
     87     name: "android.heapprofd"
     88     heapprofd_config {{
     89 
     90       shmem_size_bytes: {shmem_size}
     91       sampling_interval_bytes: {interval}
     92 {target_cfg}
     93 {continuous_dump_cfg}
     94     }}
     95   }}
     96 }}
     97 
     98 duration_ms: {duration}
     99 flush_timeout_ms: 30000
    100 '''
    101 
    102 CONTINUOUS_DUMP = """
    103       continuous_dump_config {{
    104         dump_phase_ms: 0
    105         dump_interval_ms: {dump_interval}
    106       }}
    107 """
    108 
    109 PERFETTO_CMD=('CFG=\'{cfg}\'; echo ${{CFG}} | '
    110               'perfetto --txt -c - -o '
    111               '/data/misc/perfetto-traces/profile-{user} -d')
    112 IS_INTERRUPTED = False
    113 def sigint_handler(sig, frame):
    114   global IS_INTERRUPTED
    115   IS_INTERRUPTED = True
    116 
    117 
    118 def main(argv):
    119   parser = argparse.ArgumentParser()
    120   parser.add_argument("-i", "--interval", help="Sampling interval. "
    121                       "Default 4096 (4KiB)", type=int, default=4096)
    122   parser.add_argument("-d", "--duration", help="Duration of profile (ms). "
    123                       "Default 7 days.", type=int, default=604800000)
    124   parser.add_argument("--no-start", help="Do not start heapprofd.",
    125                       action='store_true')
    126   parser.add_argument("-p", "--pid", help="Comma-separated list of PIDs to "
    127                       "profile.", metavar="PIDS")
    128   parser.add_argument("-n", "--name", help="Comma-separated list of process "
    129                       "names to profile.", metavar="NAMES")
    130   parser.add_argument("-c", "--continuous-dump",
    131                       help="Dump interval in ms. 0 to disable continuous dump.",
    132                       type=int, default=0)
    133   parser.add_argument("--disable-selinux", action="store_true",
    134                       help="Disable SELinux enforcement for duration of "
    135                       "profile.")
    136   parser.add_argument("--shmem-size", help="Size of buffer between client and "
    137                       "heapprofd. Default 8MiB. Needs to be a power of two "
    138                       "multiple of 4096, at least 8192.", type=int,
    139                       default=8 * 1048576)
    140   parser.add_argument("--block-client", help="When buffer is full, block the "
    141                       "client to wait for buffer space. Use with caution as "
    142                       "this can significantly slow down the client.",
    143                       action="store_true")
    144   parser.add_argument("--simpleperf", action="store_true",
    145                       help="Get simpleperf profile of heapprofd. This is "
    146                       "only for heapprofd development.")
    147   parser.add_argument("--trace-to-text-binary",
    148                       help="Path to local trace to text. For debugging.")
    149 
    150   args = parser.parse_args()
    151 
    152   fail = False
    153   if args.pid is None and args.name is None:
    154     print("FATAL: Neither PID nor NAME given.", file=sys.stderr)
    155     fail = True
    156   if args.duration is None:
    157     print("FATAL: No duration given.", file=sys.stderr)
    158     fail = True
    159   if args.interval is None:
    160     print("FATAL: No interval given.", file=sys.stderr)
    161     fail = True
    162   if args.shmem_size % 4096:
    163     print("FATAL: shmem-size is not a multiple of 4096.", file=sys.stderr)
    164     fail = True
    165   if args.shmem_size < 8192:
    166     print("FATAL: shmem-size is less than 8192.", file=sys.stderr)
    167     fail = True
    168   if args.shmem_size & (args.shmem_size - 1):
    169     print("FATAL: shmem-size is not a power of two.", file=sys.stderr)
    170     fail = True
    171 
    172   target_cfg = ""
    173   if args.block_client:
    174     target_cfg += "block_client: true\n"
    175   if args.pid:
    176     for pid in args.pid.split(','):
    177       try:
    178         pid = int(pid)
    179       except ValueError:
    180         print("FATAL: invalid PID %s" % pid, file=sys.stderr)
    181         fail = True
    182       target_cfg += '{}pid: {}\n'.format(CFG_IDENT, pid)
    183   if args.name:
    184     for name in args.name.split(','):
    185       target_cfg += '{}process_cmdline: "{}"\n'.format(CFG_IDENT, name)
    186 
    187   if fail:
    188     parser.print_help()
    189     return 1
    190 
    191   trace_to_text_binary = args.trace_to_text_binary
    192   if trace_to_text_binary is None:
    193     platform = None
    194     if sys.platform.startswith('linux'):
    195       platform = 'linux'
    196     elif sys.platform.startswith('darwin'):
    197       platform = 'mac'
    198     else:
    199       print("Invalid platform: {}".format(sys.platform), file=sys.stderr)
    200       return 1
    201 
    202     trace_to_text_binary = load_trace_to_text(platform)
    203 
    204   continuous_dump_cfg = ""
    205   if args.continuous_dump:
    206     continuous_dump_cfg = CONTINUOUS_DUMP.format(
    207         dump_interval=args.continuous_dump)
    208   cfg = CFG.format(interval=args.interval,
    209                    duration=args.duration, target_cfg=target_cfg,
    210                    continuous_dump_cfg=continuous_dump_cfg,
    211                    shmem_size=args.shmem_size)
    212 
    213   if args.disable_selinux:
    214     enforcing = subprocess.check_output(['adb', 'shell', 'getenforce'])
    215     atexit.register(subprocess.check_call,
    216         ['adb', 'shell', 'su root setenforce %s' % enforcing])
    217     subprocess.check_call(['adb', 'shell', 'su root setenforce 0'])
    218 
    219   if not args.no_start:
    220     heapprofd_prop = subprocess.check_output(
    221         ['adb', 'shell', 'getprop persist.heapprofd.enable'])
    222     if heapprofd_prop.strip() != '1':
    223       subprocess.check_call(
    224           ['adb', 'shell', 'setprop persist.heapprofd.enable 1'])
    225       atexit.register(subprocess.check_call,
    226           ['adb', 'shell', 'setprop persist.heapprofd.enable 0'])
    227 
    228   user = subprocess.check_output(['adb', 'shell', 'whoami']).strip()
    229 
    230   if args.simpleperf:
    231     subprocess.check_call(
    232         ['adb', 'shell',
    233          'mkdir -p /data/local/tmp/heapprofd_profile && '
    234          'cd /data/local/tmp/heapprofd_profile &&'
    235          '(nohup simpleperf record -g -p $(pgrep heapprofd) 2>&1 &) '
    236          '> /dev/null'])
    237 
    238   perfetto_pid = subprocess.check_output(
    239       ['adb', 'exec-out', PERFETTO_CMD.format(cfg=cfg, user=user)]).strip()
    240   try:
    241     int(perfetto_pid.strip())
    242   except ValueError:
    243     print("Failed to invoke perfetto: {}".format(perfetto_pid),
    244           file=sys.stderr)
    245     return 1
    246 
    247   old_handler = signal.signal(signal.SIGINT, sigint_handler)
    248   print("Profiling active. Press Ctrl+C to terminate.")
    249   print("You may disconnect your device.")
    250   exists = True
    251   device_connected = True
    252   while not device_connected or (exists and not IS_INTERRUPTED):
    253     exists = subprocess.call(
    254         ['adb', 'shell', '[ -d /proc/{} ]'.format(perfetto_pid)],
    255         **NOOUT) == 0
    256     device_connected = subprocess.call(['adb', 'shell', 'true'], **NOOUT) == 0
    257     time.sleep(1)
    258   signal.signal(signal.SIGINT, old_handler)
    259   if IS_INTERRUPTED:
    260     # Not check_call because it could have existed in the meantime.
    261     subprocess.call(['adb', 'shell', 'kill', '-INT', perfetto_pid])
    262   if args.simpleperf:
    263     subprocess.check_call(['adb', 'shell', 'killall', '-INT', 'simpleperf'])
    264     print("Waiting for simpleperf to exit.")
    265     while subprocess.call(
    266         ['adb', 'shell', '[ -f /proc/$(pgrep simpleperf)/exe ]'],
    267         **NOOUT) == 0:
    268       time.sleep(1)
    269     subprocess.check_call(['adb', 'pull', '/data/local/tmp/heapprofd_profile',
    270                            '/tmp'])
    271     print("Pulled simpleperf profile to /tmp/heapprofd_profile")
    272 
    273   # Wait for perfetto cmd to return.
    274   while exists:
    275     exists = subprocess.call(
    276         ['adb', 'shell', '[ -d /proc/{} ]'.format(perfetto_pid)]) == 0
    277     time.sleep(1)
    278 
    279   subprocess.check_call(['adb', 'pull',
    280                          '/data/misc/perfetto-traces/profile-{}'.format(user),
    281                          '/tmp/profile'], stdout=NULL)
    282   trace_to_text_output = subprocess.check_output(
    283       [trace_to_text_binary, 'profile', '/tmp/profile'])
    284   profile_path = None
    285   for word in trace_to_text_output.split():
    286     if 'heap_profile-' in word:
    287       profile_path = word
    288   if profile_path is None:
    289     print("Could not find trace_to_text output path.", file=sys.stderr)
    290     return 1
    291 
    292   profile_files = os.listdir(profile_path)
    293   if not profile_files:
    294     print("No profiles generated", file=sys.stderr)
    295     return 1
    296 
    297   subprocess.check_call(['gzip'] + [os.path.join(profile_path, x) for x in
    298                                     os.listdir(profile_path)])
    299 
    300   symlink_path = os.path.join(os.path.dirname(profile_path),
    301                                         "heap_profile-latest")
    302   if os.path.lexists(symlink_path):
    303     os.unlink(symlink_path)
    304   os.symlink(profile_path, symlink_path)
    305 
    306   print("Wrote profiles to {} (symlink {})".format(profile_path, symlink_path))
    307   print("These can be viewed using pprof. Googlers: head to pprof/ and "
    308         "upload them.")
    309 
    310 
    311 if __name__ == '__main__':
    312   sys.exit(main(sys.argv))
    313