diff options
author | Giampaolo Rodola <g.rodola@gmail.com> | 2017-12-01 14:49:53 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-12-01 14:49:53 +0100 |
commit | f0094db79ad9e4f2246997cb8c2046b71c465a29 (patch) | |
tree | 77421ddf6b10692ce8f636d4635d18ceb8209bff | |
parent | 7c6b6c2a6d89a1a270d6357be3b77fd8e0e2cbe7 (diff) | |
download | psutil-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.rst | 5 | ||||
-rw-r--r-- | docs/index.rst | 8 | ||||
-rw-r--r-- | psutil/__init__.py | 82 | ||||
-rw-r--r-- | psutil/_pslinux.py | 24 |
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. |