Skip to content

Commit

Permalink
Merge branch 'walles/ram-usage' into python
Browse files Browse the repository at this point in the history
Fixes #79.
  • Loading branch information
walles committed Aug 25, 2020
2 parents e04c8a5 + 8a77338 commit 578a5ae
Show file tree
Hide file tree
Showing 12 changed files with 433 additions and 9 deletions.
4 changes: 2 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,8 @@ information; the command will work without it but might miss some information.
* Note how the default sort order of CPU-usage-since-``ptop``-started makes the
display rather stable.
* Note the system load bar that correlates the system load with the number of
CPU cores in the system. Green is load handled by physical cores, yellow
(not shown here) is load handled by hyperthreading cores, and red is load
CPU cores in the system. **Green** is load handled by physical cores, **yellow**
(not shown here) is load handled by hyperthreading cores, and **red** is load
over the number of cores.
* Note the fifteen minute load history graph in the load bar. On this system the
load has been high for the last fifteen minutes. This is a visualization of
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.
4 changes: 2 additions & 2 deletions px/px_cpuinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ def get_core_count():
return return_me

uname = str(platform.uname())
platform = uname + " Python " + sys.version
platform_s = uname + " Python " + sys.version

raise IOError("Unable to get cores info " + platform)
raise IOError("Unable to get cores info " + platform_s)


def get_core_count_from_proc_cpuinfo(proc_cpuinfo="/proc/cpuinfo"):
Expand Down
270 changes: 270 additions & 0 deletions px/px_meminfo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
import os
import re
import sys
import errno
import platform
import subprocess

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


def get_meminfo():
# type: () -> text_type

CSI = u"\x1b["
NORMAL = CSI + u"m"
RED = CSI + u"1;37;41m"
YELLOW = CSI + u"1;30;43m"
GREEN = CSI + u"1;30;42m"

total_ram_bytes, wanted_ram_bytes = _get_ram_numbers()
percentage = (100.0 * wanted_ram_bytes) / total_ram_bytes

ram_text = "".join([
str(int(round(percentage))),
"% ",
bytes_to_string(wanted_ram_bytes),
"/",
bytes_to_string(total_ram_bytes)
])

# "80"? I made it up.
if percentage < 80:
color = GREEN
elif percentage < 100:
color = YELLOW
else:
color = RED

return color + " " + ram_text + " " + NORMAL


def bytes_to_string(bytes_count):
# type: (int) -> text_type
"""
Turn byte counts into strings like "14MB"
"""
KB = 1024 ** 1
MB = 1024 ** 2
GB = 1024 ** 3
TB = 1024 ** 4

if bytes_count < 7 * KB:
return str(bytes_count) + "B"

if bytes_count < 7 * MB:
return str(int(round(float(bytes_count) / KB))) + "KB"

if bytes_count < 7 * GB:
return str(int(round(float(bytes_count) / MB))) + "MB"

if bytes_count < 7 * TB:
return str(int(round(float(bytes_count) / GB))) + "GB"

return str(int(round(float(bytes_count) / TB))) + "TB"


def _get_ram_numbers():
# type: () -> Tuple[int, int]
"""
Returns a tuple containing two numbers:
* Total amount of RAM installed in the machine (in bytes)
* Wanted amount of RAM by the system (in bytes)
If wanted > total it generally implies that we're swapping.
"""
return_me = _get_ram_numbers_from_proc()
if return_me is not None:
return return_me

vm_stat_lines = _get_vmstat_output_lines()
if vm_stat_lines is not None:
return_me = _get_ram_numbers_from_vm_stat_output(vm_stat_lines)
if return_me is not None:
return return_me

uname = str(platform.uname())
platform_s = uname + " Python " + sys.version

raise IOError("Unable to get memory info " + platform_s)


def _update_from_meminfo(base, line, name):
# type: (Optional[int], text_type, text_type) -> Optional[int]
if not line.startswith(name + ":"):
return base

just_the_number = line[len(name) + 1:len(line) - 3]

return int(just_the_number)


def _get_ram_numbers_from_proc(proc_meminfo="/proc/meminfo"):
# type: (str) -> Optional[Tuple[int, int]]

total_kb = None # type: Optional[int]
available_kb = None # type: Optional[int]
free_kb = None # type: Optional[int]
buffers_kb = None # type: Optional[int]
cached_kb = None # type: Optional[int]
swapcached_kb = None # type: Optional[int]
swaptotal_kb = None # type: Optional[int]
swapfree_kb = None # type: Optional[int]

try:
with open(proc_meminfo) as f:
for line in f:
total_kb = _update_from_meminfo(total_kb, line, "MemTotal")
available_kb = _update_from_meminfo(available_kb, line, "MemAvailable")
free_kb = _update_from_meminfo(free_kb, line, "MemFree")
buffers_kb = _update_from_meminfo(buffers_kb, line, "Buffers")
cached_kb = _update_from_meminfo(cached_kb, line, "Cached")
swapcached_kb = _update_from_meminfo(swapcached_kb, line, "SwapCached")
swaptotal_kb = _update_from_meminfo(swaptotal_kb, line, "SwapTotal")
swapfree_kb = _update_from_meminfo(swapfree_kb, line, "SwapFree")
except (IOError, OSError) as e:
if e.errno == errno.ENOENT:
# /proc/meminfo not found, we're probably not on Linux
return None

raise

if total_kb is None:
return None
if swaptotal_kb is None:
return None
if swapfree_kb is None:
return None
swapused_kb = swaptotal_kb - swapfree_kb

if available_kb is not None:
# Use MemAvailable if we can:
# https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=34e431b0ae398fc54ea69ff85ec700722c9da773

ramused_kb = total_kb - available_kb

return (total_kb * 1024, (swapused_kb + ramused_kb) * 1024)

if free_kb is None:
return None
if buffers_kb is None:
return None
if cached_kb is None:
return None
if swapcached_kb is None:
return None

ramused_kb = total_kb - (free_kb + buffers_kb + cached_kb + swapcached_kb)

return (total_kb * 1024, (swapused_kb + ramused_kb) * 1024)


def _get_vmstat_output_lines():
# type: () -> Optional[List[text_type]]
env = os.environ.copy()
if "LANG" in env:
del env["LANG"]

try:
vm_stat = subprocess.Popen(["vm_stat"],
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
env=env)
except (IOError, OSError) as e:
if e.errno == errno.ENOENT:
# vm_stat not found, we're probably not on OSX
return None

raise

vm_stat_stdout = vm_stat.communicate()[0].decode('utf-8')
vm_stat_lines = vm_stat_stdout.split('\n')

return vm_stat_lines


def _update_if_prefix(base, line, prefix):
# type: (Optional[int], text_type, text_type) -> Optional[int]
if not line.startswith(prefix):
return base

no_ending_dot = line.rstrip(".")

return int(no_ending_dot[len(prefix):])


def _get_ram_numbers_from_vm_stat_output(vm_stat_lines):
# type: (List[text_type]) -> Optional[Tuple[int, int]]

PAGE_SIZE_RE = re.compile(r"page size of ([0-9]+) bytes")

# List based on https://apple.stackexchange.com/a/196925/182882
page_size_bytes = None
pages_free = None
pages_active = None
pages_inactive = None
pages_speculative = None
pages_wired = None
pages_anonymous = None
pages_purgeable = None
pages_compressed = None # "Pages occupied by compressor"
pages_uncompressed = None # "Pages stored in compressor"

for line in vm_stat_lines:
page_size_match = PAGE_SIZE_RE.search(line)
if page_size_match:
page_size_bytes = int(page_size_match.group(1))
continue

pages_free = _update_if_prefix(pages_free, line, "Pages free:")
pages_active = _update_if_prefix(pages_active, line, "Pages active:")
pages_inactive = _update_if_prefix(pages_inactive, line, "Pages inactive:")
pages_speculative = _update_if_prefix(pages_speculative, line, "Pages speculative:")
pages_wired = _update_if_prefix(pages_wired, line, "Pages wired down:")
pages_anonymous = _update_if_prefix(pages_anonymous, line, "Anonymous pages:")
pages_purgeable = _update_if_prefix(pages_purgeable, line, "Pages purgeable:")
pages_compressed = _update_if_prefix(pages_compressed, line, "Pages occupied by compressor:")
pages_uncompressed = _update_if_prefix(pages_uncompressed, line, "Pages stored in compressor:")

if page_size_bytes is None:
return None
if pages_free is None:
return None
if pages_active is None:
return None
if pages_inactive is None:
return None
if pages_speculative is None:
return None
if pages_wired is None:
return None
if pages_anonymous is None:
return None
if pages_purgeable is None:
return None
if pages_compressed is None:
return None
if pages_uncompressed is None:
return None

# In experiments, this has added up well to the amount of physical RAM in my machine
total_ram_pages = \
pages_free + pages_active + pages_inactive + pages_speculative + pages_wired + pages_compressed

# This matches what the Activity Monitor shows in macOS 10.15.6
#
# For anonymous - purgeable: https://stackoverflow.com/a/36721309/473672
#
# FIXME: We want to add swapped out pages to this as well, since those also
# represent a want for pages.
wanted_ram_pages = \
pages_anonymous - pages_purgeable + pages_wired + pages_compressed

total_ram_bytes = total_ram_pages * page_size_bytes
wanted_ram_bytes = wanted_ram_pages * page_size_bytes

return (total_ram_bytes, wanted_ram_bytes)
7 changes: 5 additions & 2 deletions px/px_top.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from . import px_terminal
from . import px_load_bar
from . import px_cpuinfo
from . import px_meminfo
from . import px_processinfo
from . import px_launchcounter

Expand Down Expand Up @@ -294,7 +295,7 @@ def get_screen_lines(
toplist = list(filter(lambda p: p.match(search, require_exact_user=False), toplist))

# Hand out different amount of lines to the different sections
header_height = 2
header_height = 3 # System load, RAM load, empty line
footer_height = 0
cputop_minheight = 10
if include_footer:
Expand All @@ -304,8 +305,10 @@ def get_screen_lines(
load = px_load.get_load_values()
loadstring = px_load.get_load_string(load)
loadbar = load_bar.get_bar(load=load[0], columns=40, text=loadstring)
meminfo = px_meminfo.get_meminfo()
lines = [
u"System load: " + loadbar,
u"Sysload: " + loadbar,
u"RAM Use: " + meminfo,
u""]

# Create a launchers section
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
42 changes: 42 additions & 0 deletions tests/proc-meminfo-2010.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
MemTotal: 1921988 kB
MemFree: 1374408 kB
Buffers: 32688 kB
Cached: 370540 kB
SwapCached: 0 kB
Active: 344604 kB
Inactive: 80800 kB
Active(anon): 22364 kB
Inactive(anon): 4 kB
Active(file): 322240 kB
Inactive(file): 80796 kB
Unevictable: 0 kB
Mlocked: 0 kB
SwapTotal: 1048572 kB
SwapFree: 1048572 kB
Dirty: 48 kB
Writeback: 0 kB
AnonPages: 22260 kB
Mapped: 13628 kB
Shmem: 196 kB
Slab: 91648 kB
SReclaimable: 34024 kB
SUnreclaim: 57624 kB
KernelStack: 2880 kB
PageTables: 3620 kB
NFS_Unstable: 0 kB
Bounce: 0 kB
WritebackTmp: 0 kB
CommitLimit: 2009564 kB
Committed_AS: 134216 kB
VmallocTotal: 34359738367 kB
VmallocUsed: 12276 kB
VmallocChunk: 34359712840 kB
HardwareCorrupted: 0 kB
AnonHugePages: 0 kB
HugePages_Total: 0
HugePages_Free: 0
HugePages_Rsvd: 0
HugePages_Surp: 0
Hugepagesize: 2048 kB
DirectMap4k: 8064 kB
DirectMap2M: 2088960 kB
Loading

0 comments on commit 578a5ae

Please sign in to comment.