diff options
-rw-r--r-- | psutil/__init__.py | 50 | ||||
-rw-r--r-- | psutil/_common.py | 63 | ||||
-rw-r--r-- | psutil/_psbsd.py | 6 | ||||
-rw-r--r-- | psutil/_pslinux.py | 11 | ||||
-rw-r--r-- | psutil/_psosx.py | 6 | ||||
-rw-r--r-- | psutil/_pssunos.py | 6 | ||||
-rw-r--r-- | psutil/_pswindows.py | 6 | ||||
-rw-r--r-- | psutil/tests/test_misc.py | 28 | ||||
-rw-r--r-- | psutil/tests/test_process.py | 5 |
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') |