Skip to content

Commit 372b290

Browse files
committed
Refactor to compute memory of generators and list/set/dict comprehensions
1 parent f30ff08 commit 372b290

3 files changed

Lines changed: 115 additions & 48 deletions

File tree

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ test:
88
$(PYTHON) -m memory_profiler test/test_as.py
99
$(PYTHON) -m memory_profiler test/test_global.py
1010
$(PYTHON) -m memory_profiler test/test_precision_command_line.py
11+
$(PYTHON) -m memory_profiler test/test_gen.py
1112
$(PYTHON) test/test_import.py
1213
$(PYTHON) test/test_memory_usage.py
1314
$(PYTHON) test/test_precision_import.py

memory_profiler.py

Lines changed: 73 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -402,15 +402,64 @@ def show_results(self, stream=None):
402402
(function_name,) + ts[0] + ts[1]))
403403

404404

405+
class CodeMap(dict):
406+
407+
def __init__(self, include_children):
408+
self.include_children = include_children
409+
self._toplevel = []
410+
411+
def add(self, code, toplevel_code=None):
412+
if code in self:
413+
return
414+
415+
if toplevel_code is None:
416+
filename = code.co_filename
417+
if filename.endswith((".pyc", ".pyo")):
418+
filename = filename[:-1]
419+
if not os.path.exists(filename):
420+
print('ERROR: Could not find file ' + filename)
421+
if filename.startswith(("ipython-input", "<ipython-input")):
422+
print("NOTE: %mprun can only be used on functions defined in "
423+
"physical files, and not in the IPython environment.")
424+
return
425+
426+
toplevel_code = code
427+
(sub_lines, start_line) = inspect.getsourcelines(code)
428+
linenos = range(start_line,
429+
start_line + len(sub_lines))
430+
self._toplevel.append((filename, code, linenos))
431+
self[code] = {}
432+
else:
433+
self[code] = self[toplevel_code]
434+
435+
for subcode in filter(inspect.iscode, code.co_consts):
436+
self.add(subcode, toplevel_code=toplevel_code)
437+
438+
def trace(self, code, lineno):
439+
memory = _get_memory(-1, include_children=self.include_children)
440+
# if there is already a measurement for that line get the max
441+
previous_memory = self[code].get(lineno, 0)
442+
self[code][lineno] = max(memory, previous_memory)
443+
444+
def items(self):
445+
"""Iterate on the toplevel code blocks."""
446+
for (filename, code, linenos) in self._toplevel:
447+
measures = self[code]
448+
if not measures:
449+
continue # skip if no measurement
450+
line_iterator = ((line, measures.get(line)) for line in linenos)
451+
yield (filename, line_iterator)
452+
453+
405454
class LineProfiler(object):
406455
""" A profiler that records the amount of memory for each line """
407456

408457
def __init__(self, **kw):
409-
self.code_map = {}
458+
include_children = kw.get('include_children', False)
459+
self.code_map = CodeMap(include_children=include_children)
410460
self.enable_count = 0
411461
self.max_mem = kw.get('max_mem', None)
412-
self.prevline = None
413-
self.include_children = kw.get('include_children', False)
462+
self.prevlines = []
414463

415464
def __call__(self, func=None, precision=1):
416465
if func is not None:
@@ -426,13 +475,6 @@ def inner_partial(f):
426475
return self.__call__(f, precision=precision)
427476
return inner_partial
428477

429-
def add_code(self, code, toplevel_code=None):
430-
if code not in self.code_map:
431-
self.code_map[code] = {}
432-
433-
for subcode in filter(inspect.iscode, code.co_consts):
434-
self.add_code(subcode)
435-
436478
def add_function(self, func):
437479
""" Record line profiling information for the given Python function.
438480
"""
@@ -443,7 +485,7 @@ def add_function(self, func):
443485
warnings.warn("Could not extract a code object for the object %r"
444486
% func)
445487
else:
446-
self.add_code(code)
488+
self.code_map.add(code)
447489

448490
def wrap_function(self, func):
449491
""" Wrap a function to profile it.
@@ -493,15 +535,15 @@ def disable_by_count(self):
493535

494536
def trace_memory_usage(self, frame, event, arg):
495537
"""Callback for sys.settrace"""
496-
if (event in ('call', 'line', 'return')
497-
and frame.f_code in self.code_map):
498-
if event != 'call':
538+
if frame.f_code in self.code_map:
539+
if event == 'call':
499540
# "call" event just saves the lineno but not the memory
500-
mem = _get_memory(-1, include_children=self.include_children)
501-
# if there is already a measurement for that line get the max
502-
old_mem = self.code_map[frame.f_code].get(self.prevline, 0)
503-
self.code_map[frame.f_code][self.prevline] = max(mem, old_mem)
504-
self.prevline = frame.f_lineno
541+
self.prevlines.append(frame.f_lineno)
542+
elif event == 'line':
543+
self.code_map.trace(frame.f_code, self.prevlines[-1])
544+
self.prevlines[-1] = frame.f_lineno
545+
elif event == 'return':
546+
self.code_map.trace(frame.f_code, self.prevlines.pop())
505547

506548
if self._original_trace_function is not None:
507549
(self._original_trace_function)(frame, event, arg)
@@ -553,45 +595,28 @@ def show_results(prof, stream=None, precision=1):
553595
stream = sys.stdout
554596
template = '{0:>6} {1:>12} {2:>12} {3:<}'
555597

556-
for code in prof.code_map:
557-
lines = prof.code_map[code]
558-
if not lines:
559-
# .. measurements are empty ..
560-
continue
561-
filename = code.co_filename
562-
if filename.endswith((".pyc", ".pyo")):
563-
filename = filename[:-1]
564-
stream.write('Filename: ' + filename + '\n\n')
565-
if not os.path.exists(filename):
566-
stream.write('ERROR: Could not find file ' + filename + '\n')
567-
if any([filename.startswith(k) for k in
568-
("ipython-input", "<ipython-input")]):
569-
print("NOTE: %mprun can only be used on functions defined in "
570-
"physical files, and not in the IPython environment.")
571-
continue
572-
all_lines = linecache.getlines(filename)
573-
sub_lines = inspect.getblock(all_lines[code.co_firstlineno - 1:])
574-
linenos = range(code.co_firstlineno,
575-
code.co_firstlineno + len(sub_lines))
576-
598+
for (filename, lines) in prof.code_map.items():
577599
header = template.format('Line #', 'Mem usage', 'Increment',
578600
'Line Contents')
601+
602+
stream.write('Filename: ' + filename + '\n\n')
579603
stream.write(header + '\n')
580604
stream.write('=' * len(header) + '\n')
581605

582-
mem_old = lines[min(lines.keys())]
606+
all_lines = linecache.getlines(filename)
607+
mem_old = None
583608
float_format = '{0}.{1}f'.format(precision + 4, precision)
584609
template_mem = '{0:' + float_format + '} MiB'
585-
for line in linenos:
586-
mem = ''
587-
inc = ''
588-
if line in lines:
589-
mem = lines[line]
590-
inc = mem - mem_old
610+
for (lineno, mem) in lines:
611+
if mem:
612+
inc = (mem - mem_old) if mem_old else 0
591613
mem_old = mem
592614
mem = template_mem.format(mem)
593615
inc = template_mem.format(inc)
594-
stream.write(template.format(line, mem, inc, all_lines[line - 1]))
616+
else:
617+
mem = ''
618+
inc = ''
619+
stream.write(template.format(lineno, mem, inc, all_lines[lineno - 1]))
595620
stream.write('\n\n')
596621

597622

test/test_gen.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
2+
@profile
3+
def test_comprehension():
4+
# Dict comprehension
5+
d_comp = {str(k*k): [v] * (1<<17)
6+
for (v, k) in enumerate(range(99, 111))}
7+
8+
# List comprehension
9+
l_comp = [[i] * (i<<9) for i in range(99)]
10+
del l_comp
11+
del d_comp
12+
13+
def hh(x=1):
14+
# Set comprehension
15+
s_comp = {('Z',) * (k<<13) for k in range(x, 19 + 2*x)}
16+
return s_comp
17+
18+
val = [range(1, 4), max(1, 4), 42 + len(hh())]
19+
val = hh() | hh(4)
20+
val.add(40)
21+
l1_comp = [[(1, i)] * (i<<9) for i in range(99)]
22+
l2_comp = [[(3, i)] * (i<<9) for i in range(99)]
23+
24+
return val
25+
26+
27+
@profile
28+
def test_generator():
29+
a_gen = ([42] * (1<<20) for __ in '123')
30+
huge_lst = list(a_gen)
31+
32+
b_gen = ([24] * (1<<20) for __ in '123')
33+
del b_gen
34+
del huge_lst
35+
36+
return a_gen
37+
38+
39+
if __name__ == '__main__':
40+
test_generator()
41+
test_comprehension()

0 commit comments

Comments
 (0)