Source

CherryPy / cherrypy / lib / gctools.py

Full commit
Robert Brewer 3199e7e 

Robert Brewer c259b1c 

Robert Brewer fe78b28 
Robert Brewer c259b1c 




Robert Brewer 3199e7e 
Robert Brewer c259b1c 
Robert Brewer fe78b28 
Robert Brewer af33286 
Robert Brewer 3199e7e 
























Robert Brewer bc568ce 



Robert Brewer 3199e7e 
Robert Brewer bc568ce 
Robert Brewer 3199e7e 


























































Robert Brewer fe78b28 













Robert Brewer a9c0e54 











Robert Brewer 3199e7e 






Robert Brewer fe78b28 

Robert Brewer 3199e7e 








Robert Brewer fe78b28 






Robert Brewer 3199e7e 

Joseph Tate 55f54dc 

Robert Brewer 3199e7e 


Robert Brewer a9c0e54 




Robert Brewer 3199e7e 


Robert Brewer a9c0e54 





Robert Brewer fe78b28 
Robert Brewer 3199e7e 



Robert Brewer a9c0e54 
Robert Brewer 3199e7e 
Robert Brewer a9c0e54 



Robert Brewer 3199e7e 









Robert Brewer fe78b28 
Robert Brewer 3199e7e 
Robert Brewer c259b1c 
Robert Brewer fe78b28 
Robert Brewer a9c0e54 
Robert Brewer fe78b28 
Robert Brewer a9c0e54 

Robert Brewer fe78b28 

Robert Brewer 3199e7e 





import gc
import inspect
import os
import sys
import time
 
try:
    import objgraph
except ImportError:
    objgraph = None

import cherrypy
from cherrypy import _cprequest, _cpwsgi
from processbus.plugins import SimplePlugin


class ReferrerTree(object):
    """An object which gathers all referrers of an object to a given depth."""

    peek_length = 40

    def __init__(self, ignore=None, maxdepth=2, maxparents=10):
        self.ignore = ignore or []
        self.ignore.append(inspect.currentframe().f_back)
        self.maxdepth = maxdepth
        self.maxparents = maxparents

    def ascend(self, obj, depth=1):
        """Return a nested list containing referrers of the given object."""
        depth += 1
        parents = []

        # Gather all referrers in one step to minimize
        # cascading references due to repr() logic.
        refs = gc.get_referrers(obj)
        self.ignore.append(refs)
        if len(refs) > self.maxparents:
            return [("[%s referrers]" % len(refs), [])]

        try:
            ascendcode = self.ascend.__code__
        except AttributeError:
            ascendcode = self.ascend.im_func.func_code
        for parent in refs:
            if inspect.isframe(parent) and parent.f_code is ascendcode:
                continue
            if parent in self.ignore:
                continue
            if depth <= self.maxdepth:
                parents.append((parent, self.ascend(parent, depth)))
            else:
                parents.append((parent, []))

        return parents

    def peek(self, s):
        """Return s, restricted to a sane length."""
        if len(s) > (self.peek_length + 3):
            half = self.peek_length // 2
            return s[:half] + '...' + s[-half:]
        else:
            return s

    def _format(self, obj, descend=True):
        """Return a string representation of a single object."""
        if inspect.isframe(obj):
            filename, lineno, func, context, index = inspect.getframeinfo(obj)
            return "<frame of function '%s'>" % func

        if not descend:
            return self.peek(repr(obj))

        if isinstance(obj, dict):
            return "{" + ", ".join(["%s: %s" % (self._format(k, descend=False),
                                                self._format(v, descend=False))
                                    for k, v in obj.items()]) + "}"
        elif isinstance(obj, list):
            return "[" + ", ".join([self._format(item, descend=False)
                                    for item in obj]) + "]"
        elif isinstance(obj, tuple):
            return "(" + ", ".join([self._format(item, descend=False)
                                    for item in obj]) + ")"

        r = self.peek(repr(obj))
        if isinstance(obj, (str, int, float)):
            return r
        return "%s: %s" % (type(obj), r)

    def format(self, tree):
        """Return a list of string reprs from a nested list of referrers."""
        output = []
        def ascend(branch, depth=1):
            for parent, grandparents in branch:
                output.append(("    " * depth) + self._format(parent))
                if grandparents:
                    ascend(grandparents, depth + 1)
        ascend(tree)
        return output


def get_instances(cls):
    return [x for x in gc.get_objects() if isinstance(x, cls)]


class RequestCounter(SimplePlugin):
    
    def start(self):
        self.count = 0
    
    def before_request(self):
        self.count += 1
    
    def after_request(self):
        self.count -=1
request_counter = RequestCounter(cherrypy.engine)
request_counter.subscribe()


def get_context(obj):
    if isinstance(obj, _cprequest.Request):
        return "path=%s;stage=%s" % (obj.path_info, obj.stage)
    elif isinstance(obj, _cprequest.Response):
        return "status=%s" % obj.status
    elif isinstance(obj, _cpwsgi.AppResponse):
        return "PATH_INFO=%s" % obj.environ.get('PATH_INFO', '')
    elif hasattr(obj, "tb_lineno"):
        return "tb_lineno=%s" % obj.tb_lineno
    return ""


class GCRoot(object):
    """A CherryPy page handler for testing reference leaks."""

    classes = [(_cprequest.Request, 2, 2,
                "Should be 1 in this request thread and 1 in the main thread."),
               (_cprequest.Response, 2, 2,
                "Should be 1 in this request thread and 1 in the main thread."),
               (_cpwsgi.AppResponse, 1, 1,
                "Should be 1 in this request thread only."),
               ]

    def index(self):
        return "Hello, world!"
    index.exposed = True

    def stats(self):
        output = ["Statistics:"]
        
        for trial in range(10):
            if request_counter.count > 0:
                break
            time.sleep(0.5)
        else:
            output.append("\nNot all requests closed properly.")
        
        # gc_collect isn't perfectly synchronous, because it may
        # break reference cycles that then take time to fully
        # finalize. Call it thrice and hope for the best.
        gc.collect()
        gc.collect()
        unreachable = gc.collect()
        if unreachable:
            if objgraph is not None:
                final = objgraph.by_type('Nondestructible')
                if final:
                    objgraph.show_backrefs(final, filename='finalizers.png')

            trash = {}
            for x in gc.garbage:
                trash[type(x)] = trash.get(type(x), 0) + 1
            if trash:
                output.insert(0, "\n%s unreachable objects:" % unreachable)
                trash = [(v, k) for k, v in trash.items()]
                trash.sort()
                for pair in trash:
                    output.append("    " + repr(pair))

        # Check declared classes to verify uncollected instances.
        # These don't have to be part of a cycle; they can be
        # any objects that have unanticipated referrers that keep
        # them from being collected.
        allobjs = {}
        for cls, minobj, maxobj, msg in self.classes:
            allobjs[cls] = get_instances(cls)

        for cls, minobj, maxobj, msg in self.classes:
            objs = allobjs[cls]
            lenobj = len(objs)
            if lenobj < minobj or lenobj > maxobj:
                if minobj == maxobj:
                    output.append(
                        "\nExpected %s %r references, got %s." %
                        (minobj, cls, lenobj))
                else:
                    output.append(
                        "\nExpected %s to %s %r references, got %s." %
                        (minobj, maxobj, cls, lenobj))

                for obj in objs:
                    if objgraph is not None:
                        ig = [id(objs), id(inspect.currentframe())]
                        fname = "graph_%s_%s.png" % (cls.__name__, id(obj))
                        objgraph.show_backrefs(
                            obj, extra_ignore=ig, max_depth=4, too_many=20,
                            filename=fname, extra_info=get_context)
                    output.append("\nReferrers for %s (refcount=%s):" %
                                  (repr(obj), sys.getrefcount(obj)))
                    t = ReferrerTree(ignore=[objs], maxdepth=3)
                    tree = t.ascend(obj)
                    output.extend(t.format(tree))
        
        return "\n".join(output)
    stats.exposed = True