Skip to content

Commit

Permalink
Merge branch 'johan/cumulative-cputime' into python
Browse files Browse the repository at this point in the history
It's "aggregated", but still.

This change adds another sort order, showing a process tree sorted by
aggregated CPU time. Get there by pressing "m" twice.

Fixes #124.
  • Loading branch information
walles committed Mar 28, 2024
2 parents 31b0c04 + ee86034 commit 4ed7125
Show file tree
Hide file tree
Showing 7 changed files with 208 additions and 87 deletions.
15 changes: 12 additions & 3 deletions px/px_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@


from typing import Dict
from typing import MutableSet
from typing import Optional
from typing import List
from typing import Iterable
Expand Down Expand Up @@ -92,6 +91,7 @@ def __init__(
memory_percent: Optional[float] = None,
cpu_percent: Optional[float] = None,
cpu_time: Optional[float] = None,
aggregated_cpu_time: float = 0.0,
) -> None:
self.pid: int = pid
self.ppid: Optional[int] = ppid
Expand Down Expand Up @@ -142,10 +142,15 @@ def __init__(
self.cpu_percent_s = f"{cpu_percent:.0f}%"

self.set_cpu_time_seconds(cpu_time)
self.set_aggregated_cpu_time_seconds(aggregated_cpu_time)

self.children: MutableSet[PxProcess] = set()
self.children: List[PxProcess] = []
self.parent: Optional[PxProcess] = None

# How many levels down the tree this process is. Kernel is level 0, init
# level 1 and everything else 2 and up.
self.level = 0

def __repr__(self):
# I guess this is really what __str__ should be doing, but the point of
# implementing this method is to make the py.test output more readable,
Expand Down Expand Up @@ -173,6 +178,10 @@ def set_cpu_time_seconds(self, seconds: Optional[float]) -> None:
self.cpu_time_s = seconds_to_str(seconds)
self.cpu_time_seconds = seconds

def set_aggregated_cpu_time_seconds(self, seconds: float) -> None:
self.aggregated_cpu_time_s = seconds_to_str(seconds)
self.aggregated_cpu_time_seconds = seconds

def match(self, string, require_exact_user=True):
"""
Returns True if this process matches the string.
Expand Down Expand Up @@ -393,7 +402,7 @@ def resolve_links(processes: Dict[int, PxProcess], now: datetime.datetime) -> No
process.parent = processes.get(process.ppid)

if process.parent is not None:
process.parent.children.add(process)
process.parent.children.append(process)


def remove_process_and_descendants(processes: Dict[int, PxProcess], pid: int) -> None:
Expand Down
14 changes: 14 additions & 0 deletions px/px_sort_order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import enum


class SortOrder(enum.Enum):
CPU = 1
MEMORY = 2
AGGREGATED_CPU = 3

def next(self):
if self == SortOrder.CPU:
return SortOrder.MEMORY
if self == SortOrder.MEMORY:
return SortOrder.AGGREGATED_CPU
return SortOrder.CPU
67 changes: 48 additions & 19 deletions px/px_terminal.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from typing import Optional
from typing import Iterable
from . import px_process
from . import px_sort_order


# NOTE: To work with this list it can be useful to find the text "Uncomment to
Expand Down Expand Up @@ -314,29 +315,36 @@ def format_with_widths(widths: List[int], strings: List[str]) -> str:
def to_screen_lines(
procs: List[px_process.PxProcess],
row_to_highlight: Optional[int],
highlight_heading: Optional[str],
sort_order: Optional[px_sort_order.SortOrder],
with_username: bool = True,
) -> List[str]:
"""
Returns an array of lines that can be printed to screen. Lines are not
cropped, so they can be longer than the screen width.
If highligh_heading contains a column name, that column will be highlighted.
The column name must be from the hard coded list in this function, see below.
If sort_order is set, the sort order column will be highlighted.
"""

cputime_name = "CPUTIME"
if sort_order == px_sort_order.SortOrder.AGGREGATED_CPU:
cputime_name = "AGGRCPU"
headings = [
"PID",
"COMMAND",
"USERNAME",
"CPU",
"CPUTIME",
cputime_name,
"RAM",
"COMMANDLINE",
]
highlight_column: Optional[int] = None
if highlight_heading is not None:
highlight_column = headings.index(highlight_heading)
highlight_column = None
if sort_order == px_sort_order.SortOrder.MEMORY:
highlight_column = 5 # "RAM"
elif sort_order in [
px_sort_order.SortOrder.CPU,
px_sort_order.SortOrder.AGGREGATED_CPU,
]:
highlight_column = 4 # "CPUTIME" or "AGGRCPU"

# Compute widest width for pid, command, user, cpu and memory usage columns
pid_width = len(headings[0])
Expand All @@ -347,10 +355,15 @@ def to_screen_lines(
mem_width = len(headings[5])
for proc in procs:
pid_width = max(pid_width, len(str(proc.pid)))
command_width = max(command_width, len(proc.command))
command_width = max(command_width, len(proc.command) + proc.level * 2)
username_width = max(username_width, len(proc.username))
cpu_width = max(cpu_width, len(proc.cpu_percent_s))
cputime_width = max(cputime_width, len(proc.cpu_time_s))

cputime_s = proc.cpu_time_s
if sort_order == px_sort_order.SortOrder.AGGREGATED_CPU:
cputime_s = proc.aggregated_cpu_time_s
cputime_width = max(cputime_width, len(cputime_s))

mem_width = max(mem_width, len(proc.memory_percent_s))

column_widths = [
Expand All @@ -363,8 +376,8 @@ def to_screen_lines(
0, # The command line can have any length
]

username_index = headings.index("USERNAME")
if not with_username:
username_index = headings.index("USERNAME")
del headings[username_index]
del column_widths[username_index]

Expand All @@ -391,11 +404,18 @@ def to_screen_lines(
if proc.memory_percent is not None and proc.memory_percent > max_memory_percent:
max_memory_percent = proc.memory_percent
max_memory_percent_s = proc.memory_percent_s
if (
proc.cpu_time_seconds is not None
and proc.cpu_time_seconds > max_cpu_time_seconds
):
max_cpu_time_seconds = proc.cpu_time_seconds

cpu_time_seconds = proc.cpu_time_seconds
if sort_order == px_sort_order.SortOrder.AGGREGATED_CPU:
if proc.pid <= 1:
# Both the kernel (PID 0) and the init process (PID 1) will just
# contain the total time of all other processes. Since we only
# use this max value for highlighting (see below), if we include
# these only they will be highlighted. So we skip them.
continue
cpu_time_seconds = proc.aggregated_cpu_time_seconds
if cpu_time_seconds is not None and cpu_time_seconds > max_cpu_time_seconds:
max_cpu_time_seconds = cpu_time_seconds

current_user = os.environ.get("SUDO_USER") or getpass.getuser()
for line_number, proc in enumerate(procs):
Expand All @@ -412,12 +432,17 @@ def to_screen_lines(
memory_percent_s = bold(memory_percent_s.rjust(mem_width))

cpu_time_s = proc.cpu_time_s
if not proc.cpu_time_seconds:
cpu_time_seconds = proc.cpu_time_seconds
if sort_order == px_sort_order.SortOrder.AGGREGATED_CPU:
cpu_time_s = proc.aggregated_cpu_time_s
cpu_time_seconds = proc.aggregated_cpu_time_seconds

if not cpu_time_seconds:
# Zero or undefined
cpu_time_s = faint(cpu_time_s.rjust(cputime_width))
elif proc.cpu_time_seconds > 0.75 * max_cpu_time_seconds:
elif cpu_time_seconds > 0.75 * max_cpu_time_seconds:
cpu_time_s = bold(cpu_time_s.rjust(cputime_width))
elif proc.cpu_time_seconds < 0.1 * max_cpu_time_seconds:
elif cpu_time_seconds < 0.1 * max_cpu_time_seconds:
cpu_time_s = faint(cpu_time_s.rjust(cputime_width))

# NOTE: This logic should match its friend in
Expand All @@ -429,9 +454,13 @@ def to_screen_lines(
# Neither root nor ourselves, highlight!
owner = bold(owner)

indent = ""
if sort_order == px_sort_order.SortOrder.AGGREGATED_CPU:
indent = " " * proc.level * 2

columns = [
str(proc.pid),
proc.command,
indent + proc.command,
owner,
cpu_percent_s,
cpu_time_s,
Expand Down
Loading

0 comments on commit 4ed7125

Please sign in to comment.