blob: 0689a1d1db3a0b2d5b1c2056983daeef81144908 [file] [log] [blame]
Richard Uhlera1787322017-05-17 13:07:54 -07001#!/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
19import os
20import os.path
21import re
22import subprocess
23import sys
24
25usage = """
26Usage:
271. 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
362. 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
53verbose = False
54html_output = False
55reverse_frames = False
56product_out = os.getenv("ANDROID_PRODUCT_OUT")
57if product_out:
58 symboldir = product_out + "/symbols"
59else:
60 symboldir = "./symbols"
61
62args = sys.argv[1:]
63while 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
80if len(args) != 1:
81 print usage
82 exit(0)
83
84native_heap = args[0]
85
86re_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
88class Backtrace:
89 def __init__(self, is_zygote, size, frames):
90 self.is_zygote = is_zygote
91 self.size = size
92 self.frames = frames
93
94class 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
101class FrameDescription:
102 def __init__(self, function, location, library):
103 self.function = function
104 self.location = location
105 self.library = library
106
107
108backtraces = []
109mappings = []
110
111for 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.
133def 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
151addr_offsets = {}
152addrs_by_lib = {}
153resolved_addrs = {}
154EMPTY_FRAME_DESCRIPTION = FrameDescription("???", "???", "???")
155for 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
170if html_output == False:
171 print "Resolving symbols using directory %s..." % symboldir
172for 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
201def addr2line(addr):
202 if addr == "ZYGOTE" or addr == "APP":
203 return FrameDescription("", "", "")
204
205 return resolved_addrs[int(addr, 16)]
206
207class 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
223zygote = AddrInfo("ZYGOTE")
224app = AddrInfo("APP")
225
226def display(indent, total, parent_total, node):
227 fd = addr2line(node.addr)
Christopher Ferrisf4276552017-05-24 16:26:56 -0700228 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)
Richard Uhlera1787322017-05-17 13:07:54 -0700235 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
239label_count=0
240def 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)
Mathieu Chartier9ae5ff42017-06-13 10:55:48 -0700247 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)
Richard Uhlera1787322017-05-17 13:07:54 -0700251 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>'
269for 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
279html_header = """
280<!DOCTYPE html>
281<html><head><style>
282li input {
283 display: none;
284}
285li input:checked + ol > li {
286 display: block;
287}
288li input + ol > li {
289 display: none;
290}
291li {
292 font-family: Roboto Mono,monospace;
293}
294label {
295 font-family: Roboto Mono,monospace;
296 cursor: pointer
297}
298</style></head><body>Native allocation HTML viewer<ol>
299"""
300html_footer = "</ol></body></html>"
301
302if 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
308else:
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