don't use gc.get_referents (it's slow), more fine-grained CPU usage

This commit is contained in:
Darren Ranalli 2007-04-07 08:34:50 +00:00
parent e5fc82670d
commit 786af32d44

View File

@ -1,16 +1,29 @@
from pandac.PandaModules import PStatCollector
from direct.directnotify.DirectNotifyGlobal import directNotify from direct.directnotify.DirectNotifyGlobal import directNotify
from direct.showbase.PythonUtil import Queue, invertDictLossless from direct.showbase.PythonUtil import Queue, invertDictLossless
from direct.showbase.PythonUtil import itype, serialNum, safeRepr from direct.showbase.PythonUtil import itype, serialNum, safeRepr
from direct.showbase.Job import Job from direct.showbase.Job import Job
import types, weakref, gc, random, __builtin__ import types, weakref, random, __builtin__
def _createContainerLeak(): def _createContainerLeak():
def leakContainer(task): def leakContainer(task):
if not hasattr(simbase, 'leakContainer'): base = getBase()
simbase.leakContainer = [] if not hasattr(base, 'leakContainer'):
simbase.leakContainer.append(1) base.leakContainer = {}
# use tuples as keys since they can't be weakref'd, and use an instance
# since it can't be repr/eval'd
# that will force the leak detector to hold a normal 'non-weak' reference
class LeakKey:
pass
base.leakContainer[(LeakKey(),)] = {}
# test the non-weakref object reference handling
if random.random() < .01:
key = random.choice(base.leakContainer.keys())
ContainerLeakDetector.notify.debug(
'removing reference to leakContainer key %s so it will be garbage-collected' % key)
del base.leakContainer[key]
return task.cont return task.cont
taskMgr.add(leakContainer, 'leakContainer-%s' % serialNum()) task = taskMgr.add(leakContainer, 'leakContainer-%s' % serialNum())
class CheckContainers(Job): class CheckContainers(Job):
""" """
@ -41,13 +54,14 @@ class CheckContainers(Job):
container = self._leakDetector.getContainerById(id) container = self._leakDetector.getContainerById(id)
except Exception, e: except Exception, e:
# this container no longer exists # this container no longer exists
self.notify.debug('container %s no longer exists; caught exception in getContainerById (%s)' % ( self.notify.debug(
'%s no longer exists; caught exception in getContainerById (%s)' % (
self._leakDetector.getContainerNameById(id), e)) self._leakDetector.getContainerNameById(id), e))
self._leakDetector.removeContainerById(id) self._leakDetector.removeContainerById(id)
continue continue
if container is None: if container is None:
# this container no longer exists # this container no longer exists
self.notify.debug('container %s no longer exists; getContainerById returned None' % self.notify.debug('%s no longer exists; getContainerById returned None' %
self._leakDetector.getContainerNameById(id)) self._leakDetector.getContainerNameById(id))
self._leakDetector.removeContainerById(id) self._leakDetector.removeContainerById(id)
continue continue
@ -55,8 +69,9 @@ class CheckContainers(Job):
cLen = len(container) cLen = len(container)
except Exception, e: except Exception, e:
# this container no longer exists # this container no longer exists
self.notify.debug('%s is no longer a container, it is now %s (%s)' % self.notify.debug(
(self._leakDetector.getContainerNameById(id), safeRepr(container), e)) '%s is no longer a container, it is now %s (%s)' %
(self._leakDetector.getContainerNameById(id), safeRepr(container), e))
self._leakDetector.removeContainerById(id) self._leakDetector.removeContainerById(id)
continue continue
self._leakDetector._index2containerId2len[self._index][id] = cLen self._leakDetector._index2containerId2len[self._index][id] = cLen
@ -72,7 +87,12 @@ class CheckContainers(Job):
minutes = (self._leakDetector._index2delay[self._index] - minutes = (self._leakDetector._index2delay[self._index] -
self._leakDetector._index2delay[self._index-1]) / 60. self._leakDetector._index2delay[self._index-1]) / 60.
name = self._leakDetector.getContainerNameById(id) name = self._leakDetector.getContainerNameById(id)
self.notify.warning('container %s grew > 200%% in %s minutes' % (name, minutes)) if idx2id2len[self._index-1][id] != 0:
percent = int(100. * (float(diff) / idx2id2len[self._index-1][id]))
self.notify.warning(
'%s grew %s%% in %s minutes (currently %s items)' % (
name, percent, minutes, idx2id2len[self._index][id]))
yield None
if (self._index > 3 and if (self._index > 3 and
id in idx2id2len[self._index-2] and id in idx2id2len[self._index-2] and
id in idx2id2len[self._index-3]): id in idx2id2len[self._index-3]):
@ -81,32 +101,36 @@ class CheckContainers(Job):
if self._index <= 5: if self._index <= 5:
if diff > 0 and diff2 > 0 and diff3 > 0: if diff > 0 and diff2 > 0 and diff3 > 0:
name = self._leakDetector.getContainerNameById(id) name = self._leakDetector.getContainerNameById(id)
msg = ('%s consistently increased in length over the last 3 periods (currently %s items)' % msg = ('%s consistently increased in length over the last '
'3 periods (currently %s items)' %
(name, idx2id2len[self._index][id])) (name, idx2id2len[self._index][id]))
self.notify.warning(msg) self.notify.warning(msg)
yield None
elif (id in idx2id2len[self._index-4] and elif (id in idx2id2len[self._index-4] and
id in idx2id2len[self._index-5]): id in idx2id2len[self._index-5]):
# if size has consistently increased over the last 5 checks, send out a warning # if size has consistently increased over the last 5 checks,
# send out a warning
diff4 = idx2id2len[self._index-3][id] - idx2id2len[self._index-4][id] diff4 = idx2id2len[self._index-3][id] - idx2id2len[self._index-4][id]
diff5 = idx2id2len[self._index-4][id] - idx2id2len[self._index-5][id] diff5 = idx2id2len[self._index-4][id] - idx2id2len[self._index-5][id]
if diff > 0 and diff2 > 0 and diff3 > 0 and diff4 > 0 and diff5 > 0: if diff > 0 and diff2 > 0 and diff3 > 0 and diff4 > 0 and diff5 > 0:
name = self._leakDetector.getContainerNameById(id) name = self._leakDetector.getContainerNameById(id)
msg = ('%s consistently increased in length over the last 5 periods (currently %s items), notifying system' % msg = ('%s consistently increased in length over the last '
'5 periods (currently %s items)' %
(name, idx2id2len[self._index][id])) (name, idx2id2len[self._index][id]))
self.notify.warning(msg) self.notify.warning('%s, sending notification' % msg)
yield None
messenger.send(self._leakDetector.getLeakEvent(), [msg]) messenger.send(self._leakDetector.getLeakEvent(), [msg])
yield Job.Done yield Job.Done
class PruneContainerRefs(Job): class PruneContainerRefs(Job):
""" """
Job to destroy any container refs that have Indirections that are holding references Job to destroy any container refs that are no longer valid.
to objects that should be garbage-collected Checks validity by asking for each container
""" """
def __init__(self, name, leakDetector): def __init__(self, name, leakDetector):
Job.__init__(self, name) Job.__init__(self, name)
self._leakDetector = leakDetector self._leakDetector = leakDetector
self.notify = self._leakDetector.notify self.notify = self._leakDetector.notify
self._index = index
ContainerLeakDetector.addPrivateId(self.__dict__) ContainerLeakDetector.addPrivateId(self.__dict__)
def destroy(self): def destroy(self):
@ -114,23 +138,29 @@ class PruneContainerRefs(Job):
Job.destroy(self) Job.destroy(self)
def getPriority(self): def getPriority(self):
return Job.Priorities.Normal return Job.Priorities.Normal-1
def run(self): def run(self):
ids = self._leakDetector._id2ref.keys() ids = self._leakDetector._id2ref.keys()
for id in ids: for id in ids:
yield None yield None
ref = self._leakDetector._id2ref[id] try:
ref.destroyIfGarbageDictKey() container = self._leakDetector.getContainerById(id)
except:
# reference is invalid, remove it
self._leakDetector.removeContainerById(id)
yield Job.Done yield Job.Done
class NoDictKey: class NoDictKey:
pass pass
class Indirection: class Indirection:
# represents the indirection that brings you from a container to an element of the container """
# stored as a string to be used as part of an eval Represents the indirection that brings you from a container to an element of the container.
# each dictionary dereference is individually eval'd since the dict key might have been garbage-collected Stored as a string to be used as part of an eval, or as a key to be looked up in a dict.
Each dictionary dereference is individually eval'd since the dict key might have been
garbage-collected
"""
def __init__(self, evalStr=None, dictKey=NoDictKey): def __init__(self, evalStr=None, dictKey=NoDictKey):
# if this is a dictionary lookup, pass dictKey instead of evalStr # if this is a dictionary lookup, pass dictKey instead of evalStr
@ -161,11 +191,13 @@ class Indirection:
self.dictKey = weakref.ref(dictKey) self.dictKey = weakref.ref(dictKey)
self._isWeakRef = True self._isWeakRef = True
except TypeError, e: except TypeError, e:
ContainerLeakDetector.notify.debug('could not weakref dict key %s' % dictKey)
self.dictKey = dictKey self.dictKey = dictKey
self._isWeakRef = False self._isWeakRef = False
def destroy(self): def destroy(self):
del self.dictKey # re-entrant
self.dictKey = NoDictKey
def acquire(self): def acquire(self):
self._refCount += 1 self._refCount += 1
@ -177,13 +209,6 @@ class Indirection:
def isDictKey(self): def isDictKey(self):
# is this an indirection through a dictionary? # is this an indirection through a dictionary?
return self.dictKey is not NoDictKey return self.dictKey is not NoDictKey
def isGarbageDictKey(self):
# are we holding a non-weak reference to an object that should be
# garbage-collected?
if self.isDictKey() and not self._isWeakRef:
referrers = gc.get_referrers(self.dictKey)
print referrers
import pdb;pdb.set_trace()
def _getNonWeakDictKey(self): def _getNonWeakDictKey(self):
if not self._isWeakRef: if not self._isWeakRef:
@ -216,8 +241,6 @@ class Indirection:
return self.evalStr return self.evalStr
# we're stored as a dict key # we're stored as a dict key
# this might not eval, but that's OK, we're not using this string to find
# the object, we dereference the parent dict
keyRepr = safeRepr(self._getNonWeakDictKey()) keyRepr = safeRepr(self._getNonWeakDictKey())
# if the previous indirection was an instance dict, change our syntax from ['key'] to .key # if the previous indirection was an instance dict, change our syntax from ['key'] to .key
if prevIndirection is not None and prevIndirection.evalStr is not None: if prevIndirection is not None and prevIndirection.evalStr is not None:
@ -231,7 +254,7 @@ class Indirection:
class ContainerRef: class ContainerRef:
""" """
stores a reference to a container in a way that does not prevent garbage stores a reference to a container in a way that does not prevent garbage
collection of the container collection of the container if possible
stored as a series of 'indirections' (obj.foo -> '.foo', dict[key] -> '[key]', etc.) stored as a series of 'indirections' (obj.foo -> '.foo', dict[key] -> '[key]', etc.)
""" """
class FailedEval(Exception): class FailedEval(Exception):
@ -246,17 +269,10 @@ class ContainerRef:
self.addIndirection(indirection) self.addIndirection(indirection)
def destroy(self): def destroy(self):
# re-entrant
for indirection in self._indirections: for indirection in self._indirections:
indirection.release() indirection.release()
del self._indirections self._indirections = []
def destroyIfGarbageDictKey(self):
# if any of our indirections are holding onto objects that
# should be garbage-collected, destroy
for indirection in self._indirections:
if indirection.isGarbageDictKey():
self.destroy()
return
def addIndirection(self, indirection): def addIndirection(self, indirection):
indirection.acquire() indirection.acquire()
@ -283,6 +299,7 @@ class ContainerRef:
evalStr = '' evalStr = ''
curObj = None curObj = None
for indirection in self._indirections: for indirection in self._indirections:
yield None
if not indirection.isDictKey(): if not indirection.isDictKey():
# build up a string to be eval'd # build up a string to be eval'd
evalStr += indirection.getString() evalStr += indirection.getString()
@ -294,7 +311,7 @@ class ContainerRef:
curObj = indirection.dereferenceDictKey(curObj) curObj = indirection.dereferenceDictKey(curObj)
evalStr = '' evalStr = ''
return self._evalWithObj(evalStr, curObj) yield self._evalWithObj(evalStr, curObj)
def __repr__(self): def __repr__(self):
str = '' str = ''
@ -319,7 +336,8 @@ class ContainerLeakDetector(Job):
""" """
Low-priority Python object-graph walker that looks for leaking containers. Low-priority Python object-graph walker that looks for leaking containers.
To reduce memory usage, this does a random walk of the Python objects to To reduce memory usage, this does a random walk of the Python objects to
discover containers rather than keep a set of all visited objects. discover containers rather than keep a set of all visited objects; it may
visit the same object many times but eventually it will discover every object.
Checks container sizes at ever-increasing intervals. Checks container sizes at ever-increasing intervals.
""" """
notify = directNotify.newCategory("ContainerLeakDetector") notify = directNotify.newCategory("ContainerLeakDetector")
@ -371,6 +389,7 @@ class ContainerLeakDetector(Job):
])) ]))
def destroy(self): def destroy(self):
self.ignoreAll()
if self._checkContainersJob is not None: if self._checkContainersJob is not None:
jobMgr.remove(self._checkContainersJob) jobMgr.remove(self._checkContainersJob)
self._checkContainersJob = None self._checkContainersJob = None
@ -401,17 +420,19 @@ class ContainerLeakDetector(Job):
return self._id2ref.keys() return self._id2ref.keys()
def getContainerById(self, id): def getContainerById(self, id):
return self._id2ref[id].getContainer() for result in self._id2ref[id].getContainer():
pass
return result
def getContainerNameById(self, id): def getContainerNameById(self, id):
return repr(self._id2ref[id]) return repr(self._id2ref[id])
def removeContainerById(self, id): def removeContainerById(self, id):
self._id2ref[id].destroy()
del self._id2ref[id] del self._id2ref[id]
def run(self): def run(self):
taskMgr.doMethodLater(self._nextCheckDelay, self._checkForLeaks, taskMgr.doMethodLater(self._nextCheckDelay, self._checkForLeaks,
self._getCheckTaskName()) self._getCheckTaskName())
taskMgr.doMethodLater(self._pruneTaskPeriod, self._pruneContainerRefs, self._scheduleNextPruning()
self._getPruneTaskName())
while True: while True:
# yield up here instead of at the end, since we skip back to the # yield up here instead of at the end, since we skip back to the
@ -422,19 +443,16 @@ class ContainerLeakDetector(Job):
if self._curObjRef is None: if self._curObjRef is None:
self._curObjRef = self._baseObjRef self._curObjRef = self._baseObjRef
try: try:
curObj = self._curObjRef.getContainer() for result in self._curObjRef.getContainer():
yield None
curObj = result
except: except:
self.notify.debug('lost current container: %s' % self._curObjRef) self.notify.debug('lost current container: %s' % self._curObjRef)
while len(self._id2ref): # that container is gone, try again
_id = random.choice(self._id2ref.keys()) self._curObjRef = None
curObj = self.getContainerById(_id) continue
if curObj is not None:
break
# container is no longer valid
del self._id2ref[_id]
self._curObjRef = self._id2ref[_id]
#print '%s: %s, %s' % (id(curObj), type(curObj), self._id2ref[id(curObj)]) #print '%s: %s, %s' % (id(curObj), type(curObj), self._id2ref[id(curObj)])
#self.notify.debug('--> %s' % self._curObjRef) self.notify.debug('--> %s' % self._curObjRef)
# keep a copy of this obj's eval str, it might not be in _id2ref # keep a copy of this obj's eval str, it might not be in _id2ref
curObjRef = self._curObjRef curObjRef = self._curObjRef
@ -443,34 +461,48 @@ class ContainerLeakDetector(Job):
if type(curObj) in (types.ModuleType, types.InstanceType): if type(curObj) in (types.ModuleType, types.InstanceType):
child = curObj.__dict__ child = curObj.__dict__
if not self._isDeadEnd(child): isContainer = self._isContainer(child)
self._curObjRef = ContainerRef(Indirection(evalStr='.__dict__'), curObjRef) notDeadEnd = not self._isDeadEnd(child)
if self._isContainer(child): if isContainer or notDeadEnd:
self._nameContainer(child, self._curObjRef) objRef = ContainerRef(Indirection(evalStr='.__dict__'), curObjRef)
yield None
if isContainer:
self._nameContainer(child, objRef)
if notDeadEnd:
self._curObjRef = objRef
continue continue
if type(curObj) is types.DictType: if type(curObj) is types.DictType:
key = None key = None
attr = None attr = None
keys = curObj.keys() keys = curObj.keys()
# we will continue traversing the object graph via the last container # we will continue traversing the object graph via one key of the dict,
# in the list; shuffle the list to randomize the traversal # choose it at random without taking a big chunk of CPU time
random.shuffle(keys) numKeysLeft = len(keys)
nextObjRef = None
for key in keys: for key in keys:
yield None
try: try:
attr = curObj[key] attr = curObj[key]
except KeyError, e: except KeyError, e:
self.notify.warning('could not index into %s with key %s' % (curObjRef, key)) self.notify.warning('could not index into %s with key %s' % (curObjRef, key))
continue continue
if not self._isDeadEnd(attr, key): isContainer = self._isContainer(attr)
notDeadEnd = False
if nextObjRef is None:
notDeadEnd = not self._isDeadEnd(attr, key)
if isContainer or notDeadEnd:
if curObj is __builtin__.__dict__: if curObj is __builtin__.__dict__:
indirection=Indirection(evalStr=key) objRef = ContainerRef(Indirection(evalStr=key))
self._curObjRef = ContainerRef(indirection)
else: else:
indirection=Indirection(dictKey=key) objRef = ContainerRef(Indirection(dictKey=key), curObjRef)
self._curObjRef = ContainerRef(indirection, curObjRef) yield None
if self._isContainer(attr): if isContainer:
self._nameContainer(attr, self._curObjRef) self._nameContainer(attr, objRef)
if notDeadEnd and nextObjRef is None:
if random.randrange(numKeysLeft) == 0:
nextObjRef = objRef
numKeysLeft -= 1
del key del key
del attr del attr
continue continue
@ -485,6 +517,7 @@ class ContainerLeakDetector(Job):
index = 0 index = 0
attrs = [] attrs = []
while 1: while 1:
yield None
try: try:
attr = itr.next() attr = itr.next()
except: except:
@ -492,14 +525,25 @@ class ContainerLeakDetector(Job):
attr = None attr = None
break break
attrs.append(attr) attrs.append(attr)
# we will continue traversing the object graph via the last container # we will continue traversing the object graph via one attr,
# in the list; shuffle the list to randomize the traversal # choose it at random without taking a big chunk of CPU time
random.shuffle(attrs) numAttrsLeft = len(attrs)
nextObjRef = None
for attr in attrs: for attr in attrs:
if not self._isDeadEnd(attr): yield None
self._curObjRef = ContainerRef(Indirection(evalStr='[%s]' % index), curObjRef) isContainer = self._isContainer(attr)
if self._isContainer(attr): notDeadEnd = False
self._nameContainer(attr, self._curObjRef) if nextObjRef is None:
notDeadEnd = not self._isDeadEnd(attr)
if isContainer or notDeadEnd:
objRef = ContainerRef(Indirection(evalStr='[%s]' % index), curObjRef)
yield None
if isContainer:
self._nameContainer(attr, objRef)
if notDeadEnd and nextObjRef is None:
if random.randrange(numAttrsLeft) == 0:
nextObjRef = objRef
numAttrsLeft -= 1
index += 1 index += 1
del attr del attr
except StopIteration, e: except StopIteration, e:
@ -514,15 +558,26 @@ class ContainerLeakDetector(Job):
else: else:
childName = None childName = None
child = None child = None
# we will continue traversing the object graph via the last container # we will continue traversing the object graph via one child,
# in the list; shuffle the list to randomize the traversal # choose it at random without taking a big chunk of CPU time
random.shuffle(childNames) numChildrenLeft = len(childNames)
nextObjRef = None
for childName in childNames: for childName in childNames:
yield None
child = getattr(curObj, childName) child = getattr(curObj, childName)
if not self._isDeadEnd(child, childName): isContainer = self._isContainer(child)
self._curObjRef = ContainerRef(Indirection(evalStr='.%s' % childName), curObjRef) notDeadEnd = False
if self._isContainer(child): if nextObjRef is None:
self._nameContainer(child, self._curObjRef) notDeadEnd = not self._isDeadEnd(child, childName)
if isContainer or notDeadEnd:
objRef = ContainerRef(Indirection(evalStr='.%s' % childName), curObjRef)
yield None
if isContainer:
self._nameContainer(child, objRef)
if notDeadEnd and nextObjRef is None:
if random.randrange(numChildrenLeft) == 0:
nextObjRef = objRef
numChildrenLeft -= 1
del childName del childName
del child del child
continue continue
@ -535,9 +590,8 @@ class ContainerLeakDetector(Job):
types.FloatType, types.IntType, types.LongType, types.FloatType, types.IntType, types.LongType,
types.NoneType, types.NotImplementedType, types.NoneType, types.NotImplementedType,
types.TypeType, types.CodeType, types.FunctionType, types.TypeType, types.CodeType, types.FunctionType,
types.StringType, types.UnicodeType): types.StringType, types.UnicodeType,
return True types.TupleType):
if id(obj) in self._id2ref:
return True return True
# if it's an internal object, ignore it # if it's an internal object, ignore it
if id(obj) in ContainerLeakDetector.PrivateIds: if id(obj) in ContainerLeakDetector.PrivateIds:
@ -553,7 +607,6 @@ class ContainerLeakDetector(Job):
if className == 'method-wrapper': if className == 'method-wrapper':
return True return True
return False return False
def _isContainer(self, obj): def _isContainer(self, obj):
try: try:
@ -572,21 +625,32 @@ class ContainerLeakDetector(Job):
# if this container is new, or the objRef repr is shorter than what we already have, # if this container is new, or the objRef repr is shorter than what we already have,
# put it in the table # put it in the table
if contId not in self._id2ref or len(repr(objRef)) < len(repr(self._id2ref[contId])): if contId not in self._id2ref or len(repr(objRef)) < len(repr(self._id2ref[contId])):
if contId in self._id2ref:
self.removeContainerById(contId)
self._id2ref[contId] = objRef self._id2ref[contId] = objRef
def _scheduleNextLeakCheck(self):
taskMgr.doMethodLater(self._nextCheckDelay, self._checkForLeaks,
self._getCheckTaskName())
self._nextCheckDelay *= 2
def _checkForLeaks(self, task=None): def _checkForLeaks(self, task=None):
self._index2delay[len(self._index2containerId2len)] = self._nextCheckDelay self._index2delay[len(self._index2containerId2len)] = self._nextCheckDelay
self._checkContainersJob = CheckContainers( self._checkContainersJob = CheckContainers(
'%s-checkForLeaks' % self.getJobName(), self, len(self._index2containerId2len)) '%s-checkForLeaks' % self.getJobName(), self, len(self._index2containerId2len))
self.acceptOnce(self._checkContainersJob.getFinishedEvent(),
self._scheduleNextLeakCheck)
jobMgr.add(self._checkContainersJob) jobMgr.add(self._checkContainersJob)
self._nextCheckDelay *= 2
taskMgr.doMethodLater(self._nextCheckDelay, self._checkForLeaks,
self._getCheckTaskName())
return task.done return task.done
def _pruneContainerRefs(self, task=None): def _scheduleNextPruning(self):
taskMgr.doMethodLater(self._pruneTaskPeriod, self._pruneContainerRefs, taskMgr.doMethodLater(self._pruneTaskPeriod, self._pruneContainerRefs,
self._getPruneTaskName()) self._getPruneTaskName())
return task.done
def _pruneContainerRefs(self, task=None):
self._pruneContainersJob = PruneContainerRefs(
'%s-pruneContainerRefs' % self.getJobName(), self)
self.acceptOnce(self._pruneContainersJob.getFinishedEvent(),
self._scheduleNextPruning)
jobMgr.add(self._pruneContainersJob)
return task.done