summaryrefslogtreecommitdiffstats
path: root/memory/replace/dmd/block_analyzer.py
diff options
context:
space:
mode:
Diffstat (limited to 'memory/replace/dmd/block_analyzer.py')
-rw-r--r--memory/replace/dmd/block_analyzer.py261
1 files changed, 261 insertions, 0 deletions
diff --git a/memory/replace/dmd/block_analyzer.py b/memory/replace/dmd/block_analyzer.py
new file mode 100644
index 000000000..cc0da1e11
--- /dev/null
+++ b/memory/replace/dmd/block_analyzer.py
@@ -0,0 +1,261 @@
+#!/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.
+
+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',
+ # 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=150,
+ 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,
+ 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))
+
+ invocation = j['invocation']
+
+ 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 not block in blocks:
+ print 'Object', 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()