Richard Uhler | a178732 | 2017-05-17 13:07:54 -0700 | [diff] [blame] | 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) |
Christopher Ferris | f427655 | 2017-05-24 16:26:56 -0700 | [diff] [blame] | 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) |
Richard Uhler | a178732 | 2017-05-17 13:07:54 -0700 | [diff] [blame] | 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) |
Mathieu Chartier | 9ae5ff4 | 2017-06-13 10:55:48 -0700 | [diff] [blame] | 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) |
Richard Uhler | a178732 | 2017-05-17 13:07:54 -0700 | [diff] [blame] | 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 | |