diff --git a/direct/src/showbase/ObjectPool.py b/direct/src/showbase/ObjectPool.py new file mode 100755 index 0000000000..988d1d14f9 --- /dev/null +++ b/direct/src/showbase/ObjectPool.py @@ -0,0 +1,115 @@ +from direct.directnotify.DirectNotifyGlobal import directNotify +from direct.showbase.PythonUtil import invertDict, makeList, safeRepr +from direct.showbase.PythonUtil import getNumberedTypedString +import types +import gc + +class Diff: + def __init__(self, lost, gained): + self.lost=lost + self.gained=gained + def __repr__(self): + s = 'lost %s objects, gained %s objects' % (len(self.lost), len(self.gained)) + s += '\n\nself.lost\n' + s += self.lost.typeFreqStr() + s += '\n\nself.gained\n' + s += self.gained.typeFreqStr() + s += '\n\nGAINED-OBJECT REFERRERS\n' + s += self.gained.referrersStr(1) + return s + +class ObjectPool: + """manipulate a pool of Python objects""" + notify = directNotify.newCategory('ObjectPool') + + def __init__(self, objects): + self._objs = list(objects) + self._type2objs = {} + self._count2types = {} + type2count = {} + for obj in self._objs: + typ = type(obj) + # isn't all that useful + if typ is types.InstanceType: + typ = '%s of %s' % (typ, repr(obj.__class__)) + type2count.setdefault(typ, 0) + type2count[typ] += 1 + self._type2objs.setdefault(typ, []) + self._type2objs[typ].append(obj) + self._count2types = invertDict(type2count) + + def _getInternalObjs(self): + return (self._objs, self._type2objs, self._count2types) + + def destroy(self): + del self._objs + del self._type2objs + del self._count2types + + def getTypes(self): + return self._type2objs.keys() + + def getObjsOfType(self, type): + return self._type2objs.get(type, []) + + def printObjsOfType(self, type): + for obj in self._type2objs.get(type, []): + print repr(obj) + + def diff(self, other): + """print difference between this pool and 'other' pool""" + thisId2obj = {} + otherId2obj = {} + for obj in self._objs: + thisId2obj[id(obj)] = obj + for obj in other._objs: + otherId2obj[id(obj)] = obj + thisIds = set(thisId2obj.keys()) + otherIds = set(otherId2obj.keys()) + lostIds = thisIds.difference(otherIds) + print 'lost: %s' % lostIds + gainedIds = otherIds.difference(thisIds) + del thisIds + del otherIds + lostObjs = [] + for i in lostIds: + lostObjs.append(thisId2obj[i]) + gainedObjs = [] + for i in gainedIds: + gainedObjs.append(otherId2obj[i]) + return Diff(self.__class__(lostObjs), self.__class__(gainedObjs)) + + def typeFreqStr(self): + s = 'Object Pool: Type Frequencies' + s += '\n=============================' + counts = list(set(self._count2types.keys())) + counts.sort() + counts.reverse() + for count in counts: + types = makeList(self._count2types[count]) + for typ in types: + s += '\n%s\t%s' % (count, typ) + return s + + def referrersStr(self, numEach=3): + """referrers of the first few of each type of object""" + s = '' + counts = list(set(self._count2types.keys())) + counts.sort() + counts.reverse() + for count in counts: + types = makeList(self._count2types[count]) + for typ in types: + s += '\n\nTYPE: %s' % typ + for i in xrange(min(numEach,len(self._type2objs[typ]))): + obj = self._type2objs[typ][i] + s += '\nOBJ: %s\n' % safeRepr(obj) + referrers = gc.get_referrers(obj) + if len(referrers): + s += getNumberedTypedString(referrers) + else: + s += '' + return s + + def __len__(self): + return len(self._objs) diff --git a/direct/src/showbase/ObjectReport.py b/direct/src/showbase/ObjectReport.py new file mode 100755 index 0000000000..7e0b8b9408 --- /dev/null +++ b/direct/src/showbase/ObjectReport.py @@ -0,0 +1,151 @@ +from direct.directnotify.DirectNotifyGlobal import directNotify +from direct.showbase import DirectObject, ObjectPool +from direct.showbase.PythonUtil import makeList, Sync +import gc +import sys + +""" +>>> from direct.showbase import ObjectReport + +>>> o=ObjectReport.ObjectReport('baseline') +>>> run() +... + +>>> o2=ObjectReport.ObjectReport('') +>>> o.diff(o2) + +""" + +class ExclusiveObjectPool(DirectObject.DirectObject): + """ObjectPool specialization that excludes particular objects""" + # IDs of objects to globally exclude from reporting + _ExclObjIds = {} + _SyncMaster = Sync('ExclusiveObjectPool.ExcludedObjectList') + _SerialNumGen = SerialNumGen() + + @classmethod + def addExclObjs(cls, *objs): + for obj in makeList(objs): + cls._ExclObjIds.setdefault(id(obj), 0) + cls._ExclObjIds[id(obj)] += 1 + cls._SyncMaster.change() + @classmethod + def removeExclObjs(cls, *objs): + for obj in makeList(objs): + assert id(obj) in cls._ExclObjIds + del cls._ExclObjIds[id(obj)] + cls._SyncMaster.change() + + def __init__(self, objects): + self._objects = list(objects) + self._postFilterObjs = [] + self._sync = Sync('%s-%s' % (self.__class__.__name__, + self._SerialNumGen.next()), + self._SyncMaster) + self._sync.invalidate() + ExclusiveObjectPool.addExclObjs(self._objects, self._postFilterObjs, + self._sync) + + def destroy(self): + self.ignoreAll() + ExclusiveObjectPool.removeExclObjs(self._objects, self._postFilterObjs, + self._sync) + del self._objects + del self._postFilterObjs + del self._sync + + def _resync(self): + if self._sync.isSynced(self._SyncMaster): + return + if hasattr(self, '_filteredPool'): + ExclusiveObjectPool.removeExclObjs(*self._filteredPool._getInternalObjs()) + ExclusiveObjectPool.removeExclObjs(self._filteredPool) + del self._filteredPool + del self._postFilterObjs[:] + for obj in self._objects: + if id(obj) not in ExclusiveObjectPool._ExclObjIds: + self._postFilterObjs.append(obj) + self._filteredPool = ObjectPool.ObjectPool(self._postFilterObjs) + ExclusiveObjectPool.addExclObjs(self._filteredPool) + ExclusiveObjectPool.addExclObjs(*self._filteredPool._getInternalObjs()) + self._sync.sync(self._SyncMaster) + + def getObjsOfType(self, type): + self._resync() + return self._filteredPool.getObjsOfType(type) + def printObjsOfType(self, type): + self._resync() + return self._filteredPool.printObjsOfType(type) + def diff(self, other): + self._resync() + return self._filteredPool.diff(other._filteredPool) + def typeFreqStr(self): + self._resync() + return self._filteredPool.typeFreqStr() + def __len__(self): + self._resync() + return len(self._filteredPool) + +class ObjectReport: + """report on every Python object in the current process""" + notify = directNotify.newCategory('ObjectReport') + + def __init__(self, name, log=True, garbageCollect=True): + if not hasattr(sys, 'getobjects'): + self.notify.error( + '%s only works in debug Python (pyd-shell)' % self.__class__.__name__) + if garbageCollect: + import gc + gc.collect() + self._name = name + self._pool = ExclusiveObjectPool(self._getObjectList()) + ExclusiveObjectPool.addExclObjs(self, self._pool, self._name) + if log: + self.notify.info('===== ObjectReport: \'%s\' =====\n%s' % (self._name, self.typeFreqStr())) + + def destroy(self): + ExclusiveObjectPool.removeExclObjs(self, self._pool, self._name) + self._pool.destroy() + del self._pool + del self._name + + def typeFreqStr(self): + return self._pool.typeFreqStr() + + def diff(self, other): + return self._pool.diff(other._pool) + + def _getObjectList(self): + if hasattr(sys, 'getobjects'): + return sys.getobjects(0) + else: + objs = [] + stateStack = Stack() + root = __builtins__ + objIds = set([id(root)]) + stateStack.push((root, None, 0)) + while True: + if len(stateStack) == 0: + break + obj, adjacents, resumeIndex = stateStack.pop() + objs.append(obj) + print id(obj) + if adjacents is None: + adjacents = gc.get_referents(obj) + adjacents.extend(gc.get_referrers(obj)) + # pare the list down to the ones that have not already been visited + # to minimize calls to get_referents/get_referrers + newObjs = [] + for adj in adjacents: + adjId = id(adj) + if adjId not in objIds: + objIds.add(adjId) + newObjs.append(adj) + adjacents = newObjs + if len(adjacents) == 0: + print 'DEAD END' + for i in xrange(resumeIndex, len(adjacents)): + adj = adjacents[i] + stateStack.push((obj, adjacents, i+1)) + stateStack.push((adj, None, 0)) + continue