summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--psutil/__init__.py50
-rw-r--r--psutil/_common.py63
-rw-r--r--psutil/_psbsd.py6
-rw-r--r--psutil/_pslinux.py11
-rw-r--r--psutil/_psosx.py6
-rw-r--r--psutil/_pssunos.py6
-rw-r--r--psutil/_pswindows.py6
-rw-r--r--psutil/tests/test_misc.py28
-rw-r--r--psutil/tests/test_process.py5
9 files changed, 172 insertions, 9 deletions
diff --git a/psutil/__init__.py b/psutil/__init__.py
index 5a48592b..01f962fb 100644
--- a/psutil/__init__.py
+++ b/psutil/__init__.py
@@ -12,6 +12,7 @@ in Python.
from __future__ import division
import collections
+import contextlib
import errno
import functools
import os
@@ -378,6 +379,7 @@ class Process(object):
self._create_time = None
self._gone = False
self._hash = None
+ self._oneshot_inctx = False
# used for caching on Windows only (on POSIX ppid may change)
self._ppid = None
# platform-specific modules define an _psplatform.Process
@@ -445,6 +447,51 @@ class Process(object):
# --- utility methods
+ @contextlib.contextmanager
+ def oneshot(self):
+ """Utility context manager which considerably speeds up the
+ retrieval of multiple process information at the same time.
+
+ Internally different process info (e.g. name, ppid, uids,
+ gids, ...) may be fetched by using the same routine, but
+ only one information is returned and the others are discarded.
+ When using this context manager the internal routine is
+ executed once (in the example below on name()) and the
+ other info are cached.
+
+ The cache is cleared when exiting the context manager block.
+ The advice is to use this every time you retrieve more than
+ one information about the process. If you're lucky, you'll
+ get a hell of a speedup.
+
+ >>> p = Process()
+ >>> with p.oneshot():
+ ... p.name() # execute internal routine
+ ... p.ppid() # use cached value
+ ... p.uids() # use cached value
+ ... p.gids() # use cached value
+ ...
+ """
+ if self._oneshot_inctx:
+ # NOOP: this covers the use case where the user enters the
+ # context twice. Since as_dict() internally uses oneshot()
+ # I expect that the code below will be a pretty common
+ # "mistake" that the user will make, so let's guard
+ # against that:
+ #
+ # >>> with p.oneshot():
+ # ... p.as_dict()
+ # ...
+ yield
+ else:
+ self._oneshot_inctx = True
+ try:
+ self._proc.oneshot_enter()
+ yield
+ finally:
+ self._oneshot_inctx = False
+ self._proc.oneshot_exit()
+
def as_dict(self, attrs=None, ad_value=None):
"""Utility method returning process information as a
hashable dictionary.
@@ -460,7 +507,8 @@ class Process(object):
"""
excluded_names = set(
['send_signal', 'suspend', 'resume', 'terminate', 'kill', 'wait',
- 'is_running', 'as_dict', 'parent', 'children', 'rlimit'])
+ 'is_running', 'as_dict', 'parent', 'children', 'rlimit',
+ 'oneshot'])
retdict = dict()
ls = set(attrs or [x for x in dir(self)])
for name in ls:
diff --git a/psutil/_common.py b/psutil/_common.py
index 0c612077..e7c831fe 100644
--- a/psutil/_common.py
+++ b/psutil/_common.py
@@ -125,11 +125,8 @@ def memoize(fun):
def cache_clear():
"""Clear cache."""
- lock.acquire()
- try:
+ with lock:
cache.clear()
- finally:
- lock.release()
lock = threading.RLock()
cache = {}
@@ -137,6 +134,64 @@ def memoize(fun):
return wrapper
+def memoize_when_activated(fun):
+ """A memoize decorator which is disabled by default. It can be
+ activated and deactivated on request.
+
+ >>> @memoize
+ ... def foo()
+ ... print(1)
+ ...
+ >>> # deactivated (default)
+ >>> foo()
+ 1
+ >>> foo()
+ 1
+ >>>
+ >>> # activated
+ >>> foo.cache_activate()
+ >>> foo()
+ 1
+ >>> foo()
+ >>> foo()
+ >>>
+ """
+ @functools.wraps(fun)
+ def wrapper(*args, **kwargs):
+ if not wrapper.cache_activated:
+ return fun(*args, **kwargs)
+ else:
+ key = (args, frozenset(sorted(kwargs.items())))
+ with lock:
+ try:
+ return cache[key]
+ except KeyError:
+ ret = cache[key] = fun(*args, **kwargs)
+ return ret
+
+ def cache_clear():
+ """Clear cache."""
+ with lock:
+ cache.clear()
+
+ def cache_activate():
+ """Activate cache."""
+ wrapper.cache_activated = True
+
+ def cache_deactivate():
+ """Deactivate and clear cache."""
+ wrapper.cache_activated = False
+ cache_clear()
+
+ lock = threading.RLock()
+ cache = {}
+ wrapper.cache_activated = False
+ wrapper.cache_activate = cache_activate
+ wrapper.cache_deactivate = cache_deactivate
+ wrapper.cache_clear = cache_clear
+ return wrapper
+
+
def isfile_strict(path):
"""Same as os.path.isfile() but does not swallow EACCES / EPERM
exceptions, see:
diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py
index 76d6d588..f6afae8d 100644
--- a/psutil/_psbsd.py
+++ b/psutil/_psbsd.py
@@ -404,6 +404,12 @@ class Process(object):
self._name = None
self._ppid = None
+ def oneshot_enter(self):
+ pass
+
+ def oneshot_exit(self):
+ pass
+
@wrap_exceptions
def name(self):
return cext.proc_name(self.pid)
diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py
index 7a213a71..b0d44f6b 100644
--- a/psutil/_pslinux.py
+++ b/psutil/_pslinux.py
@@ -25,6 +25,7 @@ from . import _psutil_linux as cext
from . import _psutil_posix as cext_posix
from ._common import isfile_strict
from ._common import memoize
+from ._common import memoize_when_activated
from ._common import parse_environ_block
from ._common import NIC_DUPLEX_FULL
from ._common import NIC_DUPLEX_HALF
@@ -943,6 +944,15 @@ class Process(object):
self._ppid = None
self._procfs_path = get_procfs_path()
+ def oneshot_enter(self):
+ self._parse_stat.cache_activate()
+ self._parse_status.cache_activate()
+
+ def oneshot_exit(self):
+ self._parse_stat.cache_deactivate()
+ self._parse_status.cache_deactivate()
+
+ @memoize_when_activated
def _parse_status(self):
fpath = "%s/%s/status" % (self._procfs_path, self.pid)
ppid = uids = gids = volctx = unvolctx = num_threads = status = None
@@ -978,6 +988,7 @@ class Process(object):
num_threads=num_threads,
status=status)
+ @memoize_when_activated
def _parse_stat(self):
with open_text("%s/%s/stat" % (self._procfs_path, self.pid)) as f:
data = f.read()
diff --git a/psutil/_psosx.py b/psutil/_psosx.py
index 77e5ed58..392d5fe9 100644
--- a/psutil/_psosx.py
+++ b/psutil/_psosx.py
@@ -228,6 +228,12 @@ class Process(object):
self._name = None
self._ppid = None
+ def oneshot_enter(self):
+ pass
+
+ def oneshot_exit(self):
+ pass
+
@wrap_exceptions
def name(self):
return cext.proc_name(self.pid)
diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py
index bca54527..3c75c6f2 100644
--- a/psutil/_pssunos.py
+++ b/psutil/_pssunos.py
@@ -297,6 +297,12 @@ class Process(object):
self._ppid = None
self._procfs_path = get_procfs_path()
+ def oneshot_enter(self):
+ pass
+
+ def oneshot_exit(self):
+ pass
+
@wrap_exceptions
def name(self):
# note: max len == 15
diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py
index 22fccf52..10f31d29 100644
--- a/psutil/_pswindows.py
+++ b/psutil/_pswindows.py
@@ -505,6 +505,12 @@ class Process(object):
self._name = None
self._ppid = None
+ def oneshot_enter(self):
+ pass
+
+ def oneshot_exit(self):
+ pass
+
@wrap_exceptions
def name(self):
"""Return process name, which on Windows is always the final
diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py
index 53dfd899..9c86379c 100644
--- a/psutil/tests/test_misc.py
+++ b/psutil/tests/test_misc.py
@@ -21,6 +21,8 @@ from psutil import OPENBSD
from psutil import OSX
from psutil import POSIX
from psutil import WINDOWS
+from psutil._common import memoize
+from psutil._common import memoize_when_activated
from psutil._common import supports_ipv6
from psutil.tests import APPVEYOR
from psutil.tests import SCRIPTS_DIR
@@ -167,8 +169,6 @@ class TestMisc(unittest.TestCase):
psutil.__version__)
def test_memoize(self):
- from psutil._common import memoize
-
@memoize
def foo(*args, **kwargs):
"foo docstring"
@@ -203,6 +203,30 @@ class TestMisc(unittest.TestCase):
# docstring
self.assertEqual(foo.__doc__, "foo docstring")
+ def test_memoize_when_activated(self):
+ @memoize_when_activated
+ def foo():
+ calls.append(None)
+
+ calls = []
+ foo()
+ foo()
+ self.assertEqual(len(calls), 2)
+
+ # activate
+ calls = []
+ foo.cache_activate()
+ foo()
+ foo()
+ self.assertEqual(len(calls), 1)
+
+ # deactivate
+ calls = []
+ foo.cache_deactivate()
+ foo()
+ foo()
+ self.assertEqual(len(calls), 2)
+
def test_parse_environ_block(self):
from psutil._common import parse_environ_block
diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py
index 56a35def..0e6ff522 100644
--- a/psutil/tests/test_process.py
+++ b/psutil/tests/test_process.py
@@ -1247,7 +1247,8 @@ class TestProcess(unittest.TestCase):
# self.assertFalse(p.pid in psutil.pids(), msg="retcode = %s" %
# retcode)
- excluded_names = ['pid', 'is_running', 'wait', 'create_time']
+ excluded_names = ['pid', 'is_running', 'wait', 'create_time',
+ 'oneshot']
if LINUX and not RLIMIT_SUPPORT:
excluded_names.append('rlimit')
for name in dir(p):
@@ -1532,7 +1533,7 @@ class TestFetchAllProcesses(unittest.TestCase):
excluded_names = set([
'send_signal', 'suspend', 'resume', 'terminate', 'kill', 'wait',
'as_dict', 'cpu_percent', 'parent', 'children', 'pid',
- 'memory_info_ex',
+ 'memory_info_ex', 'oneshot',
])
if LINUX and not RLIMIT_SUPPORT:
excluded_names.add('rlimit')