summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGiampaolo Rodola <g.rodola@gmail.com>2017-12-01 14:49:53 +0100
committerGitHub <noreply@github.com>2017-12-01 14:49:53 +0100
commitf0094db79ad9e4f2246997cb8c2046b71c465a29 (patch)
tree77421ddf6b10692ce8f636d4635d18ceb8209bff
parent7c6b6c2a6d89a1a270d6357be3b77fd8e0e2cbe7 (diff)
downloadpsutil-f0094db79ad9e4f2246997cb8c2046b71c465a29.tar.gz
Speedup Process.children() (#1185)
* update HISTORY * update doc * #1183: speedup Process.children() by 2.2x * fix windows err * #1083 / #1084: implement linux-specific ppid_map() function speending things up from 2x to 2.4x * add ESRCH to err handling * update doc
-rw-r--r--HISTORY.rst5
-rw-r--r--docs/index.rst8
-rw-r--r--psutil/__init__.py82
-rw-r--r--psutil/_pslinux.py24
4 files changed, 72 insertions, 47 deletions
diff --git a/HISTORY.rst b/HISTORY.rst
index bfaa2568..8bc3f784 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -10,6 +10,7 @@
- 1173_: introduced PSUTIL_DEBUG environment variable which can be set in order
to print useful debug messages on stderr (useful in case of nasty errors).
- 1177_: added support for sensors_battery() on OSX. (patch by Arnon Yaari)
+- 1183_: Process.children() is 2x faster on UNIX and 2.4x faster on Linux.
**Bug fixes**
@@ -17,8 +18,8 @@
- 1169_: [Linux] users() "hostname" returns username instead. (patch by
janderbrain)
- 1172_: [Windows] `make test` does not work.
-- 1179_: [Linux] Process.cmdline() correctly splits cmdline args for
- misbehaving processes who overwrite /proc/pid/cmdline by using spaces
+- 1179_: [Linux] Process.cmdline() is now able to splits cmdline args for
+ misbehaving processes which overwrite /proc/pid/cmdline and use spaces
instead of null bytes as args separator.
- 1181_: [OSX] Process.memory_maps() may raise ENOENT.
diff --git a/docs/index.rst b/docs/index.rst
index d34a1c45..ea2384bb 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -1716,9 +1716,9 @@ Process class
.. method:: children(recursive=False)
- Return the children of this process as a list of :Class:`Process` objects,
- preemptively checking whether PID has been reused. If recursive is `True`
- return all the parent descendants.
+ Return the children of this process as a list of :class:`Process`
+ instances.
+ If recursive is `True` return all the parent descendants.
Pseudo code example assuming *A == this process*:
::
@@ -1738,7 +1738,7 @@ Process class
Note that in the example above if process X disappears process Y won't be
returned either as the reference to process A is lost.
This concept is well summaried by this
- `unit test <https://github.com/giampaolo/psutil/blob/fb9ae861cf3cf175c3da4a3cd4e558c6cbd6af91/psutil/tests/test_process.py#L1236-L1247>`__.
+ `unit test <https://github.com/giampaolo/psutil/blob/65a52341b55faaab41f68ebc4ed31f18f0929754/psutil/tests/test_process.py#L1064-L1075>`__.
See also how to `kill a process tree <#kill-process-tree>`__ and
`terminate my children <#terminate-my-children>`__.
diff --git a/psutil/__init__.py b/psutil/__init__.py
index c8da0a3d..a8447973 100644
--- a/psutil/__init__.py
+++ b/psutil/__init__.py
@@ -250,10 +250,31 @@ if (int(__version__.replace('.', '')) !=
# =====================================================================
-# --- Process class
+# --- Utils
# =====================================================================
+if hasattr(_psplatform, 'ppid_map'):
+ # Faster version (Windows and Linux).
+ _ppid_map = _psplatform.ppid_map
+else:
+ def _ppid_map():
+ """Return a {pid: ppid, ...} dict for all running processes in
+ one shot. Used to speed up Process.children().
+ """
+ ret = {}
+ for pid in pids():
+ try:
+ proc = _psplatform.Process(pid)
+ ppid = proc.ppid()
+ except (NoSuchProcess, AccessDenied):
+ # Note: AccessDenied is unlikely to happen.
+ pass
+ else:
+ ret[pid] = ppid
+ return ret
+
+
def _assert_pid_not_reused(fun):
"""Decorator which raises NoSuchProcess in case a process is no
longer running or its PID has been reused.
@@ -266,6 +287,11 @@ def _assert_pid_not_reused(fun):
return wrapper
+# =====================================================================
+# --- Process class
+# =====================================================================
+
+
class Process(object):
"""Represents an OS process with the given PID.
If PID is omitted current process PID (os.getpid()) is used.
@@ -848,55 +874,29 @@ class Process(object):
process Y won't be listed as the reference to process A
is lost.
"""
- if hasattr(_psplatform, 'ppid_map'):
- # Windows only: obtain a {pid:ppid, ...} dict for all running
- # processes in one shot (faster).
- ppid_map = _psplatform.ppid_map()
- else:
- ppid_map = None
-
+ ppid_map = _ppid_map()
ret = []
if not recursive:
- if ppid_map is None:
- # 'slow' version, common to all platforms except Windows
- for p in process_iter():
+ for pid, ppid in ppid_map.items():
+ if ppid == self.pid:
try:
- if p.ppid() == self.pid:
- # if child happens to be older than its parent
- # (self) it means child's PID has been reused
- if self.create_time() <= p.create_time():
- ret.append(p)
+ child = Process(pid)
+ # if child happens to be older than its parent
+ # (self) it means child's PID has been reused
+ if self.create_time() <= child.create_time():
+ ret.append(child)
except (NoSuchProcess, ZombieProcess):
pass
- else: # pragma: no cover
- # Windows only (faster)
- for pid, ppid in ppid_map.items():
- if ppid == self.pid:
- try:
- child = Process(pid)
- # if child happens to be older than its parent
- # (self) it means child's PID has been reused
- if self.create_time() <= child.create_time():
- ret.append(child)
- except (NoSuchProcess, ZombieProcess):
- pass
else:
# construct a dict where 'values' are all the processes
# having 'key' as their parent
table = collections.defaultdict(list)
- if ppid_map is None:
- for p in process_iter():
- try:
- table[p.ppid()].append(p)
- except (NoSuchProcess, ZombieProcess):
- pass
- else: # pragma: no cover
- for pid, ppid in ppid_map.items():
- try:
- p = Process(pid)
- table[ppid].append(p)
- except (NoSuchProcess, ZombieProcess):
- pass
+ for pid, ppid in ppid_map.items():
+ try:
+ p = Process(pid)
+ table[ppid].append(p)
+ except (NoSuchProcess, ZombieProcess):
+ pass
# At this point we have a mapping table where table[self.pid]
# are the current process' children.
# Below, we look for all descendants recursively, similarly
diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py
index a5a3fd89..b57adb34 100644
--- a/psutil/_pslinux.py
+++ b/psutil/_pslinux.py
@@ -1356,6 +1356,30 @@ def pid_exists(pid):
return pid in pids()
+def ppid_map():
+ """Obtain a {pid: ppid, ...} dict for all running processes in
+ one shot. Used to speed up Process.children().
+ """
+ ret = {}
+ procfs_path = get_procfs_path()
+ for pid in pids():
+ try:
+ with open_binary("%s/%s/stat" % (procfs_path, pid)) as f:
+ data = f.read()
+ except EnvironmentError as err:
+ # Note: we should be able to access /stat for all processes
+ # so we won't bump into EPERM, which is good.
+ if err.errno not in (errno.ENOENT, errno.ESRCH,
+ errno.EPERM, errno.EACCES):
+ raise
+ else:
+ rpar = data.rfind(b')')
+ dset = data[rpar + 2:].split()
+ ppid = int(dset[1])
+ ret[pid] = ppid
+ return ret
+
+
def wrap_exceptions(fun):
"""Decorator which translates bare OSError and IOError exceptions
into NoSuchProcess and AccessDenied.