# -*- test-case-name: mv3d.test.util.test_profiler -*- # Copyright (C) 2010-2012 Mortal Coil Games # See LICENSE for details. """ Real time profiler for MV3D. This is not threadsafe. @author: mike """ import os from time import time from twisted.python.util import mergeFunctionMetadata from twisted.spread import pb from twisted.internet.defer import Deferred if __debug__: enableProfiling = True else: enableProfiling = False def timed(func): """ Decorator that can be used to record timing of a function call """ if not enableProfiling: return func funcKey = "%s (%s:%d)" % (func.__name__, os.path.basename(func.func_code.co_filename), func.func_code.co_firstlineno) def inner(*args, **kwargs): code = theProfiler[funcKey] code.currentInner = 0.0 timing = theProfiler.timing theProfiler.timing = code stime = theProfiler.timer() try: retVal = func(*args, **kwargs) except: deltaTime = theProfiler.timer() - stime theProfiler.timing = timing if theProfiler.timing is not None: theProfiler.timing.currentInner += deltaTime code.totalTime += deltaTime code.exclusiveTime += deltaTime - code.currentInner code.totalCalls += 1 code.totalErrors += 1 raise def finish(res, stime): deltaTime = theProfiler.timer() - stime if timing is not None: timing.currentInner += deltaTime code.totalTime += deltaTime code.totalCalls += 1 return res if isinstance(retVal, Deferred): etime = theProfiler.timer() deltaTime = etime - stime code.totalTime += deltaTime code.exclusiveTime += deltaTime - code.currentInner retVal.addBoth(finish, etime) else: deltaTime = theProfiler.timer() - stime code.exclusiveTime += deltaTime - code.currentInner if theProfiler.timing is not None: theProfiler.timing.currentInner += deltaTime finish(None, stime) theProfiler.timing = timing return retVal mergeFunctionMetadata(func, inner) return inner class TimedCode(pb.Copyable, pb.RemoteCopy): """ Holds data for a section of code that was timed. """ totalCalls = 0 totalErrors = 0 totalTime = 0.0 exclusiveTime = 0.0 currentInner = 0.0 deferredInner = 0.0 def __init__(self, name, timer): self.name = name self.timer = timer self.calls = [] def __str__(self): """ Turns into a string for easy printing. """ return "%40s\t%d\t%d\t%.3f\t%.3f\t%.3f" % (self.name, self.totalCalls, self.totalErrors, self.totalTime, self.exclusiveTime, self.averageTime) def _getAverageTime(self): """ Calculate the average time """ avgTime = 0 if self.totalCalls > 0: avgTime = self.totalTime / self.totalCalls return avgTime averageTime = property(_getAverageTime) def getStateToCopy(self): """ Gather all the attributes to copy. """ return dict(totalCalls=self.totalCalls, totalErrors=self.totalErrors, totalTime=self.totalTime, exclusiveTime=self.exclusiveTime, name=self.name, averageTime=self.averageTime) class RuntimeProfiler(object): """ Holds all the data for a runtime profiling session. """ timer = time timing = None def __init__(self, timer=None): if timer is not None: self.timer = timer self.entries = {} def __getitem__(self, name): """ Returns a TimedCode object for this name """ try: return self.entries[name] except KeyError: entry = TimedCode(name, self.timer) self.entries[name] = entry return entry def reset(self): """ Reset the profiling information """ self.entries = {} def top(self, count=20, key="totalTime", reverse=False): """ Returns a sorted list of the top code sections based on the sort key. Up to count are returned. """ return sorted(self.entries.values(), key=lambda obj: getattr(obj, key), reverse=reverse)[:count] theProfiler = RuntimeProfiler() pb.setUnjellyableForClass("mv3d.util.profiler.TimedCode", TimedCode)