Skip to content

Commit 86acef8

Browse files
demiurg906superbobry
authored andcommitted
ENH added optional tracemalloc backend
It is now possible to use ``tracemalloc`` to analyze memory usage Python code on Python 3.4 and above. ``tracemalloc`` allows for more precise measurements compared to ``psutil``. However, it only works either for pure Python code or for C extensions allocating memory via ``PyMem_Alloc``. To use the new backend code run ``memory_profiler`` with ``--backend`` option, e.g. $ python -m memory_profiler --backend=tracemalloc script.py Also if you use ``memory_profiler`` with imported decorator you can specify backend as an argument to the decorator function: @Profile(backend='tracemalloc') def f(n): a = [0] * n return a ``backend`` parameter to ``@profile`` has priority over ``--backend``. Note that using ``tracemalloc`` in ``mprof`` and IPython magic is not supported at the moment.
1 parent aa8b30c commit 86acef8

3 files changed

Lines changed: 191 additions & 57 deletions

File tree

memory_profiler.py

Lines changed: 125 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
import subprocess
1818
import logging
1919

20+
from collections import OrderedDict
21+
2022
# TODO: provide alternative when multiprocessing is not available
2123
try:
2224
from multiprocessing import Process, Pipe
@@ -39,7 +41,9 @@
3941
import __builtin__ as builtins
4042
else:
4143
import builtins
42-
def unicode(x, *args): return str(x)
44+
45+
def unicode(x, *args):
46+
return str(x)
4347

4448
# .. get available packages ..
4549
try:
@@ -48,6 +52,15 @@ def unicode(x, *args): return str(x)
4852
except ImportError:
4953
has_psutil = False
5054

55+
try:
56+
import tracemalloc
57+
has_tracemalloc = True
58+
except ImportError:
59+
has_tracemalloc = False
60+
61+
_backend_chosen = False
62+
_backend = 'psutil'
63+
5164

5265
class MemitResult(object):
5366
"""memit magic run details.
@@ -73,14 +86,24 @@ def _repr_pretty_(self, p , cycle):
7386
p.text(u'<MemitResult : '+msg+u'>')
7487

7588

76-
def _get_memory(pid, timestamps=False, include_children=False):
89+
def _get_memory(pid, timestamps=False, include_children=False, filename=None):
7790

7891
# .. only for current process and only on unix..
7992
if pid == -1:
8093
pid = os.getpid()
8194

82-
# .. cross-platform but but requires psutil ..
83-
if has_psutil:
95+
def tracemalloc_tool():
96+
# .. cross-platform but but requires Python 3.4 or higher
97+
stat = next(filter(lambda item: str(item).startswith(filename),
98+
tracemalloc.take_snapshot().statistics('filename')))
99+
mem = stat.size / _TWO_20
100+
if timestamps:
101+
return mem, time.time()
102+
else:
103+
return mem
104+
105+
def ps_util_tool():
106+
# .. cross-platform but but requires psutil ..
84107
process = psutil.Process(pid)
85108
try:
86109
# avoid useing get_memory_info since it does not exists
@@ -96,15 +119,15 @@ def _get_memory(pid, timestamps=False, include_children=False):
96119
for p in process.children(recursive=True):
97120
mem += getattr(p, meminfo_attr)()[0] / _TWO_20
98121
if timestamps:
99-
return (mem, time.time())
122+
return mem, time.time()
100123
else:
101124
return mem
102125
except psutil.AccessDenied:
103126
pass
104127
# continue and try to get this from ps
105128

106-
# .. scary stuff ..
107-
if os.name == 'posix':
129+
def posix_tool():
130+
# .. scary stuff ..
108131
if include_children:
109132
raise NotImplementedError('The psutil module is required when to'
110133
' monitor memory usage of children'
@@ -122,17 +145,20 @@ def _get_memory(pid, timestamps=False, include_children=False):
122145
vsz_index = out[0].split().index(b'RSS')
123146
mem = float(out[1].split()[vsz_index]) / 1024
124147
if timestamps:
125-
return(mem, time.time())
148+
return mem, time.time()
126149
else:
127150
return mem
128151
except:
129152
if timestamps:
130-
return (-1, time.time())
153+
return -1, time.time()
131154
else:
132155
return -1
133-
else:
134-
raise NotImplementedError('The psutil module is required for non-unix '
135-
'platforms')
156+
157+
if _backend == 'tracemalloc' and (filename is None or filename == '<unknown>'):
158+
raise RuntimeError('There is no access to source file of the profiled function')
159+
160+
tools = {'tracemalloc': tracemalloc_tool, 'psutil': ps_util_tool, 'posix': posix_tool}
161+
return tools[_backend]()
136162

137163

138164
class MemTimer(Process):
@@ -152,6 +178,7 @@ def __init__(self, monitor_pid, interval, pipe, max_usage=False,
152178
self.include_children = kw.pop("include_children", False)
153179

154180
# get baseline memory usage
181+
# TODO: add filename
155182
self.mem_usage = [
156183
_get_memory(self.monitor_pid, timestamps=self.timestamps,
157184
include_children=self.include_children)]
@@ -161,6 +188,7 @@ def run(self):
161188
self.pipe.send(0) # we're ready
162189
stop = False
163190
while True:
191+
# TODO: add filename
164192
cur_mem = _get_memory(self.monitor_pid, timestamps=self.timestamps,
165193
include_children=self.include_children)
166194
if not self.max_usage:
@@ -275,13 +303,15 @@ def memory_usage(proc=-1, interval=.1, timeout=None, timestamps=False,
275303
line_count = 0
276304
while True:
277305
if not max_usage:
306+
# TODO: add filename
278307
mem_usage = _get_memory(proc.pid, timestamps=timestamps,
279308
include_children=include_children)
280309
if stream is not None:
281310
stream.write("MEM {0:.6f} {1:.4f}\n".format(*mem_usage))
282311
else:
283312
ret.append(mem_usage)
284313
else:
314+
# TODO: add filename
285315
ret = max(ret,
286316
_get_memory(proc.pid,
287317
include_children=include_children))
@@ -306,13 +336,15 @@ def memory_usage(proc=-1, interval=.1, timeout=None, timestamps=False,
306336
while counter < max_iter:
307337
counter += 1
308338
if not max_usage:
339+
# TODO: add filename
309340
mem_usage = _get_memory(proc, timestamps=timestamps,
310341
include_children=include_children)
311342
if stream is not None:
312343
stream.write("MEM {0:.6f} {1:.4f}\n".format(*mem_usage))
313344
else:
314345
ret.append(mem_usage)
315346
else:
347+
# TODO: add filename
316348
ret = max([ret,
317349
_get_memory(proc, include_children=include_children)
318350
])
@@ -351,14 +383,15 @@ def _find_script(script_name):
351383
class _TimeStamperCM(object):
352384
"""Time-stamping context manager."""
353385

354-
def __init__(self, timestamps):
386+
def __init__(self, timestamps, filename):
355387
self._timestamps = timestamps
388+
self._filename = filename
356389

357390
def __enter__(self):
358-
self._timestamps.append(_get_memory(os.getpid(), timestamps=True))
391+
self._timestamps.append(_get_memory(os.getpid(), timestamps=True, filename=self._filename))
359392

360393
def __exit__(self, *args):
361-
self._timestamps.append(_get_memory(os.getpid(), timestamps=True))
394+
self._timestamps.append(_get_memory(os.getpid(), timestamps=True, filename=self._filename))
362395

363396

364397
class TimeStamper:
@@ -396,7 +429,11 @@ def timestamp(self, name="<block>"):
396429
self.functions[func].append(timestamps)
397430
# A new object is required each time, since there can be several
398431
# nested context managers.
399-
return _TimeStamperCM(timestamps)
432+
try:
433+
filename = inspect.getsourcefile(func)
434+
except TypeError:
435+
filename = '<unknown>'
436+
return _TimeStamperCM(timestamps, filename)
400437

401438
def add_function(self, func):
402439
if func not in self.functions:
@@ -407,13 +444,17 @@ def wrap_function(self, func):
407444
"""
408445
def f(*args, **kwds):
409446
# Start time
410-
timestamps = [_get_memory(os.getpid(), timestamps=True)]
447+
try:
448+
filename = inspect.getsourcefile(func)
449+
except TypeError:
450+
filename = '<unknown>'
451+
timestamps = [_get_memory(os.getpid(), timestamps=True, filename=filename)]
411452
self.functions[func].append(timestamps)
412453
try:
413454
return func(*args, **kwds)
414455
finally:
415456
# end time
416-
timestamps.append(_get_memory(os.getpid(), timestamps=True))
457+
timestamps.append(_get_memory(os.getpid(), timestamps=True, filename=filename))
417458
return f
418459

419460
def show_results(self, stream=None):
@@ -461,7 +502,7 @@ def add(self, code, toplevel_code=None):
461502
self.add(subcode, toplevel_code=toplevel_code)
462503

463504
def trace(self, code, lineno):
464-
memory = _get_memory(-1, include_children=self.include_children)
505+
memory = _get_memory(-1, include_children=self.include_children, filename=code.co_filename)
465506
# if there is already a measurement for that line get the max
466507
previous_memory = self[code].get(lineno, 0)
467508
self[code][lineno] = max(memory, previous_memory)
@@ -578,7 +619,7 @@ def trace_memory_usage(self, frame, event, arg):
578619
def trace_max_mem(self, frame, event, arg):
579620
# run into PDB as soon as memory is higher than MAX_MEM
580621
if event in ('line', 'return') and frame.f_code in self.code_map:
581-
c = _get_memory(-1)
622+
c = _get_memory(-1, filename=frame.f_code.co_filename)
582623
if c >= self.max_mem:
583624
t = ('Current memory {0:.2f} MiB exceeded the '
584625
'maximum of {1:.2f} MiB\n'.format(c, self.max_mem))
@@ -918,10 +959,16 @@ def load_ipython_extension(ip):
918959
MemoryProfilerMagics.register_magics(ip)
919960

920961

921-
def profile(func=None, stream=None, precision=1):
962+
def profile(func=None, stream=None, precision=1, backend='psutil'):
922963
"""
923964
Decorator that will run the function and print a line-by-line profile
924965
"""
966+
global _backend
967+
_backend = backend
968+
if not _backend_chosen:
969+
choose_backend()
970+
if _backend == 'tracemalloc' and not tracemalloc.is_tracing():
971+
tracemalloc.start()
925972
if func is not None:
926973
def wrapper(*args, **kwargs):
927974
prof = LineProfiler()
@@ -931,10 +978,37 @@ def wrapper(*args, **kwargs):
931978
return wrapper
932979
else:
933980
def inner_wrapper(f):
934-
return profile(f, stream=stream, precision=precision)
981+
return profile(f, stream=stream, precision=precision, backend=backend)
935982
return inner_wrapper
936983

937984

985+
def choose_backend():
986+
"""
987+
Function that tries to setup backend, chosen by user, and if failed,
988+
setup one of the allowable backends
989+
"""
990+
global _backend
991+
old_backend = _backend
992+
backends = OrderedDict([
993+
('psutil', has_psutil),
994+
('posix', os.name == 'posix'),
995+
('tracemalloc', has_tracemalloc),
996+
('no_backend', True)
997+
])
998+
backends.move_to_end(_backend, last=False)
999+
for n_backend, is_available in backends.items():
1000+
if is_available:
1001+
_backend = n_backend
1002+
break
1003+
if _backend == 'no_backend':
1004+
raise NotImplementedError('Tracemalloc or psutil module is required for non-unix '
1005+
'platforms')
1006+
if _backend != old_backend:
1007+
print('{} can not be used, {} used instead'.format(old_backend, _backend))
1008+
global _backend_chosen
1009+
_backend_chosen = True
1010+
1011+
9381012
# Insert in the built-ins to have profile
9391013
# globally defined (global variables is not enough
9401014
# for all cases, e.g. a script that imports another
@@ -943,14 +1017,22 @@ def inner_wrapper(f):
9431017
def exec_with_profiler(filename, profiler):
9441018
builtins.__dict__['profile'] = profiler
9451019
ns = dict(_CLEAN_GLOBALS, profile=profiler)
1020+
choose_backend()
9461021
execfile(filename, ns, ns)
9471022
else:
9481023
def exec_with_profiler(filename, profiler):
1024+
if _backend == 'tracemalloc' and has_tracemalloc:
1025+
tracemalloc.start()
9491026
builtins.__dict__['profile'] = profiler
9501027
# shadow the profile decorator defined above
9511028
ns = dict(_CLEAN_GLOBALS, profile=profiler)
952-
with open(filename) as f:
953-
exec(compile(f.read(), filename, 'exec'), ns, ns)
1029+
choose_backend()
1030+
try:
1031+
with open(filename) as f:
1032+
exec(compile(f.read(), filename, 'exec'), ns, ns)
1033+
finally:
1034+
if tracemalloc.is_tracing():
1035+
tracemalloc.stop()
9541036

9551037

9561038
class LogFile(object):
@@ -987,33 +1069,38 @@ def flush(self):
9871069
parser = OptionParser(usage=_CMD_USAGE, version=__version__)
9881070
parser.disable_interspersed_args()
9891071
parser.add_option(
990-
"--pdb-mmem", dest="max_mem", metavar="MAXMEM",
991-
type="float", action="store",
992-
help="step into the debugger when memory exceeds MAXMEM")
1072+
'--pdb-mmem', dest='max_mem', metavar='MAXMEM',
1073+
type='float', action='store',
1074+
help='step into the debugger when memory exceeds MAXMEM')
9931075
parser.add_option(
994-
'--precision', dest="precision", type="int",
995-
action="store", default=3,
996-
help="precision of memory output in number of significant digits")
997-
parser.add_option("-o", dest="out_filename", type="str",
998-
action="store", default=None,
999-
help="path to a file where results will be written")
1000-
parser.add_option("--timestamp", dest="timestamp", default=False,
1001-
action="store_true",
1002-
help="""print timestamp instead of memory measurement for
1003-
decorated functions""")
1076+
'--precision', dest='precision', type='int',
1077+
action='store', default=3,
1078+
help='precision of memory output in number of significant digits')
1079+
parser.add_option('-o', dest='out_filename', type='str',
1080+
action='store', default=None,
1081+
help='path to a file where results will be written')
1082+
parser.add_option('--timestamp', dest='timestamp', default=False,
1083+
action='store_true',
1084+
help='''print timestamp instead of memory measurement for
1085+
decorated functions''')
1086+
parser.add_option('--backend', dest='backend', type='choice', action='store',
1087+
choices=['tracemalloc', 'psutil', 'posix'], default='psutil',
1088+
help='backend using for getting memory info (one of the {tracemalloc, psutil, posix})')
10041089

10051090
if not sys.argv[1:]:
10061091
parser.print_help()
10071092
sys.exit(2)
10081093

10091094
(options, args) = parser.parse_args()
10101095
sys.argv[:] = args # Remove every memory_profiler arguments
1096+
_backend = options.backend
10111097

1098+
script_filename = _find_script(args[0])
10121099
if options.timestamp:
10131100
prof = TimeStamper()
10141101
else:
10151102
prof = LineProfiler(max_mem=options.max_mem)
1016-
script_filename = _find_script(args[0])
1103+
10171104
try:
10181105
exec_with_profiler(script_filename, prof)
10191106
finally:

0 commit comments

Comments
 (0)