GarbageReport.py 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. from direct.directnotify import DirectNotifyGlobal
  2. from direct.showbase import PythonUtil
  3. from direct.showbase.TaskThreaded import TaskThreaded
  4. import gc
  5. class FakeObject:
  6. pass
  7. def _createGarbage():
  8. a = FakeObject()
  9. b = FakeObject()
  10. a.other = b
  11. b.other = a
  12. class GarbageReport(TaskThreaded):
  13. """Detects leaked Python objects (via gc.collect()) and reports on garbage
  14. items, garbage-to-garbage references, and garbage cycles.
  15. If you just want to dump the report to the log, use GarbageLogger."""
  16. notify = DirectNotifyGlobal.directNotify.newCategory("GarbageReport")
  17. NotGarbage = 'NG'
  18. def __init__(self, name, log=True, verbose=False, fullReport=False, findCycles=True,
  19. threaded=False, doneCallback=None):
  20. # if log is True, GarbageReport will self-destroy after logging
  21. # if false, caller is responsible for calling destroy()
  22. # if threaded is True, processing will be performed over multiple frames
  23. TaskThreaded.__init__(self, name, threaded)
  24. # stick the arguments onto a ScratchPad so we can access them from the thread
  25. # functions and delete them all at once
  26. self._args = ScratchPad()
  27. self._args.name = name
  28. self._args.log = log
  29. self._args.verbose = verbose
  30. self._args.fullReport = fullReport
  31. self._args.findCycles = findCycles
  32. self._args.doneCallback = doneCallback
  33. # do the garbage collection
  34. wasOn = PythonUtil.gcDebugOn()
  35. oldFlags = gc.get_debug()
  36. if not wasOn:
  37. gc.set_debug(gc.DEBUG_SAVEALL)
  38. gc.collect()
  39. self.notify.debug('gc.garbage == %s' % gc.garbage)
  40. self.garbage = list(gc.garbage)
  41. self.notify.debug('self.garbage == %s' % self.garbage)
  42. del gc.garbage[:]
  43. if not wasOn:
  44. gc.set_debug(oldFlags)
  45. self.numGarbage = len(self.garbage)
  46. if self._args.verbose:
  47. self.notify.info('found %s garbage items' % self.numGarbage)
  48. self.scheduleNext(self.T_getReferrers)
  49. def T_getReferrers(self):
  50. # grab the referrers (pointing to garbage)
  51. self.referrersByReference = {}
  52. self.referrersByNumber = {}
  53. if self._args.fullReport:
  54. if self._args.verbose:
  55. self.notify.info('getting referrers...')
  56. # we need referents to detect cycles, but we don't need referrers
  57. for i in xrange(self.numGarbage):
  58. byNum, byRef = self._getReferrers(self.garbage[i])
  59. self.referrersByNumber[i] = byNum
  60. self.referrersByReference[i] = byRef
  61. self.scheduleNext(self.T_getReferents)
  62. def T_getReferents(self):
  63. # grab the referents (pointed to by garbage)
  64. self.referentsByReference = {}
  65. self.referentsByNumber = {}
  66. if self._args.verbose:
  67. self.notify.info('getting referents...')
  68. for i in xrange(self.numGarbage):
  69. byNum, byRef = self._getReferents(self.garbage[i])
  70. self.referentsByNumber[i] = byNum
  71. self.referentsByReference[i] = byRef
  72. self.scheduleNext(self.T_getCycles)
  73. def T_getCycles(self):
  74. # find the cycles
  75. if self._args.findCycles and self.numGarbage > 0:
  76. if self._args.verbose:
  77. self.notify.info('detecting cycles...')
  78. self.cycles = self._getCycles()
  79. self.scheduleNext(self.T_createReport)
  80. def T_createReport(self):
  81. s = '\n===== GarbageReport: \'%s\' (%s items) =====' % (self._args.name, self.numGarbage)
  82. if self.numGarbage > 0:
  83. # log each individual item with a number in front of it
  84. s += '\n\n===== Garbage Items ====='
  85. digits = 0
  86. n = self.numGarbage
  87. while n > 0:
  88. digits += 1
  89. n /= 10
  90. format = '\n%0' + '%s' % digits + 'i:%s \t%s'
  91. for i in range(len(self.garbage)):
  92. s += format % (i, type(self.garbage[i]), self.garbage[i])
  93. if self._args.findCycles:
  94. format = '\n%s'
  95. s += '\n\n===== Cycles ====='
  96. for cycle in self.cycles:
  97. s += format % cycle
  98. if self._args.fullReport:
  99. format = '\n%0' + '%s' % digits + 'i:%s'
  100. s += '\n\n===== Referrers By Number (what is referring to garbage item?) ====='
  101. for i in xrange(self.numGarbage):
  102. s += format % (i, self.referrersByNumber[i])
  103. s += '\n\n===== Referents By Number (what is garbage item referring to?) ====='
  104. for i in xrange(self.numGarbage):
  105. s += format % (i, self.referentsByNumber[i])
  106. s += '\n\n===== Referrers (what is referring to garbage item?) ====='
  107. for i in xrange(self.numGarbage):
  108. s += format % (i, self.referrersByReference[i])
  109. s += '\n\n===== Referents (what is garbage item referring to?) ====='
  110. for i in xrange(self.numGarbage):
  111. s += format % (i, self.referentsByReference[i])
  112. self._report = s
  113. self.scheduleNext(self.T_printReport)
  114. def T_printReport(self):
  115. if self._args.log:
  116. self.notify.info(self._report)
  117. self.scheduleNext(self.T_completed)
  118. def T_completed(self):
  119. if self._args.doneCallback:
  120. self._args.doneCallback(self)
  121. def destroy(self):
  122. del self._args
  123. del self.garbage
  124. del self.numGarbage
  125. del self.referrersByReference
  126. del self.referrersByNumber
  127. del self.referentsByReference
  128. del self.referentsByNumber
  129. if hasattr(self, 'cycles'):
  130. del self.cycles
  131. del self._report
  132. def getNumItems(self):
  133. return self.numGarbage
  134. def getGarbage(self):
  135. return self.garbage
  136. def getReport(self):
  137. return self._report
  138. def _getReferrers(self, obj):
  139. # referrers (pointing to garbage)
  140. # returns two lists, first by index into gc.garbage, second by
  141. # direct reference
  142. byRef = gc.get_referrers(obj)
  143. # look to see if each referrer is another garbage item
  144. byNum = []
  145. for referrer in byRef:
  146. try:
  147. num = self.garbage.index(referrer)
  148. byNum.append(num)
  149. except:
  150. #num = GarbageReport.NotGarbage
  151. pass
  152. return byNum, byRef
  153. def _getReferents(self, obj):
  154. # referents (pointed to by garbage)
  155. # returns two lists, first by index into gc.garbage, second by
  156. # direct reference
  157. byRef = gc.get_referents(obj)
  158. # look to see if each referent is another garbage item
  159. byNum = []
  160. for referent in byRef:
  161. try:
  162. num = self.garbage.index(referent)
  163. byNum.append(num)
  164. except:
  165. #num = GarbageReport.NotGarbage
  166. pass
  167. return byNum, byRef
  168. def _getCycles(self):
  169. assert self.notify.debugCall()
  170. # returns list of lists, sublists are garbage reference cycles
  171. cycles = []
  172. # sets of cycle members, to avoid duplicates
  173. cycleSets = []
  174. stateStack = Stack()
  175. for rootId in xrange(len(self.garbage)):
  176. assert len(stateStack) == 0
  177. stateStack.push(([rootId], rootId, 0))
  178. while True:
  179. if len(stateStack) == 0:
  180. break
  181. candidateCycle, curId, resumeIndex = stateStack.pop()
  182. if self.notify.getDebug():
  183. print 'restart: %s root=%s cur=%s resume=%s' % (
  184. candidateCycle, rootId, curId, resumeIndex)
  185. for index in xrange(resumeIndex, len(self.referentsByNumber[curId])):
  186. refId = self.referentsByNumber[curId][index]
  187. if self.notify.getDebug():
  188. print ' : %s -> %s' % (curId, refId)
  189. if refId == rootId:
  190. # we found a cycle! mark it down and move on to the next refId
  191. if not set(candidateCycle) in cycleSets:
  192. if self.notify.getDebug():
  193. print ' FOUND: ', list(candidateCycle) + [refId]
  194. cycles.append(list(candidateCycle) + [refId])
  195. cycleSets.append(set(candidateCycle))
  196. elif refId in candidateCycle:
  197. pass
  198. else:
  199. # this refId does not complete a cycle. Mark down
  200. # where we are in this list of referents, then
  201. # start looking through the referents of the new refId
  202. stateStack.push((list(candidateCycle), curId, index+1))
  203. stateStack.push((list(candidateCycle) + [refId], refId, 0))
  204. break
  205. return cycles
  206. class GarbageLogger(GarbageReport):
  207. """If you just want to log the current garbage to the log file, make
  208. one of these. It automatically destroys itself after logging"""
  209. def __init__(self, *args, **kArgs):
  210. kArgs['log'] = True
  211. GarbageReport.__init__(self, *args, **kArgs)
  212. def T_completed(self):
  213. GarbageReport.T_completed(self)
  214. self.destroy()