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("&", "&") 252 label = label.replace("'", "'") 253 label = label.replace('"', """) 254 label = label.replace("<", "<") 255 label = label.replace(">", ">") 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