315 lines
9.9 KiB
Python
Executable file
315 lines
9.9 KiB
Python
Executable file
#!/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 ""
|
|
|