"""Python version of the _lsprof module.""" import sys import time import types import collections def hpTimer(): return int(time.time() * 1000000) hpTimerUnit = 0.000001 DOUBLE_TIMER_PRECISION = 4294967296.0 POF_ENABLED = 1 POF_SUBCALLS = 2 POF_BUILTINS = 3 def PyCFunction_Check(obj): return type(obj) is types.BuiltinFunctionType def normalize_function(obj): if not PyCFunction_Check(obj): return obj if obj.__self__ is None: mod = obj.__module__ if mod and mod != "__builtin__": return "<%s.%s>" % (mod, obj.__name__) else: return "<%s>" % (obj.__name__,) else: try: return repr(getattr(type(obj.__self__), obj.__name__)) except: return "" % (obj.__name__,) class ProfilerSubEntry(object): __slots__ = ('tt', 'it', 'callcount', 'recursivecallcount', 'recursionLevel') def __init__(self, entry, caller): self.tt = 0 self.it = 0 self.callcount = 0 self.recursivecallcount = 0 self.recursionLevel = 0 caller.calls[entry] = self class ProfilerEntry(object): __slots__ = ('userObj', 'tt', 'it', 'callcount', 'recursivecallcount', 'recursionLevel', 'calls') def __init__(self, profiler, key): self.userObj = key self.tt = 0 self.it = 0 self.callcount = 0 self.recursivecallcount = 0 self.recursionLevel = 0 self.calls = {} profiler.profilerEntries[key] = self class ProfilerContext(object): __slots__ = ('t0', 'subt', 'previous', 'ctxEntry') def __init__(self): self.t0 = 0 self.subt = 0 self.ctxEntry = None self.previous = None def start(self, profiler, entry): self.ctxEntry = entry self.subt = 0 self.previous = profiler.currentProfilerContext profiler.currentProfilerContext = self entry.recursionLevel += 1 if POF_SUBCALLS in profiler.flags and self.previous: caller = self.previous.ctxEntry subentry = caller.calls.get(entry) if subentry is None: subentry = ProfilerSubEntry(entry, caller) else: subentry.recursionLevel += 1 self.t0 = profiler.call_timer() def stop(self, profiler, entry): tt = profiler.call_timer() - self.t0 it = tt - self.subt if self.previous: self.previous.subt += tt profiler.currentProfilerContext = self.previous entry.recursionLevel -= 1 if entry.recursionLevel == 0: entry.tt += tt else: entry.recursivecallcount += 1 entry.it += it entry.callcount += 1 if POF_SUBCALLS in profiler.flags and self.previous: caller = self.previous.ctxEntry subentry = caller.calls.get(entry) if subentry: subentry.recursionLevel -= 1 if subentry.recursionLevel == 0: subentry.tt += tt else: subentry.recursivecallcount += 1 subentry.it += it subentry.callcount += 1 profiler_entry = collections.namedtuple( "profiler_entry", ("code, callcount, reccallcount, totaltime, inlinetime, calls") ) profiler_subentry = collections.namedtuple( "profiler_subentry", ("code, callcount, reccallcount, totaltime, inlinetime") ) class StatsCollector(object): def __init__(self, factor): self.list = [] self.factor = factor def stats_for_subentry(self, node, sublist): entry, subentry = node sublist.append(profiler_subentry( entry.userObj, subentry.callcount, subentry.recursivecallcount, self.factor * subentry.tt, self.factor * subentry.it)) def stats_for_entry(self, entry): # XXX When callcount is zero? if entry.callcount == 0: return if entry.calls: sublist = [] for node in entry.calls.items(): self.stats_for_subentry(node, sublist) else: sublist = None self.list.append(profiler_entry( entry.userObj, entry.callcount, entry.recursivecallcount, self.factor * entry.tt, self.factor * entry.it, sublist)) class Profiler(object): def __init__(self, timer=None, time_unit=None, subcalls=True, builtins=True): self.flags = set() self.profilerEntries = {} self.currentProfilerContext = None self.freelistProfilerContext = None self.externalTimer = timer self.externalTimerUnit = time_unit self._set_config(subcalls, builtins) def __del__(self): self.disable() self.clear() def enable(self, subcalls=None, builtins=None): self._set_config(subcalls, builtins) # XXX cProfile adds the flag after setting up the profiler. # This is deliberately different to avoid profiling the profiler. self.flags.add(POF_ENABLED) sys.setprofile(self._callback) def disable(self): if POF_ENABLED in self.flags: # XXX cProfile discards the flag before disabling the profiler. sys.setprofile(None) self.flags.discard(POF_ENABLED) self._flush_unmatched() def clear(self): self.profilerEntries.clear() def getstats(self): if self.externalTimer is None: collect = StatsCollector(hpTimerUnit) elif self.externalTimerUnit > 0.0: collect = StatsCollector(self.externalTimerUnit) else: collect = StatsCollector(1.0 / DOUBLE_TIMER_PRECISION) for entry in self.profilerEntries.values(): collect.stats_for_entry(entry) return collect.list def call_timer(self): if self.externalTimer: result = self.externalTimer() if self.externalTimerUnit > 0.0: # Interpret the result as an integer that will be scaled # in self.getstats(). return int(result) else: # Interpret the result as a float measured in seconds. As # the profiler works with integers internally we convert # it to a large integer. return int(result * DOUBLE_TIMER_PRECISION) else: return hpTimer() def _set_config(self, subcalls, builtins): if subcalls is not None: if subcalls: self.flags.add(POF_SUBCALLS) else: self.flags.discard(POF_SUBCALLS) if builtins is not None: if builtins: self.flags.add(POF_BUILTINS) else: self.flags.discard(POF_BUILTINS) def _callback(self, frame, what, arg): if what == "call": self._ptrace_enter_call(frame.f_code) elif what == "return": self._ptrace_leave_call(frame.f_code) elif what == "c_call": if POF_BUILTINS in self.flags: self._ptrace_enter_call(arg) elif what == "c_return" or what == "c_exception": if POF_BUILTINS in self.flags: self._ptrace_leave_call(arg) def _flush_unmatched(self): while self.currentProfilerContext: pContext = self.currentProfilerContext profEntry = pContext.ctxEntry if profEntry: pContext.stop(self, profEntry) else: self.currentProfilerContext = pContext.previous def _ptrace_enter_call(self, key): # XXX cProfile restores exceptions raised here. This seems unneeded for # the Python version. key = normalize_function(key) profEntry = self.profilerEntries.get(key) if profEntry is None: profEntry = ProfilerEntry(self, key) pContext = self.freelistProfilerContext if pContext: self.freelistProfilerContext = pContext.previous else: pContext = ProfilerContext() pContext.start(self, profEntry) def _ptrace_leave_call(self, key): pContext = self.currentProfilerContext if pContext is None: return profEntry = self.profilerEntries.get(normalize_function(key)) if profEntry: pContext.stop(self, profEntry) else: self.currentProfilerContext = pContext.previous pContext.previous = self.freelistProfilerContext self.freelistProfilerContext = pContext