Skip to content

Commit

Permalink
Merge pull request #94 from walles/johan/top-ram-programs
Browse files Browse the repository at this point in the history
Graph top RAM using programs

Adds a graphical visualization of which the top memory using programs are.

They are grouped and summed by their name, so if you have multiple Firefox processes their RAM use will be summed into one entry for the purpose of this visualization.

See updated screenshot for details.
  • Loading branch information
walles authored Dec 22, 2021
2 parents 3f301b4 + e47b0f9 commit 724efee
Show file tree
Hide file tree
Showing 9 changed files with 136 additions and 15 deletions.
2 changes: 2 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ Output
average throughput since ``ptop`` launched.
* Note that binaries launched while ``ptop`` is running are listed at the bottom
of the display.
* Note the visualization of which programs are using your memory next to the
memory numbers
* Selecting a process with Enter will offer you to see detailed information
about that process, in ``$PAGER``, `moar`_ or ``less``. Or to kill it.
* After you press ``q`` to quit, the display is retained and some lines at the
Expand Down
Binary file modified doc/ptop-screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 15 additions & 8 deletions px/px_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@

# Match + group: " 7708 1 Mon Mar 7 09:33:11 2016 netbios 0.1 0:00.08 0.0 /usr/sbin/netbiosd hj"
PS_LINE = re.compile(
" *([0-9]+) +([0-9]+) +([A-Za-z0-9: ]+) +([^ ]+) +([0-9.]+) +([-0-9.:]+) +([0-9.]+) +(.*)"
" *([0-9]+) +([0-9]+) +([0-9]+) +([A-Za-z0-9: ]+) +([^ ]+) +([0-9.]+) +([-0-9.:]+) +([0-9.]+) +(.*)"
)

# Match + group: "1:02.03"
Expand Down Expand Up @@ -93,6 +93,7 @@ def __init__(
self,
cmdline, # type: Text
pid, # type: int
rss_kb, # type: int
start_time_string, # type: Text
username, # type: Text
now, # type: datetime.datetime
Expand All @@ -104,6 +105,7 @@ def __init__(
# type: (...) -> None
self.pid = pid # type: int
self.ppid = ppid # type: Optional[int]
self.rss_kb = rss_kb # type: int

self.cmdline = cmdline # type: text_type
self.command = self._get_command() # type: text_type
Expand Down Expand Up @@ -266,6 +268,7 @@ def __init__(self):
self.cmdline = None # type: Optional[Text]
self.pid = None # type: Optional[int]
self.ppid = None # type: Optional[int]
self.rss_kb = None # type: Optional[int]
self.start_time_string = None # type: Optional[Text]
self.username = None # type: Optional[Text]
self.cpu_percent = None # type: Optional[float]
Expand All @@ -288,12 +291,14 @@ def build(self, now):
# type: (datetime.datetime) -> PxProcess
assert self.cmdline
assert self.pid is not None
assert self.rss_kb is not None
assert self.start_time_string
assert self.username
return PxProcess(
cmdline=self.cmdline,
pid=self.pid,
ppid=self.ppid,
rss_kb=self.rss_kb,
start_time_string=self.start_time_string,
username=self.username,
now=now,
Expand Down Expand Up @@ -354,12 +359,13 @@ def ps_line_to_process(ps_line, now):
process_builder = PxProcessBuilder()
process_builder.pid = int(match.group(1))
process_builder.ppid = int(match.group(2))
process_builder.start_time_string = match.group(3)
process_builder.username = uid_to_username(int(match.group(4)))
process_builder.cpu_percent = float(match.group(5))
process_builder.cpu_time = parse_time(match.group(6))
process_builder.memory_percent = float(match.group(7))
process_builder.cmdline = match.group(8)
process_builder.rss_kb = int(match.group(3))
process_builder.start_time_string = match.group(4)
process_builder.username = uid_to_username(int(match.group(5)))
process_builder.cpu_percent = float(match.group(6))
process_builder.cpu_time = parse_time(match.group(7))
process_builder.memory_percent = float(match.group(8))
process_builder.cmdline = match.group(9)

return process_builder.build(now)

Expand All @@ -379,6 +385,7 @@ def create_kernel_process(now):
"%c"
)

process_builder.rss_kb = 0
process_builder.username = u"root"
process_builder.cpu_time = None
process_builder.memory_percent = None
Expand Down Expand Up @@ -449,7 +456,7 @@ def get_all():
"/bin/ps",
"-ax",
"-o",
"pid=,ppid=,lstart=,uid=,pcpu=,time=,%mem=,command=",
"pid=,ppid=,rss=,lstart=,uid=,pcpu=,time=,%mem=,command=",
]

with open(os.devnull, "w") as DEVNULL:
Expand Down
84 changes: 84 additions & 0 deletions px/px_rambar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import sys
import operator

from px import px_terminal

from . import px_process

if sys.version_info.major >= 3:
# For mypy PEP-484 static typing validation
from typing import List # NOQA
from typing import Dict # NOQA
from typing import Tuple # NOQA
from six import text_type # NOQA

GROUPS_COUNT = 4


def get_categories(all_processes):
# type: (List[px_process.PxProcess]) -> List[Tuple[text_type, int]]
"""
Group processes by pretty names, keeping track of the total rss_kb in each
group.
Return the top groups in order plus one "other" which is the sum of the
rest. The total number of returned groups will be GROUPS_COUNT.
"""

names_to_kilobytes = {} # type: Dict[text_type, int]
for process in all_processes:
base_kb = 0
if process.command in names_to_kilobytes:
base_kb = names_to_kilobytes[process.command]
else:
base_kb = 0

names_to_kilobytes[process.command] = base_kb + process.rss_kb

sorted_names = sorted(
names_to_kilobytes.items(), key=operator.itemgetter(1), reverse=True
)
other_total_kb = 0
return_me = [] # type: List[Tuple[text_type, int]]
for i, name_and_kilobytes in enumerate(sorted_names):
if i < GROUPS_COUNT - 1:
return_me.append(name_and_kilobytes)
else:
other_total_kb += name_and_kilobytes[1]
return_me.append(("...", other_total_kb))

return return_me


def rambar(ram_bar_length, all_processes):
# type: (int, List[px_process.PxProcess]) -> text_type

categories = get_categories(all_processes)
total_kilobytes = 0
for category in categories:
total_kilobytes += category[1]

bar = u""
for i, category in enumerate(categories):
name = category[0]
kilobytes = category[1]

chars = int(round(ram_bar_length * kilobytes * 1.0 / total_kilobytes))
if i == len(categories) - 1:
# Use all remaining chars
chars = ram_bar_length - px_terminal.visual_length(bar)

add_to_bar = px_terminal.get_string_of_length(" " + name, chars)
if i == 0:
# First red
add_to_bar = px_terminal.red(add_to_bar)
elif i == 1:
# Second yellow
add_to_bar = px_terminal.yellow(add_to_bar)
elif i % 2 == 1:
# Then alternating between normal and inverse video
add_to_bar = px_terminal.inverse_video(add_to_bar)

bar += add_to_bar

return bar
2 changes: 1 addition & 1 deletion px/px_terminal.py
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,7 @@ def red(string):
global _enable_color
if not _enable_color:
return string
return CSI + "1;30;41m" + string + CSI + "49;39;22m"
return CSI + "1;97;41m" + string + CSI + "49;39;22m"


def yellow(string):
Expand Down
25 changes: 23 additions & 2 deletions px/px_top.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from . import px_processinfo
from . import px_process_menu
from . import px_poller
from . import px_rambar

if sys.version_info.major >= 3:
# For mypy PEP-484 static typing validation
Expand Down Expand Up @@ -206,11 +207,18 @@ def get_screen_lines(
toplist, # type: List[px_process.PxProcess]
poller, # type: px_poller.PxPoller
rows, # type: int
columns, # type: int
include_footer=True, # type: bool
search=None, # type: Optional[text_type]
):
# type: (...) -> List[text_type]
"""
Note that the columns parameter is only used for layout purposes. Lines
returned from this function will still need to be cropped before being
printed to screen.
"""

all_processes = toplist
if search:
# Note that we accept partial user name match, otherwise incrementally typing
# a username becomes weird for the ptop user
Expand All @@ -224,10 +232,23 @@ def get_screen_lines(
if include_footer:
footer_height = 1

memory_line = px_terminal.bold(u"RAM Use: ") + poller.get_meminfo()
assert columns > 0
ram_bar_length = columns - px_terminal.visual_length(memory_line) - 18
if ram_bar_length > 30:
# Enough space for a usable RAM bar. Limit picked entirely arbitrarily,
# feel free to change it if you have a better number.
memory_line += (
px_terminal.bold(" RAM Users")
+ ": [ "
+ px_rambar.rambar(ram_bar_length, all_processes)
+ " ]"
)

# Print header
lines = [
px_terminal.bold(u"Sysload: ") + poller.get_loadstring(),
px_terminal.bold(u"RAM Use: ") + poller.get_meminfo(),
memory_line,
px_terminal.bold(u"IO Load: ") + poller.get_ioload_string(),
u"",
]
Expand Down Expand Up @@ -317,7 +338,7 @@ def redraw(
"""
global search_string
lines = get_screen_lines(
toplist, poller, rows, include_footer, search=search_string
toplist, poller, rows, columns, include_footer, search=search_string
)

px_terminal.draw_screen_lines(lines, columns)
Expand Down
4 changes: 4 additions & 0 deletions tests/px_process_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def test_create_process():
process_builder = px_process.PxProcessBuilder()
process_builder.pid = 7
process_builder.ppid = 1
process_builder.rss_kb = 123
process_builder.start_time_string = testutils.TIMESTRING
process_builder.username = "usernamex"
process_builder.cpu_time = 1.3
Expand All @@ -28,6 +29,7 @@ def test_create_process():

assert test_me.pid == 7
assert test_me.ppid == 1
assert test_me.rss_kb == 123
assert test_me.username == "usernamex"
assert test_me.cpu_time_s == "1.3s"
assert test_me.memory_percent_s == "43%"
Expand Down Expand Up @@ -57,6 +59,7 @@ def test_create_future_process():
# These values are required to not fail in other ways
process_builder.cmdline = "hej kontinent"
process_builder.pid = 1
process_builder.rss_kb = 123
process_builder.username = "johan"

# Test it!
Expand Down Expand Up @@ -104,6 +107,7 @@ def test_ps_line_to_process_3():
process = px_process.ps_line_to_process(
" 5328"
" 4432"
" 123"
" Thu Feb 25 07:42:36 2016"
" " + str(os.getuid()) + " 5.5"
" 1-19:31:31"
Expand Down
8 changes: 4 additions & 4 deletions tests/px_top_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ def test_get_screen_lines_low_screen():

SCREEN_ROWS = 10
px_terminal._enable_color = True
lines = px_top.get_screen_lines(baseline, poller, SCREEN_ROWS)
lines = px_top.get_screen_lines(baseline, poller, SCREEN_ROWS, 99)

# Top row should contain ANSI escape codes
CSI = u"\x1b["
Expand All @@ -120,7 +120,7 @@ def test_get_screen_lines_high_screen():

SCREEN_ROWS = 100
px_terminal._enable_color = True
lines = px_top.get_screen_lines(baseline, poller, SCREEN_ROWS)
lines = px_top.get_screen_lines(baseline, poller, SCREEN_ROWS, 99)

# Top row should contain ANSI escape codes
CSI = u"\x1b["
Expand All @@ -146,7 +146,7 @@ def test_get_screen_lines_with_many_launches():
poller._launchcounter_screen_lines = launchcounter.get_screen_lines()

SCREEN_ROWS = 100
lines = px_top.get_screen_lines(baseline, poller, SCREEN_ROWS)
lines = px_top.get_screen_lines(baseline, poller, SCREEN_ROWS, 99)

assert len(lines) == SCREEN_ROWS

Expand All @@ -156,6 +156,6 @@ def test_get_screen_lines_returns_enough_lines():
poller = px_poller.PxPoller()

SCREEN_ROWS = 100000
lines = px_top.get_screen_lines(baseline, poller, SCREEN_ROWS)
lines = px_top.get_screen_lines(baseline, poller, SCREEN_ROWS, 99)

assert len(lines) == SCREEN_ROWS
3 changes: 3 additions & 0 deletions tests/testutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ def now():
def create_process(
pid=47536,
ppid=1234,
rss_kb=12345,
timestring=TIMESTRING,
uid=0,
cpuusage="0.0",
Expand All @@ -58,6 +59,8 @@ def create_process(
+ spaces()
+ str(ppid)
+ spaces()
+ str(rss_kb)
+ spaces()
+ timestring
+ spaces()
+ str(uid)
Expand Down

0 comments on commit 724efee

Please sign in to comment.