src/ztfy/utils/profilehooks.py
branchZTK-1.1
changeset 148 d3668ecd9137
parent 70 82d8de021806
equal deleted inserted replaced
147:044dc196ec8a 148:d3668ecd9137
       
     1 """
       
     2 Profiling hooks
       
     3 
       
     4 This module contains a couple of decorators (`profile` and `coverage`) that
       
     5 can be used to wrap functions and/or methods to produce profiles and line
       
     6 coverage reports.
       
     7 
       
     8 Usage example (Python 2.4 or newer):
       
     9 
       
    10     from profilehooks import profile, coverage
       
    11 
       
    12     @profile    # or @coverage
       
    13     def fn(n):
       
    14         if n < 2: return 1
       
    15         else: return n * fn(n-1)
       
    16 
       
    17     print fn(42)
       
    18 
       
    19 Usage example (Python 2.3 or older):
       
    20 
       
    21     from profilehooks import profile, coverage
       
    22 
       
    23     def fn(n):
       
    24         if n < 2: return 1
       
    25         else: return n * fn(n-1)
       
    26 
       
    27     # Now wrap that function in a decorator
       
    28     fn = profile(fn) # or coverage(fn)
       
    29 
       
    30     print fn(42)
       
    31 
       
    32 Reports for all thusly decorated functions will be printed to sys.stdout
       
    33 on program termination.  You can alternatively request for immediate
       
    34 reports for each call by passing immediate=True to the profile decorator.
       
    35 
       
    36 There's also a @timecall decorator for printing the time to sys.stderr
       
    37 every time a function is called, when you just want to get a rough measure
       
    38 instead of a detailed (but costly) profile.
       
    39 
       
    40 Caveats
       
    41 
       
    42   A thread on python-dev convinced me that hotshot produces bogus numbers.
       
    43   See http://mail.python.org/pipermail/python-dev/2005-November/058264.html
       
    44 
       
    45   I don't know what will happen if a decorated function will try to call
       
    46   another decorated function.  All decorators probably need to explicitly
       
    47   support nested profiling (currently TraceFuncCoverage is the only one that
       
    48   supports this, while HotShotFuncProfile has support for recursive functions.)
       
    49 
       
    50   Profiling with hotshot creates temporary files (*.prof and *.prof.pickle for
       
    51   profiling, *.cprof for coverage) in the current directory.  These files are
       
    52   not cleaned up.  (In fact, the *.pickle versions are intentionally written
       
    53   out in case you want to look at the profiler results without having to parse
       
    54   the big *.prof file with hotshot.stats.load, which takes ages.  Just unpickle
       
    55   the file and you'll get a pstats object.)
       
    56 
       
    57   Coverage analysis with hotshot seems to miss some executions resulting in
       
    58   lower line counts and some lines errorneously marked as never executed.  For
       
    59   this reason coverage analysis now uses trace.py which is slower, but more
       
    60   accurate.
       
    61 
       
    62   Decorating functions causes doctest.testmod() to ignore doctests in those
       
    63   functions.
       
    64 
       
    65 Copyright (c) 2004--2006 Marius Gedminas <marius@pov.lt>
       
    66 
       
    67 Released under the MIT licence since December 2006:
       
    68 
       
    69     Permission is hereby granted, free of charge, to any person obtaining a
       
    70     copy of this software and associated documentation files (the "Software"),
       
    71     to deal in the Software without restriction, including without limitation
       
    72     the rights to use, copy, modify, merge, publish, distribute, sublicense,
       
    73     and/or sell copies of the Software, and to permit persons to whom the
       
    74     Software is furnished to do so, subject to the following conditions:
       
    75 
       
    76     The above copyright notice and this permission notice shall be included in
       
    77     all copies or substantial portions of the Software.
       
    78 
       
    79     THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
       
    80     IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
       
    81     FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
       
    82     AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
       
    83     LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
       
    84     FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
       
    85     DEALINGS IN THE SOFTWARE.
       
    86 
       
    87 (Previously it was distributed under the GNU General Public Licence.)
       
    88 """
       
    89 # $Id: profilehooks.py,v 1.2 2008/07/18 14:51:08 tflorac Exp $
       
    90 
       
    91 __author__ = "Marius Gedminas (marius@gedmin.as)"
       
    92 __copyright__ = "Copyright 2004-2006, Marius Gedminas"
       
    93 __license__ = "MIT"
       
    94 __version__ = "1.0"
       
    95 __date__ = "2006-12-06"
       
    96 
       
    97 
       
    98 import atexit
       
    99 import inspect
       
   100 import sys
       
   101 import re
       
   102 import cPickle as pickle
       
   103 
       
   104 # For profiling
       
   105 from profile import Profile
       
   106 import pstats
       
   107 
       
   108 # For hotshot profiling (inaccurate!)
       
   109 import hotshot
       
   110 import hotshot.stats
       
   111 
       
   112 # For trace.py coverage
       
   113 import trace
       
   114 
       
   115 # For hotshot coverage (inaccurate!; uses undocumented APIs; might break)
       
   116 import _hotshot
       
   117 import hotshot.log
       
   118 
       
   119 # For timecall
       
   120 import time
       
   121 
       
   122 
       
   123 def profile(fn=None, skip=0, filename=None, immediate=False):
       
   124     """Mark `fn` for profiling.
       
   125 
       
   126     If `immediate` is False, profiling results will be printed to sys.stdout on
       
   127     program termination.  Otherwise results will be printed after each call.
       
   128 
       
   129     If `skip` is > 0, first `skip` calls to `fn` will not be profiled.
       
   130 
       
   131     If `filename` is specified, the profile stats will be pickled and stored in
       
   132     a file.
       
   133 
       
   134     Usage:
       
   135 
       
   136         def fn(...):
       
   137             ...
       
   138         fn = profile(fn, skip=1)
       
   139 
       
   140     If you are using Python 2.4, you should be able to use the decorator
       
   141     syntax:
       
   142 
       
   143         @profile(skip=3)
       
   144         def fn(...):
       
   145             ...
       
   146 
       
   147     or just
       
   148 
       
   149         @profile
       
   150         def fn(...):
       
   151             ...
       
   152 
       
   153     """
       
   154     if fn is None: # @profile() syntax -- we are a decorator maker
       
   155         def decorator(fn):
       
   156             return profile(fn, skip=skip, filename=filename,
       
   157                            immediate=immediate)
       
   158         return decorator
       
   159     # @profile syntax -- we are a decorator.
       
   160     fp = FuncProfile(fn, skip=skip, filename=filename, immediate=immediate)
       
   161     # or HotShotFuncProfile
       
   162     # We cannot return fp or fp.__call__ directly as that would break method
       
   163     # definitions, instead we need to return a plain function.
       
   164     def new_fn(*args, **kw):
       
   165         return fp(*args, **kw)
       
   166     new_fn.__doc__ = fn.__doc__
       
   167     return new_fn
       
   168 
       
   169 
       
   170 def coverage(fn):
       
   171     """Mark `fn` for line coverage analysis.
       
   172 
       
   173     Results will be printed to sys.stdout on program termination.
       
   174 
       
   175     Usage:
       
   176 
       
   177         def fn(...):
       
   178             ...
       
   179         fn = coverage(fn)
       
   180 
       
   181     If you are using Python 2.4, you should be able to use the decorator
       
   182     syntax:
       
   183 
       
   184         @coverage
       
   185         def fn(...):
       
   186             ...
       
   187 
       
   188     """
       
   189     fp = TraceFuncCoverage(fn) # or HotShotFuncCoverage
       
   190     # We cannot return fp or fp.__call__ directly as that would break method
       
   191     # definitions, instead we need to return a plain function.
       
   192     def new_fn(*args, **kw):
       
   193         return fp(*args, **kw)
       
   194     new_fn.__doc__ = fn.__doc__
       
   195     return new_fn
       
   196 
       
   197 
       
   198 def coverage_with_hotshot(fn):
       
   199     """Mark `fn` for line coverage analysis.
       
   200 
       
   201     Uses the 'hotshot' module for fast coverage analysis.
       
   202 
       
   203     BUG: Produces inaccurate results.
       
   204 
       
   205     See the docstring of `coverage` for usage examples.
       
   206     """
       
   207     fp = HotShotFuncCoverage(fn)
       
   208     # We cannot return fp or fp.__call__ directly as that would break method
       
   209     # definitions, instead we need to return a plain function.
       
   210     def new_fn(*args, **kw):
       
   211         return fp(*args, **kw)
       
   212     new_fn.__doc__ = fn.__doc__
       
   213     return new_fn
       
   214 
       
   215 
       
   216 class FuncProfile:
       
   217     """Profiler for a function (uses profile)."""
       
   218 
       
   219     # This flag is shared between all instances
       
   220     in_profiler = False
       
   221 
       
   222     def __init__(self, fn, skip=0, filename=None, immediate=False):
       
   223         """Creates a profiler for a function.
       
   224 
       
   225         Every profiler has its own log file (the name of which is derived from
       
   226         the function name).
       
   227 
       
   228         HotShotFuncProfile registers an atexit handler that prints profiling
       
   229         information to sys.stderr when the program terminates.
       
   230 
       
   231         The log file is not removed and remains there to clutter the current
       
   232         working directory.
       
   233         """
       
   234         self.fn = fn
       
   235         self.filename = filename
       
   236         self.immediate = immediate
       
   237         self.stats = pstats.Stats(Profile())
       
   238         self.ncalls = 0
       
   239         self.skip = skip
       
   240         self.skipped = 0
       
   241         atexit.register(self.atexit)
       
   242 
       
   243     def __call__(self, *args, **kw):
       
   244         """Profile a singe call to the function."""
       
   245         self.ncalls += 1
       
   246         if self.skip > 0:
       
   247             self.skip -= 1
       
   248             self.skipped += 1
       
   249             return self.fn(*args, **kw)
       
   250         if FuncProfile.in_profiler:
       
   251             # handle recursive calls
       
   252             return self.fn(*args, **kw)
       
   253         # You cannot reuse the same profiler for many calls and accumulate
       
   254         # stats that way.  :-/
       
   255         profiler = Profile()
       
   256         try:
       
   257             FuncProfile.in_profiler = True
       
   258             return profiler.runcall(self.fn, *args, **kw)
       
   259         finally:
       
   260             FuncProfile.in_profiler = False
       
   261             self.stats.add(profiler)
       
   262             if self.immediate:
       
   263                 self.print_stats()
       
   264                 self.reset_stats()
       
   265 
       
   266     def print_stats(self):
       
   267         """Print profile information to sys.stdout."""
       
   268         funcname = self.fn.__name__
       
   269         filename = self.fn.func_code.co_filename
       
   270         lineno = self.fn.func_code.co_firstlineno
       
   271         print
       
   272         print "*** PROFILER RESULTS ***"
       
   273         print "%s (%s:%s)" % (funcname, filename, lineno)
       
   274         print "function called %d times" % self.ncalls,
       
   275         if self.skipped:
       
   276             print "(%d calls not profiled)" % self.skipped
       
   277         else:
       
   278             print
       
   279         print
       
   280         stats = self.stats
       
   281         if self.filename:
       
   282             pickle.dump(stats, file(self.filename, 'w'))
       
   283         stats.strip_dirs()
       
   284         stats.sort_stats('cumulative', 'time', 'calls')
       
   285         stats.print_stats(40)
       
   286 
       
   287     def reset_stats(self):
       
   288         """Reset accumulated profiler statistics."""
       
   289         self.stats = pstats.Stats(Profile())
       
   290         self.ncalls = 0
       
   291         self.skipped = 0
       
   292 
       
   293     def atexit(self):
       
   294         """Stop profiling and print profile information to sys.stdout.
       
   295 
       
   296         This function is registered as an atexit hook.
       
   297         """
       
   298         if not self.immediate:
       
   299             self.print_stats()
       
   300 
       
   301 
       
   302 class HotShotFuncProfile:
       
   303     """Profiler for a function (uses hotshot)."""
       
   304 
       
   305     # This flag is shared between all instances
       
   306     in_profiler = False
       
   307 
       
   308     def __init__(self, fn, skip=0, filename=None):
       
   309         """Creates a profiler for a function.
       
   310 
       
   311         Every profiler has its own log file (the name of which is derived from
       
   312         the function name).
       
   313 
       
   314         HotShotFuncProfile registers an atexit handler that prints profiling
       
   315         information to sys.stderr when the program terminates.
       
   316 
       
   317         The log file is not removed and remains there to clutter the current
       
   318         working directory.
       
   319         """
       
   320         self.fn = fn
       
   321         self.filename = filename
       
   322         if self.filename:
       
   323             self.logfilename = filename + ".raw"
       
   324         else:
       
   325             self.logfilename = fn.__name__ + ".prof"
       
   326         self.profiler = hotshot.Profile(self.logfilename)
       
   327         self.ncalls = 0
       
   328         self.skip = skip
       
   329         self.skipped = 0
       
   330         atexit.register(self.atexit)
       
   331 
       
   332     def __call__(self, *args, **kw):
       
   333         """Profile a singe call to the function."""
       
   334         self.ncalls += 1
       
   335         if self.skip > 0:
       
   336             self.skip -= 1
       
   337             self.skipped += 1
       
   338             return self.fn(*args, **kw)
       
   339         if HotShotFuncProfile.in_profiler:
       
   340             # handle recursive calls
       
   341             return self.fn(*args, **kw)
       
   342         try:
       
   343             HotShotFuncProfile.in_profiler = True
       
   344             return self.profiler.runcall(self.fn, *args, **kw)
       
   345         finally:
       
   346             HotShotFuncProfile.in_profiler = False
       
   347 
       
   348     def atexit(self):
       
   349         """Stop profiling and print profile information to sys.stderr.
       
   350 
       
   351         This function is registered as an atexit hook.
       
   352         """
       
   353         self.profiler.close()
       
   354         funcname = self.fn.__name__
       
   355         filename = self.fn.func_code.co_filename
       
   356         lineno = self.fn.func_code.co_firstlineno
       
   357         print
       
   358         print "*** PROFILER RESULTS ***"
       
   359         print "%s (%s:%s)" % (funcname, filename, lineno)
       
   360         print "function called %d times" % self.ncalls,
       
   361         if self.skipped:
       
   362             print "(%d calls not profiled)" % self.skipped
       
   363         else:
       
   364             print
       
   365         print
       
   366         stats = hotshot.stats.load(self.logfilename)
       
   367         # hotshot.stats.load takes ages, and the .prof file eats megabytes, but
       
   368         # a pickled stats object is small and fast
       
   369         if self.filename:
       
   370             pickle.dump(stats, file(self.filename, 'w'))
       
   371         # it is best to pickle before strip_dirs
       
   372         stats.strip_dirs()
       
   373         stats.sort_stats('cumulative', 'time', 'calls')
       
   374         stats.print_stats(200)
       
   375 
       
   376 
       
   377 class HotShotFuncCoverage:
       
   378     """Coverage analysis for a function (uses _hotshot).
       
   379 
       
   380     HotShot coverage is reportedly faster than trace.py, but it appears to
       
   381     have problems with exceptions; also line counts in coverage reports
       
   382     are generally lower from line counts produced by TraceFuncCoverage.
       
   383     Is this my bug, or is it a problem with _hotshot?
       
   384     """
       
   385 
       
   386     def __init__(self, fn):
       
   387         """Creates a profiler for a function.
       
   388 
       
   389         Every profiler has its own log file (the name of which is derived from
       
   390         the function name).
       
   391 
       
   392         HotShotFuncCoverage registers an atexit handler that prints profiling
       
   393         information to sys.stderr when the program terminates.
       
   394 
       
   395         The log file is not removed and remains there to clutter the current
       
   396         working directory.
       
   397         """
       
   398         self.fn = fn
       
   399         self.logfilename = fn.__name__ + ".cprof"
       
   400         self.profiler = _hotshot.coverage(self.logfilename)
       
   401         self.ncalls = 0
       
   402         atexit.register(self.atexit)
       
   403 
       
   404     def __call__(self, *args, **kw):
       
   405         """Profile a singe call to the function."""
       
   406         self.ncalls += 1
       
   407         return self.profiler.runcall(self.fn, args, kw)
       
   408 
       
   409     def atexit(self):
       
   410         """Stop profiling and print profile information to sys.stderr.
       
   411 
       
   412         This function is registered as an atexit hook.
       
   413         """
       
   414         self.profiler.close()
       
   415         funcname = self.fn.__name__
       
   416         filename = self.fn.func_code.co_filename
       
   417         lineno = self.fn.func_code.co_firstlineno
       
   418         print
       
   419         print "*** COVERAGE RESULTS ***"
       
   420         print "%s (%s:%s)" % (funcname, filename, lineno)
       
   421         print "function called %d times" % self.ncalls
       
   422         print
       
   423         fs = FuncSource(self.fn)
       
   424         reader = hotshot.log.LogReader(self.logfilename)
       
   425         for what, (filename, lineno, funcname), tdelta in reader:
       
   426             if filename != fs.filename:
       
   427                 continue
       
   428             if what == hotshot.log.LINE:
       
   429                 fs.mark(lineno)
       
   430             if what == hotshot.log.ENTER:
       
   431                 # hotshot gives us the line number of the function definition
       
   432                 # and never gives us a LINE event for the first statement in
       
   433                 # a function, so if we didn't perform this mapping, the first
       
   434                 # statement would be marked as never executed
       
   435                 if lineno == fs.firstlineno:
       
   436                     lineno = fs.firstcodelineno
       
   437                 fs.mark(lineno)
       
   438         reader.close()
       
   439         print fs
       
   440 
       
   441 
       
   442 class TraceFuncCoverage:
       
   443     """Coverage analysis for a function (uses trace module).
       
   444 
       
   445     HotShot coverage analysis is reportedly faster, but it appears to have
       
   446     problems with exceptions.
       
   447     """
       
   448 
       
   449     # Shared between all instances so that nested calls work
       
   450     tracer = trace.Trace(count=True, trace=False,
       
   451                          ignoredirs=[sys.prefix, sys.exec_prefix])
       
   452 
       
   453     # This flag is also shared between all instances
       
   454     tracing = False
       
   455 
       
   456     def __init__(self, fn):
       
   457         """Creates a profiler for a function.
       
   458 
       
   459         Every profiler has its own log file (the name of which is derived from
       
   460         the function name).
       
   461 
       
   462         TraceFuncCoverage registers an atexit handler that prints profiling
       
   463         information to sys.stderr when the program terminates.
       
   464 
       
   465         The log file is not removed and remains there to clutter the current
       
   466         working directory.
       
   467         """
       
   468         self.fn = fn
       
   469         self.logfilename = fn.__name__ + ".cprof"
       
   470         self.ncalls = 0
       
   471         atexit.register(self.atexit)
       
   472 
       
   473     def __call__(self, *args, **kw):
       
   474         """Profile a singe call to the function."""
       
   475         self.ncalls += 1
       
   476         if TraceFuncCoverage.tracing:
       
   477             return self.fn(*args, **kw)
       
   478         try:
       
   479             TraceFuncCoverage.tracing = True
       
   480             return self.tracer.runfunc(self.fn, *args, **kw)
       
   481         finally:
       
   482             TraceFuncCoverage.tracing = False
       
   483 
       
   484     def atexit(self):
       
   485         """Stop profiling and print profile information to sys.stderr.
       
   486 
       
   487         This function is registered as an atexit hook.
       
   488         """
       
   489         funcname = self.fn.__name__
       
   490         filename = self.fn.func_code.co_filename
       
   491         lineno = self.fn.func_code.co_firstlineno
       
   492         print
       
   493         print "*** COVERAGE RESULTS ***"
       
   494         print "%s (%s:%s)" % (funcname, filename, lineno)
       
   495         print "function called %d times" % self.ncalls
       
   496         print
       
   497         fs = FuncSource(self.fn)
       
   498         for (filename, lineno), count in self.tracer.counts.items():
       
   499             if filename != fs.filename:
       
   500                 continue
       
   501             fs.mark(lineno, count)
       
   502         print fs
       
   503         never_executed = fs.count_never_executed()
       
   504         if never_executed:
       
   505             print "%d lines were not executed." % never_executed
       
   506 
       
   507 
       
   508 class FuncSource:
       
   509     """Source code annotator for a function."""
       
   510 
       
   511     blank_rx = re.compile(r"^\s*finally:\s*(#.*)?$")
       
   512 
       
   513     def __init__(self, fn):
       
   514         self.fn = fn
       
   515         self.filename = inspect.getsourcefile(fn)
       
   516         self.source, self.firstlineno = inspect.getsourcelines(fn)
       
   517         self.sourcelines = {}
       
   518         self.firstcodelineno = self.firstlineno
       
   519         self.find_source_lines()
       
   520 
       
   521     def find_source_lines(self):
       
   522         """Mark all executable source lines in fn as executed 0 times."""
       
   523         strs = trace.find_strings(self.filename)
       
   524         lines = trace.find_lines_from_code(self.fn.func_code, strs)
       
   525         self.firstcodelineno = sys.maxint
       
   526         for lineno in lines:
       
   527             self.firstcodelineno = min(self.firstcodelineno, lineno)
       
   528             self.sourcelines.setdefault(lineno, 0)
       
   529         if self.firstcodelineno == sys.maxint:
       
   530             self.firstcodelineno = self.firstlineno
       
   531 
       
   532     def mark(self, lineno, count=1):
       
   533         """Mark a given source line as executed count times.
       
   534 
       
   535         Multiple calls to mark for the same lineno add up.
       
   536         """
       
   537         self.sourcelines[lineno] = self.sourcelines.get(lineno, 0) + count
       
   538 
       
   539     def count_never_executed(self):
       
   540         """Count statements that were never executed."""
       
   541         lineno = self.firstlineno
       
   542         counter = 0
       
   543         for line in self.source:
       
   544             if self.sourcelines.get(lineno) == 0:
       
   545                 if not self.blank_rx.match(line):
       
   546                     counter += 1
       
   547             lineno += 1
       
   548         return counter
       
   549 
       
   550     def __str__(self):
       
   551         """Return annotated source code for the function."""
       
   552         lines = []
       
   553         lineno = self.firstlineno
       
   554         for line in self.source:
       
   555             counter = self.sourcelines.get(lineno)
       
   556             if counter is None:
       
   557                 prefix = ' ' * 7
       
   558             elif counter == 0:
       
   559                 if self.blank_rx.match(line):
       
   560                     prefix = ' ' * 7
       
   561                 else:
       
   562                     prefix = '>' * 6 + ' '
       
   563             else:
       
   564                 prefix = '%5d: ' % counter
       
   565             lines.append(prefix + line)
       
   566             lineno += 1
       
   567         return ''.join(lines)
       
   568 
       
   569 
       
   570 def timecall(fn):
       
   571     """Wrap `fn` and print its execution time.
       
   572 
       
   573     Example:
       
   574 
       
   575         @timecall
       
   576         def somefunc(x, y):
       
   577             time.sleep(x * y)
       
   578 
       
   579         somefunc(2, 3)
       
   580 
       
   581     will print
       
   582 
       
   583         somefunc: 6.0 seconds
       
   584 
       
   585     """
       
   586     def new_fn(*args, **kw):
       
   587         try:
       
   588             start = time.time()
       
   589             return fn(*args, **kw)
       
   590         finally:
       
   591             duration = time.time() - start
       
   592             funcname = fn.__name__
       
   593             filename = fn.func_code.co_filename
       
   594             lineno = fn.func_code.co_firstlineno
       
   595             print >> sys.stderr, "\n  %s (%s:%s):\n    %.3f seconds\n" % (
       
   596                                         funcname, filename, lineno, duration)
       
   597     new_fn.__doc__ = fn.__doc__
       
   598     return new_fn
       
   599