| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268 |
- from pandac.libpandaexpressModules import TrueClock
- from direct.showbase.PythonUtil import (
- StdoutCapture, _installProfileCustomFuncs,_removeProfileCustomFuncs,
- _profileWithoutGarbageLeak, _getProfileResultFileInfo, _setProfileResultsFileInfo,
- _clearProfileResultFileInfo, )
- import __builtin__
- import profile
- import pstats
- from StringIO import StringIO
- import marshal
- class PercentStats(pstats.Stats):
- # prints more useful output when sampled durations are shorter than a millisecond
- # lots of this is copied from Python's pstats.py
- def print_stats(self, *amount):
- for filename in self.files:
- print filename
- if self.files: print
- indent = ' ' * 8
- for func in self.top_level:
- print indent, func_get_function_name(func)
- print indent, self.total_calls, "function calls",
- if self.total_calls != self.prim_calls:
- print "(%d primitive calls)" % self.prim_calls,
- # DCR
- #print "in %.3f CPU seconds" % self.total_tt
- print "in %s CPU milliseconds" % (self.total_tt * 1000.)
- print
- width, list = self.get_print_list(amount)
- if list:
- self.print_title()
- for func in list:
- self.print_line(func)
- print
- print
- return self
- @staticmethod
- def f8(x):
- return "%7.2f%%" % (x*100.)
- @staticmethod
- def func_std_string(func_name): # match what old profile produced
- return "%s:%d(%s)" % func_name
- def print_line(self, func):
- cc, nc, tt, ct, callers = self.stats[func]
- c = str(nc)
- d = self.total_tt
- f8 = PercentStats.f8
- if nc != cc:
- c = c + '/' + str(cc)
- print c.rjust(9),
- print f8(tt/d),
- if nc == 0:
- print ' '*8,
- else:
- print f8((tt/nc)/d),
- print f8(ct/d),
- if cc == 0:
- print ' '*8,
- else:
- print f8((ct/cc)/d),
- print PercentStats.func_std_string(func)
- class ProfileSession:
- # class that encapsulates a profile of a single callable using Python's standard
- # 'profile' module
- #
- # defers formatting of profile results until they are requested
- #
- # implementation sidesteps memory leak in Python profile module,
- # and redirects file output to RAM file for efficiency
- DefaultFilename = 'profilesession'
- DefaultLines = 80
- DefaultSorts = ['cumulative', 'time', 'calls']
- TrueClock = TrueClock.getGlobalPtr()
-
- def __init__(self, func, name):
- self._func = func
- self._name = name
- self._filenameBase = 'profileData-%s-%s' % (self._name, id(self))
- self._refCount = 0
- self._reset()
- self.acquire()
- def acquire(self):
- self._refCount += 1
- def release(self):
- self._refCount -= 1
- if not self._refCount:
- self._destroy()
- def _destroy(self):
- del self._func
- del self._name
- del self._filenameBase
- del self._filenameCounter
- del self._filenames
- del self._duration
- del self._filename2ramFile
- del self._resultCache
- del self._successfulProfiles
- def _reset(self):
- self._filenameCounter = 0
- self._filenames = []
- # index of next file to be added to stats object
- self._statFileCounter = 0
- self._successfulProfiles = 0
- self._duration = None
- self._filename2ramFile = {}
- self._stats = None
- self._resultCache = {}
- def _getNextFilename(self):
- filename = '%s-%s' % (self._filenameBase, self._filenameCounter)
- self._filenameCounter += 1
- return filename
- def run(self, aggregate=True):
- # make sure this instance doesn't get destroyed inside self._func
- self.acquire()
- if not aggregate:
- self._reset()
- # if we're already profiling, just run the func and don't profile
- if 'globalProfileFunc' in __builtin__.__dict__:
- result = self._func()
- if self._duration is None:
- self._duration = 0.
- else:
- # put the function in the global namespace so that profile can find it
- __builtin__.globalProfileFunc = self._func
- __builtin__.globalProfileResult = [None]
- # set up the RAM file
- self._filenames.append(self._getNextFilename())
- filename = self._filenames[-1]
- _installProfileCustomFuncs(filename)
- # do the profiling
- Profile = profile.Profile
- statement = 'globalProfileResult[0]=globalProfileFunc()'
- sort = -1
- retVal = None
- # this is based on profile.run, the code is replicated here to allow us to
- # eliminate a memory leak
- prof = Profile()
- try:
- prof = prof.run(statement)
- except SystemExit:
- pass
- # this has to be run immediately after profiling for the timings to be accurate
- # tell the Profile object to generate output to the RAM file
- prof.dump_stats(filename)
- # eliminate the memory leak
- del prof.dispatcher
- # store the RAM file for later
- profData = _getProfileResultFileInfo(filename)
- self._filename2ramFile[filename] = profData
- # calculate the duration (this is dependent on the internal Python profile data format.
- # see profile.py and pstats.py, this was copied from pstats.Stats.strip_dirs)
- maxTime = 0.
- for cc, nc, tt, ct, callers in profData[1].itervalues():
- if ct > maxTime:
- maxTime = ct
- self._duration = maxTime
- # clean up the RAM file support
- _removeProfileCustomFuncs(filename)
- # clean up the globals
- result = globalProfileResult[0]
- del __builtin__.__dict__['globalProfileFunc']
- del __builtin__.__dict__['globalProfileResult']
- self._successfulProfiles += 1
-
- self.release()
- return result
- def getDuration(self):
- return self._duration
- def profileSucceeded(self):
- return self._successfulProfiles > 0
- def _restoreRamFile(self, filename):
- # set up the RAM file
- _installProfileCustomFuncs(filename)
- # install the stored RAM file from self.run()
- _setProfileResultsFileInfo(filename, self._filename2ramFile[filename])
- def _discardRamFile(self, filename):
- # take down the RAM file
- _removeProfileCustomFuncs(filename)
- # and discard it
- del self._filename2ramFile[filename]
-
- def getResults(self,
- lines=80,
- sorts=['cumulative', 'time', 'calls'],
- callInfo=False):
- if not self.profileSucceeded():
- output = '%s: profiler already running, could not profile' % self._name
- else:
- # make sure our stats object exists and is up-to-date
- statsChanged = (self._statFileCounter < len(self._filenames))
- if self._stats is None:
- for filename in self._filenames:
- self._restoreRamFile(filename)
- self._stats = PercentStats(*self._filenames)
- self._statFileCounter = len(self._filenames)
- for filename in self._filenames:
- self._discardRamFile(filename)
- else:
- while self._statFileCounter < len(self._filenames):
- filename = self._filenames[self._statFileCounter]
- self._restoreRamFile(filename)
- self._stats.add(filename)
- self._discardRamFile(filename)
- if statsChanged:
- self._stats.strip_dirs()
- # throw out any cached result strings
- self._resultCache = {}
- # make sure the arguments will hash efficiently if callers provide different types
- lines = int(lines)
- sorts = list(sorts)
- callInfo = bool(callInfo)
- k = str((lines, sorts, callInfo))
- if k in self._resultCache:
- # we've already created this output string, get it from the cache
- output = self._resultCache[k]
- else:
- # now get human-readable output from the profile stats
- # capture print output to a string
- sc = StdoutCapture()
- # print the info to stdout
- s = self._stats
- for sort in sorts:
- s.sort_stats(sort)
- s.print_stats(lines)
- if callInfo:
- s.print_callees(lines)
- s.print_callers(lines)
- # make a copy of the print output
- output = sc.getString()
- # restore stdout to what it was before
- sc.destroy()
- # cache this result
- self._resultCache[k] = output
- return output
-
|