Browse Source

support task profiling in new task manager

David Rose 17 years ago
parent
commit
64d24fb65b

+ 4 - 9
direct/src/showbase/ShowBase.py

@@ -367,12 +367,11 @@ class ShowBase(DirectObject.DirectObject):
         if self.windowType != 'none':
         if self.windowType != 'none':
             self.__doStartDirect()
             self.__doStartDirect()
 
 
-
-        self._wantGcTask = config.GetBool('want-garbage-collect-task', 1)
-        self._gcTask = None
-        if self._wantGcTask:
+        if config.GetBool('want-garbage-collect-task', 1):
             # manual garbage-collect task
             # manual garbage-collect task
-            self._gcTask = taskMgr.add(self._garbageCollect, self.GarbageCollectTaskName, 200)
+            taskMgr.add(self._garbageCollect, self.GarbageCollectTaskName, 200)
+            
+        taskMgr.finalInit()
 
 
         # Start IGLOOP
         # Start IGLOOP
         self.restart()
         self.restart()
@@ -417,10 +416,6 @@ class ShowBase(DirectObject.DirectObject):
 
 
         This function is designed to be safe to call multiple times."""
         This function is designed to be safe to call multiple times."""
 
 
-        if self._gcTask:
-            self._gcTask.remove()
-            self._gcTask = None
-
         taskMgr.destroy()
         taskMgr.destroy()
 
 
         if getattr(self, 'musicManager', None):
         if getattr(self, 'musicManager', None):

+ 146 - 1
direct/src/task/TaskNew.py

@@ -8,9 +8,13 @@ __all__ = ['Task', 'TaskManager',
 
 
 from direct.directnotify.DirectNotifyGlobal import *
 from direct.directnotify.DirectNotifyGlobal import *
 from direct.showbase import ExceptionVarDump
 from direct.showbase import ExceptionVarDump
+from direct.showbase.PythonUtil import *
+from direct.showbase.ProfileSession import ProfileSession
 import signal
 import signal
 import types
 import types
 import time
 import time
+import random
+import string
 
 
 from pandac.PandaModules import *
 from pandac.PandaModules import *
 
 
@@ -107,6 +111,24 @@ class TaskManager:
         self.fKeyboardInterrupt = False
         self.fKeyboardInterrupt = False
         self.interruptCount = 0
         self.interruptCount = 0
 
 
+        self._profileFrames = False
+
+        # this will be set when it's safe to import StateVar
+        self._profileTasks = None
+        self._taskProfiler = None
+        self._profileInfo = ScratchPad(
+            taskId = None,
+            profiled = False,
+            session = None,
+            )
+
+    def finalInit(self):
+        # This function should be called once during startup, after
+        # most things are imported.
+        from direct.fsm.StatePush import StateVar
+        self._profileTasks = StateVar(False)
+        self.setProfileTasks(ConfigVariableBool('profile-task-spikes', 0).getValue())
+
     def destroy(self):
     def destroy(self):
         self.mgr.cleanup()
         self.mgr.cleanup()
 
 
@@ -419,7 +441,11 @@ class TaskManager:
             self.running = True
             self.running = True
             while self.running:
             while self.running:
                 try:
                 try:
-                    self.step()
+                    if self._profileFrames:
+                        self._profileFrames = False
+                        self._doProfiledFrames()
+                    else:
+                        self.step()
                 except KeyboardInterrupt:
                 except KeyboardInterrupt:
                     self.stop()
                     self.stop()
                 except IOError, ioError:
                 except IOError, ioError:
@@ -496,6 +522,125 @@ class TaskManager:
         from direct.tkpanels import TaskManagerPanel
         from direct.tkpanels import TaskManagerPanel
         return TaskManagerPanel.TaskManagerPanel(self)
         return TaskManagerPanel.TaskManagerPanel(self)
 
 
+    def profileFrames(self, num=None):
+        self._profileFrames = True
+        if num is None:
+            num = 1
+        self._profileFrameCount = num
+
+    @profiled()
+    def _doProfiledFrames(self, *args, **kArgs):
+        print '** profiling %s frames' % self._profileFrameCount
+        for i in xrange(self._profileFrameCount):
+            result = self.step(*args, **kArgs)
+        return result
+
+    def getProfileTasks(self):
+        return self._profileTasks.get()
+
+    def getProfileTasksSV(self):
+        return self._profileTasks
+
+    def setProfileTasks(self, profileTasks):
+        self._profileTasks.set(profileTasks)
+        if (not self._taskProfiler) and profileTasks:
+            # import here due to import dependencies
+            from direct.task.TaskProfiler import TaskProfiler
+            self._taskProfiler = TaskProfiler()
+
+    def logTaskProfiles(self, name=None):
+        if self._taskProfiler:
+            self._taskProfiler.logProfiles(name)
+
+    def flushTaskProfiles(self, name=None):
+        if self._taskProfiler:
+            self._taskProfiler.flush(name)
+
+    def _setProfileTask(self, task):
+        if self._profileInfo.session:
+            self._profileInfo.session.release()
+            self._profileInfo.session = None
+        self._profileInfo = ScratchPad(
+            taskFunc = task.getFunction(),
+            taskArgs = task.getArgs(),
+            task = task,
+            profiled = False,
+            session = None,
+            )
+
+        # Temporarily replace the task's own function with our
+        # _profileTask method.
+        task.setFunction(self._profileTask)
+        task.setArgs([self._profileInfo], True)
+
+    def _profileTask(self, profileInfo, task):
+        # This is called instead of the task function when we have
+        # decided to profile a task.
+
+        assert profileInfo.task == task
+        # don't profile the same task twice in a row
+        assert not profileInfo.profiled
+
+        # Restore the task's proper function for next time.
+        appendTask = False
+        taskArgs = profileInfo.taskArgs
+        if taskArgs and taskArgs[-1] == task:
+            appendTask = True
+            taskArgs = taskArgs[:-1]
+        task.setArgs(taskArgs, appendTask)
+        task.setFunction(profileInfo.taskFunc)
+
+        profileSession = ProfileSession(Functor(profileInfo.taskFunc, *profileInfo.taskArgs),
+                                        'profiled-task-%s' % task.getName())
+        ret = profileSession.run()
+
+        # set these values *after* profiling in case we're profiling the TaskProfiler
+        profileInfo.session = profileSession
+        profileInfo.profiled = True
+
+        return ret
+
+    def _hasProfiledDesignatedTask(self):
+        # have we run a profile of the designated task yet?
+        return self._profileInfo.profiled
+
+    def _getLastProfileSession(self):
+        return self._profileInfo.session
+
+    def _getRandomTask(self):
+        tasks = self.mgr.getActiveTasks()
+        numTasks = tasks.getNumTasks()
+        doLaters = self.mgr.getSleepingTasks()
+        numDoLaters = doLaters.getNumTasks()
+        if random.random() < (numDoLaters / float(numTasks + numDoLaters)):
+            # grab a doLater that will most likely trigger in the next frame
+            tNow = globalClock.getFrameTime()
+            avgFrameRate = globalClock.getAverageFrameRate()
+            if avgFrameRate < .00001:
+                avgFrameDur = 0.
+            else:
+                avgFrameDur = (1. / globalClock.getAverageFrameRate())
+            tNext = tNow + avgFrameDur
+            # binary search to find doLaters that are likely to trigger on the next frame
+            curIndex = int(numDoLaters / 2)
+            rangeStart = 0
+            rangeEnd = numDoLaters
+            while True:
+                if tNext < doLaters[curIndex].wakeTime:
+                    rangeEnd = curIndex
+                else:
+                    rangeStart = curIndex
+                prevIndex = curIndex
+                curIndex = int((rangeStart + rangeEnd) / 2)
+                if curIndex == prevIndex:
+                    break
+            index = curIndex
+            task = doLaters[random.randrange(index+1)]
+        else:
+            # grab a task
+            task = tasks[random.randint(0, numTasks - 1)]
+        return task
+
     def __repr__(self):
     def __repr__(self):
         return str(self.mgr)
         return str(self.mgr)
 
 

+ 24 - 0
direct/src/task/TaskOrig.py

@@ -190,6 +190,25 @@ class Task:
             taskName = self.name
             taskName = self.name
         return ''.join([c for c in taskName if c not in digits])
         return ''.join([c for c in taskName if c not in digits])
 
 
+    def getNamePrefix(self):
+        # get a version of the task name, omitting a hyphen or
+        # underscore followed by a string of digits at the end of the
+        # name.
+        name = self.name
+        trimmed = len(name)
+        p = trimmed
+        while True:
+            while p > 0 and name[p - 1] in string.digits:
+                p -= 1
+            if p > 0 and name[p - 1] in '-_':
+                p -= 1
+                trimmed = p
+            else:
+                p = trimmed
+                break
+
+        return name[:trimmed]
+
     def setupPStats(self):
     def setupPStats(self):
         if __debug__ and TaskManager.taskTimerVerbose and not self.pstats:
         if __debug__ and TaskManager.taskTimerVerbose and not self.pstats:
             # Get the PStats name for the task.  By convention,
             # Get the PStats name for the task.  By convention,
@@ -440,6 +459,11 @@ class TaskManager:
         # A default task.
         # A default task.
         self._doLaterTask = self.add(self.__doLaterProcessor, "doLaterProcessor", -10)
         self._doLaterTask = self.add(self.__doLaterProcessor, "doLaterProcessor", -10)
 
 
+    def finalInit(self):
+        # This function should be called once during startup, after
+        # most things are imported.
+        pass
+
     def destroy(self):
     def destroy(self):
         if self._doLaterTask:
         if self._doLaterTask:
             self._doLaterTask.remove()
             self._doLaterTask.remove()

+ 20 - 20
direct/src/task/TaskProfiler.py

@@ -5,14 +5,14 @@ from direct.showbase.PythonUtil import Averager
 class TaskTracker:
 class TaskTracker:
     # call it TaskProfiler to avoid confusion for the user
     # call it TaskProfiler to avoid confusion for the user
     notify = directNotify.newCategory("TaskProfiler")
     notify = directNotify.newCategory("TaskProfiler")
-    def __init__(self, namePattern):
-        self._namePattern = namePattern
-        self._durationAverager = Averager('%s-durationAverager' % namePattern)
+    def __init__(self, namePrefix):
+        self._namePrefix = namePrefix
+        self._durationAverager = Averager('%s-durationAverager' % namePrefix)
         self._avgSession = None
         self._avgSession = None
         self._maxNonSpikeSession = None
         self._maxNonSpikeSession = None
     def destroy(self):
     def destroy(self):
         self.flush()
         self.flush()
-        del self._namePattern
+        del self._namePrefix
         del self._durationAverager
         del self._durationAverager
     def flush(self):
     def flush(self):
         self._durationAverager.reset()
         self._durationAverager.reset()
@@ -22,8 +22,8 @@ class TaskTracker:
             self._maxNonSpikeSession.release()
             self._maxNonSpikeSession.release()
         self._avgSession = None
         self._avgSession = None
         self._maxNonSpikeSession = None
         self._maxNonSpikeSession = None
-    def getNamePattern(self, namePattern):
-        return self._namePattern
+    def getNamePrefix(self, namePrefix):
+        return self._namePrefix
     def addProfileSession(self, session, isSpike):
     def addProfileSession(self, session, isSpike):
         duration = session.getWallClockDuration()
         duration = session.getWallClockDuration()
         self._durationAverager.addValue(duration)
         self._durationAverager.addValue(duration)
@@ -66,14 +66,14 @@ class TaskTracker:
             self.notify.info('task CPU profile (%s):\n'
             self.notify.info('task CPU profile (%s):\n'
                              '== AVERAGE (%s wall-clock seconds)\n%s\n'
                              '== AVERAGE (%s wall-clock seconds)\n%s\n'
                              '== LONGEST NON-SPIKE (%s wall-clock seconds)\n%s' % (
                              '== LONGEST NON-SPIKE (%s wall-clock seconds)\n%s' % (
-                self._namePattern,
+                self._namePrefix,
                 self._avgSession.getWallClockDuration(),
                 self._avgSession.getWallClockDuration(),
                 self._avgSession.getResults(),
                 self._avgSession.getResults(),
                 self._maxNonSpikeSession.getWallClockDuration(),
                 self._maxNonSpikeSession.getWallClockDuration(),
                 self._maxNonSpikeSession.getResults(),
                 self._maxNonSpikeSession.getResults(),
                 ))
                 ))
         else:
         else:
-            self.notify.info('task CPU profile (%s): no data collected' % self._namePattern)
+            self.notify.info('task CPU profile (%s): no data collected' % self._namePrefix)
 
 
 class TaskProfiler:
 class TaskProfiler:
     # this does intermittent profiling of tasks running on the system
     # this does intermittent profiling of tasks running on the system
@@ -83,7 +83,7 @@ class TaskProfiler:
     def __init__(self):
     def __init__(self):
         self._enableFC = FunctionCall(self._setEnabled, taskMgr.getProfileTasksSV())
         self._enableFC = FunctionCall(self._setEnabled, taskMgr.getProfileTasksSV())
         # table of task name pattern to TaskTracker
         # table of task name pattern to TaskTracker
-        self._namePattern2tracker = {}
+        self._namePrefix2tracker = {}
         self._task = None
         self._task = None
         # number of samples required before spikes start getting identified
         # number of samples required before spikes start getting identified
         self._minSamples = config.GetInt('profile-task-spike-min-samples', 30)
         self._minSamples = config.GetInt('profile-task-spike-min-samples', 30)
@@ -94,16 +94,16 @@ class TaskProfiler:
         if taskMgr.getProfileTasks():
         if taskMgr.getProfileTasks():
             self._setEnabled(False)
             self._setEnabled(False)
         self._enableFC.destroy()
         self._enableFC.destroy()
-        for tracker in self._namePattern2tracker.itervalues():
+        for tracker in self._namePrefix2tracker.itervalues():
             tracker.destroy()
             tracker.destroy()
-        del self._namePattern2tracker
+        del self._namePrefix2tracker
         del self._task
         del self._task
 
 
     def logProfiles(self, name=None):
     def logProfiles(self, name=None):
         if name:
         if name:
             name = name.lower()
             name = name.lower()
-        for namePattern, tracker in self._namePattern2tracker.iteritems():
-            if (name and (name not in namePattern.lower())):
+        for namePrefix, tracker in self._namePrefix2tracker.iteritems():
+            if (name and (name not in namePrefix.lower())):
                 continue
                 continue
             tracker.log()
             tracker.log()
 
 
@@ -111,8 +111,8 @@ class TaskProfiler:
         if name:
         if name:
             name = name.lower()
             name = name.lower()
         # flush stored task profiles
         # flush stored task profiles
-        for namePattern, tracker in self._namePattern2tracker.iteritems():
-            if (name and (name not in namePattern.lower())):
+        for namePrefix, tracker in self._namePrefix2tracker.iteritems():
+            if (name and (name not in namePrefix.lower())):
                 continue
                 continue
             tracker.flush()
             tracker.flush()
 
 
@@ -132,10 +132,10 @@ class TaskProfiler:
             # if we couldn't profile, throw this result out
             # if we couldn't profile, throw this result out
             if session.profileSucceeded():
             if session.profileSucceeded():
                 sessionDur = session.getWallClockDuration()
                 sessionDur = session.getWallClockDuration()
-                namePattern = self._task.getNamePattern()
-                if namePattern not in self._namePattern2tracker:
-                    self._namePattern2tracker[namePattern] = TaskTracker(namePattern)
-                tracker = self._namePattern2tracker[namePattern]
+                namePrefix = self._task.getNamePrefix()
+                if namePrefix not in self._namePrefix2tracker:
+                    self._namePrefix2tracker[namePrefix] = TaskTracker(namePrefix)
+                tracker = self._namePrefix2tracker[namePrefix]
                 isSpike = False
                 isSpike = False
                 # do we have enough samples?
                 # do we have enough samples?
                 if tracker.getNumDurationSamples() > self._minSamples:
                 if tracker.getNumDurationSamples() > self._minSamples:
@@ -150,7 +150,7 @@ class TaskProfiler:
                                          '== AVERAGE (%s wall-clock seconds)\n%s\n'
                                          '== AVERAGE (%s wall-clock seconds)\n%s\n'
                                          '== LONGEST NON-SPIKE (%s wall-clock seconds)\n%s\n'
                                          '== LONGEST NON-SPIKE (%s wall-clock seconds)\n%s\n'
                                          '== SPIKE (%s wall-clock seconds)\n%s' % (
                                          '== SPIKE (%s wall-clock seconds)\n%s' % (
-                            namePattern,
+                            namePrefix,
                             avgSession.getWallClockDuration(), avgSession.getResults(),
                             avgSession.getWallClockDuration(), avgSession.getResults(),
                             maxNSSession.getWallClockDuration(), maxNSSession.getResults(),
                             maxNSSession.getWallClockDuration(), maxNSSession.getResults(),
                             sessionDur, session.getResults()))
                             sessionDur, session.getResults()))