added task profiler

This commit is contained in:
Darren Ranalli 2008-09-15 21:47:37 +00:00
parent 7ec03c1c3e
commit 6895e25221
2 changed files with 688 additions and 500 deletions

View File

@ -30,7 +30,8 @@ __all__ = ['enumerate', 'unique', 'indent', 'nonRepeatingRandomList',
'nullGen', 'loopGen', 'makeFlywheelGen', 'flywheel', 'choice', 'nullGen', 'loopGen', 'makeFlywheelGen', 'flywheel', 'choice',
'printStack', 'printReverseStack', 'listToIndex2item', 'listToItem2index', 'printStack', 'printReverseStack', 'listToIndex2item', 'listToItem2index',
'pandaBreak','pandaTrace','formatTimeCompact','DestructiveScratchPad', 'pandaBreak','pandaTrace','formatTimeCompact','DestructiveScratchPad',
'deeptype',] 'deeptype','getProfileResultString','StdoutCapture','StdoutPassthrough',
'Averager',]
import types import types
import string import string
@ -794,12 +795,47 @@ def binaryRepr(number, max_length = 32):
digits = digits [digits.index (1):] digits = digits [digits.index (1):]
return string.join (map (repr, digits), '') return string.join (map (repr, digits), '')
class StdoutCapture:
# redirects stdout to a string
def __init__(self):
self._oldStdout = sys.stdout
sys.stdout = self
self._string = ''
def destroy(self):
sys.stdout = self._oldStdout
del self._oldStdout
def getString(self):
return self._string
# internal
def write(self, string):
self._string = ''.join([self._string, string])
class StdoutPassthrough(StdoutCapture):
# like StdoutCapture but also allows output to go through to the OS as normal
# internal
def write(self, string):
self._string = ''.join([self._string, string])
self._oldStdout.write(string)
# constant profile defaults # constant profile defaults
PyUtilProfileDefaultFilename = 'profiledata' PyUtilProfileDefaultFilename = 'profiledata'
PyUtilProfileDefaultLines = 80 PyUtilProfileDefaultLines = 80
PyUtilProfileDefaultSorts = ['cumulative', 'time', 'calls'] PyUtilProfileDefaultSorts = ['cumulative', 'time', 'calls']
def profile(callback, name, terse): _ProfileResultStr = ''
def getProfileResultString():
# if you called profile with 'log' not set to True,
# you can call this function to get the results as
# a string
global _ProfileResultStr
return _ProfileResultStr
def profile(callback, name, terse, log=True):
global _ProfileResultStr
import __builtin__ import __builtin__
if 'globalProfileFunc' in __builtin__.__dict__: if 'globalProfileFunc' in __builtin__.__dict__:
# rats. Python profiler is not re-entrant... # rats. Python profiler is not re-entrant...
@ -811,10 +847,20 @@ def profile(callback, name, terse):
)) ))
return return
__builtin__.globalProfileFunc = callback __builtin__.globalProfileFunc = callback
print '***** START PROFILE: %s *****' % name __builtin__.globalProfileResult = [None]
startProfile(cmd='globalProfileFunc()', callInfo=(not terse)) prefix = '***** START PROFILE: %s *****' % name
print '***** END PROFILE: %s *****' % name if log:
print prefix
startProfile(cmd='globalProfileResult[0]=globalProfileFunc()', callInfo=(not terse), silent=not log)
suffix = '***** END PROFILE: %s *****' % name
if log:
print suffix
else:
_ProfileResultStr = '%s\n%s\n%s' % (prefix, _ProfileResultStr, suffix)
result = globalProfileResult[0]
del __builtin__.__dict__['globalProfileFunc'] del __builtin__.__dict__['globalProfileFunc']
del __builtin__.__dict__['globalProfileResult']
return result
def profiled(category=None, terse=False): def profiled(category=None, terse=False):
""" decorator for profiling functions """ decorator for profiling functions
@ -883,12 +929,14 @@ def startProfile(filename=PyUtilProfileDefaultFilename,
filename = '%s.%s' % (filename, randUint31()) filename = '%s.%s' % (filename, randUint31())
import profile import profile
profile.run(cmd, filename) profile.run(cmd, filename)
if not silent: if silent:
extractProfile(filename, lines, sorts, callInfo)
else:
printProfile(filename, lines, sorts, callInfo) printProfile(filename, lines, sorts, callInfo)
import os import os
os.remove(filename) os.remove(filename)
# call this to see the results again # call these to see the results again, as a string or in the log
def printProfile(filename=PyUtilProfileDefaultFilename, def printProfile(filename=PyUtilProfileDefaultFilename,
lines=PyUtilProfileDefaultLines, lines=PyUtilProfileDefaultLines,
sorts=PyUtilProfileDefaultSorts, sorts=PyUtilProfileDefaultSorts,
@ -903,6 +951,18 @@ def printProfile(filename=PyUtilProfileDefaultFilename,
s.print_callees(lines) s.print_callees(lines)
s.print_callers(lines) s.print_callers(lines)
# same args as printProfile
def extractProfile(*args, **kArgs):
global _ProfileResultStr
# capture print output
sc = StdoutCapture()
# print the profile output, redirected to the result string
printProfile(*args, **kArgs)
# make a copy of the print output
_ProfileResultStr = sc.getString()
# restore stdout to what it was before
sc.destroy()
def getSetterName(valueName, prefix='set'): def getSetterName(valueName, prefix='set'):
# getSetterName('color') -> 'setColor' # getSetterName('color') -> 'setColor'
# getSetterName('color', 'get') -> 'getColor' # getSetterName('color', 'get') -> 'getColor'
@ -927,8 +987,14 @@ class Functor:
self._function = function self._function = function
self._args = args self._args = args
self._kargs = kargs self._kargs = kargs
if hasattr(self._function, '__name__'):
self.__name__ = self._function.__name__ self.__name__ = self._function.__name__
else:
self.__name__ = str(itype(self._function))
if hasattr(self._function, '__doc__'):
self.__doc__ = self._function.__doc__ self.__doc__ = self._function.__doc__
else:
self.__doc__ = self.__name__
def destroy(self): def destroy(self):
del self._function del self._function
@ -1714,6 +1780,19 @@ def average(*args):
val += arg val += arg
return val / len(args) return val / len(args)
class Averager:
def __init__(self, name):
self._name = name
self._total = 0.
self._count = 0
def addValue(self, value):
self._total += value
self._count += 1
def getAverage(self):
return self._total / self._count
def getCount(self):
return self._count
def addListsByValue(a, b): def addListsByValue(a, b):
""" """
returns a new array containing the sums of the two array arguments returns a new array containing the sums of the two array arguments

View File

@ -19,6 +19,7 @@ import time
import fnmatch import fnmatch
import string import string
import signal import signal
import random
try: try:
Dtool_PreloadDLL("libp3heapq") Dtool_PreloadDLL("libp3heapq")
from libp3heapq import heappush, heappop, heapify from libp3heapq import heappush, heappop, heapify
@ -161,6 +162,13 @@ class Task:
self.time = currentTime - self.starttime self.time = currentTime - self.starttime
self.frame = currentFrame - self.startframe self.frame = currentFrame - self.startframe
def getNamePattern(self, taskName=None):
# get a version of the task name that doesn't contain any numbers
digits = '0123456789'
if taskName is None:
taskName = self.name
return ''.join([c for c in taskName if c not in digits])
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,
@ -358,6 +366,8 @@ class TaskManager:
# multiple of average frame duration # multiple of average frame duration
DefTaskDurationWarningThreshold = 40. DefTaskDurationWarningThreshold = 40.
_DidTests = False
def __init__(self): def __init__(self):
self.running = 0 self.running = 0
self.stepping = 0 self.stepping = 0
@ -370,6 +380,12 @@ class TaskManager:
self._profileFrames = False self._profileFrames = False
self.MaxEpockSpeed = 1.0/30.0; self.MaxEpockSpeed = 1.0/30.0;
# this will be set when it's safe to import StateVar
self._profileTasks = None
self._taskProfiler = None
self._profileTaskId = None
self._profileDt = None
self._lastProfileResultString = None
# We copy this value in from __builtins__ when it gets set. # We copy this value in from __builtins__ when it gets set.
# But since the TaskManager might have to run before it gets # But since the TaskManager might have to run before it gets
@ -404,6 +420,8 @@ class TaskManager:
self.add(self.__doLaterProcessor, "doLaterProcessor", -10) self.add(self.__doLaterProcessor, "doLaterProcessor", -10)
def destroy(self): def destroy(self):
if self._taskProfiler:
self._taskProfiler.destroy()
del self.nameDict del self.nameDict
del self.trueClock del self.trueClock
del self.globalClock del self.globalClock
@ -722,15 +740,27 @@ class TaskManager:
def __executeTask(self, task): def __executeTask(self, task):
task.setCurrentTimeFrame(self.currentTime, self.currentFrame) task.setCurrentTimeFrame(self.currentTime, self.currentFrame)
doProfile = (task.id == self._profileTaskId)
if not self.taskTimerVerbose: if not self.taskTimerVerbose:
startTime = self.trueClock.getShortTime() startTime = self.trueClock.getShortTime()
# don't record timing info # don't record timing info
if doProfile:
ret = profile(Functor(task, *task.extraArgs),
'TASK_PROFILE:%s' % task.name, True, log=False)
else:
ret = task(*task.extraArgs) ret = task(*task.extraArgs)
endTime = self.trueClock.getShortTime() endTime = self.trueClock.getShortTime()
# Record the dt # Record the dt
dt = endTime - startTime dt = endTime - startTime
if doProfile:
# if we profiled, record the measured duration but don't pollute the task's
# normal duration
self._profileDt = dt
dt = task.avgDt
task.dt = dt task.dt = dt
else: else:
@ -738,6 +768,10 @@ class TaskManager:
if task.pstats: if task.pstats:
task.pstats.start() task.pstats.start()
startTime = self.trueClock.getShortTime() startTime = self.trueClock.getShortTime()
if doProfile:
ret = profile(Functor(task, *task.extraArgs),
'profiled-task-%s' % task.name, True, log=False)
else:
ret = task(*task.extraArgs) ret = task(*task.extraArgs)
endTime = self.trueClock.getShortTime() endTime = self.trueClock.getShortTime()
if task.pstats: if task.pstats:
@ -745,6 +779,11 @@ class TaskManager:
# Record the dt # Record the dt
dt = endTime - startTime dt = endTime - startTime
if doProfile:
# if we profiled, record the measured duration but don't pollute the task's
# normal duration
self._profileDt = dt
dt = task.avgDt
task.dt = dt task.dt = dt
# See if this is the new max # See if this is the new max
@ -758,6 +797,9 @@ class TaskManager:
else: else:
task.avgDt = 0 task.avgDt = 0
if doProfile:
self._lastProfileResultString = self._getProfileResultString()
# warn if the task took too long # warn if the task took too long
if self.warnTaskDuration and self.globalClock: if self.warnTaskDuration and self.globalClock:
avgFrameRate = self.globalClock.getAverageFrameRate() avgFrameRate = self.globalClock.getAverageFrameRate()
@ -889,6 +931,65 @@ class TaskManager:
result = self.step(*args, **kArgs) result = self.step(*args, **kArgs)
return result 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 _setProfileTask(self, task):
self._profileTaskId = task.id
self._profileDt = None
self._lastProfileResultString = None
def _getTaskProfileDt(self):
return self._profileDt
def _getLastProfileResultString(self):
return self._lastProfileResultString
def _getRandomTask(self):
numTasks = 0
for name in self.nameDict.iterkeys():
numTasks += len(self.nameDict[name])
numDoLaters = len(self.__doLaterList)
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 < self.__doLaterList[curIndex].wakeTime:
rangeEnd = curIndex
else:
rangeStart = curIndex
prevIndex = curIndex
curIndex = int((rangeStart + rangeEnd) / 2)
if curIndex == prevIndex:
break
index = curIndex
task = self.__doLaterList[random.randrange(index+1)]
else:
# grab a task
name = random.choice(self.nameDict.keys())
task = random.choice(self.nameDict[name])
return task
def step(self): def step(self):
# assert TaskManager.notify.debug('step: begin') # assert TaskManager.notify.debug('step: begin')
self.currentTime, self.currentFrame = self.__getTimeFrame() self.currentTime, self.currentFrame = self.__getTimeFrame()
@ -952,6 +1053,16 @@ class TaskManager:
def run(self): def run(self):
# do things that couldn't be done earlier because of import dependencies
if (not TaskManager._DidTests) and __debug__:
TaskManager._DidTests = True
self._runTests()
if not self._profileTasks:
from direct.fsm.StatePush import StateVar
self._profileTasks = StateVar(False)
self.setProfileTasks(getBase().config.GetBool('profile-task-spikes', 0))
# Set the clock to have last frame's time in case we were # Set the clock to have last frame's time in case we were
# Paused at the prompt for a long time # Paused at the prompt for a long time
if self.globalClock: if self.globalClock:
@ -1279,21 +1390,8 @@ class TaskManager:
dtfmt % (totalAvgDt*1000),)) dtfmt % (totalAvgDt*1000),))
return cont return cont
def _runTests(self):
if __debug__:
# These constants are moved to the top level of the module,
# to make it easier for legacy code. In general though, putting
# constants at the top level of a module is deprecated.
exit = Task.exit
done = Task.done
cont = Task.cont
again = Task.again
if __debug__:
# keep everything in a function namespace so it's easier to clean up
def runTests():
tm = TaskManager() tm = TaskManager()
# looks like nothing runs on the first frame...? # looks like nothing runs on the first frame...?
# step to get past the first frame # step to get past the first frame
@ -1779,8 +1877,19 @@ if __debug__:
tm.destroy() tm.destroy()
del tm del tm
runTests()
del runTests # These constants are moved to the top level of the module,
# to make it easier for legacy code. In general though, putting
# constants at the top level of a module is deprecated.
exit = Task.exit
done = Task.done
cont = Task.cont
again = Task.again
if __debug__:
pass # 'if __debug__' is hint for CVS diff output
""" """