diff --git a/glances/core/glances_globals.py b/glances/core/glances_globals.py index 3ad4c3d3..6a9ecda6 100644 --- a/glances/core/glances_globals.py +++ b/glances/core/glances_globals.py @@ -66,11 +66,10 @@ logger = glancesLogger() # Instances shared between all Glances scripts # ============================================ -# glances_processes for processcount and processlist plugins +# Glances_processes for processcount and processlist plugins from glances.core.glances_processes import GlancesProcesses glances_processes = GlancesProcesses() # The global instance for the logs from glances.core.glances_logs import GlancesLogs glances_logs = GlancesLogs() - diff --git a/glances/core/glances_main.py b/glances/core/glances_main.py index 7dc2f66a..c7b9b90a 100644 --- a/glances/core/glances_main.py +++ b/glances/core/glances_main.py @@ -74,6 +74,8 @@ class GlancesMain(object): dest='disable_sensors', help=_('disable sensors module')) parser.add_argument('--disable-process', action='store_true', default=False, dest='disable_process', help=_('disable process module')) + parser.add_argument('--disable-process-extended', action='store_true', default=False, + dest='disable_process_extended', help=_('disable extended stats on top process')) parser.add_argument('--disable-log', action='store_true', default=False, dest='disable_log', help=_('disable log module')) # CSV output feature diff --git a/glances/core/glances_processes.py b/glances/core/glances_processes.py index 48f214dc..ef32e138 100644 --- a/glances/core/glances_processes.py +++ b/glances/core/glances_processes.py @@ -52,6 +52,9 @@ class GlancesProcesses(object): # Default is to enable the processes stats self.disable_tag = False + # Extended stats for top process is enable by default + self.disable_extended_tag = False + # Maximum number of processes showed in the UI interface # None if no limit self.max_processes = None @@ -65,6 +68,15 @@ class GlancesProcesses(object): """Disable process stats.""" self.disable_tag = True + def enable_extended(self): + """Enable extended process stats.""" + self.disable_extended_tag = False + self.update() + + def disable_extended(self): + """Disable extended process stats.""" + self.disable_extended_tag = True + def set_max_processes(self, value): """Set the maximum number of processes showed in the UI interfaces""" self.max_processes = value @@ -84,14 +96,16 @@ class GlancesProcesses(object): => cpu_percent, memory_percent, io_counters, name standard_stats: for all the displayed processes => username, cmdline, status, memory_info, cpu_times - extended_stats: only for top processes (!!! to be implemented) - => connections (UDP/TCP), memory_swap + extended_stats: only for top processes (see issue #403) + => connections (UDP/TCP), memory_swap... """ # Process ID (always) procstat = proc.as_dict(attrs=['pid']) if mandatory_stats: + procstat['mandatory_stats'] = True + # Process CPU, MEM percent and name procstat.update(proc.as_dict(attrs=['cpu_percent', 'memory_percent', 'name'], ad_value='')) @@ -126,6 +140,8 @@ class GlancesProcesses(object): procstat['io_counters'] += [io_tag] if standard_stats: + procstat['standard_stats'] = True + # Process username (cached with internal cache) try: self.username_cache[procstat['pid']] @@ -154,21 +170,50 @@ class GlancesProcesses(object): procstat.update(proc.as_dict(attrs=['status', 'nice', 'memory_info', 'cpu_times'])) procstat['status'] = str(procstat['status'])[:1].upper() - if extended_stats: - # Process network connections (TCP and UDP) (Experimental) + if extended_stats and not self.disable_extended_tag: + procstat['extended_stats'] = True + + # CPU affinity + # Memory extended + # Number of context switch + # Number of file descriptors (Unix only) + # Threads number + procstat.update(proc.as_dict(attrs=['cpu_affinity', + 'memory_info_ex', + 'num_ctx_switches', + 'num_fds', + 'num_threads'])) + + # Number of handles (Windows only) + if is_windows: + procstat.update(proc.as_dict(attrs=['num_handles'])) + else: + procstat['num_handles'] = None + + # SWAP memory (Only on Linux based OS) + # http://www.cyberciti.biz/faq/linux-which-process-is-using-swap/ + if is_linux: + try: + procstat['memory_swap'] = sum([v.swap for v in proc.memory_maps()]) + except psutil.AccessDenied: + procstat['memory_swap'] = None + + # Process network connections (TCP and UDP) try: procstat['tcp'] = len(proc.connections(kind="tcp")) procstat['udp'] = len(proc.connections(kind="udp")) except: - procstat['tcp'] = 0 - procstat['udp'] = 0 + procstat['tcp'] = None + procstat['udp'] = None - # SWAP memory - # Only on Linux based OS - # http://www.cyberciti.biz/faq/linux-which-process-is-using-swap/ - if is_linux: - logger.debug(proc.memory_maps()) - procstat['memory_swap'] = sum([ v.swap for v in proc.memory_maps() ]) + # IO Nice + # http://pythonhosted.org/psutil/#psutil.Process.ionice + if is_linux or is_windows: + procstat.update(proc.as_dict(attrs=['ionice'])) + else: + procstat['ionice'] = None + + #logger.debug(procstat) return procstat @@ -220,17 +265,22 @@ class GlancesProcesses(object): # Sort the internal dict and cut the top N (Return a list of tuple) # tuple=key (proc), dict (returned by __get_process_stats) processiter = sorted(processdict.items(), key=lambda x: x[1][self.getsortkey()], reverse=True) + first = True for i in processiter[0:self.get_max_processes()]: # Already existing mandatory stats procstat = i[1] # Update with standard stats + # and extended stats but only for TOP (first) process procstat.update(self.__get_process_stats(i[0], mandatory_stats=False, - standard_stats=True)) + standard_stats=True, + extended_stats=first)) # Add a specific time_since_update stats for bitrate procstat['time_since_update'] = time_since_update # Update process list self.processlist.append(procstat) + # Next... + first = False else: # Get all the processes for i in processdict.items(): diff --git a/glances/core/glances_standalone.py b/glances/core/glances_standalone.py index d40a7216..f74f120d 100644 --- a/glances/core/glances_standalone.py +++ b/glances/core/glances_standalone.py @@ -43,6 +43,14 @@ class GlancesStandalone(object): logger.warning(_("Maximum displayed processes is not configured (high CPU consumption)")) glances_processes.set_max_processes(max_processes) + # If process extended stats is disabled by user + if args.disable_process_extended: + logger.info(_("Extended stats for top process is disabled")) + glances_processes.disable_extended() + else: + logger.debug(_("Extended stats for top process is enabled (default behavor)")) + glances_processes.enable_extended() + # Initial system informations update self.stats.update() diff --git a/glances/outputs/glances_curses.py b/glances/outputs/glances_curses.py index 046dfefd..5784cf29 100644 --- a/glances/outputs/glances_curses.py +++ b/glances/outputs/glances_curses.py @@ -215,6 +215,13 @@ class GlancesCurses(object): elif self.pressedkey == ord('d'): # 'd' > Show/hide disk I/O stats self.args.disable_diskio = not self.args.disable_diskio + elif self.pressedkey == ord('e'): + # 'e' > Enable/Disable extended stats for top process + self.args.disable_process_extended = not self.args.disable_process_extended + if self.args.disable_process_extended: + glances_processes.disable_extended() + else: + glances_processes.enable_extended() elif self.pressedkey == ord('f'): # 'f' > Show/hide fs stats self.args.disable_fs = not self.args.disable_fs diff --git a/glances/plugins/glances_help.py b/glances/plugins/glances_help.py index 17a66250..9ceff591 100644 --- a/glances/plugins/glances_help.py +++ b/glances/plugins/glances_help.py @@ -124,7 +124,9 @@ class Plugin(GlancesPlugin): msg = msg_col2.format("r", _("Reset history")) ret.append(self.curse_add_line(msg)) ret.append(self.curse_new_line()) - msg = msg_col.format("q", _("Quit (Esc and Ctrl-C also work)")) + msg = msg_col.format("e", _("Enable/disable top extended stats")) + ret.append(self.curse_add_line(msg)) + msg = msg_col2.format("q", _("Quit (Esc and Ctrl-C also work)")) ret.append(self.curse_add_line(msg)) # Return the message with decoration diff --git a/glances/plugins/glances_plugin.py b/glances/plugins/glances_plugin.py index cc94261d..29071ed3 100644 --- a/glances/plugins/glances_plugin.py +++ b/glances/plugins/glances_plugin.py @@ -40,7 +40,7 @@ class GlancesPlugin(object): """Init the plugin of plugins class.""" # Plugin name (= module name without glances_) self.plugin_name = self.__class__.__module__[len('glances_'):] - logger.debug(_("Init plugin %s") % self.plugin_name) + # logger.debug(_("Init plugin %s") % self.plugin_name) # Init the args self.args = args diff --git a/glances/plugins/glances_processlist.py b/glances/plugins/glances_processlist.py index de129e03..8cd4e2ef 100644 --- a/glances/plugins/glances_processlist.py +++ b/glances/plugins/glances_processlist.py @@ -110,6 +110,7 @@ class Plugin(GlancesPlugin): tag_proc_time = True # Loop over processes (sorted by the sort key previously compute) + first = True for p in self.sortlist(process_sort_key): ret.append(self.curse_new_line()) # CPU @@ -237,6 +238,78 @@ class Plugin(GlancesPlugin): except UnicodeEncodeError: ret.append(self.curse_add_line("", splittable=True)) + # Add extended stats but only for the top processes + # !!! CPU consumption ??? + # TODO: extended stats into the web interface + if first and 'extended_stats' in p: + # Left padding + xpad = ' ' * 13 + # First line is CPU affinity + if p['cpu_affinity'] is not None: + ret.append(self.curse_new_line()) + msg = xpad + _('CPU affinity: ') + str(len(p['cpu_affinity'])) + _(' cores') + ret.append(self.curse_add_line(msg)) + # Second line is memory info + if p['memory_info_ex'] is not None: + ret.append(self.curse_new_line()) + msg = xpad + _('Memory info: ') + for k, v in p['memory_info_ex']._asdict().items(): + # Ignore rss and vms (already displayed) + if k not in ['rss', 'vms'] and v is not None: + msg += k + ' ' + self.auto_unit(v, low_precision=False) + ' ' + if p['memory_swap'] is not None: + msg += _('swap ') + self.auto_unit(p['memory_swap'], low_precision=False) + ret.append(self.curse_add_line(msg)) + # Third line is for openned files/network sessions + ret.append(self.curse_new_line()) + msg = xpad + _('Openned: ') + if p['num_threads'] is not None: + msg += _('threads ') + str(p['num_threads']) + ' ' + if p['num_fds'] is not None: + msg += _('files ') + str(p['num_fds']) + ' ' + if p['num_handles'] is not None: + msg += _('handles ') + str(p['num_handles']) + ' ' + if p['tcp'] is not None: + msg += _('TCP ') + str(p['tcp']) + ' ' + if p['udp'] is not None: + msg += _('UDP ') + str(p['udp']) + ' ' + ret.append(self.curse_add_line(msg)) + # Fouth line is IO nice level (only Linux and Windows OS) + if p['ionice'] is not None: + ret.append(self.curse_new_line()) + msg = xpad + _('IO nice: ') + k = _('Class is ') + v = p['ionice'].ioclass + # Linux: The scheduling class. 0 for none, 1 for real time, 2 for best-effort, 3 for idle. + # Windows: On Windows only ioclass is used and it can be set to 2 (normal), 1 (low) or 0 (very low). + if is_windows: + if v == 0: + msg += k + 'Very Low' + elif v == 1: + msg += k + 'Low' + elif v == 2: + msg += _('No specific I/O priority') + else: + msg += k + str(v) + else: + if v == 0: + msg += _('No specific I/O priority') + elif v == 1: + msg += k + 'Real Time' + elif v == 2: + msg += k + 'Best Effort' + elif v == 3: + msg += k + 'IDLE' + else: + msg += k + str(v) + # value is a number which goes from 0 to 7. + # The higher the value, the lower the I/O priority of the process. + if hasattr(p['ionice'], 'value') and p['ionice'].value != 0: + msg += _(' (value %s/7)') % str(p['ionice'].value) + ret.append(self.curse_add_line(msg)) + # End of extended stats + first = False + # Return the message with decoration return ret