Blame memory/replace/dmd/block_analyzer.py

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