Home | History | Annotate | Download | only in scripts
      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 """Generates a human-interpretable view of a native heap dump from 'am dumpheap -n'."""
     18 
     19 import os
     20 import os.path
     21 import re
     22 import subprocess
     23 import sys
     24 
     25 usage = """
     26 Usage:
     27 1. Collect a native heap dump from the device. For example:
     28    $ adb shell stop
     29    $ adb shell setprop libc.debug.malloc.program app_process
     30    $ adb shell setprop libc.debug.malloc.options backtrace=64
     31    $ adb shell start
     32     (launch and use app)
     33    $ adb shell am dumpheap -n <pid> /data/local/tmp/native_heap.txt
     34    $ adb pull /data/local/tmp/native_heap.txt
     35 
     36 2. Run the viewer:
     37    $ python native_heapdump_viewer.py [options] native_heap.txt
     38       [--verbose]: verbose output
     39       [--html]: interactive html output
     40       [--reverse]: reverse the backtraces (start the tree from the leaves)
     41       [--symbols SYMBOL_DIR] SYMBOL_DIR is the directory containing the .so files with symbols.
     42                  Defaults to $ANDROID_PRODUCT_OUT/symbols
     43    This outputs a file with lines of the form:
     44 
     45       5831776  29.09% 100.00%    10532     71b07bc0b0 /system/lib64/libandroid_runtime.so Typeface_createFromArray frameworks/base/core/jni/android/graphics/Typeface.cpp:68
     46 
     47    5831776 is the total number of bytes allocated at this stack frame, which
     48    is 29.09% of the total number of bytes allocated and 100.00% of the parent
     49    frame's bytes allocated. 10532 is the total number of allocations at this
     50    stack frame. 71b07bc0b0 is the address of the stack frame.
     51 """
     52 
     53 verbose = False
     54 html_output = False
     55 reverse_frames = False
     56 product_out = os.getenv("ANDROID_PRODUCT_OUT")
     57 if product_out:
     58     symboldir = product_out + "/symbols"
     59 else:
     60     symboldir = "./symbols"
     61 
     62 args = sys.argv[1:]
     63 while len(args) > 1:
     64     if args[0] == "--symbols":
     65         symboldir = args[1]
     66         args = args[2:]
     67     elif args[0] == "--verbose":
     68         verbose = True
     69         args = args[1:]
     70     elif args[0] == "--html":
     71         html_output = True
     72         args = args[1:]
     73     elif args[0] == "--reverse":
     74         reverse_frames = True
     75         args = args[1:]
     76     else:
     77         print "Invalid option "+args[0]
     78         break
     79 
     80 if len(args) != 1:
     81     print usage
     82     exit(0)
     83 
     84 native_heap = args[0]
     85 
     86 re_map = re.compile("(?P<start>[0-9a-f]+)-(?P<end>[0-9a-f]+) .... (?P<offset>[0-9a-f]+) [0-9a-f]+:[0-9a-f]+ [0-9]+ +(?P<name>.*)")
     87 
     88 class Backtrace:
     89     def __init__(self, is_zygote, size, frames):
     90         self.is_zygote = is_zygote
     91         self.size = size
     92         self.frames = frames
     93 
     94 class Mapping:
     95     def __init__(self, start, end, offset, name):
     96         self.start = start
     97         self.end = end
     98         self.offset = offset
     99         self.name = name
    100 
    101 class FrameDescription:
    102     def __init__(self, function, location, library):
    103         self.function = function
    104         self.location = location
    105         self.library = library
    106 
    107 
    108 backtraces = []
    109 mappings = []
    110 
    111 for line in open(native_heap, "r"):
    112     parts = line.split()
    113     if len(parts) > 7 and parts[0] == "z" and parts[2] == "sz":
    114         is_zygote = parts[1] != "1"
    115         size = int(parts[3])
    116         frames = map(lambda x: int(x, 16), parts[7:])
    117         if reverse_frames:
    118             frames = list(reversed(frames))
    119         backtraces.append(Backtrace(is_zygote, size, frames))
    120         continue
    121 
    122     m = re_map.match(line)
    123     if m:
    124         start = int(m.group('start'), 16)
    125         end = int(m.group('end'), 16)
    126         offset = int(m.group('offset'), 16)
    127         name = m.group('name')
    128         mappings.append(Mapping(start, end, offset, name))
    129         continue
    130 
    131 # Return the mapping that contains the given address.
    132 # Returns None if there is no such mapping.
    133 def find_mapping(addr):
    134     min = 0
    135     max = len(mappings) - 1
    136     while True:
    137         if max < min:
    138             return None
    139         mid = (min + max) // 2
    140         if mappings[mid].end <= addr:
    141             min = mid + 1
    142         elif mappings[mid].start > addr:
    143             max = mid - 1
    144         else:
    145             return mappings[mid]
    146 
    147 # Resolve address libraries and offsets.
    148 # addr_offsets maps addr to .so file offset
    149 # addrs_by_lib maps library to list of addrs from that library
    150 # Resolved addrs maps addr to FrameDescription
    151 addr_offsets = {}
    152 addrs_by_lib = {}
    153 resolved_addrs = {}
    154 EMPTY_FRAME_DESCRIPTION = FrameDescription("???", "???", "???")
    155 for backtrace in backtraces:
    156     for addr in backtrace.frames:
    157         if addr in addr_offsets:
    158             continue
    159         mapping = find_mapping(addr)
    160         if mapping:
    161             addr_offsets[addr] = addr - mapping.start + mapping.offset
    162             if not (mapping.name in addrs_by_lib):
    163                 addrs_by_lib[mapping.name] = []
    164             addrs_by_lib[mapping.name].append(addr)
    165         else:
    166             resolved_addrs[addr] = EMPTY_FRAME_DESCRIPTION
    167 
    168 
    169 # Resolve functions and line numbers
    170 if html_output == False:
    171   print "Resolving symbols using directory %s..." % symboldir
    172 for lib in addrs_by_lib:
    173     sofile = symboldir + lib
    174     if os.path.isfile(sofile):
    175         file_offset = 0
    176         result = subprocess.check_output(["objdump", "-w", "-j", ".text", "-h", sofile])
    177         for line in result.split("\n"):
    178             splitted = line.split()
    179             if len(splitted) > 5 and splitted[1] == ".text":
    180                 file_offset = int(splitted[5], 16)
    181                 break
    182 
    183         input_addrs = ""
    184         for addr in addrs_by_lib[lib]:
    185             input_addrs += "%s\n" % hex(addr_offsets[addr] - file_offset)
    186         p = subprocess.Popen(["addr2line", "-C", "-j", ".text", "-e", sofile, "-f"], stdout=subprocess.PIPE, stdin=subprocess.PIPE)
    187         result = p.communicate(input_addrs)[0]
    188         splitted = result.split("\n")
    189         for x in range(0, len(addrs_by_lib[lib])):
    190             function = splitted[2*x];
    191             location = splitted[2*x+1];
    192             resolved_addrs[addrs_by_lib[lib][x]] = FrameDescription(function, location, lib)
    193 
    194     else:
    195         if html_output == False:
    196             print "%s not found for symbol resolution" % lib
    197         fd = FrameDescription("???", "???", lib)
    198         for addr in addrs_by_lib[lib]:
    199             resolved_addrs[addr] = fd
    200 
    201 def addr2line(addr):
    202     if addr == "ZYGOTE" or addr == "APP":
    203         return FrameDescription("", "", "")
    204 
    205     return resolved_addrs[int(addr, 16)]
    206 
    207 class AddrInfo:
    208     def __init__(self, addr):
    209         self.addr = addr
    210         self.size = 0
    211         self.number = 0
    212         self.children = {}
    213 
    214     def addStack(self, size, stack):
    215         self.size += size
    216         self.number += 1
    217         if len(stack) > 0:
    218             child = stack[0]
    219             if not (child.addr in self.children):
    220                 self.children[child.addr] = child
    221             self.children[child.addr].addStack(size, stack[1:])
    222 
    223 zygote = AddrInfo("ZYGOTE")
    224 app = AddrInfo("APP")
    225 
    226 def display(indent, total, parent_total, node):
    227     fd = addr2line(node.addr)
    228     total_percent = 0
    229     if total != 0:
    230       total_percent = 100 * node.size / float(total)
    231     parent_percent = 0
    232     if parent_total != 0:
    233       parent_percent = 100 * node.size / float(parent_total)
    234     print "%9d %6.2f%% %6.2f%% %8d %s%s %s %s %s" % (node.size, total_percent, parent_percent, node.number, indent, node.addr, fd.library, fd.function, fd.location)
    235     children = sorted(node.children.values(), key=lambda x: x.size, reverse=True)
    236     for child in children:
    237         display(indent + "  ", total, node.size, child)
    238 
    239 label_count=0
    240 def display_html(total, node, extra):
    241     global label_count
    242     fd = addr2line(node.addr)
    243     if verbose:
    244         lib = fd.library
    245     else:
    246         lib = os.path.basename(fd.library)
    247     total_percent = 0
    248     if total != 0:
    249         total_percent = 100 * node.size / float(total)
    250     label = "%d %6.2f%% %6d %s%s %s %s" % (node.size, total_percent, node.number, extra, lib, fd.function, fd.location)
    251     label = label.replace("&", "&amp;")
    252     label = label.replace("'", "&apos;")
    253     label = label.replace('"', "&quot;")
    254     label = label.replace("<", "&lt;")
    255     label = label.replace(">", "&gt;")
    256     children = sorted(node.children.values(), key=lambda x: x.size, reverse=True)
    257     print '<li>'
    258     if len(children) > 0:
    259         print '<label for="' + str(label_count) + '">' + label + '</label>'
    260         print '<input type="checkbox" id="' + str(label_count) + '"/>'
    261         print '<ol>'
    262         label_count+=1
    263         for child in children:
    264             display_html(total, child, "")
    265         print '</ol>'
    266     else:
    267         print label
    268     print '</li>'
    269 for backtrace in backtraces:
    270     stack = []
    271     for addr in backtrace.frames:
    272         stack.append(AddrInfo("%x" % addr))
    273     stack.reverse()
    274     if backtrace.is_zygote:
    275         zygote.addStack(backtrace.size, stack)
    276     else:
    277         app.addStack(backtrace.size, stack)
    278 
    279 html_header = """
    280 <!DOCTYPE html>
    281 <html><head><style>
    282 li input {
    283     display: none;
    284 }
    285 li input:checked + ol > li {
    286     display: block;
    287 }
    288 li input + ol > li {
    289     display: none;
    290 }
    291 li {
    292     font-family: Roboto Mono,monospace;
    293 }
    294 label {
    295     font-family: Roboto Mono,monospace;
    296     cursor: pointer
    297 }
    298 </style></head><body>Native allocation HTML viewer<ol>
    299 """
    300 html_footer = "</ol></body></html>"
    301 
    302 if html_output:
    303     print html_header
    304     display_html(app.size, app, "app ")
    305     if zygote.size>0:
    306         display_html(zygote.size, zygote, "zygote ")
    307     print html_footer
    308 else:
    309     print ""
    310     print "%9s %6s %6s %8s    %s %s %s %s" % ("BYTES", "%TOTAL", "%PARENT", "COUNT", "ADDR", "LIBRARY", "FUNCTION", "LOCATION")
    311     display("", app.size, app.size + zygote.size, app)
    312     print ""
    313     display("", zygote.size, app.size + zygote.size, zygote)
    314     print ""
    315 
    316