Las funciones de abajo utilizan heapy para generar una descripción visual (en ascii) de la memoria, junto con otras estadísticas.

heapStats devuelve un diccionario pickleado, que es útil para guardarlo en un archivo para posterior análisis por ejemplo, o para ser transmitido por algún protocolo de red, como xmlrpc o http, puesto que suele utilizarse dentro de un demonio de servidor.

En los datos generados, "byclodo" significa que son datos agrupados por CLass Or Dictionary Owner, lo que suele decirnos con bastante precisión quién es el culpable de mantener vivos los objetos. "byrcs" significa que se agrupan los datos por Referrer Classification Set, que significa el "clodo" del referente, útil para detectar referencias cíclicas por ejemplo.

Finalmente, memmap tiene el mapa de la memoria, y el detalle por si se quiere generar mapas más detallados o con diferente visualización.

Probablemente para un uso concreto sea necesario extender memmap antes del memmap.sort(), agregando buffers específicos de la aplicación que no caigan dentro de lo consumido por python. Por ejemplo, los objetos mmap tienen asociados grandes pedazos de memoria que no aparecerían en el mapa, y si se usan mucho en la aplicación convendría incluirlos, o buffers significativos utilizados por bibliotecas de extensión.

Este código sólo funciona en CPython, puesto que utiliza el hecho de que id() devuelve la dirección en memoria de un objeto. Eso no es cierto en Jython, PyPy y muchas otras implementaciones.

import cPickle
from guppy import hpy as heapy
_heapy = heapy()

def report_memmap(mm):
    import re
    from bisect import bisect_left, bisect_right

    mxsz = max( s for a,s in mm )

    def usage(mn,mx,mxsz):
        rv = ' '
        for a,s in mm[bisect_left(mm,(mn-mxsz-16,0)):bisect_right(mm,(mx+1,0))]:
            if mn>=mx:
                break
            s += a + 16 # add 16 bytes for malloc headers
            if s<=mn:
                continue
            if a<mx:
                rv = '-' # touched the range, at least fragmented
            if a>mn:
                # cannot be fully used
                break
            #used up to s
            mn=s
        if mx <= mn:
            rv = '*' # used in full
        return rv

    def bytes(x):
        if x < 1024:
            return '%db' % x
        elif x < 1024*1024:
            return '%.2fKb' % (x/1024.0)
        elif x < 1024*1024*1024:
            return '%.2fMb' % (x/1024.0/1024.0)
        else:
            return '%.2fGb' % (x/1024.0/1024.0/1024.0)

    def secsize(x):
        rv = 4096
        while (x/rv/80) > 40:
            rv *= 2
        return rv

    def report(mn,mx,ss):
        smxsz = max( s for a,s in mm[bisect_left(mm,(mn-mxsz-16,0)):bisect_right(mm,(mx+1,0))] )
        mp  = ''.join([ usage(i,i+ss,smxsz) for i in range(mn,mx,ss) ])
        rv  = '%s total, %s per sector\n' % (bytes(mx-mn), bytes(ss))
        rv += lre.sub('\\1\n',mp)
        rv += """
        Fragmentation: %.2f%%
        Fragmented sectors: %d
        Contiguous used sectors: %d
        Contiguous free sectors: %d
        """ % ( mp.count('-')*100.0/len(mp),
                mp.count('-'),
                mp.count('*'),
                mp.count(' ') )
        return rv

    def domap(filterfn):
        if not any(filterfn(a) for a,s in mm):
            return 'empty'
        mn = min( a for a,s in mm if filterfn(a) )
        mx = max( a for a,s in mm if filterfn(a) )
        ss = secsize(mx-mn)
        mn = mn/ss*ss
        mx = mx/ss*ss+ss
        return report(mn,mx,ss)

    lre = re.compile('(.{80,80})')

    lomap = domap(lambda a: a <  0x80000000)
    medmap= domap(lambda a: a >= 0x80000000 and a < 0x100000000L)
    himap = domap(lambda a: a >= 0x100000000L)

    return lomap, medmap, himap


def heapStats():
    global _debug_heap
    global _heapy

    import StringIO

    statdump = StringIO.StringIO()
    heap = _heapy.heap()

    try:
        heap.dump(statdump)
    except:
        # ignore exceptions dumping... shit happens
        pass

    statdumpu = StringIO.StringIO()
    heapu = _heapy.heapu()

    try:
        heapu.dump(statdumpu)
    except:
        # ignore exceptions dumping... shit happens
        pass

    statdumpbr = StringIO.StringIO()
    heapbr = heap.byrcs

    try:
        heapbr.dump(statdumpbr)
    except:
        # ignore exceptions dumping... shit happens
        pass

    refs = None
    try:
        refs = heap.stat
        refs.rows = list(refs.get_rows())
        refs.rows.sort(lambda x,y:-cmp(x.count,y.count))

        oc = _heapy.Size.classifier.get_cli().classify
        id_ = id
        str_ = str
        memmap = [ (id_(x),oc(x)) for x in heap.nodes ]
    except:
        # At least the rest will be useful
        memmap = []

    memmap.sort()

    # Generate lowres reports from the memmap in four areas, lo, med, hi and very hi.
    # memory (memory allocations tend to group themselves in those ranges,
    # one is probably memmapped heap, the other is simple allocations and
    # the medium one must be the stack). The very high area is the mmap'd area,
    # where most big arrays end up.
    lomap, medmap, himap = report_memmap(memmap)

    # Pickle the memmap, xmlrpclib doesn't like big integers
    memmap = cPickle.dumps(memmap)

    def srepr(x):
        try:
            return repr(x)
        except Exception,e:
            return 'ERROR: %s' (e,)

    rv = dict(
        byclodo = dict(
            reachable = map(srepr, [ heap, heap.more, heap.more.more ]),
            uncollectable = map(srepr, [ heapu, heapu.more, heapu.more.more ]),
            statdump = statdump.getvalue(),
            statdumpu = statdumpu.getvalue(),
            refs = srepr(refs)
        ),
        byrcs = dict(
            reachable = map(srepr, [ heapbr, heapbr.more, heapbr.more.more ]),
            statdump = statdumpbr.getvalue()
        ),
        memmap = dict(
            detail = memmap,
            lo = lomap,
            med = medmap,
            hi = himap
        )
    )

    # return a pickle dump, not by pure xmlrpc
    #   (xmlrpc is picky, doesn't support big ints)
    return cPickle.dumps(rv, 2)