Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Speedup Process.children() #1185

Merged
merged 7 commits into from
Dec 1, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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