lh-l4v/misc/regression/cpuusage.py

204 lines
6.7 KiB
Python

#!/usr/bin/env python
#
#
# Copyright 2016, 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 total CPU usage of a process and its children. Usage is similar
to the UNIX `time` utility.
NB: In order to get up-to-date information, we don't use interfaces such
as os.times, which only includes terminated and waited-for children.
Instead, we poll the process tree regularly. This means that when a
child process dies, its CPU time since the last poll is lost and not
included in the total time.
Hence the total reported time will be an underestimate of the true
CPU usage, especially for short-lived child processes.
'''
from __future__ import print_function
import os
import psutil
import signal
import subprocess
import sys
import threading
import time
import warnings
try:
import psutil
if not hasattr(psutil.Process, "children") and hasattr(psutil.Process, "get_children"):
psutil.Process.children = psutil.Process.get_children
if not hasattr(psutil.Process, "memory_maps") and hasattr(psutil.Process, "get_memory_maps"):
psutil.Process.memory_maps = psutil.Process.get_memory_maps
except ImportError:
print("Error: 'psutil' module not available. Run\n"
"\n"
" pip install --user psutil\n"
"\n"
"to install.", file=sys.stderr)
sys.exit(1)
# The psutil.Process.cpu_times() API changed at version 4.1.0.
# Earlier versions give the user and system times for the queried
# process only. Later versions return two additional tuple members
# for the total user and system times of its child processes.
# For compatibility with both versions, we ignore the additional
# values.
def cpu_time_of(process):
cpu_times = process.cpu_times()
return cpu_times[0] + cpu_times[1]
class Poller(threading.Thread):
'''Subclass of threading.Thread that monitors CPU usage of another process.
Use run() to start the process.
Use cpu_usage() to retrieve the latest estimate of CPU usage.'''
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.finished = False
self.started = threading.Semaphore(0)
self.proc = None
# Reported stat.
self.cpu = 0.0
# Remember CPU times of recently seen children.
# This is to prevent double-counting for child processes.
self.current_children = {} # {(pid, create_time): CPU time}
# CPU time of dead children is recorded here.
self.old_children_cpu = 0.0
def run(self):
def update():
total = 0.0
# Fetch process's usage.
try:
if self.proc is None:
self.proc = psutil.Process(self.pid)
total += cpu_time_of(self.proc)
# Fetch children's usage.
new_current_children = {}
for c in self.proc.children(recursive=True):
try:
t = cpu_time_of(c)
new_current_children[(c.pid, c.create_time())] = t
total += t
except psutil.NoSuchProcess:
pass
except psutil.AccessDenied:
pass
# For children that are no longer running, remember their
# most recently recorded CPU time.
reaped_cpu = 0.0
for c_id, c_t in self.current_children.items():
if c_id not in new_current_children:
reaped_cpu += c_t
self.old_children_cpu += reaped_cpu
self.current_children = new_current_children
total += self.old_children_cpu
except psutil.AccessDenied as err:
warnings.warn("access denied: pid=%d" % err.pid, RuntimeWarning)
# Add 1 ns allowance for floating-point rounding, which occurs when we
# accumulate current_children times for dead processes into reaped_cpu.
# (Floating point epsilon is about 1e-15.)
if total + 1e-9 < self.cpu:
try:
cmd = repr(' '.join(self.proc.cmdline()))
except Exception:
cmd = '??'
warnings.warn("cpu non-monotonic: %.15f -> %.15f, pid=%d, cmd=%s" %
(self.cpu, total, self.pid, cmd),
RuntimeWarning)
return total
# Fetch a sample, and notify others that we have started.
self.cpu = update()
self.started.release()
# Poll the process periodically.
#
# We poll quickly at the beginning and use exponential backout
# to try and get better stats on short-lived processes.
#
polling_interval = 0.01
max_interval = 0.5
while not self.finished:
time.sleep(polling_interval)
try:
self.cpu = update()
except psutil.NoSuchProcess:
break
if polling_interval < max_interval:
polling_interval = min(polling_interval * 1.5, max_interval)
def cpu_usage(self):
return self.cpu
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('Usage: %s command args...\n Measure total CPU ' \
'usage of a command' % sys.argv[0], file=sys.stderr)
return -1
# Run the command requested.
try:
p = subprocess.Popen(sys.argv[1:])
except OSError:
print('command not found', file=sys.stderr)
return -1
cpu = 0
m = process_poller(p.pid)
while True:
try:
p.returncode = p.wait()
break
except KeyboardInterrupt:
# The user Ctrl-C-ed us. The child should have received SIGINT;
# continue waiting for it to finish
pass
print('Total cpu %f seconds' % m.cpu_usage(), file=sys.stderr)
return p.returncode
if __name__ == '__main__':
sys.exit(main())