提交 fafcd176 编写于 作者: N Nicolas Hennion

Merge pull request #444 from desbma/process-tree

......@@ -16,6 +16,9 @@ http://ifup.org/
Jon Renner (aka) Jrenner
https://github.com/jrenner
Maxime Desbrus (aka) desbma
https://github.com/desbma
=========
Packagers
=========
......
......@@ -132,6 +132,8 @@ class GlancesMain(object):
if not is_windows:
parser.add_argument('--hide-kernel-threads', action='store_true', default=False,
dest='no_kernel_threads', help=_('hide kernel threads in process list'))
parser.add_argument('--tree', action='store_true', default=False,
dest='process_tree', help=_('display processes as a tree'))
parser.add_argument('-b', '--byte', action='store_true', default=False,
dest='byte', help=_('display network rate in byte per second'))
parser.add_argument('-1', '--percpu', action='store_true', default=False,
......
......@@ -22,10 +22,188 @@ from glances.core.glances_globals import is_linux, is_bsd, is_mac, is_windows, l
from glances.core.glances_timer import Timer, getTimeSinceLastUpdate
# Import Python lib
import collections
import psutil
import re
class ProcessTreeNode(object):
"""
Represent a process tree.
We avoid recursive algorithm to manipulate the tree because function calls are expensive with CPython.
"""
def __init__(self, process=None, stats=None, sort_key=None, root=False):
self.process = process
self.stats = stats
self.children = []
self.children_sorted = False
self.sort_key = sort_key
self.reverse_sorting = (self.sort_key != "name")
self.is_root = root
def __str__(self):
""" Return the tree as a string for debugging. """
lines = []
nodes_to_print = collections.deque([collections.deque([("#", self)])])
while nodes_to_print:
indent_str, current_node = nodes_to_print[-1].pop()
if not nodes_to_print[-1]:
nodes_to_print.pop()
if current_node.is_root:
lines.append(indent_str)
else:
lines.append("%s[%s]" % (indent_str, current_node.process.name()))
indent_str = " " * (len(lines[-1]) - 1)
children_nodes_to_print = collections.deque()
for child in current_node.children:
if child is current_node.children[-1]:
tree_char = "└─"
else:
tree_char = "├─"
children_nodes_to_print.appendleft((indent_str + tree_char, child))
if children_nodes_to_print:
nodes_to_print.append(children_nodes_to_print)
return "\n".join(lines)
def setSorting(self, key, reverse):
""" Set sorting key or func for user with __iter__ (affects the whole tree from this node). """
if (self.sort_key != key) or (self.reverse_sorting != reverse):
nodes_to_flag_unsorted = collections.deque([self])
while nodes_to_flag_unsorted:
current_node = nodes_to_flag_unsorted.pop()
current_node.children_sorted = False
current_node.sort_key = key
current_node.reverse_sorting = reverse
nodes_to_flag_unsorted.extend(current_node.children)
def getWeight(self):
""" Return "weight" of a process and all its children for sorting. """
if self.sort_key == "name":
return self.stats[self.sort_key]
# sum ressource usage for self and children
total = 0
nodes_to_sum = collections.deque([self])
while nodes_to_sum:
current_node = nodes_to_sum.pop()
if callable(self.sort_key):
total += self.sort_key(current_node.stats)
elif self.sort_key == "io_counters":
stats = current_node.stats[self.sort_key]
total += stats[0] - stats[2] + stats[1] - stats[3]
else:
total += current_node.stats[self.sort_key]
nodes_to_sum.extend(current_node.children)
return total
def __len__(self):
"""Return the number of nodes in the tree."""
total = 0
nodes_to_sum = collections.deque([self])
while nodes_to_sum:
current_node = nodes_to_sum.pop()
if not current_node.is_root:
total += 1
nodes_to_sum.extend(current_node.children)
return total
def __iter__(self):
""" Iterator returning ProcessTreeNode in sorted order, recursively. """
if not self.is_root:
yield self
if not self.children_sorted:
# optimization to avoid sorting twice (once when limiting the maximum processes to grab stats for,
# and once before displaying)
self.children.sort(key=self.__class__.getWeight, reverse=self.reverse_sorting)
self.children_sorted = True
for child in self.children:
for n in iter(child):
yield n
def iterChildren(self, exclude_incomplete_stats=True):
"""
Iterator returning ProcessTreeNode in sorted order (only children of this node, non recursive).
If exclude_incomplete_stats is True, exclude processes not having full statistics.
It can happen after a resort (change of sort key) because process stats are not grabbed immediately,
but only at next full update.
"""
if not self.children_sorted:
# optimization to avoid sorting twice (once when limiting the maximum processes to grab stats for,
# and once before displaying)
self.children.sort(key=self.__class__.getWeight, reverse=self.reverse_sorting)
self.children_sorted = True
for child in self.children:
if (not exclude_incomplete_stats) or ("time_since_update" in child.stats):
yield child
def findProcess(self, process):
""" Search in tree for the ProcessTreeNode owning process, return it or None if not found. """
nodes_to_search = collections.deque([self])
while nodes_to_search:
current_node = nodes_to_search.pop()
if (not current_node.is_root) and (current_node.process.pid == process.pid):
return current_node
nodes_to_search.extend(current_node.children)
@staticmethod
def buildTree(process_dict, sort_key, hide_kernel_threads):
""" Build a process tree using using parent/child relationships, and return the tree root node. """
tree_root = ProcessTreeNode(root=True)
nodes_to_add_last = collections.deque()
# first pass: add nodes whose parent are in the tree
for process, stats in process_dict.items():
new_node = ProcessTreeNode(process, stats, sort_key)
try:
parent_process = process.parent()
except psutil.NoSuchProcess:
# parent is dead, consider no parent
parent_process = None
if parent_process is None:
# no parent, add this node at the top level
tree_root.children.append(new_node)
elif hide_kernel_threads and (not is_windows) and (parent_process.gids().real == 0):
# parent is a kernel thread, add this node at the top level
tree_root.children.append(new_node)
else:
parent_node = tree_root.findProcess(parent_process)
if parent_node is not None:
# parent is already in the tree, add a new child
parent_node.children.append(new_node)
else:
# parent is not in tree, add this node later
nodes_to_add_last.append(new_node)
# next pass(es): add nodes to their parents if it could not be done in previous pass
while nodes_to_add_last:
node_to_add = nodes_to_add_last.popleft() # pop from left and append to right to avoid infinite loop
try:
parent_process = node_to_add.process.parent()
except psutil.NoSuchProcess:
# parent is dead, consider no parent, add this node at the top level
tree_root.children.append(node_to_add)
else:
if parent_process is None:
# parent is None now, but was not at previous pass (can occur on Windows only)
# consider no parent, add this node at the top level
tree_root.children.append(node_to_add)
else:
parent_node = tree_root.findProcess(parent_process)
if parent_node is not None:
# parent is already in the tree, add a new child
parent_node.children.append(node_to_add)
else:
# parent is not in tree, add this node later
nodes_to_add_last.append(node_to_add)
return tree_root
class GlancesProcesses(object):
"""Get processed stats using the psutil library."""
......@@ -46,6 +224,10 @@ class GlancesProcesses(object):
# value = [ read_bytes_old, write_bytes_old ]
self.io_old = {}
# Wether or not to enable process tree
self._enable_tree = False
self.process_tree = None
# Init stats
self.resetsort()
self.processlist = []
......@@ -135,6 +317,14 @@ class GlancesProcesses(object):
""" Ignore kernel threads in process list. """
self.no_kernel_threads = True
def enable_tree(self):
""" Enable process tree. """
self._enable_tree = True
def is_tree_enabled(self):
""" Return True if process tree is enabled, False instead. """
return self._enable_tree
def __get_process_stats(self, proc,
mandatory_stats=True,
standard_stats=True,
......@@ -383,44 +573,66 @@ class GlancesProcesses(object):
except:
pass
# Process optimization
# Only retreive stats for visible processes (get_max_processes)
if self.get_max_processes() is not None:
# Sort the internal dict and cut the top N (Return a list of tuple)
# tuple=key (proc), dict (returned by __get_process_stats)
try:
processiter = sorted(
processdict.items(), key=lambda x: x[1][self.getsortkey()], reverse=True)
except TypeError:
# Fallback to all process (issue #423)
processloop = processdict.items()
first = False
else:
processloop = processiter[0:self.get_max_processes()]
first = True
if self._enable_tree:
self.process_tree = ProcessTreeNode.buildTree(processdict,
self.getsortkey(),
self.no_kernel_threads)
for i, node in enumerate(self.process_tree):
# Only retreive stats for visible processes (get_max_processes)
if (self.get_max_processes() is not None) and (i >= self.get_max_processes()):
break
# add standard stats
new_stats = self.__get_process_stats(node.process,
mandatory_stats=False,
standard_stats=True,
extended_stats=False)
if new_stats is not None:
node.stats.update(new_stats)
# Add a specific time_since_update stats for bitrate
node.stats['time_since_update'] = time_since_update
else:
# Get all processes stats
processloop = processdict.items()
first = False
for i in processloop:
# Already existing mandatory stats
procstat = i[1]
# Process optimization
# Only retreive stats for visible processes (get_max_processes)
if self.get_max_processes() is not None:
# Update with standard stats
# and extended stats but only for TOP (first) process
s = self.__get_process_stats(i[0],
mandatory_stats=False,
standard_stats=True,
extended_stats=first)
if s is None:
continue
procstat.update(s)
# 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
# Sort the internal dict and cut the top N (Return a list of tuple)
# tuple=key (proc), dict (returned by __get_process_stats)
try:
processiter = sorted(
processdict.items(), key=lambda x: x[1][self.getsortkey()], reverse=True)
except TypeError:
# Fallback to all process (issue #423)
processloop = processdict.items()
first = False
else:
processloop = processiter[0:self.get_max_processes()]
first = True
else:
# Get all processes stats
processloop = processdict.items()
first = False
for i in processloop:
# Already existing mandatory stats
procstat = i[1]
if self.get_max_processes() is not None:
# Update with standard stats
# and extended stats but only for TOP (first) process
s = self.__get_process_stats(i[0],
mandatory_stats=False,
standard_stats=True,
extended_stats=first)
if s is None:
continue
procstat.update(s)
# 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
# Clean internals caches if timeout is reached
if self.cache_timer.finished():
......@@ -437,6 +649,10 @@ class GlancesProcesses(object):
"""Get the processlist."""
return self.processlist
def gettree(self):
"""Get the process tree."""
return self.process_tree
def getsortkey(self):
"""Get the current sort key"""
if self.getmanualsortkey() is not None:
......@@ -455,6 +671,8 @@ class GlancesProcesses(object):
def setmanualsortkey(self, sortedby):
"""Set the current sort key for manual sort."""
self.processmanualsort = sortedby
if self._enable_tree and (self.process_tree is not None):
self.process_tree.setSorting(sortedby, sortedby != "name")
return self.processmanualsort
def setautosortkey(self, sortedby):
......
......@@ -49,10 +49,14 @@ class GlancesStandalone(object):
if args.process_filter is not None:
glances_processes.set_process_filter(args.process_filter)
# Ignore kernel threads in process list
if (not is_windows) and args.no_kernel_threads:
# Ignore kernel threads in process list
glances_processes.disable_kernel_threads()
if args.process_tree:
# Enable process tree view
glances_processes.enable_tree()
# Initial system informations update
self.stats.update()
......
......@@ -431,7 +431,7 @@ class _GlancesCurses(object):
# Adapt number of processes to the available space
max_processes_displayed = screen_y - 11 - \
self.get_stats_display_height(stats_alert)
if self.args.enable_process_extended:
if self.args.enable_process_extended and not self.args.process_tree:
max_processes_displayed -= 4
if max_processes_displayed < 0:
max_processes_displayed = 0
......@@ -715,7 +715,14 @@ class _GlancesCurses(object):
pass
else:
# New column
x = x + len(m['msg'])
try:
# Python 2: we need to decode to get real screen size because utf-8 special tree chars
# occupy several bytes
offset = len(m['msg'].decode("utf-8"))
except AttributeError:
# Python 3: strings are strings and bytes are bytes, all is good
offset = len(m['msg'])
x = x + offset
# Compute the next Glances column/line position
self.next_column = max(self.next_column, x + self.space_between_column)
......
......@@ -125,6 +125,7 @@ class Plugin(GlancesPlugin):
else:
msg = _("sorted by {0}").format(glances_processes.getmanualsortkey())
ret.append(self.curse_add_line(msg))
ret[-1]["msg"] += ", %s view" % ("tree" if glances_processes.is_tree_enabled() else "flat")
# Return the message with decoration
return ret
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册