forked from mirrors/gecko-dev
		
	 05bf8c42eb
			
		
	
	
		05bf8c42eb
		
	
	
	
	
		
			
			"print" had no params so it wasn't doing anything, causing there to be less newlines than needed. The default stack frame width is a bit narrow, so I increased it. There are a few new uninteresting functions showing up, so I've added them to the allocator list. Differential Revision: https://phabricator.services.mozilla.com/D118381
		
			
				
	
	
		
			296 lines
		
	
	
	
		
			8.2 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			296 lines
		
	
	
	
		
			8.2 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| #!/usr/bin/python
 | |
| 
 | |
| # This Source Code Form is subject to the terms of the Mozilla Public
 | |
| # License, v. 2.0. If a copy of the MPL was not distributed with this
 | |
| # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 | |
| 
 | |
| # From a scan mode DMD log, extract some information about a
 | |
| # particular block, such as its allocation stack or which other blocks
 | |
| # contain pointers to it. This can be useful when investigating leaks
 | |
| # caused by unknown references to refcounted objects.
 | |
| 
 | |
| from __future__ import absolute_import, print_function
 | |
| 
 | |
| import json
 | |
| import gzip
 | |
| import sys
 | |
| import argparse
 | |
| import re
 | |
| 
 | |
| 
 | |
| # The DMD output version this script handles.
 | |
| outputVersion = 5
 | |
| 
 | |
| # If --ignore-alloc-fns is specified, stack frames containing functions that
 | |
| # match these strings will be removed from the *start* of stack traces. (Once
 | |
| # we hit a non-matching frame, any subsequent frames won't be removed even if
 | |
| # they do match.)
 | |
| allocatorFns = [
 | |
|     "malloc (",
 | |
|     "replace_malloc",
 | |
|     "replace_calloc",
 | |
|     "replace_realloc",
 | |
|     "replace_memalign",
 | |
|     "replace_posix_memalign",
 | |
|     "malloc_zone_malloc",
 | |
|     "moz_xmalloc",
 | |
|     "moz_xcalloc",
 | |
|     "moz_xrealloc",
 | |
|     "operator new(",
 | |
|     "operator new[](",
 | |
|     "g_malloc",
 | |
|     "g_slice_alloc",
 | |
|     "callocCanGC",
 | |
|     "reallocCanGC",
 | |
|     "vpx_malloc",
 | |
|     "vpx_calloc",
 | |
|     "vpx_realloc",
 | |
|     "vpx_memalign",
 | |
|     "js_malloc",
 | |
|     "js_calloc",
 | |
|     "js_realloc",
 | |
|     "pod_malloc",
 | |
|     "pod_calloc",
 | |
|     "pod_realloc",
 | |
|     "nsTArrayInfallibleAllocator::Malloc",
 | |
|     "Allocator<ReplaceMallocBase>::malloc(",
 | |
|     "mozilla::dmd::StackTrace::Get(",
 | |
|     "mozilla::dmd::AllocCallback(",
 | |
|     "mozilla::dom::DOMArena::Allocate(",
 | |
|     # This one necessary to fully filter some sequences of allocation functions
 | |
|     # that happen in practice. Note that ??? entries that follow non-allocation
 | |
|     # functions won't be stripped, as explained above.
 | |
|     "???",
 | |
| ]
 | |
| 
 | |
| ####
 | |
| 
 | |
| # Command line arguments
 | |
| 
 | |
| 
 | |
| def range_1_24(string):
 | |
|     value = int(string)
 | |
|     if value < 1 or value > 24:
 | |
|         msg = "{:s} is not in the range 1..24".format(string)
 | |
|         raise argparse.ArgumentTypeError(msg)
 | |
|     return value
 | |
| 
 | |
| 
 | |
| parser = argparse.ArgumentParser(
 | |
|     description="Analyze the heap graph to find out things about an object. \
 | |
| By default this prints out information about blocks that point to the given block."
 | |
| )
 | |
| 
 | |
| parser.add_argument("dmd_log_file_name", help="clamped DMD log file name")
 | |
| 
 | |
| parser.add_argument("block", help="address of the block of interest")
 | |
| 
 | |
| parser.add_argument(
 | |
|     "--info",
 | |
|     dest="info",
 | |
|     action="store_true",
 | |
|     default=False,
 | |
|     help="Print out information about the block.",
 | |
| )
 | |
| 
 | |
| parser.add_argument(
 | |
|     "-sfl",
 | |
|     "--max-stack-frame-length",
 | |
|     type=int,
 | |
|     default=300,
 | |
|     help="Maximum number of characters to print from each stack frame",
 | |
| )
 | |
| 
 | |
| parser.add_argument(
 | |
|     "-a",
 | |
|     "--ignore-alloc-fns",
 | |
|     action="store_true",
 | |
|     help="ignore allocation functions at the start of traces",
 | |
| )
 | |
| 
 | |
| parser.add_argument(
 | |
|     "-f",
 | |
|     "--max-frames",
 | |
|     type=range_1_24,
 | |
|     default=8,
 | |
|     help="maximum number of frames to consider in each trace",
 | |
| )
 | |
| 
 | |
| parser.add_argument(
 | |
|     "-c",
 | |
|     "--chain-reports",
 | |
|     action="store_true",
 | |
|     help="if only one block is found to hold onto the object, report "
 | |
|     "the next one, too",
 | |
| )
 | |
| 
 | |
| 
 | |
| ####
 | |
| 
 | |
| 
 | |
| class BlockData:
 | |
|     def __init__(self, json_block):
 | |
|         self.addr = json_block["addr"]
 | |
| 
 | |
|         if "contents" in json_block:
 | |
|             contents = json_block["contents"]
 | |
|         else:
 | |
|             contents = []
 | |
|         self.contents = []
 | |
|         for c in contents:
 | |
|             self.contents.append(int(c, 16))
 | |
| 
 | |
|         self.req_size = json_block["req"]
 | |
| 
 | |
|         self.alloc_stack = json_block["alloc"]
 | |
| 
 | |
| 
 | |
| def print_trace_segment(args, stacks, block):
 | |
|     (traceTable, frameTable) = stacks
 | |
| 
 | |
|     for l in traceTable[block.alloc_stack]:
 | |
|         # The 5: is to remove the bogus leading "#00: " from the stack frame.
 | |
|         print("  " + frameTable[l][5 : args.max_stack_frame_length])
 | |
| 
 | |
| 
 | |
| def show_referrers(args, blocks, stacks, block):
 | |
|     visited = set([])
 | |
| 
 | |
|     anyFound = False
 | |
| 
 | |
|     while True:
 | |
|         referrers = {}
 | |
| 
 | |
|         for b, data in blocks.iteritems():
 | |
|             which_edge = 0
 | |
|             for e in data.contents:
 | |
|                 if e == block:
 | |
|                     # 8 is the number of bytes per word on a 64-bit system.
 | |
|                     # XXX This means that this output will be wrong for logs from 32-bit systems!
 | |
|                     referrers.setdefault(b, []).append(8 * which_edge)
 | |
|                     anyFound = True
 | |
|                 which_edge += 1
 | |
| 
 | |
|         for r in referrers:
 | |
|             sys.stdout.write(
 | |
|                 "0x{} size = {} bytes".format(blocks[r].addr, blocks[r].req_size)
 | |
|             )
 | |
|             plural = "s" if len(referrers[r]) > 1 else ""
 | |
|             sys.stdout.write(
 | |
|                 " at byte offset"
 | |
|                 + plural
 | |
|                 + " "
 | |
|                 + (", ".join(str(x) for x in referrers[r]))
 | |
|             )
 | |
|             print()
 | |
|             print_trace_segment(args, stacks, blocks[r])
 | |
|             print()
 | |
| 
 | |
|         if args.chain_reports:
 | |
|             if len(referrers) == 0:
 | |
|                 sys.stdout.write("Found no more referrers.\n")
 | |
|                 break
 | |
|             if len(referrers) > 1:
 | |
|                 sys.stdout.write("Found too many referrers.\n")
 | |
|                 break
 | |
| 
 | |
|             sys.stdout.write("Chaining to next referrer.\n\n")
 | |
|             for r in referrers:
 | |
|                 block = r
 | |
|             if block in visited:
 | |
|                 sys.stdout.write("Found a loop.\n")
 | |
|                 break
 | |
|             visited.add(block)
 | |
|         else:
 | |
|             break
 | |
| 
 | |
|     if not anyFound:
 | |
|         print("No referrers found.")
 | |
| 
 | |
| 
 | |
| def show_block_info(args, blocks, stacks, block):
 | |
|     b = blocks[block]
 | |
|     sys.stdout.write("block: 0x{}\n".format(b.addr))
 | |
|     sys.stdout.write("requested size: {} bytes\n".format(b.req_size))
 | |
|     sys.stdout.write("\n")
 | |
|     sys.stdout.write("block contents: ")
 | |
|     for c in b.contents:
 | |
|         v = "0" if c == 0 else blocks[c].addr
 | |
|         sys.stdout.write("0x{} ".format(v))
 | |
|     sys.stdout.write("\n\n")
 | |
|     sys.stdout.write("allocation stack:\n")
 | |
|     print_trace_segment(args, stacks, b)
 | |
|     return
 | |
| 
 | |
| 
 | |
| def cleanupTraceTable(args, frameTable, traceTable):
 | |
|     # Remove allocation functions at the start of traces.
 | |
|     if args.ignore_alloc_fns:
 | |
|         # Build a regexp that matches every function in allocatorFns.
 | |
|         escapedAllocatorFns = map(re.escape, allocatorFns)
 | |
|         fn_re = re.compile("|".join(escapedAllocatorFns))
 | |
| 
 | |
|         # Remove allocator fns from each stack trace.
 | |
|         for traceKey, frameKeys in traceTable.items():
 | |
|             numSkippedFrames = 0
 | |
|             for frameKey in frameKeys:
 | |
|                 frameDesc = frameTable[frameKey]
 | |
|                 if re.search(fn_re, frameDesc):
 | |
|                     numSkippedFrames += 1
 | |
|                 else:
 | |
|                     break
 | |
|             if numSkippedFrames > 0:
 | |
|                 traceTable[traceKey] = frameKeys[numSkippedFrames:]
 | |
| 
 | |
|     # Trim the number of frames.
 | |
|     for traceKey, frameKeys in traceTable.items():
 | |
|         if len(frameKeys) > args.max_frames:
 | |
|             traceTable[traceKey] = frameKeys[: args.max_frames]
 | |
| 
 | |
| 
 | |
| def loadGraph(options):
 | |
|     # Handle gzipped input if necessary.
 | |
|     isZipped = options.dmd_log_file_name.endswith(".gz")
 | |
|     opener = gzip.open if isZipped else open
 | |
| 
 | |
|     with opener(options.dmd_log_file_name, "rb") as f:
 | |
|         j = json.load(f)
 | |
| 
 | |
|     if j["version"] != outputVersion:
 | |
|         raise Exception("'version' property isn't '{:d}'".format(outputVersion))
 | |
| 
 | |
|     block_list = j["blockList"]
 | |
|     blocks = {}
 | |
| 
 | |
|     for json_block in block_list:
 | |
|         blocks[int(json_block["addr"], 16)] = BlockData(json_block)
 | |
| 
 | |
|     traceTable = j["traceTable"]
 | |
|     frameTable = j["frameTable"]
 | |
| 
 | |
|     cleanupTraceTable(options, frameTable, traceTable)
 | |
| 
 | |
|     return (blocks, (traceTable, frameTable))
 | |
| 
 | |
| 
 | |
| def analyzeLogs():
 | |
|     options = parser.parse_args()
 | |
| 
 | |
|     (blocks, stacks) = loadGraph(options)
 | |
| 
 | |
|     block = int(options.block, 16)
 | |
| 
 | |
|     if block not in blocks:
 | |
|         print("Object " + options.block + " not found in traces.")
 | |
|         print("It could still be the target of some nodes.")
 | |
|         return
 | |
| 
 | |
|     if options.info:
 | |
|         show_block_info(options, blocks, stacks, block)
 | |
|         return
 | |
| 
 | |
|     show_referrers(options, blocks, stacks, block)
 | |
| 
 | |
| 
 | |
| if __name__ == "__main__":
 | |
|     analyzeLogs()
 |