open_toontown_panda3d/direct/src/showbase/GarbageReport.py

381 lines
16 KiB
Python
Executable File

from direct.directnotify.DirectNotifyGlobal import directNotify
from direct.showbase.PythonUtil import gcDebugOn, safeRepr, fastRepr
from direct.showbase.TaskThreaded import TaskThreaded, TaskThread
import gc
class FakeObject:
pass
def _createGarbage():
a = FakeObject()
b = FakeObject()
a.other = b
b.other = a
class GarbageReport(TaskThreaded):
"""Detects leaked Python objects (via gc.collect()) and reports on garbage
items, garbage-to-garbage references, and garbage cycles.
If you just want to dump the report to the log, use GarbageLogger."""
notify = directNotify.newCategory("GarbageReport")
NotGarbage = 'NG'
def __init__(self, name, log=True, verbose=False, fullReport=False, findCycles=True,
threaded=False, timeslice=None, doneCallback=None):
# if log is True, GarbageReport will self-destroy after logging
# if false, caller is responsible for calling destroy()
# if threaded is True, processing will be performed over multiple frames
TaskThreaded.__init__(self, name, threaded, timeslice=timeslice)
# stick the arguments onto a ScratchPad so we can access them from the thread
# functions and delete them all at once
self._args = ScratchPad(name=name, log=log, verbose=verbose, fullReport=fullReport,
findCycles=findCycles, doneCallback=doneCallback)
# do the garbage collection
wasOn = gcDebugOn()
oldFlags = gc.get_debug()
if not wasOn:
gc.set_debug(gc.DEBUG_SAVEALL)
gc.collect()
self.notify.debug('gc.garbage == %s' % fastRepr(gc.garbage))
self.garbage = list(gc.garbage)
self.notify.debug('self.garbage == %s' % self.garbage)
del gc.garbage[:]
if not wasOn:
gc.set_debug(oldFlags)
self.numGarbage = len(self.garbage)
if self._args.verbose:
self.notify.info('found %s garbage items' % self.numGarbage)
self.referrersByReference = {}
self.referrersByNumber = {}
self.referentsByReference = {}
self.referentsByNumber = {}
self.cycles = []
self.cycleSets = []
self.cycleIds = set()
# grab the referrers (pointing to garbage)
class GetReferrers(TaskThread):
def setUp(self):
if self.parent._args.fullReport and (self.parent.numGarbage != 0):
if self.parent._args.verbose:
self.parent.notify.info('getting referrers...')
self.index = 0
else:
self.finished()
def run(self):
parent = self.parent
for i in xrange(self.index, parent.numGarbage):
byNum, byRef = parent._getReferrers(parent.garbage[i])
parent.referrersByNumber[i] = byNum
parent.referrersByReference[i] = byRef
if (not (i & 0x0F)) and (not self.timeLeft()):
# we've run out of time, save the index
self.index = i+1
return
self.finished()
def done(self):
self.parent.scheduleThread(self.parent.getReferents)
# grab the referents (pointed to by garbage)
class GetReferents(TaskThread):
def setUp(self):
if self.parent.numGarbage == 0:
self.finished()
else:
if self.parent._args.verbose:
self.parent.notify.info('getting referents...')
self.index = 0
def run(self):
parent = self.parent
for i in xrange(self.index, self.parent.numGarbage):
byNum, byRef = parent._getReferents(parent.garbage[i])
parent.referentsByNumber[i] = byNum
parent.referentsByReference[i] = byRef
if (not (i & 0x0F)) and (not self.timeLeft()):
# we've run out of time, save the index
self.index = i+1
return
self.finished()
def done(self):
self.parent.scheduleThread(self.parent.getCycles)
# find the cycles
class GetCycles(TaskThread):
def setUp(self):
if self.parent._args.findCycles and self.parent.numGarbage > 0:
if self.parent._args.verbose:
self.parent.notify.info('detecting cycles...')
self.index = 0
else:
self.finished()
def run(self):
for i in xrange(self.index, self.parent.numGarbage):
newCycles = self.parent._getCycles(i, self.parent.cycleSets)
self.parent.cycles.extend(newCycles)
# if we're not doing a full report, add this cycle's IDs to the master set
if not self.parent._args.fullReport:
for cycle in newCycles:
self.parent.cycleIds.update(set(cycle))
if (not (i & 0x0F)) and (not self.timeLeft()):
# we've run out of time, save the index
self.index = i+1
return
self.finished()
def done(self):
self.parent.scheduleThread(self.parent.createReport)
class CreateReport(TaskThread):
def setUp(self):
self.s = ['===== GarbageReport: \'%s\' (%s items) =====' % (
self.parent._args.name, self.parent.numGarbage)]
if self.parent.numGarbage == 0:
self.finished()
else:
self.curPhase = 0
self.index = 0
# make a list of the ids we will actually be printing
if self.parent._args.fullReport:
self.garbageIds = range(self.parent.numGarbage)
else:
self.garbageIds = list(self.parent.cycleIds)
self.garbageIds.sort()
self.numGarbage = len(self.garbageIds)
def run(self):
if self.curPhase == 0:
# log each individual item with a number in front of it
if self.index == 0:
if not self.parent._args.fullReport:
abbrev = '(abbreviated) '
else:
abbrev = ''
self.s.append('\n===== Garbage Items %s=====' % abbrev)
digits = 0
n = self.parent.numGarbage
while n > 0:
digits += 1
n /= 10
self.digits = digits
self.format = '%0' + '%s' % digits + 'i:%s \t%s'
for i in xrange(self.index, self.numGarbage):
id = self.garbageIds[i]
objStr = safeRepr(self.parent.garbage[id])
maxLen = 5000
if len(objStr) > maxLen:
snip = '<SNIP>'
objStr = '%s%s' % (objStr[:(maxLen-len(snip))], snip)
self.s.append(self.format % (id, itype(self.parent.garbage[id]), objStr))
if (not (i & 0x7F)) and (not self.timeLeft()):
# we've run out of time, save the index
self.index = i+1
return
self.curPhase = 1
self.index = 0
if self.curPhase == 1:
if self.parent._args.findCycles:
if self.index == 0:
self.s.append('\n===== Cycles =====')
for i in xrange(self.index, len(self.parent.cycles)):
self.s.append('%s' % self.parent.cycles[i])
if (not (i & 0x7F)) and (not self.timeLeft()):
# we've run out of time, save the index
self.index = i+1
return
self.curPhase = 2
self.index = 0
if self.parent._args.fullReport:
format = '%0' + '%s' % self.digits + 'i:%s'
if self.curPhase == 2:
if self.index == 0:
self.s.append('\n===== Referrers By Number (what is referring to garbage item?) =====')
for i in xrange(self.index, self.parent.numGarbage):
self.s.append(format % (i, self.parent.referrersByNumber[i]))
if (not (i & 0x7F)) and (not self.timeLeft()):
# we've run out of time, save the index
self.index = i+1
return
self.curPhase = 3
self.index = 0
if self.curPhase == 3:
if self.index == 0:
self.s.append('\n===== Referents By Number (what is garbage item referring to?) =====')
for i in xrange(self.index, self.parent.numGarbage):
self.s.append(format % (i, self.parent.referentsByNumber[i]))
if (not (i & 0x7F)) and (not self.timeLeft()):
# we've run out of time, save the index
self.index = i+1
return
self.curPhase = 4
self.index = 0
if self.curPhase == 4:
if self.index == 0:
self.s.append('\n===== Referrers (what is referring to garbage item?) =====')
for i in xrange(self.index, self.parent.numGarbage):
self.s.append(format % (i, self.parent.referrersByReference[i]))
if (not (i & 0x7F)) and (not self.timeLeft()):
# we've run out of time, save the index
self.index = i+1
return
self.curPhase = 5
self.index = 0
if self.curPhase == 5:
if self.index == 0:
self.s.append('\n===== Referents (what is garbage item referring to?) =====')
for i in xrange(self.index, self.parent.numGarbage):
self.s.append(format % (i, self.parent.referentsByReference[i]))
if (not (i & 0x7F)) and (not self.timeLeft()):
# we've run out of time, save the index
self.index = i+1
return
self.finished()
def done(self):
self.parent._report = self.s
self.parent.scheduleThread(self.parent.printReport)
class PrintReport(TaskThread):
def setUp(self):
if not self.parent._args.log:
self.finished()
else:
self.index = 0
def run(self):
if self.index > 0:
self.parent.notify.info('RESUME')
for i in xrange(self.index, len(self.parent._report)):
print self.parent._report[i]
if (not (i & 0x3F)) and (not self.timeLeft()):
self.parent.notify.info('SUSPEND')
# we've run out of time, save the index
self.index = i+1
return
self.finished()
def done(self):
if self.parent._args.doneCallback:
self.parent._args.doneCallback(self.parent)
self.getReferrers = GetReferrers()
self.getReferents = GetReferents()
self.getCycles = GetCycles()
self.createReport = CreateReport()
self.printReport = PrintReport()
self.scheduleThread(self.getReferrers)
def destroy(self):
print 'GarbageReport.destroy'
del self.getReferrers
del self.getReferents
del self.getCycles
del self.createReport
del self.printReport
del self._args
del self.garbage
del self.numGarbage
del self.referrersByReference
del self.referrersByNumber
del self.referentsByReference
del self.referentsByNumber
if hasattr(self, 'cycles'):
del self.cycles
del self._report
if hasattr(self, '_reportStr'):
del self._reportStr
def getNumItems(self):
return self.numGarbage
def getGarbage(self):
return self.garbage
def getReport(self):
if not hasattr(self, '_reportStr'):
self._reportStr = ''
for str in self._report:
self._reportStr += '\n' + str
return self._reportStr
def _getReferrers(self, obj):
# referrers (pointing to garbage)
# returns two lists, first by index into gc.garbage, second by
# direct reference
byRef = gc.get_referrers(obj)
# look to see if each referrer is another garbage item
byNum = []
for referrer in byRef:
try:
num = self.garbage.index(referrer)
byNum.append(num)
except:
#num = GarbageReport.NotGarbage
pass
return byNum, byRef
def _getReferents(self, obj):
# referents (pointed to by garbage)
# returns two lists, first by index into gc.garbage, second by
# direct reference
byRef = gc.get_referents(obj)
# look to see if each referent is another garbage item
byNum = []
for referent in byRef:
try:
num = self.garbage.index(referent)
byNum.append(num)
except:
#num = GarbageReport.NotGarbage
pass
return byNum, byRef
def _getCycles(self, index, cycleSets=None):
# detect garbage cycles for a particular item of garbage
assert self.notify.debugCall()
# returns list of lists, sublists are garbage reference cycles
cycles = []
# sets of cycle members, to avoid duplicates
if cycleSets is None:
cycleSets = []
stateStack = Stack()
rootId = index
stateStack.push(([rootId], rootId, 0))
while True:
if len(stateStack) == 0:
break
candidateCycle, curId, resumeIndex = stateStack.pop()
if self.notify.getDebug():
print 'restart: %s root=%s cur=%s resume=%s' % (
candidateCycle, rootId, curId, resumeIndex)
for index in xrange(resumeIndex, len(self.referentsByNumber[curId])):
refId = self.referentsByNumber[curId][index]
if self.notify.getDebug():
print ' : %s -> %s' % (curId, refId)
if refId == rootId:
# we found a cycle! mark it down and move on to the next refId
if not set(candidateCycle) in cycleSets:
if self.notify.getDebug():
print ' FOUND: ', list(candidateCycle) + [refId]
cycles.append(list(candidateCycle) + [refId])
cycleSets.append(set(candidateCycle))
elif refId in candidateCycle:
pass
else:
# this refId does not complete a cycle. Mark down
# where we are in this list of referents, then
# start looking through the referents of the new refId
stateStack.push((list(candidateCycle), curId, index+1))
stateStack.push((list(candidateCycle) + [refId], refId, 0))
break
return cycles
class GarbageLogger(GarbageReport):
"""If you just want to log the current garbage to the log file, make
one of these. It automatically destroys itself after logging"""
def __init__(self, name, *args, **kArgs):
kArgs['log'] = True
GarbageReport.__init__(self, name, *args, **kArgs)