1717import subprocess
1818import logging
1919
20+ from collections import OrderedDict
21+
2022# TODO: provide alternative when multiprocessing is not available
2123try :
2224 from multiprocessing import Process , Pipe
3941 import __builtin__ as builtins
4042else :
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 ..
4549try :
@@ -48,6 +52,15 @@ def unicode(x, *args): return str(x)
4852except 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
5265class 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
138164class 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):
351383class _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
364397class 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 )
9471022else :
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
9561038class 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