From ed196c71959159c60cad145486463cc4382ea953 Mon Sep 17 00:00:00 2001 From: Johan Walles Date: Wed, 6 Jul 2022 22:20:04 +0200 Subject: [PATCH 1/9] Parameterize the RAM bar --- px/px_rambar.py | 44 +++++++++++++------------------------------- 1 file changed, 13 insertions(+), 31 deletions(-) diff --git a/px/px_rambar.py b/px/px_rambar.py index 59ef469..c448f23 100644 --- a/px/px_rambar.py +++ b/px/px_rambar.py @@ -4,13 +4,14 @@ from . import px_process -from typing import List +from typing import Callable, List from typing import Dict from typing import Tuple def get_process_categories( all_processes: List[px_process.PxProcess], + get_category: Callable[[px_process.PxProcess], str], ) -> List[Tuple[str, int]]: """ Group processes by pretty names, keeping track of the total rss_kb in each @@ -19,34 +20,9 @@ def get_process_categories( names_to_kilobytes: Dict[str, 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 - - return sorted(names_to_kilobytes.items(), key=operator.itemgetter(1), reverse=True) - - -def get_user_categories( - all_processes: List[px_process.PxProcess], -) -> List[Tuple[str, int]]: - """ - Group processes by user names, keeping track of the total rss_kb in each - group. - """ - - names_to_kilobytes: Dict[str, int] = {} - for process in all_processes: - base_kb = 0 - if process.username in names_to_kilobytes: - base_kb = names_to_kilobytes[process.username] - else: - base_kb = 0 - - names_to_kilobytes[process.username] = base_kb + process.rss_kb + category = get_category(process) + base_kb = names_to_kilobytes.get(category, 0) + names_to_kilobytes[category] = base_kb + process.rss_kb return sorted(names_to_kilobytes.items(), key=operator.itemgetter(1), reverse=True) @@ -111,10 +87,16 @@ def render_bar(bar_length: int, names_and_numbers: List[Tuple[str, int]]) -> str def rambar_by_process( ram_bar_length: int, all_processes: List[px_process.PxProcess] ) -> str: - return render_bar(ram_bar_length, get_process_categories(all_processes)) + return render_bar( + ram_bar_length, + get_process_categories(all_processes, lambda process: process.command), + ) def rambar_by_user( ram_bar_length: int, all_processes: List[px_process.PxProcess] ) -> str: - return render_bar(ram_bar_length, get_user_categories(all_processes)) + return render_bar( + ram_bar_length, + get_process_categories(all_processes, lambda process: process.username), + ) From 6235cf60975f30b308c03718f1bb84956211f1fd Mon Sep 17 00:00:00 2001 From: Johan Walles Date: Thu, 7 Jul 2022 06:53:13 +0200 Subject: [PATCH 2/9] Try adding CPU time bars Tests fail, ptop crashes. --- px/{px_rambar.py => px_category_bar.py} | 70 ++++++++++++++++++------- px/px_top.py | 30 +++++++---- tests/px_rambar_test.py | 6 +-- 3 files changed, 73 insertions(+), 33 deletions(-) rename px/{px_rambar.py => px_category_bar.py} (57%) diff --git a/px/px_rambar.py b/px/px_category_bar.py similarity index 57% rename from px/px_rambar.py rename to px/px_category_bar.py index c448f23..5b2a463 100644 --- a/px/px_rambar.py +++ b/px/px_category_bar.py @@ -4,35 +4,39 @@ from . import px_process -from typing import Callable, List +from typing import Callable, List, Optional from typing import Dict from typing import Tuple -def get_process_categories( +def cluster_processes( all_processes: List[px_process.PxProcess], get_category: Callable[[px_process.PxProcess], str], -) -> List[Tuple[str, int]]: + get_value: Callable[[px_process.PxProcess], Optional[float]], +) -> List[Tuple[str, float]]: """ - Group processes by pretty names, keeping track of the total rss_kb in each - group. + Group processes by category and value, summing up the values in each group. """ - names_to_kilobytes: Dict[str, int] = {} + names_to_kilobytes: Dict[str, float] = {} for process in all_processes: category = get_category(process) - base_kb = names_to_kilobytes.get(category, 0) - names_to_kilobytes[category] = base_kb + process.rss_kb + value = get_value(process) + if value is None: + continue + + total = names_to_kilobytes.get(category, 0) + names_to_kilobytes[category] = total + value return sorted(names_to_kilobytes.items(), key=operator.itemgetter(1), reverse=True) -def render_bar(bar_length: int, names_and_numbers: List[Tuple[str, int]]) -> str: +def render_bar(bar_length: int, names_and_numbers: List[Tuple[str, float]]) -> str: """ You probably want to use rambar() instead, this is just a utility function. """ - total = 0 + total = 0.0 for category in names_and_numbers: total += category[1] assert total > 0 @@ -84,19 +88,45 @@ def render_bar(bar_length: int, names_and_numbers: List[Tuple[str, int]]) -> str return bar -def rambar_by_process( - ram_bar_length: int, all_processes: List[px_process.PxProcess] -) -> str: +def ram_by_program(length: int, all_processes: List[px_process.PxProcess]) -> str: + return render_bar( + length, + cluster_processes( + all_processes, + lambda process: process.command, + lambda process: process.rss_kb, + ), + ) + + +def ram_by_user(length: int, all_processes: List[px_process.PxProcess]) -> str: + return render_bar( + length, + cluster_processes( + all_processes, + lambda process: process.username, + lambda process: process.rss_kb, + ), + ) + + +def cpu_by_program(length: int, all_processes: List[px_process.PxProcess]) -> str: return render_bar( - ram_bar_length, - get_process_categories(all_processes, lambda process: process.command), + length, + cluster_processes( + all_processes, + lambda process: process.command, + lambda process: process.cpu_time_seconds, + ), ) -def rambar_by_user( - ram_bar_length: int, all_processes: List[px_process.PxProcess] -) -> str: +def cpu_by_user(length: int, all_processes: List[px_process.PxProcess]) -> str: return render_bar( - ram_bar_length, - get_process_categories(all_processes, lambda process: process.username), + length, + cluster_processes( + all_processes, + lambda process: process.username, + lambda process: process.cpu_time_seconds, + ), ) diff --git a/px/px_top.py b/px/px_top.py index c6dcea6..7f07b41 100644 --- a/px/px_top.py +++ b/px/px_top.py @@ -9,7 +9,7 @@ from . import px_processinfo from . import px_process_menu from . import px_poller -from . import px_rambar +from . import px_category_bar from typing import List from typing import Dict @@ -224,25 +224,35 @@ def get_screen_lines( footer_height = 1 assert screen_columns > 0 - ram_bar_length = screen_columns - 16 - if ram_bar_length > 20: - # Enough space for a usable RAM bar. Limit picked entirely arbitrarily, - # feel free to change it if you have a better number. - rambar_by_process = ( - "[" + px_rambar.rambar_by_process(ram_bar_length, all_processes) + "]" + bar_length = screen_columns - 16 + if bar_length > 20: + # Enough space for usable category bars. Length limit ^ picked entirely + # arbitrarily, feel free to change it if you have a better number. + cpubar_by_program = ( + "[" + px_category_bar.cpu_by_program(bar_length, all_processes) + "]" + ) + cpubar_by_user = ( + "[" + px_category_bar.cpu_by_user(bar_length, all_processes) + "]" + ) + rambar_by_program = ( + "[" + px_category_bar.ram_by_program(bar_length, all_processes) + "]" ) rambar_by_user = ( - "[" + px_rambar.rambar_by_user(ram_bar_length, all_processes) + "]" + "[" + px_category_bar.ram_by_user(bar_length, all_processes) + "]" ) else: - rambar_by_process = "[ ... ]" + cpubar_by_program = "[ ... ]" + cpubar_by_user = "[ ... ]" + rambar_by_program = "[ ... ]" rambar_by_user = "[ ... ]" # Print header lines = [ px_terminal.bold("Sysload: ") + poller.get_loadstring(), + " By program: " + cpubar_by_program, + " By user: " + cpubar_by_user, px_terminal.bold("RAM Use: ") + poller.get_meminfo(), - " By process: " + rambar_by_process, + " By program: " + rambar_by_program, " By user: " + rambar_by_user, px_terminal.bold("IO Load: ") + poller.get_ioload_string(), "", diff --git a/tests/px_rambar_test.py b/tests/px_rambar_test.py index e2586c3..4b05db7 100644 --- a/tests/px_rambar_test.py +++ b/tests/px_rambar_test.py @@ -1,4 +1,4 @@ -from px import px_rambar +from px import px_category_bar from px import px_terminal @@ -6,7 +6,7 @@ def test_render_bar_happy_path(): names_and_numbers = [("apa", 1000), ("bepa", 300), ("cepa", 50)] + [ ("long tail", 1) ] * 300 - assert px_rambar.render_bar(10, names_and_numbers) == ( + assert px_category_bar.render_bar(10, names_and_numbers) == ( px_terminal.red(" apa ") + px_terminal.yellow(" b") + px_terminal.blue(" ") @@ -18,7 +18,7 @@ def test_render_bar_happy_path_unicode(): names_and_numbers = [("åpa", 1000), ("bäpa", 300), ("cäpa", 50)] + [ ("lång svans", 1) ] * 300 - assert px_rambar.render_bar(10, names_and_numbers) == ( + assert px_category_bar.render_bar(10, names_and_numbers) == ( px_terminal.red(" åpa ") + px_terminal.yellow(" b") + px_terminal.blue(" ") From 10376d5ff95a98b5a897aa5b744be83c4168b70b Mon Sep 17 00:00:00 2001 From: Johan Walles Date: Thu, 7 Jul 2022 06:59:46 +0200 Subject: [PATCH 3/9] Fix ptop crash --- px/px_category_bar.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/px/px_category_bar.py b/px/px_category_bar.py index 5b2a463..7d32820 100644 --- a/px/px_category_bar.py +++ b/px/px_category_bar.py @@ -39,7 +39,8 @@ def render_bar(bar_length: int, names_and_numbers: List[Tuple[str, float]]) -> s total = 0.0 for category in names_and_numbers: total += category[1] - assert total > 0 + if total == 0: + return "" bar = "" bar_chars = 0 From 832fabe802af924e8535e0a522f12cf2eb07c629 Mon Sep 17 00:00:00 2001 From: Johan Walles Date: Thu, 7 Jul 2022 07:22:46 +0200 Subject: [PATCH 4/9] Don't list too many processes Much closer, but the tests still fail. --- px/px_category_bar.py | 6 ++++++ px/px_top.py | 7 ++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/px/px_category_bar.py b/px/px_category_bar.py index 7d32820..b06cd86 100644 --- a/px/px_category_bar.py +++ b/px/px_category_bar.py @@ -112,6 +112,9 @@ def ram_by_user(length: int, all_processes: List[px_process.PxProcess]) -> str: def cpu_by_program(length: int, all_processes: List[px_process.PxProcess]) -> str: + # FIXME: If all CPU times are zero / unset, use CPU percentages instead. + # That would make the switch between the first and the second screen frames + # less jarring to look at. return render_bar( length, cluster_processes( @@ -123,6 +126,9 @@ def cpu_by_program(length: int, all_processes: List[px_process.PxProcess]) -> st def cpu_by_user(length: int, all_processes: List[px_process.PxProcess]) -> str: + # FIXME: If all CPU times are zero / unset, use CPU percentages instead. + # That would make the switch between the first and the second screen frames + # less jarring to look at. return render_bar( length, cluster_processes( diff --git a/px/px_top.py b/px/px_top.py index 7f07b41..d81dba1 100644 --- a/px/px_top.py +++ b/px/px_top.py @@ -317,9 +317,10 @@ def get_screen_lines( assert top_mode == MODE_BASE lines += [SEARCH_PROMPT_INACTIVE + px_terminal.bold(search or "")] - lines += toplist_table_lines[ - 0 : max_process_count + 1 - ] # +1 for the column headings + if max_process_count > 0: + lines += toplist_table_lines[ + 0 : max_process_count + 1 + ] # +1 for the column headings lines += launchlines From 87440e8016c1e381fcc4d98b34706437e05a7a43 Mon Sep 17 00:00:00 2001 From: Johan Walles Date: Thu, 7 Jul 2022 08:14:30 +0200 Subject: [PATCH 5/9] Make the test pass This is kind of cheating, but it's not *too* bad and it does solve the problem. --- tests/px_top_test.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/px_top_test.py b/tests/px_top_test.py index 3300b2e..d3f03dc 100644 --- a/tests/px_top_test.py +++ b/tests/px_top_test.py @@ -100,7 +100,10 @@ def test_get_screen_lines_low_screen(): baseline = px_process.get_all() poller = px_poller.PxPoller() - SCREEN_ROWS = 10 + # We have to make up some number for "How low screens can we cope with?". + # Here's the number I made up. + SCREEN_ROWS = 11 + px_terminal._enable_color = True lines = px_top.get_screen_lines(baseline, poller, SCREEN_ROWS, 99) From ff26a5726467dbabff7a37af09c79f6dc7a76198 Mon Sep 17 00:00:00 2001 From: Johan Walles Date: Thu, 7 Jul 2022 18:18:49 +0200 Subject: [PATCH 6/9] Extract ptop header generation function --- px/px_load.py | 4 +-- px/px_top.py | 68 +++++++++++++++++++++++++++++---------------------- 2 files changed, 41 insertions(+), 31 deletions(-) diff --git a/px/px_load.py b/px/px_load.py index 91ea47f..4cfcc83 100644 --- a/px/px_load.py +++ b/px/px_load.py @@ -87,7 +87,7 @@ def get_load_values() -> Tuple[float, float, float]: def get_load_string(load_values: Tuple[float, float, float] = None) -> str: """ Example return string, underlines indicate bold: - "1.5 [4 cores | 8 virtual] [15m load history: GRAPH]" + "1.5 [4 cores | 8 virtual] [15m history: GRAPH]" ^^^ ^^^^^^^ ^^^^^ Load number is color coded: @@ -114,4 +114,4 @@ def get_load_string(load_values: Tuple[float, float, float] = None) -> str: # Increase intensity for more recent times graph = px_terminal.faint(graph[0:3]) + graph[3:6] + px_terminal.bold(graph[6:]) - return "{} {} [15m load history: {}]".format(load_string, cores_string, graph) + return "{} {} [15m history: {}]".format(load_string, cores_string, graph) diff --git a/px/px_top.py b/px/px_top.py index d81dba1..7f69621 100644 --- a/px/px_top.py +++ b/px/px_top.py @@ -195,50 +195,27 @@ def get_line_to_highlight( return last_highlighted_row -def get_screen_lines( - toplist: List[px_process.PxProcess], +def generate_header( + filtered_processes: List[px_process.PxProcess], poller: px_poller.PxPoller, - screen_rows: int, screen_columns: int, - include_footer: bool = True, - search: Optional[str] = None, ) -> List[str]: - """ - 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 - toplist = list( - filter(lambda p: p.match(search, require_exact_user=False), toplist) - ) - - # Hand out different amount of lines to the different sections - footer_height = 0 - cputop_minheight = 10 - if include_footer: - footer_height = 1 - assert screen_columns > 0 bar_length = screen_columns - 16 if bar_length > 20: # Enough space for usable category bars. Length limit ^ picked entirely # arbitrarily, feel free to change it if you have a better number. cpubar_by_program = ( - "[" + px_category_bar.cpu_by_program(bar_length, all_processes) + "]" + "[" + px_category_bar.cpu_by_program(bar_length, filtered_processes) + "]" ) cpubar_by_user = ( - "[" + px_category_bar.cpu_by_user(bar_length, all_processes) + "]" + "[" + px_category_bar.cpu_by_user(bar_length, filtered_processes) + "]" ) rambar_by_program = ( - "[" + px_category_bar.ram_by_program(bar_length, all_processes) + "]" + "[" + px_category_bar.ram_by_program(bar_length, filtered_processes) + "]" ) rambar_by_user = ( - "[" + px_category_bar.ram_by_user(bar_length, all_processes) + "]" + "[" + px_category_bar.ram_by_user(bar_length, filtered_processes) + "]" ) else: cpubar_by_program = "[ ... ]" @@ -258,6 +235,39 @@ def get_screen_lines( "", ] + return lines + + +def get_screen_lines( + toplist: List[px_process.PxProcess], + poller: px_poller.PxPoller, + screen_rows: int, + screen_columns: int, + include_footer: bool = True, + search: Optional[str] = None, +) -> List[str]: + """ + 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 + toplist = list( + filter(lambda p: p.match(search, require_exact_user=False), toplist) + ) + + # Hand out different amount of lines to the different sections + footer_height = 0 + cputop_minheight = 10 + if include_footer: + footer_height = 1 + + lines = generate_header(all_processes, poller, screen_columns) + # Create a launches section header_height = len(lines) main_area_height = screen_rows - header_height - footer_height From e2d15d2bf826d160ddc5a11aa6834f8a6cf867d6 Mon Sep 17 00:00:00 2001 From: Johan Walles Date: Thu, 7 Jul 2022 18:37:56 +0200 Subject: [PATCH 7/9] Make a split header on wider screens On narrower screens, maintain the old behavior. --- px/px_top.py | 52 +++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/px/px_top.py b/px/px_top.py index 7f69621..d583d1d 100644 --- a/px/px_top.py +++ b/px/px_top.py @@ -201,7 +201,40 @@ def generate_header( screen_columns: int, ) -> List[str]: assert screen_columns > 0 - bar_length = screen_columns - 16 + + sysload_line = px_terminal.bold("Sysload: ") + poller.get_loadstring() + ramuse_line = px_terminal.bold("RAM Use: ") + poller.get_meminfo() + + if px_terminal.visual_length(sysload_line) > (screen_columns // 2 - 1): + # Traditional header + bar_length = screen_columns - 16 + if bar_length > 20: + # Enough space for usable category bars. Length limit ^ picked entirely + # arbitrarily, feel free to change it if you have a better number. + rambar_by_program = ( + "[" + + px_category_bar.ram_by_program(bar_length, filtered_processes) + + "]" + ) + rambar_by_user = ( + "[" + px_category_bar.ram_by_user(bar_length, filtered_processes) + "]" + ) + else: + rambar_by_program = "[ ... ]" + rambar_by_user = "[ ... ]" + + # Print header + return [ + sysload_line, + ramuse_line, + " By program: " + rambar_by_program, + " By user: " + rambar_by_user, + px_terminal.bold("IO Load: ") + poller.get_ioload_string(), + "", + ] + + # Make a split header + bar_length = screen_columns // 2 - 3 if bar_length > 20: # Enough space for usable category bars. Length limit ^ picked entirely # arbitrarily, feel free to change it if you have a better number. @@ -223,20 +256,17 @@ def generate_header( rambar_by_program = "[ ... ]" rambar_by_user = "[ ... ]" - # Print header - lines = [ - px_terminal.bold("Sysload: ") + poller.get_loadstring(), - " By program: " + cpubar_by_program, - " By user: " + cpubar_by_user, - px_terminal.bold("RAM Use: ") + poller.get_meminfo(), - " By program: " + rambar_by_program, - " By user: " + rambar_by_user, + return [ + px_terminal.get_string_of_length(sysload_line, screen_columns // 2) + + ramuse_line, + px_terminal.get_string_of_length(cpubar_by_program, screen_columns // 2) + + rambar_by_program, + px_terminal.get_string_of_length(cpubar_by_user, screen_columns // 2) + + rambar_by_user, px_terminal.bold("IO Load: ") + poller.get_ioload_string(), "", ] - return lines - def get_screen_lines( toplist: List[px_process.PxProcess], From 4cab34427ad78ffbdfdac39247ff7c6515e0a6df Mon Sep 17 00:00:00 2001 From: Johan Walles Date: Thu, 7 Jul 2022 18:42:19 +0200 Subject: [PATCH 8/9] Make the tests pass --- px/px_category_bar.py | 3 ++- tests/{px_rambar_test.py => px_categorybar_test.py} | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) rename tests/{px_rambar_test.py => px_categorybar_test.py} (79%) diff --git a/px/px_category_bar.py b/px/px_category_bar.py index b06cd86..ee45679 100644 --- a/px/px_category_bar.py +++ b/px/px_category_bar.py @@ -33,7 +33,8 @@ def cluster_processes( def render_bar(bar_length: int, names_and_numbers: List[Tuple[str, float]]) -> str: """ - You probably want to use rambar() instead, this is just a utility function. + You probably want to use x_by_y() functions at the end of this file instead, + this is just an internal utility function. """ total = 0.0 diff --git a/tests/px_rambar_test.py b/tests/px_categorybar_test.py similarity index 79% rename from tests/px_rambar_test.py rename to tests/px_categorybar_test.py index 4b05db7..40ac967 100644 --- a/tests/px_rambar_test.py +++ b/tests/px_categorybar_test.py @@ -3,7 +3,7 @@ def test_render_bar_happy_path(): - names_and_numbers = [("apa", 1000), ("bepa", 300), ("cepa", 50)] + [ + names_and_numbers = [("apa", 1000.0), ("bepa", 300.0), ("cepa", 50.0)] + [ ("long tail", 1) ] * 300 assert px_category_bar.render_bar(10, names_and_numbers) == ( @@ -15,7 +15,7 @@ def test_render_bar_happy_path(): def test_render_bar_happy_path_unicode(): - names_and_numbers = [("åpa", 1000), ("bäpa", 300), ("cäpa", 50)] + [ + names_and_numbers = [("åpa", 1000.0), ("bäpa", 300.0), ("cäpa", 50.0)] + [ ("lång svans", 1) ] * 300 assert px_category_bar.render_bar(10, names_and_numbers) == ( From 333736e60d4dcc13112fff5580428c4c83a932d7 Mon Sep 17 00:00:00 2001 From: Johan Walles Date: Thu, 7 Jul 2022 20:38:25 +0200 Subject: [PATCH 9/9] Make a CPU bar on the first frame as well Use CPU percentages for the first frame, before we have managed to capture any timings. --- px/px_category_bar.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/px/px_category_bar.py b/px/px_category_bar.py index ee45679..dad41e0 100644 --- a/px/px_category_bar.py +++ b/px/px_category_bar.py @@ -112,29 +112,39 @@ def ram_by_user(length: int, all_processes: List[px_process.PxProcess]) -> str: ) +def create_cpu_getter( + all_processes: List[px_process.PxProcess], +) -> Callable[[px_process.PxProcess], Optional[float]]: + """ + Getter for cpu_time_seconds if there are any, otherwise for cpu_percent (of + which we know there are a bunch). + """ + for process in all_processes: + if process.cpu_time_seconds is None: + continue + if process.cpu_time_seconds > 0: + return lambda p: p.cpu_time_seconds + + return lambda p: p.cpu_percent + + def cpu_by_program(length: int, all_processes: List[px_process.PxProcess]) -> str: - # FIXME: If all CPU times are zero / unset, use CPU percentages instead. - # That would make the switch between the first and the second screen frames - # less jarring to look at. return render_bar( length, cluster_processes( all_processes, lambda process: process.command, - lambda process: process.cpu_time_seconds, + create_cpu_getter(all_processes), ), ) def cpu_by_user(length: int, all_processes: List[px_process.PxProcess]) -> str: - # FIXME: If all CPU times are zero / unset, use CPU percentages instead. - # That would make the switch between the first and the second screen frames - # less jarring to look at. return render_bar( length, cluster_processes( all_processes, lambda process: process.username, - lambda process: process.cpu_time_seconds, + create_cpu_getter(all_processes), ), )