forked from pythonprofilers/memory_profiler
-
Notifications
You must be signed in to change notification settings - Fork 0
/
memory_profiler.py
272 lines (228 loc) · 8.21 KB
/
memory_profiler.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
"""Get process information"""
__version__ = '0.8'
_CMD_USAGE = "python -m memory_profiler script_file.py"
import time, sys, os
import linecache, inspect
import subprocess
if os.name == 'posix' and os.uname()[0] == 'Linux':
import resource
def _get_memory(pid, pagesize=resource.getpagesize()):
return float(open("/proc/%d/statm" % pid).read().split(None, 2)[1]) * pagesize / 1024.0**2
elif os.name == 'posix':
def _get_memory(pid):
# ..
# .. memory usage in MB ..
# .. this should work on both Mac and Linux ..
# .. subprocess.check_output appeared in 2.7, using Popen ..
# .. for backwards compatibility ..
out = subprocess.Popen(['ps', 'v', '-p', str(pid)],
stdout=subprocess.PIPE).communicate()[0].split('\n')
try:
vsz_index = out[0].split().index('RSS')
return float(out[1].split()[vsz_index]) / 1024
except:
return -1
else:
# ..
# .. better to be safe than sorry ..
raise NotImplementedError('Only UNIX supported at the moment')
def memory_usage(proc= -1, num= -1, interval=.1):
"""
Return the memory usage of a process or piece of code
Parameters
----------
proc : {int, string, tuple}
The process to monitor. Can be given by a PID or by a string
containing a filename. A tuple containing (f, args, kwargs) specifies
to run the function f(*args, **kwargs). Set to -1 (default)for
current process.
interval : int, optional
num : int, optional
Number of samples to generate. In the case of
defaults to -1, meaning
to wait until the process has finished if proc is a string or
to get just one if proc is an integer.
locals : dict
Local variables.
Returns
-------
mm : list of integers
memory usage, in KB
"""
ret = []
if str(proc).endswith('.py'):
filename = _find_script(proc)
f = open(filename, 'r')
proc = f.read()
f.close()
# TODO: make sure script's directory is on sys.path
def f_exec(x, locals):
# function interface for exec
exec(x, locals)
proc = (f_exec, (), {})
if isinstance(proc, (list, tuple)):
from multiprocessing import Process
if len(proc) == 1:
proc = (proc[0], (), {})
elif len(proc) == 2:
proc = (proc[0], proc[1], {})
p = Process(target=proc[0], args=proc[1], kwargs=proc[2])
p.start()
while p.is_alive(): # FIXME: or num
ret.append(_get_memory(p.pid))
time.sleep(interval)
else:
if proc == -1:
proc = os.getpid()
if num == -1:
num = 1
for _ in range(num):
ret.append(_get_memory(proc))
time.sleep(interval)
return ret
# ..
# .. utility functions for line-by-line ..
def _find_script(script_name):
""" Find the script.
If the input is not a file, then $PATH will be searched.
"""
if os.path.isfile(script_name):
return script_name
path = os.getenv('PATH', os.defpath).split(os.pathsep)
for dir in path:
if dir == '':
continue
fn = os.path.join(dir, script_name)
if os.path.isfile(fn):
return fn
print >> sys.stderr, 'Could not find script %s' % script_name
raise SystemExit(1)
class LineProfiler:
""" A profiler that records the amount of memory for each line """
def __init__(self, *functions):
self.functions = list(functions)
self.code_map = {}
self.enable_count = 0
def __call__(self, func):
self.add_function(func)
f = self.wrap_function(func)
f.__module__ = func.__module__
f.__name__ = func.__name__
f.__doc__ = func.__doc__
f.__dict__.update(getattr(func, '__dict__', {}))
return f
def add_function(self, func):
""" Record line profiling information for the given Python function.
"""
try:
code = func.func_code
except AttributeError:
import warnings
warnings.warn("Could not extract a code object for the object %r" % (func,))
return
if code not in self.code_map:
self.code_map[code] = {}
self.functions.append(func)
def wrap_function(self, func):
""" Wrap a function to profile it.
"""
def f(*args, **kwds):
self.enable_by_count()
try:
result = func(*args, **kwds)
finally:
self.disable_by_count()
return result
return f
def enable_by_count(self):
""" Enable the profiler if it hasn't been enabled before.
"""
if self.enable_count == 0:
self.enable()
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 trace_memory_usage(self, frame, event, arg):
if event in ('line', 'return'):
if frame.f_code in self.code_map:
lineno = frame.f_lineno
if event == 'return':
lineno += 1
entry = self.code_map[frame.f_code].setdefault(lineno, [])
entry.append(_get_memory(os.getpid()))
# why this is needed, I don't know
return self.trace_memory_usage
def __enter__(self):
self.enable_by_count()
def __exit__(self, exc_type, exc_val, exc_tb):
self.disable_by_count()
def enable(self):
sys.settrace(self.trace_memory_usage)
def disable(self):
self.last_time = {}
sys.settrace(None)
def show_results(prof, stream=None):
if stream is None:
stream = sys.stdout
template = '%6s %12s %-s'
header = template % ('Line #', 'Mem usage', 'Line Contents')
stream.write(header + '\n')
stream.write('=' * len(header) + '\n')
for code in prof.code_map:
lines = prof.code_map[code]
filename = code.co_filename
if (filename.endswith(".pyc") or
filename.endswith(".pyo")):
filename = filename[:-1]
all_lines = linecache.getlines(filename)
sub_lines = inspect.getblock(all_lines[code.co_firstlineno-1:])
linenos = range(code.co_firstlineno, code.co_firstlineno + len(sub_lines))
lines_normalized = {}
# move everything one frame up
keys = lines.keys()
keys.sort()
lines_normalized[code.co_firstlineno+1] = lines[keys[0]]
while len(keys) > 1:
v = keys.pop(0)
lines_normalized[v] = lines[keys[0]]
for l in linenos:
mem = ''
if lines_normalized.has_key(l):
mem = '%5.2f MB' % max(lines_normalized.get(l))
line = linecache.getline(filename, l)
stream.write(template % (l, mem, line))
if __name__ == '__main__':
from optparse import OptionParser
parser = OptionParser(usage=_CMD_USAGE)
parser.add_option('-o', '--outfile', dest='outfile',
help='Save stats to <outfile>', default=None)
parser.add_option('-v', '--visualize', action='store_true',
dest='visualize', help='Visualize result at exit',
default=False)
parser.add_option('-l', '--line', action='store_true',
dest='line', help='Do line-by-line timings',
default=False)
if not sys.argv[1:] or sys.argv[1] in ("--help", "-h"):
parser.print_help()
sys.exit(2)
(options, args) = parser.parse_args()
sys.argv[:] = args
if options.line:
prof = LineProfiler()
__file__ = _find_script(args[0])
if sys.version_info[0] < 3:
import __builtin__
__builtin__.__dict__['profile'] = prof
execfile(__file__, locals(), locals())
else:
import builtins
builtins.__dict__['profile'] = prof
exec(compile(open(__file__).read(), __file__, 'exec'), locals(), globals())
if options.visualize:
show_results(prof)