Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show CPU usage, sorting and reverse sorting #65

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 63 additions & 36 deletions ptop/interfaces/GUI.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,26 @@
'''
Graphical User Interface for ptop
'''
from enum import Enum

import npyscreen, math, drawille
import psutil, logging, weakref, sys
from ptop.utils import ThreadJob
from ptop.constants import SYSTEM_USERS, SUPPORTED_THEMES


class SortOption(Enum):
PROCESS_RELEVANCE = 0
MEMORY = 1
MEMORY_REVERSED = 2
TIME = 3
TIME_REVERSED = 4
CPU = 5
CPU_REVERSED = 6


# global flags defining actions, would like them to be object vars
TIME_SORT = False
MEMORY_SORT = False
PROCESS_RELEVANCE_SORT = True
CURRENT_SORTING = SortOption.PROCESS_RELEVANCE
PREVIOUS_TERMINAL_WIDTH = None
PREVIOUS_TERMINAL_HEIGHT = None

Expand Down Expand Up @@ -119,6 +128,7 @@ def __init__(self,*args,**kwargs):
self.add_handlers({
"^N" : self._sort_by_memory,
"^T" : self._sort_by_time,
"^U" : self._sort_by_cpu,
"^K" : self._kill_process,
"^Q" : self._quit,
"^R" : self._reset,
Expand Down Expand Up @@ -171,24 +181,32 @@ def _get_list_of_open_files(self,process_pid):
def _sort_by_time(self,*args,**kwargs):
# fuck .. that's why NPSManaged was required, i.e you can access the app instance within widgets
self._logger.info("Sorting the process table by time")
global TIME_SORT,MEMORY_SORT
MEMORY_SORT = False
TIME_SORT = True
PROCESS_RELEVANCE_SORT = False
global CURRENT_SORTING
if CURRENT_SORTING == SortOption.TIME_REVERSED:
CURRENT_SORTING = SortOption.TIME
else:
CURRENT_SORTING = SortOption.TIME_REVERSED

def _sort_by_cpu(self,*args,**kwargs):
self._logger.info("Sorting the process table by cpu")
global CURRENT_SORTING
if CURRENT_SORTING == SortOption.CPU_REVERSED:
CURRENT_SORTING = SortOption.CPU
else:
CURRENT_SORTING = SortOption.CPU_REVERSED

def _sort_by_memory(self,*args,**kwargs):
self._logger.info("Sorting the process table by memory")
global TIME_SORT,MEMORY_SORT
TIME_SORT = False
MEMORY_SORT = True
PROCESS_RELEVANCE_SORT = False
global CURRENT_SORTING
if CURRENT_SORTING == SortOption.MEMORY_REVERSED:
CURRENT_SORTING = SortOption.MEMORY
else:
CURRENT_SORTING = SortOption.MEMORY_REVERSED

def _reset(self,*args,**kwargs):
self._logger.info("Resetting the process table")
global TIME_SORT, MEMORY_SORT
TIME_SORT = False
MEMORY_SORT = False
PROCESS_RELEVANCE_SORT = True
global CURRENT_SORTING
CURRENT_SORTING = SortOption.PROCESS_RELEVANCE
self._filtering_flag = False

def _do_process_filtering_work(self,*args,**kwargs):
Expand Down Expand Up @@ -462,31 +480,36 @@ def update(self):
self._processes_data = self.statistics['Process']['table']

# check sorting flags
if MEMORY_SORT:
sorted_processes_data = sorted(self._processes_data,key=lambda k:k['memory'],reverse=True)
self._logger.info("Memory sorting done for process table")
elif TIME_SORT:
sorted_processes_data = sorted(self._processes_data,key=lambda k:k['rawtime'],reverse=True)
self._logger.info("Time sorting done for process table")
elif PROCESS_RELEVANCE_SORT:
sorted_processes_data = sorted(self._processes_data,key=lambda k:k['rawtime'])
self._logger.info("Sorting on the basis of relevance")
if CURRENT_SORTING == SortOption.MEMORY:
sorted_processes_data = sorted(self._processes_data, key=lambda k: k['memory'], reverse=False)
if CURRENT_SORTING == SortOption.MEMORY_REVERSED:
sorted_processes_data = sorted(self._processes_data, key=lambda k: k['memory'], reverse=True)
elif CURRENT_SORTING == SortOption.TIME:
sorted_processes_data = sorted(self._processes_data, key=lambda k: k['rawtime'], reverse=False)
elif CURRENT_SORTING == SortOption.TIME_REVERSED:
sorted_processes_data = sorted(self._processes_data, key=lambda k: k['rawtime'], reverse=True)
elif CURRENT_SORTING == SortOption.CPU:
sorted_processes_data = sorted(self._processes_data, key=lambda k: k['cpu'], reverse=False)
elif CURRENT_SORTING == SortOption.CPU_REVERSED:
sorted_processes_data = sorted(self._processes_data, key=lambda k: k['cpu'], reverse=True)
elif CURRENT_SORTING == SortOption.PROCESS_RELEVANCE:
sorted_processes_data = sorted(self._processes_data, key=lambda k: k['rawtime'])
else:
sorted_processes_data = self._processes_data
self._logger.info("Resetting the sorting behavior")
self._logger.info(f"Process table sorted by {CURRENT_SORTING.name}")

# to keep things pre computed
curtailed_processes_data = []
for proc in sorted_processes_data:
curtailed_processes_data.append("{0: <30} {1: >5}{6}{2: <10}{6}{3}{6}{4: >6.2f} % {6}{5}\
".format( (proc['name'][:25] + '...') if len(proc['name']) > 25 else proc['name'],
proc['id'],
proc['user'],
proc['time'],
proc['memory'],
proc['local_ports'],
" "*int(5*self.X_SCALING_FACTOR))
)
curtailed_processes_data.append("{0: <30}{1: >7}{7}{2: <10}{7}{3}{7}{4}{7}{5}{7}{6}\
".format((proc['name'][:25] + '...') if len(proc['name']) > 25 else proc['name'],
proc['id'],
proc['user'],
proc['time'],
self.format_percentage(proc['cpu']),
self.format_percentage(proc['memory']),
proc['local_ports'],
" " * int(5 * self.X_SCALING_FACTOR)))
if not self.processes_table.entry_widget.is_filtering_on():
self.processes_table.entry_widget.values = curtailed_processes_data
# Set the processes data dictionary to uncurtailed processes data
Expand All @@ -503,6 +526,10 @@ def update(self):
except KeyError:
self._logger.info("Some of the stats reading failed",exc_info=True)

@staticmethod
def format_percentage(float_value):
return f'{float_value: >6.2f}'.rjust(2, ' ') + ' %'

def draw(self):
# Setting the main window form
self.window = WindowForm(parentApp=self,
Expand Down Expand Up @@ -610,7 +637,7 @@ def draw(self):
PROCESSES_INFO_WIDGET_REL_Y+PROCESSES_INFO_WIDGET_HEIGHT)
)
self.processes_table = self.window.add(MultiLineActionWidget,
name="Processes ( name - PID - user - age - memory - system_ports )",
name="Processes ( name - PID - user - age - cpu - memory - system_ports )",
relx=PROCESSES_INFO_WIDGET_REL_X,
rely=PROCESSES_INFO_WIDGET_REL_Y,
max_height=PROCESSES_INFO_WIDGET_HEIGHT,
Expand All @@ -633,7 +660,7 @@ def draw(self):
relx=ACTIONS_WIDGET_REL_X,
rely=ACTIONS_WIDGET_REL_Y
)
self.actions.value = "^K:Kill\t\t^N:Memory Sort\t\t^T:Time Sort\t\t^R:Reset\t\tg:Top\t\t^Q:Quit\t\t^F:Filter\t\t^L:Process Info"
self.actions.value = "^K:Kill\t\t^N:Memory Sort\t\t^T:Time Sort\t\t^U:CPU Sort\t\t^R:Reset\t\tg:Top\t\t^Q:Quit\t\t^F:Filter\t\t^L:Process Info"
self.actions.display()
self.actions.editable = False

Expand Down
39 changes: 28 additions & 11 deletions ptop/plugins/process_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@


class ProcessSensor(Plugin):
def __init__(self,**kwargs):
PROCESS_LIST_CLEANUP_TICKS = 100

def __init__(self, **kwargs):
super(ProcessSensor,self).__init__(**kwargs)
# there will be two parts of the returned value, one will be text and other graph
# there can be many text (key,value) pairs to display corresponding to each key
Expand All @@ -22,6 +24,9 @@ def __init__(self,**kwargs):
self.currentValue['table'] = []
self._currentSystemUser = getpass.getuser()
self._logger = logging.getLogger(__name__)
# processes once retrieved from psutil are stored in a list to make cpu percentage measurement possible
self._process_list = {}
self._tick = 0

def format_time(self, d):
ret = '{0} day{1} '.format(d.days, ' s'[d.days > 1]) if d.days else ''
Expand All @@ -31,7 +36,7 @@ def format_time(self, d):
s -= m * 60
return ret + '{0:2d}:{1:02d}:{2:02d}'.format(h, m, s)

# overriding the upate method
# overriding the update method
def update(self):
# flood the data
thread_count = 0 #keep track number of threads
Expand All @@ -50,7 +55,7 @@ def update(self):
because getting further process info for root processes as a normal
user will give Permission Denied #10
'''
p = psutil.Process(proc.pid)
p = self.retrieve_process_by_pid(proc.pid)
proc_info['user'] = p.username()
try:
if ((proc_info['user'] == self._currentSystemUser) or (self._currentSystemUser in PRIVELAGED_USERS)) \
Expand All @@ -75,16 +80,12 @@ def update(self):
SYSTEM_USERS.append(proc_info['user'])
except:
'''
In case ptop does not have privelages to access info for some of the processes
In case ptop does not have privileges to access info for some of the processes
just log them and don't show them in the processes table
'''
self._logger.info('''Not able to get info for process {0} with status {1} invoked by user {2}, ptop
is invoked by the user {3}'''.format(str(p.pid),
p.status(),
p.username(),
self._currentSystemUser
),
exc_info=True)
self._logger.info(f"""Not able to get info for process {p.pid} with status {p.status()} invoked by user {p.username()}, ptop
is invoked by the user {self._currentSystemUser}""",
exc_info=True)

# padding time
time_len = max((len(proc['time']) for proc in proc_info_list))
Expand All @@ -96,5 +97,21 @@ def update(self):
self.currentValue['text']['running_processes'] = str(proc_count)
self.currentValue['text']['running_threads'] = str(thread_count)

self.tick(proc_info_list)

def tick(self, proc_info_list):
self._tick = (self._tick + 1) % ProcessSensor.PROCESS_LIST_CLEANUP_TICKS
if self._tick == 0:
self._process_list = {key: value for (key, value) in self._process_list.items() if key in [y['id'] for y in proc_info_list]}

def retrieve_process_by_pid(self, pid):
try:
process = self._process_list[pid]
except KeyError:
process = psutil.Process(pid)
self._process_list[pid] = process
return process


# make the process sensor less frequent as it takes more time to fetch info
process_sensor = ProcessSensor(name='Process',sensorType='table',interval=1)