Skip to content

Commit

Permalink
Merge branch 'walles/load-bar' into python
Browse files Browse the repository at this point in the history
This adds a load bar to ptop.

Fixes #13
  • Loading branch information
walles committed Feb 21, 2017
2 parents 12a2d30 + f34ba51 commit 3b3676f
Show file tree
Hide file tree
Showing 10 changed files with 519 additions and 23 deletions.
96 changes: 96 additions & 0 deletions px/px_cpuinfo.py
@@ -0,0 +1,96 @@
import os
import errno
import subprocess


def get_core_count():
"""
Count the number of cores in the system.
Returns a tuple (physical, logical) with counts of physical and logical
cores.
"""
return_me = get_core_count_from_proc_cpuinfo()
if return_me is not None:
return return_me

return_me = get_core_count_from_sysctl()
if return_me is not None:
return return_me

return None


def get_core_count_from_proc_cpuinfo(proc_cpuinfo="/proc/cpuinfo"):
"""
Count the number of cores in /proc/cpuinfo.
Returns a tuple (physical, logical) with counts of physical and logical
cores.
"""
# Note the ending spaces, they must be there for number extraction to work!
PROCESSOR_NO_PREFIX = 'processor\t: '
CORE_ID_PREFIX = 'core id\t\t: '

core_ids = set()
max_processor_no = 0
try:
with open(proc_cpuinfo) as f:
for line in f:
if line.startswith(PROCESSOR_NO_PREFIX):
processor_no = int(line[len(PROCESSOR_NO_PREFIX):])
max_processor_no = max(processor_no, max_processor_no)
elif line.startswith(CORE_ID_PREFIX):
core_id = int(line[len(CORE_ID_PREFIX)])
core_ids.add(core_id)
except (IOError, OSError) as e:
if e.errno == errno.ENOENT:
# /proc/cpuinfo not found, we're probably not on Linux
return None

raise

physical = len(core_ids)
logical = max_processor_no + 1
if physical == 0:
# I get this on my cell phone
physical = logical
return (physical, logical)


def get_core_count_from_sysctl():
env = os.environ.copy()
if "LANG" in env:
del env["LANG"]

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

raise

sysctl_stdout = sysctl.communicate()[0].decode('utf-8')
sysctl_lines = sysctl_stdout.split('\n')

# Note the ending spaces, they must be there for number extraction to work!
PHYSICAL_PREFIX = 'hw.physicalcpu: '
LOGICAL_PREFIX = 'hw.logicalcpu: '

physical = None
logical = None
for line in sysctl_lines:
if line.startswith(PHYSICAL_PREFIX):
physical = int(line[len(PHYSICAL_PREFIX):])
elif line.startswith(LOGICAL_PREFIX):
logical = int(line[len(LOGICAL_PREFIX)])

if physical is None or logical is None:
# On Linux, sysctl exists but it doesn't contain the values we want
return None

return (physical, logical)
8 changes: 4 additions & 4 deletions px/px_install.py
Expand Up @@ -30,22 +30,22 @@ def _install(src, dest):
return

if not os.path.isfile(src):
raise IOError("Source is not a file: %s" % src)
raise IOError("Source is not a file: %s" % (src,))

parent = os.path.dirname(dest)
if not os.path.isdir(parent):
raise IOError("Destination parent is not a directory: %s" % parent)
raise IOError("Destination parent is not a directory: %s" % (parent,))

if os.path.isdir(dest):
raise IOError("Destination is a directory, won't replace that: %s" % dest)
raise IOError("Destination is a directory, won't replace that: %s" % (dest,))

# Make sure nothing's in the way
try:
os.remove(dest)
except OSError:
pass
if os.path.exists(dest):
raise IOError("Can't remove existing entry: %s" % dest)
raise IOError("Can't remove existing entry: %s" % (dest,))

shutil.copyfile(src, dest)
os.chmod(dest, 0o755)
102 changes: 102 additions & 0 deletions px/px_load_bar.py
@@ -0,0 +1,102 @@
class PxLoadBar(object):
"""
Visualizes system load in a horizontal bar.
Inputs are:
* System load
* Number of physical cores
* Number of logical cores
* How many columns wide the horizontal bar should be
The output is a horizontal bar string.
Load below the number of physical cores is visualized in green.
Load between the number of physical cores and logical cores is visualized in
yellow.
Load above of the number of logical cores is visualized in red.
As long as load is below the number of physical cores, it will use only the
first half of the output string.
Load up to twice the number of physical cores will go up to the end of the
string.
"""

def __init__(self, physical=None, logical=None):
if physical is None or physical < 1:
raise ValueError("Physical must be a positive integer, was: %r" % (physical,))

if logical is None or logical < physical:
raise ValueError("Logical must be >= physical, was: %r (vs %r)" % (logical, physical))

self._physical = physical
self._logical = logical

CSI = b"\x1b["
self.normal = CSI + b"m"
self.inverse = CSI + b"7m"
self.red = CSI + b"41m"
self.yellow = CSI + b"43m"
self.green = CSI + b"42m"

def _get_colored_bytes(self, load=None, columns=None):
"Yields pairs, with each pair containing a color and a byte"

max_value = 2.0 * self._physical
if load > max_value:
max_value = 1.0 * load

UNUSED = 1000 * max_value
if load < self._physical:
yellow_start = UNUSED
red_start = UNUSED
inverse_start = load
normal_start = self._physical
else:
yellow_start = self._physical
red_start = self._logical
inverse_start = UNUSED
normal_start = load

# Scale the values to the number of columns
yellow_start = yellow_start * columns / max_value - 0.5
red_start = red_start * columns / max_value - 0.5
inverse_start = inverse_start * columns / max_value - 0.5
normal_start = normal_start * columns / max_value - 0.5

for i in range(columns):
# We always start out green
color = self.green

if i >= yellow_start:
color = self.yellow
if i >= red_start:
color = self.red
if i >= inverse_start:
color = self.inverse
if i >= normal_start:
color = self.normal

yield (color, b' ')

def get_bar(self, load=None, columns=None):
if load is None:
raise ValueError("Missing required parameter load=")

if columns is None:
raise ValueError("Missing required parameter columns=")

return_me = b''
color = self.normal
for color_and_byte in self._get_colored_bytes(load=load, columns=columns):
if color_and_byte[0] != color:
return_me += color_and_byte[0]
color = color_and_byte[0]
return_me += color_and_byte[1]

if color != self.normal:
return_me += self.normal

return return_me
57 changes: 38 additions & 19 deletions px/px_top.py
Expand Up @@ -10,6 +10,8 @@
from . import px_load
from . import px_process
from . import px_terminal
from . import px_load_bar
from . import px_cpuinfo


# Used for informing our getch() function that a window resize has occured
Expand Down Expand Up @@ -124,6 +126,14 @@ def get_toplist(baseline):
return toplist


def writebytes(bytestring):
if sys.version_info.major == 2:
sys.stdout.write(bytestring)
else:
# http://stackoverflow.com/a/908440/473672
sys.stdout.buffer.write(bytestring)


def clear_screen():
"""
Clear the screen and move cursor to top left corner:
Expand All @@ -137,36 +147,43 @@ def clear_screen():
"""

CSI = b"\x1b["
if sys.version_info.major == 2:
sys.stdout.write(CSI + b"1J")
sys.stdout.write(CSI + b"H")
else:
# http://stackoverflow.com/a/908440/473672
sys.stdout.buffer.write(CSI + b"1J")
sys.stdout.buffer.write(CSI + b"H")
sys.stdout.flush()
writebytes(CSI + b"1J")
writebytes(CSI + b"H")


def redraw(baseline, rows, columns):
"""
Refresh display relative to the given baseline.
The new display will be (at most) rows rows x columns columns.
"""
lines = ["System load: " + px_load.get_load_string(), ""]
def get_screen_lines(load_bar, baseline, rows, columns):
load = px_load.get_load_values()
loadstring = px_load.get_load_string(load).encode('utf-8')
loadbar = load_bar.get_bar(load=load[0], columns=40)
lines = [
b"System load: " + loadstring + b" |" + loadbar + b"|",
b""]

toplist_table_lines = px_terminal.to_screen_lines(get_toplist(baseline), columns)
if toplist_table_lines:
heading_line = toplist_table_lines[0]
heading_line = px_terminal.get_string_of_length(heading_line, columns)
heading_line = px_terminal.inverse_video(heading_line)
toplist_table_lines[0] = heading_line

toplist_table_lines = map(lambda s: s.encode('utf-8'), toplist_table_lines)
lines += toplist_table_lines

clear_screen()
return lines[0:rows]


def redraw(load_bar, baseline, rows, columns, clear=True):
"""
Refresh display relative to the given baseline.
The new display will be (at most) rows rows x columns columns.
"""
lines = get_screen_lines(load_bar, baseline, rows, columns)
if clear:
clear_screen()

# We need both \r and \n when the TTY is in tty.setraw() mode
sys.stdout.write("\r\n".join(lines[0:rows]))
writebytes(b"\r\n".join(lines))
sys.stdout.flush()


Expand All @@ -187,14 +204,16 @@ def get_command(**kwargs):


def _top():
physical, logical = px_cpuinfo.get_core_count()
load_bar = px_load_bar.PxLoadBar(physical, logical)
baseline = px_process.get_all()
while True:
window_size = px_terminal.get_window_size()
if window_size is None:
sys.stderr.write("Cannot find terminal window size, are you on a terminal?\r\n")
exit(1)
rows, columns = window_size
redraw(baseline, rows, columns)
redraw(load_bar, baseline, rows, columns)

command = get_command(timeout_seconds=1)

Expand All @@ -205,7 +224,7 @@ def _top():
# probably want the heading line on screen. So just do another
# update with somewhat fewer lines, and you'll get just that.
rows, columns = px_terminal.get_window_size()
redraw(baseline, rows - 4, columns)
redraw(load_bar, baseline, rows - 4, columns)
return

command = get_command(timeout_seconds=0)
Expand Down
25 changes: 25 additions & 0 deletions tests/proc-cpuinfo-1p1l
@@ -0,0 +1,25 @@
processor : 0
vendor_id : GenuineIntel
cpu family : 6
model : 62
model name : Intel(R) Xeon(R) CPU E5-2670 v2 @ 2.50GHz
stepping : 4
microcode : 0x428
cpu MHz : 2500.108
cache size : 25600 KB
physical id : 0
siblings : 1
core id : 1
cpu cores : 1
apicid : 3
initial apicid : 3
fpu : yes
fpu_exception : yes
cpuid level : 13
wp : yes
flags : fpu de tsc msr pae cx8 apic sep cmov pat clflush mmx fxsr sse sse2 ss ht syscall nx lm constant_tsc rep_good nopl pni pclmulqdq ssse3 cx16 sse4_1 sse4_2 popcnt tsc_deadline_timer aes rdrand hypervisor lahf_lm ida arat epb pln pts dtherm fsgsbase erms
bogomips : 5000.21
clflush size : 64
cache_alignment : 64
address sizes : 46 bits physical, 48 bits virtual
power management:

0 comments on commit 3b3676f

Please sign in to comment.