Commits

Robert Kern committed 2ea515d

ENH: Fold the profile-viewing capabilities into the line_profiler module (which is now executable). Add the ability for kernprof to display the profile results. Allow kernprof to work even without cProfile.

  • Participants
  • Parent commits 1f22ff9

Comments (0)

Files changed (3)

 circumstances.
 """
 
-import cProfile
 import optparse
 import os
 import sys
 
+
+# Guard the import of cProfile such that 2.4 people without lsprof can still use
+# this script with line_profiler.
+try:
+    import cProfile
+    has_cprofile = True
+except ImportError:
+    try:
+        import lsprof as cProfile
+        has_cprofile = True
+    except ImportError:
+        has_cprofile = False
+
+if has_cprofile:
+    class ContextualProfile(cProfile.Profile):
+        """ A subclass of cProfile.Profile that adds a context manager for Python
+        2.5 with: statements and a decorator.
+        """
+
+        def __init__(self, *args, **kwds):
+            super(ContextualProfile, self).__init__(*args, **kwds)
+            self.enable_count = 0
+
+        def enable_by_count(self, subcalls=True, builtins=True):
+            """ Enable the profiler if it hasn't been enabled before.
+            """
+            if self.enable_count == 0:
+                self.enable(subcalls=subcalls, builtins=builtins)
+            self.enable_count += 1
+
+        def disable_by_count(self):
+            """ Disable the profiler if the number of disable requests matches the
+            number of enable requests.
+            """
+            if self.enable_count > 0:
+                self.enable_count -= 1
+                if self.enable_count == 0:
+                    self.disable()
+
+        def __call__(self, func):
+            """ Decorate a function to start the profiler on function entry and stop
+            it on function exit.
+            """
+            def f(*args, **kwds):
+                self.enable_by_count()
+                try:
+                    result = func(*args, **kwds)
+                finally:
+                    self.disable_by_count()
+                return result
+            f.__name__ = func.__name__
+            f.__doc__ = func.__doc__
+            f.__dict__.update(func.__dict__)
+            return f
+
+        def __enter__(self):
+            self.enable_by_count()
+
+        def __exit__(self, exc_type, exc_val, exc_tb):
+            self.disable_by_count()
+
+
 def find_script(script_name):
     """ Find the script.
 
     print >>sys.stderr, 'Could not find script %s' % script_name
     raise SystemExit(1)
 
-class ContextualProfile(cProfile.Profile):
-    """ A subclass of cProfile.Profile that adds a context manager for Python
-    2.5 with: statements and a decorator.
-    """
-
-    def __init__(self, *args, **kwds):
-        super(ContextualProfile, self).__init__(*args, **kwds)
-        self.enable_count = 0
-
-    def enable_by_count(self, subcalls=True, builtins=True):
-        """ Enable the profiler if it hasn't been enabled before.
-        """
-        if self.enable_count == 0:
-            self.enable(subcalls=subcalls, builtins=builtins)
-        self.enable_count += 1
-
-    def disable_by_count(self):
-        """ Disable the profiler if the number of disable requests matches the
-        number of enable requests.
-        """
-        if self.enable_count > 0:
-            self.enable_count -= 1
-            if self.enable_count == 0:
-                self.disable()
-
-    def __call__(self, func):
-        """ Decorate a function to start the profiler on function entry and stop
-        it on function exit.
-        """
-        def f(*args, **kwds):
-            self.enable_by_count()
-            try:
-                result = func(*args, **kwds)
-            finally:
-                self.disable_by_count()
-            return result
-        f.__name__ = func.__name__
-        f.__doc__ = func.__doc__
-        f.__dict__.update(func.__dict__)
-        return f
-
-    def __enter__(self):
-        self.enable_by_count()
-
-    def __exit__(self, exc_type, exc_val, exc_tb):
-        self.disable_by_count()
-
-
 def main(args):
     usage = "%s [-s setupfile] [-o output_file_path] scriptfile [arg] ..."
     parser = optparse.OptionParser(usage=usage % sys.argv[0])
     parser.add_option('-o', '--outfile', default=None,
         help="Save stats to <outfile>")
     parser.add_option('-s', '--setup', default=None,
-       help="Code to execute before the code to profile")
+        help="Code to execute before the code to profile")
+    parser.add_option('-v', '--view', action='store_true',
+        help="View the results of the profile in addition to saving it.")
 
     if not sys.argv[1:]:
         parser.print_usage()
         import line_profiler
         prof = line_profiler.LineProfiler()
         options.builtin = True
+    elif has_cprofile:
+        prof = ContextualProfile()
     else:
-        prof = ContextualProfile()
+        raise SystemExit("You requested profiling with cProfile, but neither"
+            " cProfile nor lsprof could be imported.")
     if options.builtin:
         import __builtin__
         __builtin__.__dict__['profile'] = prof
     finally:
         prof.dump_stats(options.outfile)
         print 'Wrote profile results to %s' % options.outfile
+        if options.view:
+            prof.print_stats()
 
 if __name__ == '__main__':
     sys.exit(main(sys.argv))

File line_profiler.py

-from cProfile import label
+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+
+import inspect
+import linecache
 import marshal
+import os
 
 from _line_profiler import LineProfiler as CLineProfiler
 
 
+def label(code):
+    """ Return a (filename, first_lineno, func_name) tuple for a given code
+    object.
+
+    This is the same labelling as used by the cProfile module in Python 2.5.
+    """
+    if isinstance(code, str):
+        return ('~', 0, code)    # built-in functions ('~' sorts at the end)
+    else:
+        return (code.co_filename, code.co_firstlineno, code.co_name)
+
 class LineProfiler(CLineProfiler):
-    """ A subclass of the C version solely to provide a decorator since Cython
-    does not have closures.
+    """ A profiler that records the execution times of individual lines.
     """
 
     def __call__(self, func):
         """ Dump a representation of the data to a file as a marshalled
         dictionary from `get_stats()`.
         """
-        stats = self.get_stats()
+        stats, unit = self.get_stats()
         f = open(filename, 'wb')
         try:
-            marshal.dump(stats, f)
+            marshal.dump((stats, unit), f)
         finally:
             f.close()
 
+    def print_stats(self):
+        """ Show the gathered statistics.
+        """
+        stats, unit = self.get_stats()
+        show_text(stats, unit)
+
     def run(self, cmd):
         """ Profile a single executable statment in the main namespace.
         """
         finally:
             self.disable_by_count()
 
+
+def show_func(filename, start_lineno, func_name, timings, unit):
+    """ Show results for a single function.
+    """
+    if not os.path.exists(filename):
+        print 'Could not find file %s' % filename
+        print 'Are you sure you are running this program from the same directory'
+        print 'that you ran the profiler from?'
+        return
+    print 'File: %s' % filename
+    print 'Function: %s at line %s' % (func_name, start_lineno)
+    all_lines = linecache.getlines(filename)
+    sublines = inspect.getblock(all_lines[start_lineno-1:])
+    template = '%6s %9s %12s %8s  %-s'
+    d = {}
+    total_time = 0.0
+    for lineno, nhits, time in timings:
+        total_time += time
+    print 'Total time: %g s' % (total_time * unit)
+    for lineno, nhits, time in timings:
+        d[lineno] = (nhits, time, '%5.1f' % (100*time / total_time))
+    linenos = range(start_lineno, start_lineno + len(sublines))
+    empty = ('', '', '')
+    header = template % ('Line #', 'Hits', 'Time', '% Time', 'Line Contents')
+    print
+    print header
+    print '=' * len(header)
+    for lineno, line in zip(linenos, sublines):
+        nhits, time, percent = d.get(lineno, empty)
+        print template % (lineno, nhits, time, percent, line.rstrip('\n').rstrip('\r'))
+    print
+
+def show_text(stats, unit):
+    """ Show text for the given timings.
+    """
+    print 'Timer unit: %g s' % unit
+    print
+    for (fn, lineno, name), timings in sorted(stats.items()):
+        show_func(fn, lineno, name, stats[fn, lineno, name], unit)
+
+def main():
+    import optparse
+    usage = "usage: %prog [-t] profile.lprof"
+    parser = optparse.OptionParser(usage)
+    parser.add_option('-t', '--text', action='store_const', const='text',
+        dest='action', default='text', help="Show text output.")
+
+    options, args = parser.parse_args()
+    if len(args) != 1:
+        parser.error("Must provide a filename.")
+    f = open(args[0], 'rb')
+    stats, unit = marshal.load(f)
+    f.close()
+    if options.action == 'text':
+        show_text(stats, unit)
+    else:
+        parser.error("Only --text is supported at this time.")
+
+if __name__ == '__main__':
+    main()

File view_line_prof.py

-#!/usr/bin/env python
-# -*- coding: UTF-8 -*-
-""" View line profile timings.
-"""
-
-import inspect
-import linecache
-import marshal
-import os
-
-
-def show_func(filename, start_lineno, func_name, timings, unit):
-    """ Show results for a single function.
-    """
-    if not os.path.exists(filename):
-        print 'Could not find file %s' % filename
-        print 'Are you sure you are running this program from the same directory'
-        print 'that you ran the profiler from?'
-        return
-    print 'File: %s' % filename
-    print 'Function: %s at line %s' % (func_name, start_lineno)
-    all_lines = linecache.getlines(filename)
-    sublines = inspect.getblock(all_lines[start_lineno-1:])
-    template = '%6s %9s %12s %8s  %-s'
-    d = {}
-    total_time = 0.0
-    for lineno, nhits, time in timings:
-        total_time += time
-    print 'Total time: %g s' % (total_time * unit)
-    for lineno, nhits, time in timings:
-        d[lineno] = (nhits, time, '%5.1f' % (100*time / total_time))
-    linenos = range(start_lineno, start_lineno + len(sublines))
-    empty = ('', '', '')
-    header = template % ('Line #', 'Hits', 'Time', '% Time', 'Line Contents')
-    print
-    print header
-    print '=' * len(header)
-    for lineno, line in zip(linenos, sublines):
-        nhits, time, percent = d.get(lineno, empty)
-        print template % (lineno, nhits, time, percent, line.rstrip('\n').rstrip('\r'))
-    print
-
-def show_text(args, stats, unit):
-    """ Show text for the given timings.
-    """
-    print 'Timer unit: %g s' % unit
-    print
-    for (fn, lineno, name), timings in sorted(stats.items()):
-        show_func(fn, lineno, name, stats[fn, lineno, name], unit)
-
-
-def main():
-    import argparse
-    parser = argparse.ArgumentParser()
-    parser.add_argument('-t', '--text', action='store_const', const='text',
-        dest='action', default='text', help="Show text output.")
-    parser.add_argument('proffile',
-        help="The name of the line-profiler output file.")
-
-    args = parser.parse_args()
-    f = open(args.proffile, 'rb')
-    stats, unit = marshal.load(f)
-    f.close()
-    if args.action == 'text':
-        show_text(args, stats, unit)
-
-
-if __name__ == '__main__':
-    main()