blob: 546a04226104f6fa007fc0eb39689d132bbaf28d [file] [log] [blame] [edit]
#!/usr/bin/env python
#
#
# Copyright 2014, NICTA
#
# This software may be distributed and modified according to the terms of
# the BSD 2-Clause license. Note that NO WARRANTY is provided.
# See "LICENSE_BSD2.txt" for details.
#
# @TAG(NICTA_BSD)
#
'''
Monitors the peak memory usage of a process and its children. Usage is similar
to the UNIX `time` utility.
'''
import subprocess, sys, threading, time
PSUTIL_NOT_AVAILABLE=False
try:
import psutil
except ImportError:
PSUTIL_NOT_AVAILABLE=True
PSUTIL2 = psutil.version_info >= (2, 0) if not PSUTIL_NOT_AVAILABLE else False
if PSUTIL_NOT_AVAILABLE:
def get_usage(proc):
return 0
def get_total_usage(proc):
return 0
else:
def get_usage(proc):
'''Retrieve the memory usage of a particular psutil process without its
children. We use the proportional set size, which accounts for shared pages
to give us a more accurate total usage.'''
assert isinstance(proc, psutil.Process)
try:
if PSUTIL2:
return sum([m.pss for m in proc.memory_maps(grouped=True)])
else:
return sum([m.pss for m in proc.get_memory_maps(grouped=True)])
except psutil.AccessDenied:
# If we don't have permission to read a particular process,
# just return 0.
return 0
def get_total_usage(pid):
'''Retrieve the memory usage of a process by PID including its children. We
ignore NoSuchProcess errors to mask subprocesses exiting while the cohort
continues.'''
total = 0
# Fetch parent's usage.
try:
p = psutil.Process(pid)
total += get_usage(p)
if PSUTIL2:
children = p.children(recursive=True) #pylint: disable=E1123
else:
children = p.get_children(recursive=True) #pylint: disable=E1123
except psutil.NoSuchProcess:
return 0
# Fetch usage of children.
for proc in children:
try:
total += get_usage(proc)
except psutil.NoSuchProcess:
pass
return total
class Poller(threading.Thread):
def __init__(self, pid):
super(Poller, self).__init__()
# Daemonise ourselves to avoid delaying exit of the process of our
# calling thread.
self.daemon = True
self.pid = pid
self.high = 0
self.finished = False
self.started = threading.Semaphore(0)
def run(self):
# Fetch a sample, and notify others that we have started.
self.high = get_total_usage(self.pid)
self.started.release()
#
# Poll the process periodically to track a high water mark of its
# memory usage.
#
# We poll quickly at the beginning and use exponential backout until we
# hit 1 second to try and get better stats on short-lived processes.
#
polling_interval = 0.01
while not self.finished:
time.sleep(polling_interval)
usage = get_total_usage(self.pid)
if usage > self.high:
self.high = usage
if polling_interval < 1.0:
polling_interval = min(polling_interval * 1.5, 1.0)
def peak_mem_usage(self):
return self.high
def __enter__(self):
return self
def __exit__(self, *_):
self.finished = True
def process_poller(pid):
'''Initiate polling of a subprocess. This is intended to be used in a
`with` block.'''
# Create a new thread and start it up.
p = Poller(pid)
p.start()
# Wait for the thread to record at least one sample before continuing.
p.started.acquire()
return p
def main():
if len(sys.argv) <= 1 or sys.argv[1] in ['-?', '--help']:
print >>sys.stderr, 'Usage: %s command args...\n Measure peak memory ' \
'usage of a command' % sys.argv[0]
return -1
if PSUTIL_NOT_AVAILABLE:
print("Error: 'psutil' module not available. Run\n"
"\n"
" pip install --user psutil\n"
"\n"
"to install.")
sys.exit(1)
# Run the command requested.
try:
p = subprocess.Popen(sys.argv[1:])
except OSError:
print >>sys.stderr, 'command not found'
return -1
high = 0
try:
with process_poller(p.pid) as m: #pylint: disable=E1101
p.communicate()
high = m.peak_mem_usage()
except KeyboardInterrupt:
# The user Ctrl-C-ed us. Fake an error return code.
p.returncode = -1
print >>sys.stderr, 'Peak usage %d bytes' % high
return p.returncode
if __name__ == '__main__':
sys.exit(main())