1import gc
2import inspect
3import sys
4import time
5
6try:
7    import objgraph
8except ImportError:
9    objgraph = None
10
11import cherrypy
12from cherrypy import _cprequest, _cpwsgi
13from cherrypy.process.plugins import SimplePlugin
14
15
16class ReferrerTree(object):
17
18    """An object which gathers all referrers of an object to a given depth."""
19
20    peek_length = 40
21
22    def __init__(self, ignore=None, maxdepth=2, maxparents=10):
23        self.ignore = ignore or []
24        self.ignore.append(inspect.currentframe().f_back)
25        self.maxdepth = maxdepth
26        self.maxparents = maxparents
27
28    def ascend(self, obj, depth=1):
29        """Return a nested list containing referrers of the given object."""
30        depth += 1
31        parents = []
32
33        # Gather all referrers in one step to minimize
34        # cascading references due to repr() logic.
35        refs = gc.get_referrers(obj)
36        self.ignore.append(refs)
37        if len(refs) > self.maxparents:
38            return [('[%s referrers]' % len(refs), [])]
39
40        try:
41            ascendcode = self.ascend.__code__
42        except AttributeError:
43            ascendcode = self.ascend.im_func.func_code
44        for parent in refs:
45            if inspect.isframe(parent) and parent.f_code is ascendcode:
46                continue
47            if parent in self.ignore:
48                continue
49            if depth <= self.maxdepth:
50                parents.append((parent, self.ascend(parent, depth)))
51            else:
52                parents.append((parent, []))
53
54        return parents
55
56    def peek(self, s):
57        """Return s, restricted to a sane length."""
58        if len(s) > (self.peek_length + 3):
59            half = self.peek_length // 2
60            return s[:half] + '...' + s[-half:]
61        else:
62            return s
63
64    def _format(self, obj, descend=True):
65        """Return a string representation of a single object."""
66        if inspect.isframe(obj):
67            filename, lineno, func, context, index = inspect.getframeinfo(obj)
68            return "<frame of function '%s'>" % func
69
70        if not descend:
71            return self.peek(repr(obj))
72
73        if isinstance(obj, dict):
74            return '{' + ', '.join(['%s: %s' % (self._format(k, descend=False),
75                                                self._format(v, descend=False))
76                                    for k, v in obj.items()]) + '}'
77        elif isinstance(obj, list):
78            return '[' + ', '.join([self._format(item, descend=False)
79                                    for item in obj]) + ']'
80        elif isinstance(obj, tuple):
81            return '(' + ', '.join([self._format(item, descend=False)
82                                    for item in obj]) + ')'
83
84        r = self.peek(repr(obj))
85        if isinstance(obj, (str, int, float)):
86            return r
87        return '%s: %s' % (type(obj), r)
88
89    def format(self, tree):
90        """Return a list of string reprs from a nested list of referrers."""
91        output = []
92
93        def ascend(branch, depth=1):
94            for parent, grandparents in branch:
95                output.append(('    ' * depth) + self._format(parent))
96                if grandparents:
97                    ascend(grandparents, depth + 1)
98        ascend(tree)
99        return output
100
101
102def get_instances(cls):
103    return [x for x in gc.get_objects() if isinstance(x, cls)]
104
105
106class RequestCounter(SimplePlugin):
107
108    def start(self):
109        self.count = 0
110
111    def before_request(self):
112        self.count += 1
113
114    def after_request(self):
115        self.count -= 1
116
117
118request_counter = RequestCounter(cherrypy.engine)
119request_counter.subscribe()
120
121
122def get_context(obj):
123    if isinstance(obj, _cprequest.Request):
124        return 'path=%s;stage=%s' % (obj.path_info, obj.stage)
125    elif isinstance(obj, _cprequest.Response):
126        return 'status=%s' % obj.status
127    elif isinstance(obj, _cpwsgi.AppResponse):
128        return 'PATH_INFO=%s' % obj.environ.get('PATH_INFO', '')
129    elif hasattr(obj, 'tb_lineno'):
130        return 'tb_lineno=%s' % obj.tb_lineno
131    return ''
132
133
134class GCRoot(object):
135
136    """A CherryPy page handler for testing reference leaks."""
137
138    classes = [
139        (_cprequest.Request, 2, 2,
140         'Should be 1 in this request thread and 1 in the main thread.'),
141        (_cprequest.Response, 2, 2,
142         'Should be 1 in this request thread and 1 in the main thread.'),
143        (_cpwsgi.AppResponse, 1, 1,
144         'Should be 1 in this request thread only.'),
145    ]
146
147    @cherrypy.expose
148    def index(self):
149        return 'Hello, world!'
150
151    @cherrypy.expose
152    def stats(self):
153        output = ['Statistics:']
154
155        for trial in range(10):
156            if request_counter.count > 0:
157                break
158            time.sleep(0.5)
159        else:
160            output.append('\nNot all requests closed properly.')
161
162        # gc_collect isn't perfectly synchronous, because it may
163        # break reference cycles that then take time to fully
164        # finalize. Call it thrice and hope for the best.
165        gc.collect()
166        gc.collect()
167        unreachable = gc.collect()
168        if unreachable:
169            if objgraph is not None:
170                final = objgraph.by_type('Nondestructible')
171                if final:
172                    objgraph.show_backrefs(final, filename='finalizers.png')
173
174            trash = {}
175            for x in gc.garbage:
176                trash[type(x)] = trash.get(type(x), 0) + 1
177            if trash:
178                output.insert(0, '\n%s unreachable objects:' % unreachable)
179                trash = [(v, k) for k, v in trash.items()]
180                trash.sort()
181                for pair in trash:
182                    output.append('    ' + repr(pair))
183
184        # Check declared classes to verify uncollected instances.
185        # These don't have to be part of a cycle; they can be
186        # any objects that have unanticipated referrers that keep
187        # them from being collected.
188        allobjs = {}
189        for cls, minobj, maxobj, msg in self.classes:
190            allobjs[cls] = get_instances(cls)
191
192        for cls, minobj, maxobj, msg in self.classes:
193            objs = allobjs[cls]
194            lenobj = len(objs)
195            if lenobj < minobj or lenobj > maxobj:
196                if minobj == maxobj:
197                    output.append(
198                        '\nExpected %s %r references, got %s.' %
199                        (minobj, cls, lenobj))
200                else:
201                    output.append(
202                        '\nExpected %s to %s %r references, got %s.' %
203                        (minobj, maxobj, cls, lenobj))
204
205                for obj in objs:
206                    if objgraph is not None:
207                        ig = [id(objs), id(inspect.currentframe())]
208                        fname = 'graph_%s_%s.png' % (cls.__name__, id(obj))
209                        objgraph.show_backrefs(
210                            obj, extra_ignore=ig, max_depth=4, too_many=20,
211                            filename=fname, extra_info=get_context)
212                    output.append('\nReferrers for %s (refcount=%s):' %
213                                  (repr(obj), sys.getrefcount(obj)))
214                    t = ReferrerTree(ignore=[objs], maxdepth=3)
215                    tree = t.ascend(obj)
216                    output.extend(t.format(tree))
217
218        return '\n'.join(output)
219