Skip to content

Commit

Permalink
Speedup Process.children() (#1185)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
giampaolo authored Dec 1, 2017
1 parent 7c6b6c2 commit f0094db
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 47 deletions.
5 changes: 3 additions & 2 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,16 @@
- 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**

- 1152_: [Windows] disk_io_counters() may return an empty dict.
- 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.

Expand Down
8 changes: 4 additions & 4 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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*:
::

Expand All @@ -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>`__.

Expand Down
82 changes: 41 additions & 41 deletions psutil/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,10 +250,31 @@


# =====================================================================
# --- 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.
Expand All @@ -266,6 +287,11 @@ def wrapper(self, *args, **kwargs):
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.
Expand Down Expand Up @@ -848,55 +874,29 @@ def children(self, recursive=False):
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
Expand Down
24 changes: 24 additions & 0 deletions psutil/_pslinux.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit f0094db

Please sign in to comment.