2
0

FrameProfiler.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. from panda3d.core import ConfigVariableBool, ClockObject
  2. from direct.directnotify.DirectNotifyGlobal import directNotify
  3. from direct.fsm.StatePush import FunctionCall
  4. from direct.showbase.PythonUtil import formatTimeExact, normalDistrib, serialNum
  5. from direct.showbase.PythonUtil import Functor
  6. from .Task import Task
  7. from .TaskManagerGlobal import taskMgr
  8. class FrameProfiler:
  9. notify = directNotify.newCategory('FrameProfiler')
  10. # because of precision requirements, all times related to the profile/log
  11. # schedule are stored as integers
  12. Minute = 60
  13. Hour = 60 * Minute
  14. Day = 24 * Hour
  15. def __init__(self):
  16. Hour = FrameProfiler.Hour
  17. # how long to wait between frame profiles
  18. frequent_profiles = ConfigVariableBool('frequent-frame-profiles', False)
  19. self._period = 2 * FrameProfiler.Minute
  20. if frequent_profiles:
  21. self._period = 1 * FrameProfiler.Minute
  22. # used to prevent profile from being taken exactly every 'period' seconds
  23. self._jitterMagnitude = self._period * .75
  24. # when to log output
  25. # each entry must be an integer multiple of all previous entries
  26. # as well as an integer multiple of the period
  27. self._logSchedule = [ 1 * FrameProfiler.Hour,
  28. 4 * FrameProfiler.Hour,
  29. 12 * FrameProfiler.Hour,
  30. 1 * FrameProfiler.Day,
  31. ] # day schedule proceeds as 1, 2, 4, 8 days, etc.
  32. if frequent_profiles:
  33. self._logSchedule = [ 1 * FrameProfiler.Minute,
  34. 4 * FrameProfiler.Minute,
  35. 12 * FrameProfiler.Minute,
  36. 24 * FrameProfiler.Minute,
  37. ]
  38. for t in self._logSchedule:
  39. #assert isInteger(t)
  40. # make sure the period is evenly divisible into each element of the log schedule
  41. assert (t % self._period) == 0
  42. # make sure each element of the schedule is evenly divisible into each subsequent element
  43. for i in range(len(self._logSchedule)):
  44. e = self._logSchedule[i]
  45. for j in range(i, len(self._logSchedule)):
  46. assert (self._logSchedule[j] % e) == 0
  47. #assert isInteger(self._period)
  48. self._enableFC = FunctionCall(self._setEnabled, taskMgr.getProfileFramesSV())
  49. self._enableFC.pushCurrentState()
  50. def destroy(self):
  51. self._enableFC.set(False)
  52. self._enableFC.destroy()
  53. def _setEnabled(self, enabled):
  54. if enabled:
  55. self.notify.info('frame profiler started')
  56. self._startTime = ClockObject.getGlobalClock().getFrameTime()
  57. self._profileCounter = 0
  58. self._jitter = None
  59. self._period2aggregateProfile = {}
  60. self._id2session = {}
  61. self._id2task = {}
  62. # don't profile process startup
  63. self._task = taskMgr.doMethodLater(self._period, self._scheduleNextProfileDoLater,
  64. 'FrameProfilerStart-%s' % serialNum())
  65. else:
  66. self._task.remove()
  67. del self._task
  68. for session in self._period2aggregateProfile.values():
  69. session.release()
  70. del self._period2aggregateProfile
  71. for task in self._id2task.values():
  72. task.remove()
  73. del self._id2task
  74. for session in self._id2session.values():
  75. session.release()
  76. del self._id2session
  77. self.notify.info('frame profiler stopped')
  78. def _scheduleNextProfileDoLater(self, task):
  79. self._scheduleNextProfile()
  80. return Task.done
  81. def _scheduleNextProfile(self):
  82. self._profileCounter += 1
  83. self._timeElapsed = self._profileCounter * self._period
  84. #assert isInteger(self._timeElapsed)
  85. time = self._startTime + self._timeElapsed
  86. # vary the actual delay between profiles by a random amount to prevent interaction
  87. # with periodic events
  88. jitter = self._jitter
  89. if jitter is None:
  90. jitter = normalDistrib(-self._jitterMagnitude, self._jitterMagnitude)
  91. time += jitter
  92. else:
  93. time -= jitter
  94. jitter = None
  95. self._jitter = jitter
  96. sessionId = serialNum()
  97. session = taskMgr.getProfileSession('FrameProfile-%s' % sessionId)
  98. self._id2session[sessionId] = session
  99. taskMgr.profileFrames(num=1, session=session, callback=Functor(
  100. self._analyzeResults, sessionId))
  101. # schedule the next profile
  102. delay = max(time - ClockObject.getGlobalClock().getFrameTime(), 0.)
  103. self._task = taskMgr.doMethodLater(delay, self._scheduleNextProfileDoLater,
  104. 'FrameProfiler-%s' % serialNum())
  105. def _analyzeResults(self, sessionId):
  106. # do the analysis in a task 1) to separate the processing from the profiled frame,
  107. # and 2) to get the processing to show up in a named task instead of in the taskMgr
  108. self._id2task[sessionId] = taskMgr.add(
  109. Functor(self._doAnalysis, sessionId), 'FrameProfilerAnalysis-%s' % sessionId)
  110. def _doAnalysis(self, sessionId, task):
  111. if hasattr(task, '_generator'):
  112. gen = task._generator
  113. else:
  114. gen = self._doAnalysisGen(sessionId)
  115. task._generator = gen
  116. result = next(gen)
  117. if result == Task.done:
  118. del task._generator
  119. return result
  120. def _doAnalysisGen(self, sessionId):
  121. # generator to limit max number of profile loggings per frame
  122. p2ap = self._period2aggregateProfile
  123. self._id2task.pop(sessionId)
  124. session = self._id2session.pop(sessionId)
  125. if session.profileSucceeded():
  126. # always add this profile to the first aggregated profile
  127. period = self._logSchedule[0]
  128. if period not in self._period2aggregateProfile:
  129. p2ap[period] = session.getReference()
  130. else:
  131. p2ap[period].aggregate(session)
  132. else:
  133. self.notify.warning('frame profile did not succeed')
  134. session.release()
  135. session = None
  136. counter = 0
  137. # log profiles when it's time, and aggregate them upwards into the
  138. # next-longer profile
  139. for pi in range(len(self._logSchedule)):
  140. period = self._logSchedule[pi]
  141. if (self._timeElapsed % period) == 0:
  142. if period in p2ap:
  143. # delay until the next frame if we've already processed N profiles this frame
  144. if counter >= 3:
  145. counter = 0
  146. yield Task.cont
  147. self.notify.info('aggregate profile of sampled frames over last %s\n%s' %
  148. (formatTimeExact(period), p2ap[period].getResults()))
  149. counter += 1
  150. # aggregate this profile into the next larger profile
  151. nextIndex = pi + 1
  152. if nextIndex >= len(self._logSchedule):
  153. # if we're adding a new period to the end of the log period table,
  154. # set it to double the duration of the current longest period
  155. nextPeriod = period * 2
  156. self._logSchedule.append(nextPeriod)
  157. else:
  158. nextPeriod = self._logSchedule[nextIndex]
  159. if nextPeriod not in p2ap:
  160. p2ap[nextPeriod] = p2ap[period].getReference()
  161. else:
  162. p2ap[nextPeriod].aggregate(p2ap[period])
  163. # this profile is now represented in the next larger profile
  164. # throw it out
  165. p2ap[period].release()
  166. del p2ap[period]
  167. else:
  168. # current time is not divisible evenly into selected period, and all higher
  169. # periods are multiples of this one
  170. break
  171. yield Task.done