| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191 |
- from panda3d.core import ConfigVariableBool, ClockObject
- from direct.directnotify.DirectNotifyGlobal import directNotify
- from direct.fsm.StatePush import FunctionCall
- from direct.showbase.PythonUtil import formatTimeExact, normalDistrib, serialNum
- from direct.showbase.PythonUtil import Functor
- from .Task import Task
- from .TaskManagerGlobal import taskMgr
- class FrameProfiler:
- notify = directNotify.newCategory('FrameProfiler')
- # because of precision requirements, all times related to the profile/log
- # schedule are stored as integers
- Minute = 60
- Hour = 60 * Minute
- Day = 24 * Hour
- def __init__(self):
- Hour = FrameProfiler.Hour
- # how long to wait between frame profiles
- frequent_profiles = ConfigVariableBool('frequent-frame-profiles', False)
- self._period = 2 * FrameProfiler.Minute
- if frequent_profiles:
- self._period = 1 * FrameProfiler.Minute
- # used to prevent profile from being taken exactly every 'period' seconds
- self._jitterMagnitude = self._period * .75
- # when to log output
- # each entry must be an integer multiple of all previous entries
- # as well as an integer multiple of the period
- self._logSchedule = [ 1 * FrameProfiler.Hour,
- 4 * FrameProfiler.Hour,
- 12 * FrameProfiler.Hour,
- 1 * FrameProfiler.Day,
- ] # day schedule proceeds as 1, 2, 4, 8 days, etc.
- if frequent_profiles:
- self._logSchedule = [ 1 * FrameProfiler.Minute,
- 4 * FrameProfiler.Minute,
- 12 * FrameProfiler.Minute,
- 24 * FrameProfiler.Minute,
- ]
- for t in self._logSchedule:
- #assert isInteger(t)
- # make sure the period is evenly divisible into each element of the log schedule
- assert (t % self._period) == 0
- # make sure each element of the schedule is evenly divisible into each subsequent element
- for i in range(len(self._logSchedule)):
- e = self._logSchedule[i]
- for j in range(i, len(self._logSchedule)):
- assert (self._logSchedule[j] % e) == 0
- #assert isInteger(self._period)
- self._enableFC = FunctionCall(self._setEnabled, taskMgr.getProfileFramesSV())
- self._enableFC.pushCurrentState()
- def destroy(self):
- self._enableFC.set(False)
- self._enableFC.destroy()
- def _setEnabled(self, enabled):
- if enabled:
- self.notify.info('frame profiler started')
- self._startTime = ClockObject.getGlobalClock().getFrameTime()
- self._profileCounter = 0
- self._jitter = None
- self._period2aggregateProfile = {}
- self._id2session = {}
- self._id2task = {}
- # don't profile process startup
- self._task = taskMgr.doMethodLater(self._period, self._scheduleNextProfileDoLater,
- 'FrameProfilerStart-%s' % serialNum())
- else:
- self._task.remove()
- del self._task
- for session in self._period2aggregateProfile.values():
- session.release()
- del self._period2aggregateProfile
- for task in self._id2task.values():
- task.remove()
- del self._id2task
- for session in self._id2session.values():
- session.release()
- del self._id2session
- self.notify.info('frame profiler stopped')
- def _scheduleNextProfileDoLater(self, task):
- self._scheduleNextProfile()
- return Task.done
- def _scheduleNextProfile(self):
- self._profileCounter += 1
- self._timeElapsed = self._profileCounter * self._period
- #assert isInteger(self._timeElapsed)
- time = self._startTime + self._timeElapsed
- # vary the actual delay between profiles by a random amount to prevent interaction
- # with periodic events
- jitter = self._jitter
- if jitter is None:
- jitter = normalDistrib(-self._jitterMagnitude, self._jitterMagnitude)
- time += jitter
- else:
- time -= jitter
- jitter = None
- self._jitter = jitter
- sessionId = serialNum()
- session = taskMgr.getProfileSession('FrameProfile-%s' % sessionId)
- self._id2session[sessionId] = session
- taskMgr.profileFrames(num=1, session=session, callback=Functor(
- self._analyzeResults, sessionId))
- # schedule the next profile
- delay = max(time - ClockObject.getGlobalClock().getFrameTime(), 0.)
- self._task = taskMgr.doMethodLater(delay, self._scheduleNextProfileDoLater,
- 'FrameProfiler-%s' % serialNum())
- def _analyzeResults(self, sessionId):
- # do the analysis in a task 1) to separate the processing from the profiled frame,
- # and 2) to get the processing to show up in a named task instead of in the taskMgr
- self._id2task[sessionId] = taskMgr.add(
- Functor(self._doAnalysis, sessionId), 'FrameProfilerAnalysis-%s' % sessionId)
- def _doAnalysis(self, sessionId, task):
- if hasattr(task, '_generator'):
- gen = task._generator
- else:
- gen = self._doAnalysisGen(sessionId)
- task._generator = gen
- result = next(gen)
- if result == Task.done:
- del task._generator
- return result
- def _doAnalysisGen(self, sessionId):
- # generator to limit max number of profile loggings per frame
- p2ap = self._period2aggregateProfile
- self._id2task.pop(sessionId)
- session = self._id2session.pop(sessionId)
- if session.profileSucceeded():
- # always add this profile to the first aggregated profile
- period = self._logSchedule[0]
- if period not in self._period2aggregateProfile:
- p2ap[period] = session.getReference()
- else:
- p2ap[period].aggregate(session)
- else:
- self.notify.warning('frame profile did not succeed')
- session.release()
- session = None
- counter = 0
- # log profiles when it's time, and aggregate them upwards into the
- # next-longer profile
- for pi in range(len(self._logSchedule)):
- period = self._logSchedule[pi]
- if (self._timeElapsed % period) == 0:
- if period in p2ap:
- # delay until the next frame if we've already processed N profiles this frame
- if counter >= 3:
- counter = 0
- yield Task.cont
- self.notify.info('aggregate profile of sampled frames over last %s\n%s' %
- (formatTimeExact(period), p2ap[period].getResults()))
- counter += 1
- # aggregate this profile into the next larger profile
- nextIndex = pi + 1
- if nextIndex >= len(self._logSchedule):
- # if we're adding a new period to the end of the log period table,
- # set it to double the duration of the current longest period
- nextPeriod = period * 2
- self._logSchedule.append(nextPeriod)
- else:
- nextPeriod = self._logSchedule[nextIndex]
- if nextPeriod not in p2ap:
- p2ap[nextPeriod] = p2ap[period].getReference()
- else:
- p2ap[nextPeriod].aggregate(p2ap[period])
- # this profile is now represented in the next larger profile
- # throw it out
- p2ap[period].release()
- del p2ap[period]
- else:
- # current time is not divisible evenly into selected period, and all higher
- # periods are multiples of this one
- break
- yield Task.done
|