From 39be7f554b3f462be3da8c8e665b01ed43c546c5 Mon Sep 17 00:00:00 2001 From: nicolargo Date: Thu, 2 May 2024 18:20:58 +0200 Subject: [PATCH] Stats update is now threaded / Sensors are sorted by label/Alias in the UI --- .github/workflows/build.yml | 3 +- glances/exports/export.py | 18 +++- glances/plugins/plugin/model.py | 10 +- glances/plugins/sensors/__init__.py | 136 +++++++++++++++++----------- glances/stats.py | 39 ++++---- 5 files changed, 131 insertions(+), 75 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a66f9e01..077561ee 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,7 +11,8 @@ env: # Alpine image platform: https://hub.docker.com/_/alpine DOCKER_PLATFORMS: linux/amd64,linux/arm64/v8,linux/arm/v6,linux/arm/v7 # Ubuntu image platforms list: https://hub.docker.com/_/ubuntu - DOCKER_PLATFORMS_UBUNTU: linux/amd64,linux/arm64/v8,linux/arm/v7 + # linux/arm/v7 do not work (Cargo/Rust not available) + DOCKER_PLATFORMS_UBUNTU: linux/amd64,linux/arm64/v8 on: workflow_call: diff --git a/glances/exports/export.py b/glances/exports/export.py index fe710ee0..83753c8c 100644 --- a/glances/exports/export.py +++ b/glances/exports/export.py @@ -14,8 +14,8 @@ I am your father... """ from glances.globals import json_dumps - from glances.globals import NoOptionError, NoSectionError, iteritems, iterkeys +from glances.timer import Counter from glances.logger import logger @@ -58,6 +58,22 @@ class GlancesExport(object): # Save last export list self._last_exported_list = None + def _log_result_decorator(fct): + """Log (DEBUG) the result of the function fct.""" + + def wrapper(*args, **kw): + counter = Counter() + ret = fct(*args, **kw) + duration = counter.get() + logger.debug( + "{} {} {} return {} in {} seconds".format( + args[0].__class__.__name__, args[0].__class__.__module__, fct.__name__, ret, duration + ) + ) + return ret + + return wrapper + def exit(self): """Close the export module.""" logger.debug("Finalise export interface %s" % self.export_name) diff --git a/glances/plugins/plugin/model.py b/glances/plugins/plugin/model.py index e711fb3f..9e94f6ed 100644 --- a/glances/plugins/plugin/model.py +++ b/glances/plugins/plugin/model.py @@ -402,7 +402,7 @@ class GlancesPluginModel(object): def get_stats(self): """Return the stats object in JSON format.""" - return json_dumps(self.stats) + return json_dumps(self.get_raw()) def get_json(self): """Return the stats object in JSON format.""" @@ -413,14 +413,14 @@ class GlancesPluginModel(object): Stats should be a list of dict (processlist, network...) """ - return dictlist(self.stats, item) + return dictlist(self.get_raw(), item) def get_stats_item(self, item): """Return the stats object for a specific item in JSON format. Stats should be a list of dict (processlist, network...) """ - return json_dumps_dictlist(self.stats, item) + return json_dumps_dictlist(self.get_raw(), item) def get_raw_stats_value(self, item, value): """Return the stats object for a specific item=value. @@ -428,13 +428,13 @@ class GlancesPluginModel(object): Return None if the item=value does not exist Return None if the item is not a list of dict """ - if not isinstance(self.stats, list): + if not isinstance(self.get_raw(), list): return None else: if (not isinstance(value, int) and not isinstance(value, float)) and value.isdigit(): value = int(value) try: - return {value: [i for i in self.stats if i[item] == value]} + return {value: [i for i in self.get_raw() if i[item] == value]} except (KeyError, ValueError) as e: logger.error("Cannot get item({})=value({}) ({})".format(item, value, e)) return None diff --git a/glances/plugins/sensors/__init__.py b/glances/plugins/sensors/__init__.py index 1eec639c..70438875 100644 --- a/glances/plugins/sensors/__init__.py +++ b/glances/plugins/sensors/__init__.py @@ -11,6 +11,7 @@ import psutil import warnings +import threading from glances.logger import logger from glances.globals import iteritems, to_fahrenheit @@ -26,6 +27,12 @@ SENSOR_TEMP_UNIT = 'C' SENSOR_FAN_TYPE = 'fan_speed' SENSOR_FAN_UNIT = 'R' +SENSOR_HDDTEMP_TYPE = 'temperature_hdd' +SENSOR_HDDTEMP_UNIT = 'C' + +SENSORS_BATTERY_TYPE = 'battery' +SENSORS_BATTERY_UNIT = '%' + # Define the default refresh multiplicator # Default value is 3 * Glances refresh time # Can be overwritten by the refresh option in the sensors section of the glances.conf file @@ -66,8 +73,8 @@ class PluginModel(GlancesPluginModel): """Glances sensors plugin. The stats list includes both sensors and hard disks stats, if any. - The sensors are already grouped by chip type and then sorted by name. - The hard disks are already sorted by name. + The sensors are already grouped by chip type and then sorted by label. + The hard disks are already sorted by label. """ def __init__(self, args=None, config=None): @@ -108,6 +115,55 @@ class PluginModel(GlancesPluginModel): """Return the key of the list.""" return 'label' + def __get_temperature(self, stats, index): + try: + temperature = self.__set_type(self.glances_grab_sensors.get(SENSOR_TEMP_TYPE), SENSOR_TEMP_TYPE) + except Exception as e: + logger.error("Cannot grab sensors temperatures (%s)" % e) + else: + stats[index] = self.__transform_sensors(temperature) + + def __get_fan_speed(self, stats, index): + try: + fan_speed = self.__set_type(self.glances_grab_sensors.get(SENSOR_FAN_TYPE), SENSOR_FAN_TYPE) + except Exception as e: + logger.error("Cannot grab FAN speed (%s)" % e) + else: + stats[index] = self.__transform_sensors(fan_speed) + + def __get_hddtemp(self, stats, index): + try: + hddtemp = self.__set_type(self.hddtemp_plugin.update(), SENSOR_HDDTEMP_TYPE) + except Exception as e: + logger.error("Cannot grab HDD temperature (%s)" % e) + else: + stats[index] = self.__transform_sensors(hddtemp) + + def __get_bat_percent(self, stats, index): + try: + bat_percent = self.__set_type(self.batpercent_plugin.update(), SENSORS_BATTERY_TYPE) + except Exception as e: + logger.error("Cannot grab battery percent (%s)" % e) + else: + stats[index] = self.__transform_sensors(bat_percent) + + def __transform_sensors(self, threads_stats): + """Hide, alias and sort the result""" + stats_transformed = [] + for stat in threads_stats: + # Hide sensors configured in the hide ou show configuration key + if not self.is_display(stat["label"].lower()): + continue + # Set alias for sensors + stat["label"] = self.__get_alias(stat) + # Add the stat to the stats_transformed list + stats_transformed.append(stat) + # Remove duplicates thanks to https://stackoverflow.com/a/9427216/1919431 + stats_transformed = [dict(t) for t in {tuple(d.items()) for d in stats_transformed}] + # Sort by label + stats_transformed = sorted(stats_transformed, key=lambda d: d['label']) + return stats_transformed + @GlancesPluginModel._check_decorator @GlancesPluginModel._log_result_decorator def update(self): @@ -116,60 +172,38 @@ class PluginModel(GlancesPluginModel): stats = self.get_init_value() if self.input_method == 'local': - # Update stats using the dedicated lib - stats = [] - # Get the temperature - try: - temperature = self.__set_type(self.glances_grab_sensors.get(SENSOR_TEMP_TYPE), SENSOR_TEMP_TYPE) - except Exception as e: - logger.error("Cannot grab sensors temperatures (%s)" % e) - else: - # Append temperature - stats.extend(temperature) - # Get the FAN speed - try: - fan_speed = self.__set_type(self.glances_grab_sensors.get(SENSOR_FAN_TYPE), SENSOR_FAN_TYPE) - except Exception as e: - logger.error("Cannot grab FAN speed (%s)" % e) - else: - # Append FAN speed - stats.extend(fan_speed) - # Update HDDtemp stats - try: - hddtemp = self.__set_type(self.hddtemp_plugin.update(), 'temperature_hdd') - except Exception as e: - logger.error("Cannot grab HDD temperature (%s)" % e) - else: - # Append HDD temperature - stats.extend(hddtemp) - # Update batteries stats - try: - bat_percent = self.__set_type(self.batpercent_plugin.update(), 'battery') - except Exception as e: - logger.error("Cannot grab battery percent (%s)" % e) - else: - # Append Batteries % - stats.extend(bat_percent) - + threads_stats = [None] * 4 + threads = [ + threading.Thread(name=SENSOR_TEMP_TYPE, + target=self.__get_temperature, + args=(threads_stats, 0)), + threading.Thread(name=SENSOR_FAN_TYPE, + target=self.__get_fan_speed, + args=(threads_stats, 1)), + threading.Thread(name=SENSOR_HDDTEMP_TYPE, + target=self.__get_hddtemp, + args=(threads_stats, 2)), + threading.Thread(name=SENSORS_BATTERY_TYPE, + target=self.__get_bat_percent, + args=(threads_stats, 3)) + ] + # Start threads in // + for t in threads: + t.start() + # Wait threads are finished + for t in threads: + t.join() + # Merge the results + for s in threads_stats: + stats.extend(s) elif self.input_method == 'snmp': # Update stats using SNMP # No standard: # http://www.net-snmp.org/wiki/index.php/Net-SNMP_and_lm-sensors_on_Ubuntu_10.04 pass - # Global change on stats - stats_transformed = [] - for stat in stats: - # Hide sensors configured in the hide ou show configuration key - if not self.is_display(stat["label"].lower()): - continue - # Set alias for sensors - stat["label"] = self.__get_alias(stat) - # Add the stat to the stats_transformed list - stats_transformed.append(stat) - # Update the stats - self.stats = stats_transformed + self.stats = stats return self.stats @@ -270,7 +304,7 @@ class PluginModel(GlancesPluginModel): # Stats for i in self.stats: # Do not display anything if no battery are detected - if i['type'] == 'battery' and i['value'] == []: + if i['type'] == SENSORS_BATTERY_TYPE and i['value'] == []: continue # New line ret.append(self.curse_new_line()) @@ -282,7 +316,7 @@ class PluginModel(GlancesPluginModel): self.curse_add_line(msg, self.get_views(item=i[self.get_key()], key='value', option='decoration')) ) else: - if args.fahrenheit and i['type'] != 'battery' and i['type'] != SENSOR_FAN_TYPE: + if args.fahrenheit and i['type'] != SENSORS_BATTERY_TYPE and i['type'] != SENSOR_FAN_TYPE: trend = '' value = to_fahrenheit(i['value']) unit = 'F' diff --git a/glances/stats.py b/glances/stats.py index 336476e7..8da68ba3 100644 --- a/glances/stats.py +++ b/glances/stats.py @@ -50,7 +50,7 @@ class GlancesStats(object): # Check if the attribute starts with 'get' if item.startswith('getViews'): # Get the plugin name - plugname = item[len('getViews') :].lower() + plugname = item[len('getViews'):].lower() # Get the plugin instance plugin = self._plugins[plugname] if hasattr(plugin, 'get_json_views'): @@ -61,7 +61,7 @@ class GlancesStats(object): raise AttributeError(item) elif item.startswith('get'): # Get the plugin name - plugname = item[len('get') :].lower() + plugname = item[len('get'):].lower() # Get the plugin instance plugin = self._plugins[plugname] if hasattr(plugin, 'get_stats'): @@ -260,21 +260,26 @@ class GlancesStats(object): for p in self._plugins: self._plugins[p].load_limits(config) + def __update_plugin(self, p): + """Update stats, history and views for the given plugin name p""" + self._plugins[p].update() + self._plugins[p].update_stats_history() + self._plugins[p].update_views() + def update(self): - """Wrapper method to update the stats.""" - # For standalone and server modes - # For each plugins, call the update method - for p in self._plugins: - if self._plugins[p].is_disabled(): - # If current plugin is disable - # then continue to next plugin - continue - # Update the stats... - self._plugins[p].update() - # ... the history - self._plugins[p].update_stats_history() - # ... and the views - self._plugins[p].update_views() + """Wrapper method to update the stats. + + Only called by standalone and server modes + """ + threads = [] + # Start update of all enable plugins + for p in self.getPluginsList(enable=True): + thread = threading.Thread(target=self.__update_plugin, args=(p,)) + thread.start() + threads.append(thread) + # Wait the end of the update + for t in threads: + t.join() def export(self, input_stats=None): """Export all the stats. @@ -288,7 +293,7 @@ class GlancesStats(object): input_stats = input_stats or {} - for e in self._exports: + for e in self.getExportsList(enable=True): logger.debug("Export stats using the %s module" % e) thread = threading.Thread(target=self._exports[e].update, args=(input_stats,)) thread.start()