| #!/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 <pid> /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<start>[0-9a-f]+)-(?P<end>[0-9a-f]+) .... (?P<offset>[0-9a-f]+) [0-9a-f]+:[0-9a-f]+ [0-9]+ +(?P<name>.*)") |
| |
| 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) |
| total_percent = 0 |
| if total != 0: |
| total_percent = 100 * node.size / float(total) |
| parent_percent = 0 |
| if parent_total != 0: |
| parent_percent = 100 * node.size / float(parent_total) |
| 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) |
| 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) |
| total_percent = 0 |
| if total != 0: |
| total_percent = 100 * node.size / float(total) |
| label = "%d %6.2f%% %6d %s%s %s %s" % (node.size, total_percent, 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 '<li>' |
| if len(children) > 0: |
| print '<label for="' + str(label_count) + '">' + label + '</label>' |
| print '<input type="checkbox" id="' + str(label_count) + '"/>' |
| print '<ol>' |
| label_count+=1 |
| for child in children: |
| display_html(total, child, "") |
| print '</ol>' |
| else: |
| print label |
| print '</li>' |
| 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 = """ |
| <!DOCTYPE html> |
| <html><head><style> |
| li input { |
| display: none; |
| } |
| li input:checked + ol > li { |
| display: block; |
| } |
| li input + ol > li { |
| display: none; |
| } |
| li { |
| font-family: Roboto Mono,monospace; |
| } |
| label { |
| font-family: Roboto Mono,monospace; |
| cursor: pointer |
| } |
| </style></head><body>Native allocation HTML viewer<ol> |
| """ |
| html_footer = "</ol></body></html>" |
| |
| 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 "" |
| |