ProfileSession.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. from pandac.libpandaexpressModules import TrueClock
  2. from direct.showbase.PythonUtil import (
  3. StdoutCapture, _installProfileCustomFuncs,_removeProfileCustomFuncs,
  4. _profileWithoutGarbageLeak, _getProfileResultFileInfo, _setProfileResultsFileInfo,
  5. _clearProfileResultFileInfo, )
  6. import __builtin__
  7. import profile
  8. import pstats
  9. from StringIO import StringIO
  10. import marshal
  11. class PercentStats(pstats.Stats):
  12. # prints more useful output when sampled durations are shorter than a millisecond
  13. # lots of this is copied from Python's pstats.py
  14. def print_stats(self, *amount):
  15. for filename in self.files:
  16. print filename
  17. if self.files: print
  18. indent = ' ' * 8
  19. for func in self.top_level:
  20. print indent, func_get_function_name(func)
  21. print indent, self.total_calls, "function calls",
  22. if self.total_calls != self.prim_calls:
  23. print "(%d primitive calls)" % self.prim_calls,
  24. # DCR
  25. #print "in %.3f CPU seconds" % self.total_tt
  26. print "in %s CPU milliseconds" % (self.total_tt * 1000.)
  27. print
  28. width, list = self.get_print_list(amount)
  29. if list:
  30. self.print_title()
  31. for func in list:
  32. self.print_line(func)
  33. print
  34. print
  35. return self
  36. @staticmethod
  37. def f8(x):
  38. return "%7.2f%%" % (x*100.)
  39. @staticmethod
  40. def func_std_string(func_name): # match what old profile produced
  41. return "%s:%d(%s)" % func_name
  42. def print_line(self, func):
  43. cc, nc, tt, ct, callers = self.stats[func]
  44. c = str(nc)
  45. d = self.total_tt
  46. f8 = PercentStats.f8
  47. if nc != cc:
  48. c = c + '/' + str(cc)
  49. print c.rjust(9),
  50. print f8(tt/d),
  51. if nc == 0:
  52. print ' '*8,
  53. else:
  54. print f8((tt/nc)/d),
  55. print f8(ct/d),
  56. if cc == 0:
  57. print ' '*8,
  58. else:
  59. print f8((ct/cc)/d),
  60. print PercentStats.func_std_string(func)
  61. class ProfileSession:
  62. # class that encapsulates a profile of a single callable using Python's standard
  63. # 'profile' module
  64. #
  65. # defers formatting of profile results until they are requested
  66. #
  67. # implementation sidesteps memory leak in Python profile module,
  68. # and redirects file output to RAM file for efficiency
  69. DefaultFilename = 'profilesession'
  70. DefaultLines = 80
  71. DefaultSorts = ['cumulative', 'time', 'calls']
  72. TrueClock = TrueClock.getGlobalPtr()
  73. def __init__(self, func, name):
  74. self._func = func
  75. self._name = name
  76. self._filenameBase = 'profileData-%s-%s' % (self._name, id(self))
  77. self._refCount = 0
  78. self._reset()
  79. self.acquire()
  80. def acquire(self):
  81. self._refCount += 1
  82. def release(self):
  83. self._refCount -= 1
  84. if not self._refCount:
  85. self._destroy()
  86. def _destroy(self):
  87. del self._func
  88. del self._name
  89. del self._filenameBase
  90. del self._filenameCounter
  91. del self._filenames
  92. del self._duration
  93. del self._filename2ramFile
  94. del self._resultCache
  95. del self._successfulProfiles
  96. def _reset(self):
  97. self._filenameCounter = 0
  98. self._filenames = []
  99. # index of next file to be added to stats object
  100. self._statFileCounter = 0
  101. self._successfulProfiles = 0
  102. self._duration = None
  103. self._filename2ramFile = {}
  104. self._stats = None
  105. self._resultCache = {}
  106. def _getNextFilename(self):
  107. filename = '%s-%s' % (self._filenameBase, self._filenameCounter)
  108. self._filenameCounter += 1
  109. return filename
  110. def run(self, aggregate=True):
  111. # make sure this instance doesn't get destroyed inside self._func
  112. self.acquire()
  113. if not aggregate:
  114. self._reset()
  115. # if we're already profiling, just run the func and don't profile
  116. if 'globalProfileFunc' in __builtin__.__dict__:
  117. result = self._func()
  118. if self._duration is None:
  119. self._duration = 0.
  120. else:
  121. # put the function in the global namespace so that profile can find it
  122. __builtin__.globalProfileFunc = self._func
  123. __builtin__.globalProfileResult = [None]
  124. # set up the RAM file
  125. self._filenames.append(self._getNextFilename())
  126. filename = self._filenames[-1]
  127. _installProfileCustomFuncs(filename)
  128. # do the profiling
  129. Profile = profile.Profile
  130. statement = 'globalProfileResult[0]=globalProfileFunc()'
  131. sort = -1
  132. retVal = None
  133. # this is based on profile.run, the code is replicated here to allow us to
  134. # eliminate a memory leak
  135. prof = Profile()
  136. try:
  137. prof = prof.run(statement)
  138. except SystemExit:
  139. pass
  140. # this has to be run immediately after profiling for the timings to be accurate
  141. # tell the Profile object to generate output to the RAM file
  142. prof.dump_stats(filename)
  143. # eliminate the memory leak
  144. del prof.dispatcher
  145. # store the RAM file for later
  146. profData = _getProfileResultFileInfo(filename)
  147. self._filename2ramFile[filename] = profData
  148. # calculate the duration (this is dependent on the internal Python profile data format.
  149. # see profile.py and pstats.py, this was copied from pstats.Stats.strip_dirs)
  150. maxTime = 0.
  151. for cc, nc, tt, ct, callers in profData[1].itervalues():
  152. if ct > maxTime:
  153. maxTime = ct
  154. self._duration = maxTime
  155. # clean up the RAM file support
  156. _removeProfileCustomFuncs(filename)
  157. # clean up the globals
  158. result = globalProfileResult[0]
  159. del __builtin__.__dict__['globalProfileFunc']
  160. del __builtin__.__dict__['globalProfileResult']
  161. self._successfulProfiles += 1
  162. self.release()
  163. return result
  164. def getDuration(self):
  165. return self._duration
  166. def profileSucceeded(self):
  167. return self._successfulProfiles > 0
  168. def _restoreRamFile(self, filename):
  169. # set up the RAM file
  170. _installProfileCustomFuncs(filename)
  171. # install the stored RAM file from self.run()
  172. _setProfileResultsFileInfo(filename, self._filename2ramFile[filename])
  173. def _discardRamFile(self, filename):
  174. # take down the RAM file
  175. _removeProfileCustomFuncs(filename)
  176. # and discard it
  177. del self._filename2ramFile[filename]
  178. def getResults(self,
  179. lines=80,
  180. sorts=['cumulative', 'time', 'calls'],
  181. callInfo=False):
  182. if not self.profileSucceeded():
  183. output = '%s: profiler already running, could not profile' % self._name
  184. else:
  185. # make sure our stats object exists and is up-to-date
  186. statsChanged = (self._statFileCounter < len(self._filenames))
  187. if self._stats is None:
  188. for filename in self._filenames:
  189. self._restoreRamFile(filename)
  190. self._stats = PercentStats(*self._filenames)
  191. self._statFileCounter = len(self._filenames)
  192. for filename in self._filenames:
  193. self._discardRamFile(filename)
  194. else:
  195. while self._statFileCounter < len(self._filenames):
  196. filename = self._filenames[self._statFileCounter]
  197. self._restoreRamFile(filename)
  198. self._stats.add(filename)
  199. self._discardRamFile(filename)
  200. if statsChanged:
  201. self._stats.strip_dirs()
  202. # throw out any cached result strings
  203. self._resultCache = {}
  204. # make sure the arguments will hash efficiently if callers provide different types
  205. lines = int(lines)
  206. sorts = list(sorts)
  207. callInfo = bool(callInfo)
  208. k = str((lines, sorts, callInfo))
  209. if k in self._resultCache:
  210. # we've already created this output string, get it from the cache
  211. output = self._resultCache[k]
  212. else:
  213. # now get human-readable output from the profile stats
  214. # capture print output to a string
  215. sc = StdoutCapture()
  216. # print the info to stdout
  217. s = self._stats
  218. for sort in sorts:
  219. s.sort_stats(sort)
  220. s.print_stats(lines)
  221. if callInfo:
  222. s.print_callees(lines)
  223. s.print_callers(lines)
  224. # make a copy of the print output
  225. output = sc.getString()
  226. # restore stdout to what it was before
  227. sc.destroy()
  228. # cache this result
  229. self._resultCache[k] = output
  230. return output