Top almost completed

This commit is contained in:
nicolargo 2025-05-10 20:56:26 +02:00
parent 30ab859244
commit 698c81d7f7
9 changed files with 326 additions and 185 deletions

View File

@ -409,6 +409,68 @@ def dictlist_first_key_value(data: list[dict], key, value) -> Optional[dict]:
return ret
def auto_number(number, low_precision=False, min_symbol='K', none_symbol='-', strip_zero=True):
"""Make a nice human-readable string out of number.
Number of decimal places increases as quantity approaches 1.
CASE: 613421788 RESULT: 585M low_precision: 585M
CASE: 5307033647 RESULT: 4.94G low_precision: 4.9G
CASE: 44968414685 RESULT: 41.9G low_precision: 41.9G
CASE: 838471403472 RESULT: 781G low_precision: 781G
CASE: 9683209690677 RESULT: 8.81T low_precision: 8.8T
CASE: 1073741824 RESULT: 1024M low_precision: 1024M
CASE: 1181116006 RESULT: 1.10G low_precision: 1.1G
:low_precision: returns less decimal places potentially (default is False)
sacrificing precision for more readability.
:min_symbol: Do not approach if number < min_symbol (default is K)
:decimal_count: if set, force the number of decimal number (default is None)
"""
if number is None:
return none_symbol
if number == 0.0:
return '0'
prefix = {
'Y': 1208925819614629174706176,
'Z': 1180591620717411303424,
'E': 1152921504606846976,
'P': 1125899906842624,
'T': 1099511627776,
'G': 1073741824,
'M': 1048576,
'K': 1024,
}
symbols = list(reversed(prefix.keys()))
if min_symbol in symbols:
symbols = symbols[symbols.index(min_symbol) :]
for symbol in reversed(symbols):
value = float(number) / prefix[symbol]
if value > 1:
decimal_precision = 0
if value < 10:
decimal_precision = 2
elif value < 100:
decimal_precision = 1
if low_precision:
if symbol in 'MK':
decimal_precision = 0
else:
decimal_precision = min(1, decimal_precision)
elif symbol in 'K':
decimal_precision = 0
ret = '{:.{decimal}f}{symbol}'.format(value, decimal=decimal_precision, symbol=symbol)
if strip_zero:
return ret.replace('.0', '')
return ret
if strip_zero:
return f'{number!s}'.replace('.0', '')
return f'{number!s}'
def string_value_to_float(s):
"""Convert a string with a value and an unit to a float.
Example:

View File

@ -16,6 +16,7 @@ from textual.containers import Container, Grid, VerticalScroll
from textual.reactive import reactive
from textual.widgets import Footer, Label, Placeholder
from glances.globals import auto_number
from glances.plugins.plugin.model import fields_unit_short
@ -40,8 +41,13 @@ class GlancesTuiApp(App):
self.stats = stats
# Init plugins
self.plugins_description = self.stats.getAllFieldsDescriptionAsDict()
# TODO: to be replaced by a loop
self.plugins = {}
self.plugins["cpu"] = GlancesPlugin("cpu", stats=stats, config=config, args=args)
self.plugins["mem"] = GlancesPlugin("mem", stats=stats, config=config, args=args)
self.plugins["memswap"] = GlancesPlugin("memswap", stats=stats, config=config, args=args)
self.plugins["load"] = GlancesPlugin("load", stats=stats, config=config, args=args)
def compose(self) -> ComposeResult:
# yield Header(id="header", show_clock=True)
@ -56,9 +62,9 @@ class GlancesTuiApp(App):
Placeholder(id="quicklook"),
self.plugins["cpu"],
Placeholder(id="gpu", classes="remove"),
Placeholder(id="mem"),
Placeholder(id="memswap"),
Placeholder(id="load"),
self.plugins["mem"],
self.plugins["memswap"],
self.plugins["load"],
id="top",
),
Grid(
@ -99,12 +105,32 @@ class GlancesTuiApp(App):
"""Called when the time attribute changes."""
# Start by updating Glances stats
self.stats.update()
# logger.info(self.stats.getAllAsDict()['cpu'])
# Solution 1: make the update in the GlancesTuiApp class
self.query_one("#cpu").query_one("#total").update(str(self.stats.getAllAsDict()["cpu"]["total"]))
self.query_one("#cpu").query_one("#system").update(str(self.stats.getAllAsDict()["cpu"]["system"]))
# Solution 2: implement the update method in the CpuTextualPlugin class
# ... (TODO)
# Get stats views
views = self.stats.getAllViewsAsDict()
# logger.info(views['cpu'])
# Update the stats using Textual query
for plugin in self.plugins.keys():
stats = self.stats.getAllAsDict()[plugin]
for field in self.query_one(f"#{plugin}").query('.value'):
# Ignore field not available in the stats
if field.id not in self.stats.getAllAsDict()[plugin]:
continue
# Update value
if (
field.id in self.plugins_description[plugin]
and 'rate' in self.plugins_description[plugin][field.id]
and self.plugins_description[plugin][field.id]['rate']
):
field_stat = field.id + "_rate_per_sec"
else:
field_stat = field.id
field.update(auto_number(stats.get(field_stat, None)))
# Update style
style = views[plugin][field.id].get('decoration', 'DEFAULT')
field.classes = f"value {style.lower()}"
class GlancesPlugin(Container):
@ -138,20 +164,24 @@ class GlancesPlugin(Container):
# Will generate a NoMatches exception when stats are updated
# TODO: catch it in the main update loop
yield Label(f'{self.plugin.upper()} stats not available', id=f"{self.plugin}-not-available")
return
with Grid(id=f"{self.plugin}"):
for field in self.stats.getAllAsDict()[f"{self.plugin}"].keys():
for field in self.plugin_description.keys():
# Ignore field if the display=False option is defined in the description
if not self.plugin_description[field].get('display', True):
continue
# Get the field short name
if field in self.plugin_description and 'short_name' in self.plugin_description[field]:
if 'short_name' in self.plugin_description[field]:
field_name = self.plugin_description[field]['short_name']
else:
field_name = field
yield Label(field_name, classes="name")
# Display value
yield Label('', id=field, classes="value ok")
# Display value (with decoration classes)
yield Label('', id=field, classes="value default")
# Display unit
if field in self.plugin_description and 'unit' in self.plugin_description[field]:
field_unit = fields_unit_short.get(self.plugin_description[field]['unit'], '')
else:
field_unit = ''
yield Label(field_unit, classes="unit ok")
yield Label(field_unit, classes="unit")

View File

@ -28,6 +28,7 @@ Placeholder.remove {
.unit {
text-align: right;
padding-right: 1;
color: grey;
}
.default {
@ -37,19 +38,39 @@ Placeholder.remove {
color: lime;
}
.ok_log {
color: black;
background: lime;
}
.careful {
color: cornflowerblue;
}
.careful_log {
color: black;
background: cornflowerblue;
}
.warning {
color: orangered;
color: purple;
background: $background;
}
.warning_log {
color: $foreground;
background: purple;
}
.critical {
color: red;
}
.critical_log {
color: white;
background: red;
}
# Glances layout
# ##############
@ -90,14 +111,36 @@ Placeholder.remove {
# Glances plugins
# ###############
#cpu {
grid-size: 9 4;
grid-columns: 7 5 2;
* > Label {
width: 100%;
}
#cpu > Label {
#cpu {
grid-size: 9 4;
grid-columns: 6 6 2;
width: 100%;
align: center top;
}
#mem {
grid-size: 6 4;
grid-columns: 7 7 2;
width: 100%;
align: center top;
}
#memswap {
grid-size: 3 4;
grid-columns: 6 7 2;
width: 100%;
align: center top;
}
#load {
grid-size: 3 4;
grid-columns: 5 7 2;
width: 100%;
align: right top;
}
#network {

View File

@ -18,12 +18,28 @@ from glances.plugins.plugin.model import GlancesPluginModel
# Fields description
# https://github.com/nicolargo/glances/wiki/How-to-create-a-new-plugin-%3F#create-the-plugin-script
fields_description = {
'total': {'description': 'Sum of all CPU percentages (except idle).', 'unit': 'percent', 'log': True},
'system': {
'description': 'Percent time spent in kernel space. System CPU time is the \
time spent running code in the Operating System kernel.',
'total': {
'description': 'Sum of all CPU percentages (except idle).',
'unit': 'percent',
'log': True,
'short_name': 'CPU',
},
'idle': {
'description': 'percent of CPU used by any program. Every program or task \
that runs on a computer system occupies a certain amount of processing \
time on the CPU. If the CPU has completed all tasks it is idle.',
'unit': 'percent',
'optional': True,
},
'ctx_switches': {
'description': 'number of context switches (voluntary + involuntary) per \
second. A context switch is a procedure that a computer\'s CPU (central \
processing unit) follows to change from one task (or process) to \
another while ensuring that the tasks do not conflict.',
'unit': 'number',
'rate': True,
'short_name': 'ctx_sw',
'optional': True,
},
'user': {
'description': 'CPU percent time spent in user space. \
@ -31,6 +47,41 @@ User CPU time is the time spent on the processor running your program\'s code (o
'unit': 'percent',
'log': True,
},
'irq': {
'description': '*(Linux and BSD)*: percent time spent servicing/handling \
hardware/software interrupts. Time servicing interrupts (hardware + \
software).',
'unit': 'percent',
'optional': True,
},
'interrupts': {
'description': 'number of interrupts per second.',
'unit': 'number',
'rate': True,
'short_name': 'inter',
'optional': True,
},
'system': {
'description': 'Percent time spent in kernel space. System CPU time is the \
time spent running code in the Operating System kernel.',
'unit': 'percent',
'log': True,
},
'nice': {
'description': '*(Unix)*: percent time occupied by user level processes with \
a positive nice value. The time the CPU has spent running users\' \
processes that have been *niced*.',
'unit': 'percent',
'optional': True,
},
'soft_interrupts': {
'description': 'number of software interrupts per second. Always set to \
0 on Windows and SunOS.',
'unit': 'number',
'rate': True,
'short_name': 'sw_int',
'optional': True,
},
'iowait': {
'description': '*(Linux)*: percent time spent by the CPU waiting for I/O \
operations to complete.',
@ -42,27 +93,6 @@ operations to complete.',
'unit': 'percent',
'log': True,
},
'idle': {
'description': 'percent of CPU used by any program. Every program or task \
that runs on a computer system occupies a certain amount of processing \
time on the CPU. If the CPU has completed all tasks it is idle.',
'unit': 'percent',
'optional': True,
},
'irq': {
'description': '*(Linux and BSD)*: percent time spent servicing/handling \
hardware/software interrupts. Time servicing interrupts (hardware + \
software).',
'unit': 'percent',
'optional': True,
},
'nice': {
'description': '*(Unix)*: percent time occupied by user level processes with \
a positive nice value. The time the CPU has spent running users\' \
processes that have been *niced*.',
'unit': 'percent',
'optional': True,
},
'steal': {
'description': '*(Linux)*: percentage of time a virtual CPU waits for a real \
CPU while the hypervisor is servicing another virtual processor.',
@ -76,44 +106,23 @@ systems under the control of the Linux kernel.',
'unit': 'percent',
'optional': True,
},
'ctx_switches': {
'description': 'number of context switches (voluntary + involuntary) per \
second. A context switch is a procedure that a computer\'s CPU (central \
processing unit) follows to change from one task (or process) to \
another while ensuring that the tasks do not conflict.',
'unit': 'number',
'rate': True,
'min_symbol': 'K',
'short_name': 'ctx_sw',
'optional': True,
},
'interrupts': {
'description': 'number of interrupts per second.',
'unit': 'number',
'rate': True,
'min_symbol': 'K',
'short_name': 'inter',
'optional': True,
},
'soft_interrupts': {
'description': 'number of software interrupts per second. Always set to \
0 on Windows and SunOS.',
'unit': 'number',
'rate': True,
'min_symbol': 'K',
'short_name': 'sw_int',
'optional': True,
},
'syscalls': {
'description': 'number of system calls per second. Always 0 on Linux OS.',
'unit': 'number',
'rate': True,
'min_symbol': 'K',
'short_name': 'sys_call',
'optional': True,
},
'cpucore': {'description': 'Total number of CPU core.', 'unit': 'number'},
'time_since_update': {'description': 'Number of seconds since last update.', 'unit': 'seconds'},
'cpucore': {
'description': 'Total number of CPU core.',
'unit': 'number',
'display': False,
},
'time_since_update': {
'description': 'Number of seconds since last update.',
'unit': 'seconds',
'display': False,
},
}
# SNMP OID

View File

@ -19,6 +19,7 @@ from glances.plugins.plugin.model import GlancesPluginModel
# Fields description
fields_description = {
'cpucore': {'description': 'Total number of CPU core.', 'unit': 'core', 'short_name': 'LOAD'},
'min1': {
'description': 'Average sum of the number of processes \
waiting in the run-queue plus the number currently executing \
@ -37,7 +38,6 @@ waiting in the run-queue plus the number currently executing \
over 15 minutes.',
'unit': 'float',
},
'cpucore': {'description': 'Total number of CPU core.', 'unit': 'number'},
}
# SNMP OID
@ -204,4 +204,8 @@ def get_load_average(percent: bool = False):
if load_average and percent:
return tuple([round(i / get_nb_log_core() * 100, 1) for i in load_average])
if load_average:
return tuple(float(f"{x:.2f}") for x in load_average)
# Return None if load average is not available
return load_average

View File

@ -14,66 +14,65 @@ from glances.plugins.plugin.model import GlancesPluginModel
# Fields description
fields_description = {
'total': {'description': 'Total physical memory available.', 'unit': 'bytes', 'min_symbol': 'K'},
'percent': {
'description': 'The percentage usage calculated as (total - available) / total * 100.',
'unit': 'percent',
'short_name': 'MEM',
},
'active': {
'description': '*(UNIX)*: memory currently in use or very recently used, and so it is in RAM.',
'unit': 'bytes',
'optional': True,
},
'total': {
'description': 'Total physical memory available.',
'unit': 'bytes',
},
'inactive': {
'description': '*(UNIX)*: memory that is marked as not used.',
'unit': 'bytes',
'short_name': 'inacti',
'optional': True,
},
'used': {
'description': 'Memory used, calculated differently depending on the platform and \
designed for informational purposes only.',
'unit': 'bytes',
},
'buffers': {
'description': '*(Linux, BSD)*: cache for things like file system metadata.',
'unit': 'bytes',
'short_name': 'buffer',
'optional': True,
},
'free': {
'description': 'Memory not being used at all (zeroed) that is readily available; \
note that this doesn\'t reflect the actual memory available (use \'available\' instead).',
'unit': 'bytes',
},
'cached': {
'description': '*(Linux, BSD)*: cache for various things.',
'unit': 'bytes',
'optional': True,
},
'available': {
'description': 'The actual amount of available memory that can be given instantly \
to processes that request more memory in bytes; this is calculated by summing \
different memory values depending on the platform (e.g. free + buffers + cached on Linux) \
and it is supposed to be used to monitor actual memory usage in a cross platform fashion.',
'unit': 'bytes',
'min_symbol': 'K',
},
'percent': {
'description': 'The percentage usage calculated as (total - available) / total * 100.',
'unit': 'percent',
},
'used': {
'description': 'Memory used, calculated differently depending on the platform and \
designed for informational purposes only.',
'unit': 'bytes',
'min_symbol': 'K',
},
'free': {
'description': 'Memory not being used at all (zeroed) that is readily available; \
note that this doesn\'t reflect the actual memory available (use \'available\' instead).',
'unit': 'bytes',
'min_symbol': 'K',
},
'active': {
'description': '*(UNIX)*: memory currently in use or very recently used, and so it is in RAM.',
'unit': 'bytes',
'min_symbol': 'K',
'optional': True,
},
'inactive': {
'description': '*(UNIX)*: memory that is marked as not used.',
'unit': 'bytes',
'min_symbol': 'K',
'short_name': 'inacti',
'optional': True,
},
'buffers': {
'description': '*(Linux, BSD)*: cache for things like file system metadata.',
'unit': 'bytes',
'min_symbol': 'K',
'short_name': 'buffer',
'optional': True,
},
'cached': {
'description': '*(Linux, BSD)*: cache for various things.',
'unit': 'bytes',
'min_symbol': 'K',
'optional': True,
'short_name': 'availa',
'display': False,
},
'wired': {
'description': '*(BSD, macOS)*: memory that is marked to always stay in RAM. It is never moved to disk.',
'unit': 'bytes',
'min_symbol': 'K',
'display': False,
},
'shared': {
'description': '*(BSD)*: memory that may be simultaneously accessed by multiple processes.',
'unit': 'bytes',
'min_symbol': 'K',
'display': False,
},
}

View File

@ -16,21 +16,25 @@ from glances.timer import getTimeSinceLastUpdate
# Fields description
fields_description = {
'total': {'description': 'Total swap memory.', 'unit': 'bytes', 'min_symbol': 'K'},
'used': {'description': 'Used swap memory.', 'unit': 'bytes', 'min_symbol': 'K'},
'free': {'description': 'Free swap memory.', 'unit': 'bytes', 'min_symbol': 'K'},
'percent': {'description': 'Used swap memory in percentage.', 'unit': 'percent'},
'percent': {'description': 'Used swap memory in percentage.', 'unit': 'percent', 'short_name': 'SWAP'},
'total': {'description': 'Total swap memory.', 'unit': 'bytes'},
'used': {'description': 'Used swap memory.', 'unit': 'bytes'},
'free': {'description': 'Free swap memory.', 'unit': 'bytes'},
'sin': {
'description': 'The number of bytes the system has swapped in from disk (cumulative).',
'unit': 'bytes',
'min_symbol': 'K',
'display': False,
},
'sout': {
'description': 'The number of bytes the system has swapped out from disk (cumulative).',
'unit': 'bytes',
'min_symbol': 'K',
'display': False,
},
'time_since_update': {
'description': 'Number of seconds since last update.',
'unit': 'seconds',
'display': False,
},
'time_since_update': {'description': 'Number of seconds since last update.', 'unit': 'seconds'},
}
# SNMP OID

View File

@ -17,14 +17,27 @@ import re
from glances.actions import GlancesActions
from glances.events_list import glances_events
from glances.globals import dictlist, dictlist_json_dumps, iterkeys, itervalues, json_dumps, listkeys, mean, nativestr
from glances.globals import (
auto_number,
dictlist,
dictlist_json_dumps,
iterkeys,
itervalues,
json_dumps,
listkeys,
mean,
nativestr,
)
from glances.history import GlancesHistory
from glances.logger import logger
from glances.outputs.glances_unicode import unicode_message
from glances.thresholds import glances_thresholds
from glances.timer import Counter, Timer, getTimeSinceLastUpdate
fields_unit_short = {'percent': '%'}
fields_unit_short = {
'percent': '%',
'core': 'C',
}
fields_unit_type = {
'percent': 'float',
@ -39,6 +52,7 @@ fields_unit_type = {
'seconds': 'int',
'byte': 'int',
'bytes': 'int',
'core': 'int',
}
@ -1057,60 +1071,10 @@ class GlancesPluginModel:
"""
self._align = value
def auto_unit(self, number, low_precision=False, min_symbol='K', none_symbol='-'):
"""Make a nice human-readable string out of number.
Number of decimal places increases as quantity approaches 1.
CASE: 613421788 RESULT: 585M low_precision: 585M
CASE: 5307033647 RESULT: 4.94G low_precision: 4.9G
CASE: 44968414685 RESULT: 41.9G low_precision: 41.9G
CASE: 838471403472 RESULT: 781G low_precision: 781G
CASE: 9683209690677 RESULT: 8.81T low_precision: 8.8T
CASE: 1073741824 RESULT: 1024M low_precision: 1024M
CASE: 1181116006 RESULT: 1.10G low_precision: 1.1G
:low_precision: returns less decimal places potentially (default is False)
sacrificing precision for more readability.
:min_symbol: Do not approach if number < min_symbol (default is K)
:decimal_count: if set, force the number of decimal number (default is None)
"""
if number is None:
return none_symbol
symbols = ('K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y')
if min_symbol in symbols:
symbols = symbols[symbols.index(min_symbol) :]
prefix = {
'Y': 1208925819614629174706176,
'Z': 1180591620717411303424,
'E': 1152921504606846976,
'P': 1125899906842624,
'T': 1099511627776,
'G': 1073741824,
'M': 1048576,
'K': 1024,
}
if number == 0:
# Avoid 0.0
return '0'
for symbol in reversed(symbols):
value = float(number) / prefix[symbol]
if value > 1:
decimal_precision = 0
if value < 10:
decimal_precision = 2
elif value < 100:
decimal_precision = 1
if low_precision:
if symbol in 'MK':
decimal_precision = 0
else:
decimal_precision = min(1, decimal_precision)
elif symbol in 'K':
decimal_precision = 0
return '{:.{decimal}f}{symbol}'.format(value, decimal=decimal_precision, symbol=symbol)
return f'{number!s}'
def auto_unit(self, number, low_precision=False, min_symbol='K', none_symbol='-', strip_zero=True):
return auto_number(
number, low_precision=low_precision, min_symbol=min_symbol, none_symbol=none_symbol, strip_zero=strip_zero
)
def trend_msg(self, trend, significant=1):
"""Return the trend message.

View File

@ -18,7 +18,7 @@ from datetime import datetime
from glances import __version__
from glances.events_list import GlancesEventsList
from glances.filter import GlancesFilter, GlancesFilterList
from glances.globals import LINUX, WINDOWS, pretty_date, string_value_to_float, subsample
from glances.globals import LINUX, WINDOWS, auto_number, pretty_date, string_value_to_float, subsample
from glances.main import GlancesMain
from glances.outputs.glances_bars import Bar
from glances.plugins.plugin.model import GlancesPluginModel
@ -505,6 +505,32 @@ class TestGlances(unittest.TestCase):
self.assertEqual(pretty_date(datetime(2023, 1, 1, 0, 0), datetime(2024, 1, 1, 12, 0)), 'an year')
self.assertEqual(pretty_date(datetime(2020, 1, 1, 0, 0), datetime(2024, 1, 1, 12, 0)), '4 years')
def test_022_auto_number(self):
"""Test auto_number"""
print('INFO: [TEST_022] auto_number')
self.assertEqual(auto_number(None), '-')
self.assertEqual(auto_number(0), '0')
self.assertEqual(auto_number(25), '25')
self.assertEqual(auto_number(25.0), '25')
self.assertEqual(auto_number(25.3), '25.3')
self.assertEqual(auto_number(100), '100')
self.assertEqual(auto_number(100.0), '100')
self.assertEqual(auto_number(1025.0), '1K')
self.assertEqual(auto_number(613421788), '585M')
self.assertEqual(auto_number(613421788, low_precision=True), '585M')
self.assertEqual(auto_number(5307033647), '4.94G')
self.assertEqual(auto_number(5307033647, low_precision=True), '4.9G')
self.assertEqual(auto_number(44968414685), '41.9G')
self.assertEqual(auto_number(44968414685, low_precision=True), '41.9G')
self.assertEqual(auto_number(838471403472), '781G')
self.assertEqual(auto_number(838471403472, low_precision=True), '781G')
self.assertEqual(auto_number(9683209690677), '8.81T')
self.assertEqual(auto_number(9683209690677, low_precision=True), '8.8T')
self.assertEqual(auto_number(1073741824), '1024M')
self.assertEqual(auto_number(1073741824, low_precision=True), '1024M')
self.assertEqual(auto_number(1181116006), '1.10G')
self.assertEqual(auto_number(1181116006, low_precision=True), '1.1G')
def test_094_thresholds(self):
"""Test thresholds classes"""
print('INFO: [TEST_094] Thresholds')