Using Context Manager for Profiling
Posted on Thu 24 August 2017 in Python Profiling
Recently, was implementing a class, where, we wanted to be able to 'plug-in' profiling in some functions. Summarizing some of the requirements -
-
Ability to add profiling information in several places in code. (Something like
cProfile.Profile().enable()
andcProfile.Profile.disable()
done frequently. -
Ability to enable profiling globally or not, so when globally disabled, it should add a minimum overhead.
-
Being able to related profiling information with the code.
My initial idea was to have a class wide cProfile.Profile()
object and keep enabling and disabling it for the parts of the code I wanted to profile. There are a few problems with this - viz.
-
The code gets littered with
enable
/disable
code. -
Also, to meet requirement 2. above, this would have been wrapped in an
if
statement -
Dumping stats only for some part of the code - in a section would become a little irritating to handle (see below).
For this type of problems, Context Managers in Python look like a good choice. All the enable
/disable
logic, if
conditions etc. can be neatly wrapped inside the __enter__
and __exit__
methods.
The code for the class Profiler
looks like following
class Profiler(object):
def __init__(self, parent=None, enabled=False, contextstr=None):
"""
parent: the callee
enabled: Should profile or not
contextstr: used as a marker to separate dump data.
"""
self.parent = parent
self.enabled = True
self.stream = StringIO.StringIO()
self.contextstr = contextstr or str(self.__class__)
self.profiler = cProfile.Profile()
def __enter__(self, *args):
if not self.enabled:
return
# Start profiling.
self.stream.write("profile: {}: enter\n".format(self.contextstr))
self.profiler.enable()
def __exit__(self, exc_type, exc_val, exc_tb):
if not self.enabled:
return
self.profiler.disable()
sort_by = 'cumulative'
ps = pstats.Stats(self.profiler, stream=self.stream).sort_stats(sort_by)
ps.print_stats(0.1)
self.stream.write("profile: {}: exit\n".format(self.contextstr))
print (self.stream.getvalue())
# Raise any exception if raised during execution of code.
return False
And then this code can be called as follows -
with Profiler(contextstr="---foo profiling ----", enabled=True):
# code to be profiled
Pretty simple and extremely readable. Also, this just works fine as a nested Context Manager, cool right?
Some Notes:
- Just noticed a quirk of
pstats.Stats
Constructor. If we pass aProfile
object to thepstats.Stats
Constructor as above, unless theProfile
object isenable
d, the Constructor raises an Exception that looks like -
File "/usr/lib/python2.7/pstats.py", line 81, in __init__
self.init(arg)
File "/usr/lib/python2.7/pstats.py", line 95, in init
self.load_stats(arg)
File "/usr/lib/python2.7/pstats.py", line 124, in load_stats
% (self.__class__, arg))
TypeError: Cannot create or construct a <class pstats.Stats at 0x7f683bec19a8> object from <cProfile.Profile object at 0x7f683bf1a6e0>
- Also, returning
False
would make sure, if the running code within awith
block raised an exception, that gets re-raised (so we do not mess up with the Application flow by consuming exceptions.)