diff --git a/scripts/native_heapdump_viewer.py b/scripts/native_heapdump_viewer.py new file mode 100755 index 000000000..8cda6b602 --- /dev/null +++ b/scripts/native_heapdump_viewer.py @@ -0,0 +1,306 @@ +#!/usr/bin/env python +# +# Copyright (C) 2017 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. + +"""Generates a human-interpretable view of a native heap dump from 'am dumpheap -n'.""" + +import os +import os.path +import re +import subprocess +import sys + +usage = """ +Usage: +1. Collect a native heap dump from the device. For example: + $ adb shell stop + $ adb shell setprop libc.debug.malloc.program app_process + $ adb shell setprop libc.debug.malloc.options backtrace=64 + $ adb shell start + (launch and use app) + $ adb shell am dumpheap -n /data/local/tmp/native_heap.txt + $ adb pull /data/local/tmp/native_heap.txt + +2. Run the viewer: + $ python native_heapdump_viewer.py [options] native_heap.txt + [--verbose]: verbose output + [--html]: interactive html output + [--reverse]: reverse the backtraces (start the tree from the leaves) + [--symbols SYMBOL_DIR] SYMBOL_DIR is the directory containing the .so files with symbols. + Defaults to $ANDROID_PRODUCT_OUT/symbols + This outputs a file with lines of the form: + + 5831776 29.09% 100.00% 10532 71b07bc0b0 /system/lib64/libandroid_runtime.so Typeface_createFromArray frameworks/base/core/jni/android/graphics/Typeface.cpp:68 + + 5831776 is the total number of bytes allocated at this stack frame, which + is 29.09% of the total number of bytes allocated and 100.00% of the parent + frame's bytes allocated. 10532 is the total number of allocations at this + stack frame. 71b07bc0b0 is the address of the stack frame. +""" + +verbose = False +html_output = False +reverse_frames = False +product_out = os.getenv("ANDROID_PRODUCT_OUT") +if product_out: + symboldir = product_out + "/symbols" +else: + symboldir = "./symbols" + +args = sys.argv[1:] +while len(args) > 1: + if args[0] == "--symbols": + symboldir = args[1] + args = args[2:] + elif args[0] == "--verbose": + verbose = True + args = args[1:] + elif args[0] == "--html": + html_output = True + args = args[1:] + elif args[0] == "--reverse": + reverse_frames = True + args = args[1:] + else: + print "Invalid option "+args[0] + break + +if len(args) != 1: + print usage + exit(0) + +native_heap = args[0] + +re_map = re.compile("(?P[0-9a-f]+)-(?P[0-9a-f]+) .... (?P[0-9a-f]+) [0-9a-f]+:[0-9a-f]+ [0-9]+ +(?P.*)") + +class Backtrace: + def __init__(self, is_zygote, size, frames): + self.is_zygote = is_zygote + self.size = size + self.frames = frames + +class Mapping: + def __init__(self, start, end, offset, name): + self.start = start + self.end = end + self.offset = offset + self.name = name + +class FrameDescription: + def __init__(self, function, location, library): + self.function = function + self.location = location + self.library = library + + +backtraces = [] +mappings = [] + +for line in open(native_heap, "r"): + parts = line.split() + if len(parts) > 7 and parts[0] == "z" and parts[2] == "sz": + is_zygote = parts[1] != "1" + size = int(parts[3]) + frames = map(lambda x: int(x, 16), parts[7:]) + if reverse_frames: + frames = list(reversed(frames)) + backtraces.append(Backtrace(is_zygote, size, frames)) + continue + + m = re_map.match(line) + if m: + start = int(m.group('start'), 16) + end = int(m.group('end'), 16) + offset = int(m.group('offset'), 16) + name = m.group('name') + mappings.append(Mapping(start, end, offset, name)) + continue + +# Return the mapping that contains the given address. +# Returns None if there is no such mapping. +def find_mapping(addr): + min = 0 + max = len(mappings) - 1 + while True: + if max < min: + return None + mid = (min + max) // 2 + if mappings[mid].end <= addr: + min = mid + 1 + elif mappings[mid].start > addr: + max = mid - 1 + else: + return mappings[mid] + +# Resolve address libraries and offsets. +# addr_offsets maps addr to .so file offset +# addrs_by_lib maps library to list of addrs from that library +# Resolved addrs maps addr to FrameDescription +addr_offsets = {} +addrs_by_lib = {} +resolved_addrs = {} +EMPTY_FRAME_DESCRIPTION = FrameDescription("???", "???", "???") +for backtrace in backtraces: + for addr in backtrace.frames: + if addr in addr_offsets: + continue + mapping = find_mapping(addr) + if mapping: + addr_offsets[addr] = addr - mapping.start + mapping.offset + if not (mapping.name in addrs_by_lib): + addrs_by_lib[mapping.name] = [] + addrs_by_lib[mapping.name].append(addr) + else: + resolved_addrs[addr] = EMPTY_FRAME_DESCRIPTION + + +# Resolve functions and line numbers +if html_output == False: + print "Resolving symbols using directory %s..." % symboldir +for lib in addrs_by_lib: + sofile = symboldir + lib + if os.path.isfile(sofile): + file_offset = 0 + result = subprocess.check_output(["objdump", "-w", "-j", ".text", "-h", sofile]) + for line in result.split("\n"): + splitted = line.split() + if len(splitted) > 5 and splitted[1] == ".text": + file_offset = int(splitted[5], 16) + break + + input_addrs = "" + for addr in addrs_by_lib[lib]: + input_addrs += "%s\n" % hex(addr_offsets[addr] - file_offset) + p = subprocess.Popen(["addr2line", "-C", "-j", ".text", "-e", sofile, "-f"], stdout=subprocess.PIPE, stdin=subprocess.PIPE) + result = p.communicate(input_addrs)[0] + splitted = result.split("\n") + for x in range(0, len(addrs_by_lib[lib])): + function = splitted[2*x]; + location = splitted[2*x+1]; + resolved_addrs[addrs_by_lib[lib][x]] = FrameDescription(function, location, lib) + + else: + if html_output == False: + print "%s not found for symbol resolution" % lib + fd = FrameDescription("???", "???", lib) + for addr in addrs_by_lib[lib]: + resolved_addrs[addr] = fd + +def addr2line(addr): + if addr == "ZYGOTE" or addr == "APP": + return FrameDescription("", "", "") + + return resolved_addrs[int(addr, 16)] + +class AddrInfo: + def __init__(self, addr): + self.addr = addr + self.size = 0 + self.number = 0 + self.children = {} + + def addStack(self, size, stack): + self.size += size + self.number += 1 + if len(stack) > 0: + child = stack[0] + if not (child.addr in self.children): + self.children[child.addr] = child + self.children[child.addr].addStack(size, stack[1:]) + +zygote = AddrInfo("ZYGOTE") +app = AddrInfo("APP") + +def display(indent, total, parent_total, node): + fd = addr2line(node.addr) + print "%9d %6.2f%% %6.2f%% %8d %s%s %s %s %s" % (node.size, 100*node.size/float(total), 100*node.size/float(parent_total), node.number, indent, node.addr, fd.library, fd.function, fd.location) + children = sorted(node.children.values(), key=lambda x: x.size, reverse=True) + for child in children: + display(indent + " ", total, node.size, child) + +label_count=0 +def display_html(total, node, extra): + global label_count + fd = addr2line(node.addr) + if verbose: + lib = fd.library + else: + lib = os.path.basename(fd.library) + label = "%d %6.2f%% %6d %s%s %s %s" % (node.size, 100*node.size/float(total), node.number, extra, lib, fd.function, fd.location) + label = label.replace("&", "&") + label = label.replace("'", "'") + label = label.replace('"', """) + label = label.replace("<", "<") + label = label.replace(">", ">") + children = sorted(node.children.values(), key=lambda x: x.size, reverse=True) + print '
  • ' + if len(children) > 0: + print '' + print '' + print '
      ' + label_count+=1 + for child in children: + display_html(total, child, "") + print '
    ' + else: + print label + print '
  • ' +for backtrace in backtraces: + stack = [] + for addr in backtrace.frames: + stack.append(AddrInfo("%x" % addr)) + stack.reverse() + if backtrace.is_zygote: + zygote.addStack(backtrace.size, stack) + else: + app.addStack(backtrace.size, stack) + +html_header = """ + +Native allocation HTML viewer
      +""" +html_footer = "
    " + +if html_output: + print html_header + display_html(app.size, app, "app ") + if zygote.size>0: + display_html(zygote.size, zygote, "zygote ") + print html_footer +else: + print "" + print "%9s %6s %6s %8s %s %s %s %s" % ("BYTES", "%TOTAL", "%PARENT", "COUNT", "ADDR", "LIBRARY", "FUNCTION", "LOCATION") + display("", app.size, app.size + zygote.size, app) + print "" + display("", zygote.size, app.size + zygote.size, zygote) + print "" +