| #!/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()) |