13
from cherrypy import _cprequest, _cpwsgi
14
from cherrypy.process.plugins import SimplePlugin
17
class ReferrerTree(object):
18
"""An object which gathers all referrers of an object to a given depth."""
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
28
def ascend(self, obj, depth=1):
29
"""Return a nested list containing referrers of the given object."""
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), [])]
41
ascendcode = self.ascend.__code__
42
except AttributeError:
43
ascendcode = self.ascend.im_func.func_code
45
if inspect.isframe(parent) and parent.f_code is ascendcode:
47
if parent in self.ignore:
49
if depth <= self.maxdepth:
50
parents.append((parent, self.ascend(parent, depth)))
52
parents.append((parent, []))
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:]
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
71
return self.peek(repr(obj))
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]) + ")"
84
r = self.peek(repr(obj))
85
if isinstance(obj, (str, int, float)):
87
return "%s: %s" % (type(obj), r)
89
def format(self, tree):
90
"""Return a list of string reprs from a nested list of referrers."""
92
def ascend(branch, depth=1):
93
for parent, grandparents in branch:
94
output.append((" " * depth) + self._format(parent))
96
ascend(grandparents, depth + 1)
101
def get_instances(cls):
102
return [x for x in gc.get_objects() if isinstance(x, cls)]
105
class RequestCounter(SimplePlugin):
110
def before_request(self):
113
def after_request(self):
115
request_counter = RequestCounter(cherrypy.engine)
116
request_counter.subscribe()
119
def get_context(obj):
120
if isinstance(obj, _cprequest.Request):
121
return "path=%s;stage=%s" % (obj.path_info, obj.stage)
122
elif isinstance(obj, _cprequest.Response):
123
return "status=%s" % obj.status
124
elif isinstance(obj, _cpwsgi.AppResponse):
125
return "PATH_INFO=%s" % obj.environ.get('PATH_INFO', '')
126
elif hasattr(obj, "tb_lineno"):
127
return "tb_lineno=%s" % obj.tb_lineno
131
class GCRoot(object):
132
"""A CherryPy page handler for testing reference leaks."""
134
classes = [(_cprequest.Request, 2, 2,
135
"Should be 1 in this request thread and 1 in the main thread."),
136
(_cprequest.Response, 2, 2,
137
"Should be 1 in this request thread and 1 in the main thread."),
138
(_cpwsgi.AppResponse, 1, 1,
139
"Should be 1 in this request thread only."),
143
return "Hello, world!"
147
output = ["Statistics:"]
149
for trial in range(10):
150
if request_counter.count > 0:
154
output.append("\nNot all requests closed properly.")
156
# gc_collect isn't perfectly synchronous, because it may
157
# break reference cycles that then take time to fully
158
# finalize. Call it thrice and hope for the best.
161
unreachable = gc.collect()
163
if objgraph is not None:
164
final = objgraph.by_type('Nondestructible')
166
objgraph.show_backrefs(final, filename='finalizers.png')
170
trash[type(x)] = trash.get(type(x), 0) + 1
172
output.insert(0, "\n%s unreachable objects:" % unreachable)
173
trash = [(v, k) for k, v in trash.items()]
176
output.append(" " + repr(pair))
178
# Check declared classes to verify uncollected instances.
179
# These don't have to be part of a cycle; they can be
180
# any objects that have unanticipated referrers that keep
181
# them from being collected.
183
for cls, minobj, maxobj, msg in self.classes:
184
allobjs[cls] = get_instances(cls)
186
for cls, minobj, maxobj, msg in self.classes:
189
if lenobj < minobj or lenobj > maxobj:
192
"\nExpected %s %r references, got %s." %
193
(minobj, cls, lenobj))
196
"\nExpected %s to %s %r references, got %s." %
197
(minobj, maxobj, cls, lenobj))
200
if objgraph is not None:
201
ig = [id(objs), id(inspect.currentframe())]
202
fname = "graph_%s_%s.png" % (cls.__name__, id(obj))
203
objgraph.show_backrefs(
204
obj, extra_ignore=ig, max_depth=4, too_many=20,
205
filename=fname, extra_info=get_context)
206
output.append("\nReferrers for %s (refcount=%s):" %
207
(repr(obj), sys.getrefcount(obj)))
208
t = ReferrerTree(ignore=[objs], maxdepth=3)
210
output.extend(t.format(tree))
212
return "\n".join(output)