diff --git a/glances/plugins/processlist/__init__.py b/glances/plugins/processlist/__init__.py index 2e084490..5bd4597f 100644 --- a/glances/plugins/processlist/__init__.py +++ b/glances/plugins/processlist/__init__.py @@ -122,6 +122,9 @@ class ProcesslistPlugin(GlancesPluginModel): stats is a list """ + # True between a refresh and the first read of the views, see update_views() + _views_pending = False + # Default list of processes stats to be grabbed / displayed # Can be altered by glances_processes.disable_stats enable_stats = [ @@ -259,6 +262,34 @@ class ProcesslistPlugin(GlancesPluginModel): return self.stats + def update_views(self): + """Note that the views are out of date, without building them. + + The list holds every process on the machine, while the UI shows a few dozen rows and + msg_curse() builds its own decorations rather than reading the views. Only the API + asks for them, so building them on every refresh is work nobody collects. + """ + self._views_pending = True + self.views = {} + return self.views + + def get_views(self, item=None, key=None, option=None): + """Build the views if the last refresh left them pending, then answer as usual.""" + if self._views_pending: + self._views_pending = False + super().update_views() + return super().get_views(item=item, key=key, option=option) + + def set_views(self, input_views): + """Views handed to us replace whatever a pending build would have produced.""" + self._views_pending = False + super().set_views(input_views) + + def reset_views(self): + """A reset must stay reset; without this the next read would rebuild them.""" + self._views_pending = False + super().reset_views() + def get_api(self): """Return the sorted processes list for the API.""" return glances_processes.get_list(sorted=True) diff --git a/tests/test_lazy_views.py b/tests/test_lazy_views.py new file mode 100644 index 00000000..1815d929 --- /dev/null +++ b/tests/test_lazy_views.py @@ -0,0 +1,75 @@ +"""Tests for the lazily built process views.""" + +import json + +import pytest + + +def make_process(pid): + return { + 'pid': pid, + 'key': 'pid', + 'name': f'proc{pid}', + 'cmdline': [f'proc{pid}'], + 'username': 'someone', + 'status': 'S', + 'nice': 0, + 'num_threads': 1, + 'cpu_percent': 0.0, + 'memory_percent': 0.1, + 'memory_info': {'rss': 1024, 'vms': 2048}, + 'cpu_times': {'user': 1.0, 'system': 1.0}, + 'io_counters': [0, 0, 0, 0, 0], + 'time_since_update': 1.0, + } + + +@pytest.fixture +def plugin(glances_stats): + """The shared processlist plugin, fed a fixed list and put back afterwards. + + Building a plugin here instead would write Mock attributes into the glances_processes + singleton and break every later test that reads it. + """ + p = glances_stats.get_plugin('processlist') + saved = (p.stats, p.views, getattr(p, '_views_pending', False)) + p.stats = [make_process(pid) for pid in range(10)] + yield p + p.stats, p.views, pending = saved + p._views_pending = pending + + +def test_refresh_builds_nothing(plugin): + plugin.update_views() + assert plugin.views == {} + + +def test_first_read_builds_the_views(plugin): + plugin.update_views() + views = plugin.get_views() + assert views[4]['cpu_percent']['decoration'] is not None + assert len(views) == 10 + + +def test_second_read_reuses_the_built_views(plugin): + plugin.update_views() + first = plugin.get_views() + assert plugin.get_views() is first + + +def test_json_contains_every_process(plugin): + plugin.update_views() + assert len(json.loads(plugin.get_json_views())) == 10 + + +def test_reset_stays_reset(plugin): + """Without clearing the pending flag the next read would rebuild what was reset.""" + plugin.update_views() + plugin.reset_views() + assert plugin.get_views() == {} + + +def test_set_views_is_not_overwritten(plugin): + plugin.update_views() + plugin.set_views({'given': 'by the server'}) + assert plugin.get_views() == {'given': 'by the server'}