Skip to content

Commit a846fa6

Browse files
committed
child process memory now recorded seperately in mpmprof
1 parent 0d956a1 commit a846fa6

2 files changed

Lines changed: 218 additions & 10 deletions

File tree

memory_profiler.py

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,34 @@ def _repr_pretty_(self, p , cycle):
7373
p.text(u'<MemitResult : '+msg+u'>')
7474

7575

76+
def _get_child_memory(process, meminfo_attr=None):
77+
"""
78+
Returns a generator that yields memory for all child processes.
79+
"""
80+
if not has_psutil:
81+
raise NotImplementedError((
82+
"The psutil module is required to monitor the "
83+
"memory usage of child processes."
84+
))
85+
86+
# Convert a pid to a process
87+
if isinstance(process, int):
88+
if process == -1: process = os.getpid()
89+
process = psutil.Process(process)
90+
91+
if not meminfo_attr:
92+
# Use the psutil 2.0 attr if the older version isn't passed in.
93+
meminfo_attr = 'memory_info' if hasattr(process, 'memory_info') else 'get_memory_info'
94+
95+
# Select the psutil function get the children similar to how we selected
96+
# the memory_info attr (a change from excepting the AttributeError).
97+
children_attr = 'children' if hasattr(process, 'children') else 'get_children'
98+
99+
# Loop over the child processes and yield their memory
100+
for child in getattr(process, children_attr)(recursive=True):
101+
yield getattr(child, meminfo_attr)()[0] / _TWO_20
102+
103+
76104
def _get_memory(pid, timestamps=False, include_children=False):
77105

78106
# .. only for current process and only on unix..
@@ -88,13 +116,7 @@ def _get_memory(pid, timestamps=False, include_children=False):
88116
meminfo_attr = 'memory_info' if hasattr(process, 'memory_info') else 'get_memory_info'
89117
mem = getattr(process, meminfo_attr)()[0] / _TWO_20
90118
if include_children:
91-
try:
92-
for p in process.get_children(recursive=True):
93-
mem += getattr(p, meminfo_attr)()[0] / _TWO_20
94-
except AttributeError:
95-
# fix for newer psutil
96-
for p in process.children(recursive=True):
97-
mem += getattr(p, meminfo_attr)()[0] / _TWO_20
119+
mem += sum(_get_child_memory(process, meminfo_attr))
98120
if timestamps:
99121
return (mem, time.time())
100122
else:
@@ -106,9 +128,11 @@ def _get_memory(pid, timestamps=False, include_children=False):
106128
# .. scary stuff ..
107129
if os.name == 'posix':
108130
if include_children:
109-
raise NotImplementedError('The psutil module is required when to'
110-
' monitor memory usage of children'
111-
' processes')
131+
raise NotImplementedError((
132+
"The psutil module is required to monitor the "
133+
"memory usage of child processes."
134+
))
135+
112136
warnings.warn("psutil module not found. memory_profiler will be slow")
113137
# ..
114138
# .. memory usage in MiB ..

mpmprof

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Multiprocessing version of memory profiling of Python programs.
4+
"""
5+
6+
import os
7+
import time
8+
import glob
9+
import argparse
10+
import subprocess
11+
import memory_profiler as mp
12+
13+
14+
# Command Descriptions and Constants
15+
DESCRIPTION = "Multiprocessing memory profiling over time."
16+
EPILOG = "If there are any bugs or concerns, submit an issue on Github"
17+
VERSION = "mpmprof v{}".format(mp.__version__)
18+
FILETIME = "%Y%m%d%H%M%S"
19+
BLANKS = set(' \t')
20+
21+
22+
def run_action(args):
23+
"""
24+
Run the given program and profile its memory usage.
25+
"""
26+
27+
# Determine where to write the output to
28+
if args.output is None:
29+
args.output = "mprofile_{}.dat".format(
30+
time.strftime(FILETIME, time.localtime())
31+
)
32+
33+
# Determine if the command is a Python command
34+
if args.command[0].endswith('.py') and not args.nopython:
35+
args.python = True
36+
37+
# Run the executable with the extra features
38+
if args.python:
39+
print("running as a Python program ...")
40+
if not args.command[0].startswith('python'):
41+
args.command.insert(0, 'python')
42+
43+
# Inform the user we're sampling
44+
print("mpmprof: Sampling memory every {} seconds".format(args.interval))
45+
46+
# Put the command back together from the argument parsing
47+
command = " ".join([
48+
c if BLANKS.isdisjoint(c) else "'{}'".format(c) for c in args.command
49+
])
50+
51+
# Open a subprocess to the given command
52+
proc = subprocess.Popen(args.command)
53+
54+
# This is where a call to mp.memory_usage should go.
55+
# Instead we're adding the custom code for sampling spawned memory
56+
with open(args.output, "a") as f:
57+
58+
# Write the command to the data file
59+
f.write("CMDLINE {}\n".format(command))
60+
61+
# Continue sampling until the subprocess is over, counting lines
62+
lines = 0
63+
while True:
64+
# Determine if the subprocess is still running
65+
if proc.poll() is not None: break
66+
67+
# Collect memory usage of master program and write to profile
68+
mem = mp._get_memory(proc.pid)
69+
f.write("MEM {0:.6f} {1:.4f}\n".format(mem, time.time()))
70+
lines += 1
71+
72+
# Collect memory usage of spawned children and write to profile
73+
for idx, mem in enumerate(mp._get_child_memory(proc.pid)):
74+
f.write("CHLD{0} {1:.6f} {2:.4f}\n".format(idx, mem, time.time()))
75+
lines += 1
76+
77+
# Flush every 50 lines
78+
if lines > 50:
79+
lines = 0
80+
f.flush()
81+
82+
# Sleep for the given interval
83+
time.sleep(args.interval)
84+
85+
return "memory profile written to {}".format(args.output)
86+
87+
88+
def plot_action(args):
89+
"""
90+
Use matplotlib to draw the memory usage of a mprofile .dat file.
91+
"""
92+
raise NotImplementedError("Not implemented yet.")
93+
94+
95+
if __name__ == '__main__':
96+
# Create the argument parser and subparsers for each command
97+
parser = argparse.ArgumentParser(description=DESCRIPTION, epilog=EPILOG)
98+
subparsers = parser.add_subparsers(title='commands')
99+
100+
# Add the version command
101+
parser.add_argument('-v', '--version', action='version', version=VERSION)
102+
103+
# Commands defined in an dictionary for easy adding
104+
commands = (
105+
# Run command definition
106+
{
107+
'name': 'run',
108+
'action': run_action,
109+
'help': 'monitor the memory usage of a command',
110+
'args': {
111+
'--python': {
112+
'default': False,
113+
'action': 'store_true',
114+
'help': 'activates extra features for Python programs',
115+
},
116+
'--nopython': {
117+
'default': False,
118+
'action': 'store_true',
119+
'help': 'disables extra features for Python programs',
120+
},
121+
('-T', '--interval'): {
122+
'type': float,
123+
'default': 0.1,
124+
'metavar': 'S',
125+
'help': 'sampling period (in seconds), defaults to 0.1',
126+
},
127+
('-o', '--output'): {
128+
'type': str,
129+
'default': None,
130+
'metavar': 'PATH',
131+
'help': 'location to write the memory profiler output to',
132+
},
133+
'command': {
134+
'nargs': argparse.REMAINDER,
135+
'help': 'command to run and profile memory usage',
136+
}
137+
}
138+
},
139+
140+
# Plot command definition
141+
{
142+
'name': 'plot',
143+
'action': plot_action,
144+
'help': 'plot the memory usage of a mprofile data file',
145+
'args': {
146+
('-t', '--title'): {
147+
'type': str,
148+
'default': None,
149+
'metavar': 'S',
150+
'help': 'set the title of the figure',
151+
},
152+
('-o', '--output'): {
153+
'type': str,
154+
'default': None,
155+
'metavar': 'PATH',
156+
'help': 'write the figure as a png to disk'
157+
},
158+
'profile': {
159+
'nargs': '*',
160+
'help': 'profile to plot, omit to use the latest',
161+
}
162+
}
163+
}
164+
)
165+
166+
# Add the commands and their arguments.
167+
for cmd in commands:
168+
# Create the command subparser and add the action
169+
cmd_parser = subparsers.add_parser(cmd['name'], help=cmd['help'])
170+
cmd_parser.set_defaults(func=cmd['action'])
171+
172+
# Add the arguments
173+
for args, kwargs in cmd['args'].items():
174+
if isinstance(args, str):
175+
args = (args,)
176+
cmd_parser.add_argument(*args, **kwargs)
177+
178+
# Handle input from the command line
179+
args = parser.parse_args() # Parse the arguments
180+
# try:
181+
msg = args.func(args) # Call the default function
182+
parser.exit(0, msg+"\n") # Exit cleanly with message
183+
# except Exception as e:
184+
# parser.error(str(e)) # Exit with error

0 commit comments

Comments
 (0)