mirror of
https://github.com/Tygs/0bin.git
synced 2023-08-10 21:13:00 +03:00
215 lines
7.3 KiB
Python
215 lines
7.3 KiB
Python
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 cherrypy.process.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
|
|
|