ppop 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ppop-0.1.0/LICENSE +21 -0
- ppop-0.1.0/PKG-INFO +111 -0
- ppop-0.1.0/README.md +92 -0
- ppop-0.1.0/popup/__init__.py +3 -0
- ppop-0.1.0/popup/__main__.py +5 -0
- ppop-0.1.0/popup/cli.py +46 -0
- ppop-0.1.0/popup/device.py +225 -0
- ppop-0.1.0/popup/tui.py +513 -0
- ppop-0.1.0/ppop.egg-info/PKG-INFO +111 -0
- ppop-0.1.0/ppop.egg-info/SOURCES.txt +14 -0
- ppop-0.1.0/ppop.egg-info/dependency_links.txt +1 -0
- ppop-0.1.0/ppop.egg-info/entry_points.txt +2 -0
- ppop-0.1.0/ppop.egg-info/requires.txt +1 -0
- ppop-0.1.0/ppop.egg-info/top_level.txt +1 -0
- ppop-0.1.0/pyproject.toml +32 -0
- ppop-0.1.0/setup.cfg +4 -0
ppop-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Zhongwang Lun
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
ppop-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ppop
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: An interactive PPU process viewer and resource monitor, inspired by nvitop.
|
|
5
|
+
Author-email: Zhongwang Lun <lunandcnn@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: ppu,monitor,process-viewer,tui
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Environment :: Console :: Curses
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Intended Audience :: System Administrators
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: System :: Monitoring
|
|
14
|
+
Requires-Python: >=3.8
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: psutil>=5.6.6
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# ppop
|
|
21
|
+
|
|
22
|
+
An interactive PPU process viewer and resource monitor for the terminal, inspired by [nvitop](https://github.com/XuehaiPan/nvitop).
|
|
23
|
+
|
|
24
|
+
## Features
|
|
25
|
+
|
|
26
|
+
- Real-time PPU device monitoring (utilization, memory, temperature, power)
|
|
27
|
+
- Host CPU and memory usage display
|
|
28
|
+
- Per-process PPU memory tracking
|
|
29
|
+
- Interactive curses-based TUI with color-coded utilization bars
|
|
30
|
+
- Non-interactive one-shot mode for scripts and logging
|
|
31
|
+
|
|
32
|
+
## Demo
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
+-------------------------------------------------------------------------------+
|
|
36
|
+
| ppop - PPU Monitor Driver Version: 1.5.5-7a3ae6 |
|
|
37
|
+
+-----------------------------------+----------------------+-------------------+
|
|
38
|
+
| PPU Name | Memory-Usage | PPU-Util |
|
|
39
|
+
+===================================+======================+===================+
|
|
40
|
+
| 0 PPU-ZW810E 32C 71W / 400W | 2MiB / 98304MiB | 0% Default |
|
|
41
|
+
| 1 PPU-ZW810E 32C 75W / 400W | 2MiB / 98304MiB | 0% Default |
|
|
42
|
+
+-----------------------------------+----------------------+-------------------+
|
|
43
|
+
|
|
44
|
+
+-------------------------------------------------------------------------------+
|
|
45
|
+
| Host: |
|
|
46
|
+
| CPU [|||| ] 21.3% (32 cores) MEM [|||||| ] 32.1GiB/64.0GiB (50.2%)
|
|
47
|
+
+-------------------------------------------------------------------------------+
|
|
48
|
+
|
|
49
|
+
+-------------------------------------------------------------------------------+
|
|
50
|
+
| Processes: |
|
|
51
|
+
| PPU PID USER PPU-MEM CPU% MEM% TIME Command |
|
|
52
|
+
+===============================================================================+
|
|
53
|
+
| 0 12345 user 8192MiB 3.2% 12.6% 1:23 python train.py |
|
|
54
|
+
+-------------------------------------------------------------------------------+
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Requirements
|
|
58
|
+
|
|
59
|
+
- Python 3.8+
|
|
60
|
+
- PPU device with `ppu-smi` installed (default path: `/usr/local/PPU_SDK/ppu-smi/bin/ppu-smi`)
|
|
61
|
+
|
|
62
|
+
## Installation
|
|
63
|
+
|
|
64
|
+
### From source
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
git clone https://github.com/your-username/ppop.git
|
|
68
|
+
cd ppop
|
|
69
|
+
pip install .
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### From PyPI (coming soon)
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pip install ppop
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Usage
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# Interactive mode (real-time monitoring)
|
|
82
|
+
ppop
|
|
83
|
+
|
|
84
|
+
# One-shot mode (print once and exit)
|
|
85
|
+
ppop --once
|
|
86
|
+
|
|
87
|
+
# Custom refresh interval (default: 2s)
|
|
88
|
+
ppop -i 5
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Keyboard Shortcuts (interactive mode)
|
|
92
|
+
|
|
93
|
+
| Key | Action |
|
|
94
|
+
|-----|--------|
|
|
95
|
+
| `q` | Quit |
|
|
96
|
+
| `↑` / `k` | Scroll up |
|
|
97
|
+
| `↓` / `j` | Scroll down |
|
|
98
|
+
| `PgUp` / `PgDn` | Scroll by page |
|
|
99
|
+
| `r` | Force refresh |
|
|
100
|
+
|
|
101
|
+
## Configuration
|
|
102
|
+
|
|
103
|
+
If `ppu-smi` is not at the default path, set the environment variable:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
export PPU_SMI_PATH=/path/to/ppu-smi
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
[MIT](LICENSE)
|
ppop-0.1.0/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# ppop
|
|
2
|
+
|
|
3
|
+
An interactive PPU process viewer and resource monitor for the terminal, inspired by [nvitop](https://github.com/XuehaiPan/nvitop).
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Real-time PPU device monitoring (utilization, memory, temperature, power)
|
|
8
|
+
- Host CPU and memory usage display
|
|
9
|
+
- Per-process PPU memory tracking
|
|
10
|
+
- Interactive curses-based TUI with color-coded utilization bars
|
|
11
|
+
- Non-interactive one-shot mode for scripts and logging
|
|
12
|
+
|
|
13
|
+
## Demo
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
+-------------------------------------------------------------------------------+
|
|
17
|
+
| ppop - PPU Monitor Driver Version: 1.5.5-7a3ae6 |
|
|
18
|
+
+-----------------------------------+----------------------+-------------------+
|
|
19
|
+
| PPU Name | Memory-Usage | PPU-Util |
|
|
20
|
+
+===================================+======================+===================+
|
|
21
|
+
| 0 PPU-ZW810E 32C 71W / 400W | 2MiB / 98304MiB | 0% Default |
|
|
22
|
+
| 1 PPU-ZW810E 32C 75W / 400W | 2MiB / 98304MiB | 0% Default |
|
|
23
|
+
+-----------------------------------+----------------------+-------------------+
|
|
24
|
+
|
|
25
|
+
+-------------------------------------------------------------------------------+
|
|
26
|
+
| Host: |
|
|
27
|
+
| CPU [|||| ] 21.3% (32 cores) MEM [|||||| ] 32.1GiB/64.0GiB (50.2%)
|
|
28
|
+
+-------------------------------------------------------------------------------+
|
|
29
|
+
|
|
30
|
+
+-------------------------------------------------------------------------------+
|
|
31
|
+
| Processes: |
|
|
32
|
+
| PPU PID USER PPU-MEM CPU% MEM% TIME Command |
|
|
33
|
+
+===============================================================================+
|
|
34
|
+
| 0 12345 user 8192MiB 3.2% 12.6% 1:23 python train.py |
|
|
35
|
+
+-------------------------------------------------------------------------------+
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Requirements
|
|
39
|
+
|
|
40
|
+
- Python 3.8+
|
|
41
|
+
- PPU device with `ppu-smi` installed (default path: `/usr/local/PPU_SDK/ppu-smi/bin/ppu-smi`)
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
### From source
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
git clone https://github.com/your-username/ppop.git
|
|
49
|
+
cd ppop
|
|
50
|
+
pip install .
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### From PyPI (coming soon)
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install ppop
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Usage
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# Interactive mode (real-time monitoring)
|
|
63
|
+
ppop
|
|
64
|
+
|
|
65
|
+
# One-shot mode (print once and exit)
|
|
66
|
+
ppop --once
|
|
67
|
+
|
|
68
|
+
# Custom refresh interval (default: 2s)
|
|
69
|
+
ppop -i 5
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Keyboard Shortcuts (interactive mode)
|
|
73
|
+
|
|
74
|
+
| Key | Action |
|
|
75
|
+
|-----|--------|
|
|
76
|
+
| `q` | Quit |
|
|
77
|
+
| `↑` / `k` | Scroll up |
|
|
78
|
+
| `↓` / `j` | Scroll down |
|
|
79
|
+
| `PgUp` / `PgDn` | Scroll by page |
|
|
80
|
+
| `r` | Force refresh |
|
|
81
|
+
|
|
82
|
+
## Configuration
|
|
83
|
+
|
|
84
|
+
If `ppu-smi` is not at the default path, set the environment variable:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
export PPU_SMI_PATH=/path/to/ppu-smi
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## License
|
|
91
|
+
|
|
92
|
+
[MIT](LICENSE)
|
ppop-0.1.0/popup/cli.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""CLI entry point for popup."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import curses
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def parse_arguments() -> argparse.Namespace:
|
|
11
|
+
parser = argparse.ArgumentParser(
|
|
12
|
+
prog="ppop",
|
|
13
|
+
description="An interactive PPU process viewer and resource monitor.",
|
|
14
|
+
)
|
|
15
|
+
parser.add_argument(
|
|
16
|
+
"-1", "--once",
|
|
17
|
+
action="store_true",
|
|
18
|
+
help="Report device info once and exit (non-interactive).",
|
|
19
|
+
)
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
"-i", "--interval",
|
|
22
|
+
type=float,
|
|
23
|
+
default=2.0,
|
|
24
|
+
help="Update interval in seconds (default: 2).",
|
|
25
|
+
)
|
|
26
|
+
return parser.parse_args()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def main() -> None:
|
|
30
|
+
args = parse_arguments()
|
|
31
|
+
|
|
32
|
+
from popup.tui import TUI, print_once
|
|
33
|
+
|
|
34
|
+
if args.once or not sys.stdout.isatty():
|
|
35
|
+
print_once()
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
tui = TUI(interval=args.interval)
|
|
39
|
+
try:
|
|
40
|
+
curses.wrapper(tui.run)
|
|
41
|
+
except KeyboardInterrupt:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if __name__ == "__main__":
|
|
46
|
+
main()
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""PPU device and process data collection via ppu-smi."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
|
|
10
|
+
PPU_SMI = os.environ.get("PPU_SMI_PATH", "/usr/local/PPU_SDK/ppu-smi/bin/ppu-smi")
|
|
11
|
+
|
|
12
|
+
DEVICE_QUERY_FIELDS = (
|
|
13
|
+
"index,name,uuid,pci.bus_id,memory.total,memory.used,memory.free,"
|
|
14
|
+
"utilization.ppu,utilization.memory,temperature.ppu,"
|
|
15
|
+
"power.draw,power.limit,fan.speed,clocks.current.sm,clocks.current.memory,"
|
|
16
|
+
"persistence_mode,pstate,compute_mode,ecc.errors.corrected.volatile.total"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
PROCESS_QUERY_FIELDS = "pid,process_name,used_ppu_memory,ppu_bus_id"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _parse_value(raw: str) -> str:
|
|
23
|
+
"""Strip units and whitespace from a ppu-smi CSV value."""
|
|
24
|
+
raw = raw.strip()
|
|
25
|
+
for suffix in (" MiB", " W", " C", " %", " MHz", " KB/s"):
|
|
26
|
+
if raw.endswith(suffix):
|
|
27
|
+
return raw[: -len(suffix)].strip()
|
|
28
|
+
return raw
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _parse_int(raw: str, default: int = 0) -> int:
|
|
32
|
+
v = _parse_value(raw)
|
|
33
|
+
if v in ("[N/A]", "N/A", "[Not Supported]", ""):
|
|
34
|
+
return default
|
|
35
|
+
try:
|
|
36
|
+
return int(v)
|
|
37
|
+
except ValueError:
|
|
38
|
+
return default
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _parse_float(raw: str, default: float = 0.0) -> float:
|
|
42
|
+
v = _parse_value(raw)
|
|
43
|
+
if v in ("[N/A]", "N/A", "[Not Supported]", ""):
|
|
44
|
+
return default
|
|
45
|
+
try:
|
|
46
|
+
return float(v)
|
|
47
|
+
except ValueError:
|
|
48
|
+
return default
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _parse_str(raw: str) -> str:
|
|
52
|
+
v = raw.strip()
|
|
53
|
+
if v in ("[N/A]", "[Not Supported]"):
|
|
54
|
+
return "N/A"
|
|
55
|
+
return v
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class PPUDevice:
|
|
60
|
+
index: int = 0
|
|
61
|
+
name: str = ""
|
|
62
|
+
uuid: str = ""
|
|
63
|
+
bus_id: str = ""
|
|
64
|
+
memory_total: int = 0 # MiB
|
|
65
|
+
memory_used: int = 0 # MiB
|
|
66
|
+
memory_free: int = 0 # MiB
|
|
67
|
+
ppu_utilization: int = 0 # %
|
|
68
|
+
memory_utilization: int = 0 # %
|
|
69
|
+
temperature: int = 0 # C
|
|
70
|
+
power_draw: float = 0.0 # W
|
|
71
|
+
power_limit: float = 0.0 # W
|
|
72
|
+
fan_speed: str = "N/A"
|
|
73
|
+
sm_clock: int = 0 # MHz
|
|
74
|
+
memory_clock: int = 0 # MHz
|
|
75
|
+
persistence_mode: str = "N/A"
|
|
76
|
+
pstate: str = "N/A"
|
|
77
|
+
compute_mode: str = "Default"
|
|
78
|
+
ecc_errors: int = 0
|
|
79
|
+
processes: List[PPUProcess] = field(default_factory=list)
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def memory_percent(self) -> float:
|
|
83
|
+
if self.memory_total == 0:
|
|
84
|
+
return 0.0
|
|
85
|
+
return 100.0 * self.memory_used / self.memory_total
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def power_status(self) -> str:
|
|
89
|
+
return f"{self.power_draw:.0f}W / {self.power_limit:.0f}W"
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def memory_usage(self) -> str:
|
|
93
|
+
return f"{self.memory_used}MiB / {self.memory_total}MiB"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass
|
|
97
|
+
class PPUProcess:
|
|
98
|
+
pid: int = 0
|
|
99
|
+
name: str = ""
|
|
100
|
+
ppu_memory: int = 0 # MiB
|
|
101
|
+
bus_id: str = ""
|
|
102
|
+
device_index: int = -1
|
|
103
|
+
username: str = ""
|
|
104
|
+
command: str = ""
|
|
105
|
+
cpu_percent: float = 0.0
|
|
106
|
+
host_memory: float = 0.0 # %
|
|
107
|
+
running_time: str = ""
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _run_ppu_smi(*args: str) -> str:
|
|
111
|
+
try:
|
|
112
|
+
result = subprocess.run(
|
|
113
|
+
[PPU_SMI, *args],
|
|
114
|
+
capture_output=True,
|
|
115
|
+
text=True,
|
|
116
|
+
timeout=10,
|
|
117
|
+
)
|
|
118
|
+
return result.stdout
|
|
119
|
+
except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
|
|
120
|
+
return ""
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _enrich_process(proc: PPUProcess) -> None:
|
|
124
|
+
"""Add host-level info (username, cmdline, cpu%, mem%) via psutil."""
|
|
125
|
+
try:
|
|
126
|
+
import psutil
|
|
127
|
+
|
|
128
|
+
p = psutil.Process(proc.pid)
|
|
129
|
+
proc.username = p.username()
|
|
130
|
+
try:
|
|
131
|
+
cmdline = p.cmdline()
|
|
132
|
+
proc.command = " ".join(cmdline) if cmdline else proc.name
|
|
133
|
+
except (psutil.AccessDenied, psutil.ZombieProcess):
|
|
134
|
+
proc.command = proc.name
|
|
135
|
+
proc.cpu_percent = p.cpu_percent(interval=0)
|
|
136
|
+
proc.host_memory = p.memory_percent()
|
|
137
|
+
try:
|
|
138
|
+
import time
|
|
139
|
+
elapsed = time.time() - p.create_time()
|
|
140
|
+
hours, rem = divmod(int(elapsed), 3600)
|
|
141
|
+
minutes, seconds = divmod(rem, 60)
|
|
142
|
+
if hours > 0:
|
|
143
|
+
proc.running_time = f"{hours}:{minutes:02d}:{seconds:02d}"
|
|
144
|
+
else:
|
|
145
|
+
proc.running_time = f"{minutes}:{seconds:02d}"
|
|
146
|
+
except (psutil.AccessDenied, psutil.ZombieProcess):
|
|
147
|
+
proc.running_time = "N/A"
|
|
148
|
+
except Exception:
|
|
149
|
+
pass
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def query_devices() -> List[PPUDevice]:
|
|
153
|
+
"""Query all PPU devices and their processes."""
|
|
154
|
+
# Query devices
|
|
155
|
+
output = _run_ppu_smi(f"--query-ppu={DEVICE_QUERY_FIELDS}", "--format=csv")
|
|
156
|
+
if not output.strip():
|
|
157
|
+
return []
|
|
158
|
+
|
|
159
|
+
lines = output.strip().splitlines()
|
|
160
|
+
if len(lines) < 2:
|
|
161
|
+
return []
|
|
162
|
+
|
|
163
|
+
devices: List[PPUDevice] = []
|
|
164
|
+
bus_id_to_device: dict[str, PPUDevice] = {}
|
|
165
|
+
|
|
166
|
+
for line in lines[1:]:
|
|
167
|
+
cols = line.split(",")
|
|
168
|
+
if len(cols) < 19:
|
|
169
|
+
continue
|
|
170
|
+
dev = PPUDevice(
|
|
171
|
+
index=_parse_int(cols[0]),
|
|
172
|
+
name=_parse_str(cols[1]),
|
|
173
|
+
uuid=_parse_str(cols[2]),
|
|
174
|
+
bus_id=_parse_str(cols[3]),
|
|
175
|
+
memory_total=_parse_int(cols[4]),
|
|
176
|
+
memory_used=_parse_int(cols[5]),
|
|
177
|
+
memory_free=_parse_int(cols[6]),
|
|
178
|
+
ppu_utilization=_parse_int(cols[7]),
|
|
179
|
+
memory_utilization=_parse_int(cols[8]),
|
|
180
|
+
temperature=_parse_int(cols[9]),
|
|
181
|
+
power_draw=_parse_float(cols[10]),
|
|
182
|
+
power_limit=_parse_float(cols[11]),
|
|
183
|
+
fan_speed=_parse_str(cols[12]),
|
|
184
|
+
sm_clock=_parse_int(cols[13]),
|
|
185
|
+
memory_clock=_parse_int(cols[14]),
|
|
186
|
+
persistence_mode=_parse_str(cols[15]),
|
|
187
|
+
pstate=_parse_str(cols[16]),
|
|
188
|
+
compute_mode=_parse_str(cols[17]),
|
|
189
|
+
ecc_errors=_parse_int(cols[18]),
|
|
190
|
+
)
|
|
191
|
+
devices.append(dev)
|
|
192
|
+
bus_id_to_device[dev.bus_id] = dev
|
|
193
|
+
|
|
194
|
+
# Query processes
|
|
195
|
+
proc_output = _run_ppu_smi(
|
|
196
|
+
f"--query-compute-apps={PROCESS_QUERY_FIELDS}", "--format=csv"
|
|
197
|
+
)
|
|
198
|
+
if proc_output.strip():
|
|
199
|
+
proc_lines = proc_output.strip().splitlines()
|
|
200
|
+
for pline in proc_lines[1:]:
|
|
201
|
+
pcols = pline.split(",")
|
|
202
|
+
if len(pcols) < 4:
|
|
203
|
+
continue
|
|
204
|
+
bus_id = _parse_str(pcols[3])
|
|
205
|
+
proc = PPUProcess(
|
|
206
|
+
pid=_parse_int(pcols[0]),
|
|
207
|
+
name=_parse_str(pcols[1]),
|
|
208
|
+
ppu_memory=_parse_int(pcols[2]),
|
|
209
|
+
bus_id=bus_id,
|
|
210
|
+
)
|
|
211
|
+
_enrich_process(proc)
|
|
212
|
+
dev = bus_id_to_device.get(bus_id)
|
|
213
|
+
if dev is not None:
|
|
214
|
+
proc.device_index = dev.index
|
|
215
|
+
dev.processes.append(proc)
|
|
216
|
+
|
|
217
|
+
return devices
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def get_driver_version() -> str:
|
|
221
|
+
output = _run_ppu_smi("--query-ppu=driver_version", "--format=csv")
|
|
222
|
+
lines = output.strip().splitlines()
|
|
223
|
+
if len(lines) >= 2:
|
|
224
|
+
return lines[1].strip()
|
|
225
|
+
return "N/A"
|
ppop-0.1.0/popup/tui.py
ADDED
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
"""Curses-based TUI for monitoring PPU devices."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import curses
|
|
6
|
+
import signal
|
|
7
|
+
import time
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import List, Optional
|
|
10
|
+
|
|
11
|
+
import psutil
|
|
12
|
+
|
|
13
|
+
from popup.device import PPUDevice, PPUProcess, get_driver_version, query_devices
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class HostInfo:
|
|
18
|
+
cpu_percent: float = 0.0
|
|
19
|
+
cpu_count: int = 0
|
|
20
|
+
cpu_percents: List[float] = field(default_factory=list)
|
|
21
|
+
mem_total: int = 0 # bytes
|
|
22
|
+
mem_used: int = 0 # bytes
|
|
23
|
+
mem_percent: float = 0.0
|
|
24
|
+
swap_total: int = 0
|
|
25
|
+
swap_used: int = 0
|
|
26
|
+
swap_percent: float = 0.0
|
|
27
|
+
load_avg: tuple = (0.0, 0.0, 0.0)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def query_host() -> HostInfo:
|
|
31
|
+
info = HostInfo()
|
|
32
|
+
info.cpu_count = psutil.cpu_count() or 1
|
|
33
|
+
info.cpu_percents = psutil.cpu_percent(percpu=True) or []
|
|
34
|
+
info.cpu_percent = psutil.cpu_percent()
|
|
35
|
+
mem = psutil.virtual_memory()
|
|
36
|
+
info.mem_total = mem.total
|
|
37
|
+
info.mem_used = mem.used
|
|
38
|
+
info.mem_percent = mem.percent
|
|
39
|
+
swap = psutil.swap_memory()
|
|
40
|
+
info.swap_total = swap.total
|
|
41
|
+
info.swap_used = swap.used
|
|
42
|
+
info.swap_percent = swap.percent
|
|
43
|
+
try:
|
|
44
|
+
info.load_avg = psutil.getloadavg()
|
|
45
|
+
except (AttributeError, OSError):
|
|
46
|
+
pass
|
|
47
|
+
return info
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _fmt_bytes(n: int) -> str:
|
|
51
|
+
if n >= 1 << 30:
|
|
52
|
+
return f"{n / (1 << 30):.1f}GiB"
|
|
53
|
+
elif n >= 1 << 20:
|
|
54
|
+
return f"{n / (1 << 20):.0f}MiB"
|
|
55
|
+
else:
|
|
56
|
+
return f"{n / (1 << 10):.0f}KiB"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# Color pair IDs
|
|
60
|
+
PAIR_NORMAL = 0
|
|
61
|
+
PAIR_HEADER = 1
|
|
62
|
+
PAIR_GOOD = 2
|
|
63
|
+
PAIR_WARN = 3
|
|
64
|
+
PAIR_CRIT = 4
|
|
65
|
+
PAIR_BAR_LOW = 5
|
|
66
|
+
PAIR_BAR_MED = 6
|
|
67
|
+
PAIR_BAR_HIGH = 7
|
|
68
|
+
PAIR_TITLE = 8
|
|
69
|
+
PAIR_PROCESS = 9
|
|
70
|
+
PAIR_BORDER = 10
|
|
71
|
+
PAIR_HIGHLIGHT = 11
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _init_colors() -> None:
|
|
75
|
+
curses.start_color()
|
|
76
|
+
curses.use_default_colors()
|
|
77
|
+
curses.init_pair(PAIR_HEADER, curses.COLOR_WHITE, curses.COLOR_BLUE)
|
|
78
|
+
curses.init_pair(PAIR_GOOD, curses.COLOR_GREEN, -1)
|
|
79
|
+
curses.init_pair(PAIR_WARN, curses.COLOR_YELLOW, -1)
|
|
80
|
+
curses.init_pair(PAIR_CRIT, curses.COLOR_RED, -1)
|
|
81
|
+
curses.init_pair(PAIR_BAR_LOW, curses.COLOR_GREEN, -1)
|
|
82
|
+
curses.init_pair(PAIR_BAR_MED, curses.COLOR_YELLOW, -1)
|
|
83
|
+
curses.init_pair(PAIR_BAR_HIGH, curses.COLOR_RED, -1)
|
|
84
|
+
curses.init_pair(PAIR_TITLE, curses.COLOR_CYAN, -1)
|
|
85
|
+
curses.init_pair(PAIR_PROCESS, curses.COLOR_WHITE, -1)
|
|
86
|
+
curses.init_pair(PAIR_BORDER, curses.COLOR_WHITE, -1)
|
|
87
|
+
curses.init_pair(PAIR_HIGHLIGHT, curses.COLOR_BLACK, curses.COLOR_CYAN)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _util_color(percent: float) -> int:
|
|
91
|
+
if percent < 50:
|
|
92
|
+
return curses.color_pair(PAIR_BAR_LOW) | curses.A_BOLD
|
|
93
|
+
elif percent < 80:
|
|
94
|
+
return curses.color_pair(PAIR_BAR_MED) | curses.A_BOLD
|
|
95
|
+
else:
|
|
96
|
+
return curses.color_pair(PAIR_BAR_HIGH) | curses.A_BOLD
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _temp_color(temp: int) -> int:
|
|
100
|
+
if temp < 50:
|
|
101
|
+
return curses.color_pair(PAIR_GOOD)
|
|
102
|
+
elif temp < 75:
|
|
103
|
+
return curses.color_pair(PAIR_WARN)
|
|
104
|
+
else:
|
|
105
|
+
return curses.color_pair(PAIR_CRIT)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _draw_bar(win, y: int, x: int, width: int, percent: float, label: str = "") -> None:
|
|
109
|
+
"""Draw a utilization bar [|||| ] with color."""
|
|
110
|
+
if width < 4:
|
|
111
|
+
return
|
|
112
|
+
bar_inner = width - 2 # excluding [ and ]
|
|
113
|
+
filled = int(bar_inner * percent / 100.0)
|
|
114
|
+
filled = min(filled, bar_inner)
|
|
115
|
+
|
|
116
|
+
attr = _util_color(percent)
|
|
117
|
+
try:
|
|
118
|
+
win.addstr(y, x, "[", curses.color_pair(PAIR_BORDER))
|
|
119
|
+
win.addstr(y, x + 1, "|" * filled, attr)
|
|
120
|
+
win.addstr(y, x + 1 + filled, " " * (bar_inner - filled))
|
|
121
|
+
win.addstr(y, x + width - 1, "]", curses.color_pair(PAIR_BORDER))
|
|
122
|
+
if label:
|
|
123
|
+
lx = x + width + 1
|
|
124
|
+
win.addstr(y, lx, label, attr)
|
|
125
|
+
except curses.error:
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _safe_addstr(win, y: int, x: int, text: str, attr: int = 0) -> None:
|
|
130
|
+
max_y, max_x = win.getmaxyx()
|
|
131
|
+
if y < 0 or y >= max_y or x >= max_x:
|
|
132
|
+
return
|
|
133
|
+
available = max_x - x - 1
|
|
134
|
+
if available <= 0:
|
|
135
|
+
return
|
|
136
|
+
try:
|
|
137
|
+
win.addstr(y, x, text[:available], attr)
|
|
138
|
+
except curses.error:
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _draw_header(win, y: int, width: int, driver_version: str) -> int:
|
|
143
|
+
"""Draw the top header. Returns next y."""
|
|
144
|
+
timestamp = time.strftime("%a %b %d %H:%M:%S %Y")
|
|
145
|
+
title = f" ppop - PPU Monitor"
|
|
146
|
+
ver = f"Driver: {driver_version}"
|
|
147
|
+
|
|
148
|
+
_safe_addstr(win, y, 0, " " * width, curses.color_pair(PAIR_HEADER))
|
|
149
|
+
_safe_addstr(win, y, 1, title, curses.color_pair(PAIR_HEADER) | curses.A_BOLD)
|
|
150
|
+
_safe_addstr(win, y, width - len(ver) - 2, ver, curses.color_pair(PAIR_HEADER))
|
|
151
|
+
y += 1
|
|
152
|
+
_safe_addstr(win, y, 0, " " * width, curses.color_pair(PAIR_HEADER))
|
|
153
|
+
_safe_addstr(win, y, 1, f" {timestamp}", curses.color_pair(PAIR_HEADER))
|
|
154
|
+
help_text = "q:Quit ↑↓:Scroll"
|
|
155
|
+
_safe_addstr(win, y, width - len(help_text) - 2, help_text, curses.color_pair(PAIR_HEADER))
|
|
156
|
+
return y + 1
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _draw_device_panel(win, y: int, width: int, devices: List[PPUDevice]) -> int:
|
|
160
|
+
"""Draw device info panel. Returns next y."""
|
|
161
|
+
if not devices:
|
|
162
|
+
_safe_addstr(win, y, 1, "No PPU devices found.", curses.color_pair(PAIR_CRIT) | curses.A_BOLD)
|
|
163
|
+
return y + 2
|
|
164
|
+
|
|
165
|
+
# Table header
|
|
166
|
+
border = "+" + "-" * (width - 2) + "+"
|
|
167
|
+
_safe_addstr(win, y, 0, border, curses.color_pair(PAIR_BORDER))
|
|
168
|
+
y += 1
|
|
169
|
+
|
|
170
|
+
col_header = (
|
|
171
|
+
"| PPU Name Temp Power |"
|
|
172
|
+
" Memory-Usage | PPU-Util |"
|
|
173
|
+
)
|
|
174
|
+
# Simplified header that fits
|
|
175
|
+
hdr1 = f"| {'PPU':>3} {'Name':<16} {'Temp':>5} {'Pwr:Usage/Cap':>15} | {'Memory-Usage':^22} | {'PPU-Util':^9} |"
|
|
176
|
+
_safe_addstr(win, y, 0, hdr1[:width], curses.color_pair(PAIR_TITLE) | curses.A_BOLD)
|
|
177
|
+
y += 1
|
|
178
|
+
|
|
179
|
+
sep = "|" + "-" * (width - 2) + "|"
|
|
180
|
+
_safe_addstr(win, y, 0, sep, curses.color_pair(PAIR_BORDER))
|
|
181
|
+
y += 1
|
|
182
|
+
|
|
183
|
+
for dev in devices:
|
|
184
|
+
# Row 1: index, name, temp, power, memory usage, ppu util
|
|
185
|
+
mem_usage = f"{dev.memory_used:>5}MiB / {dev.memory_total}MiB"
|
|
186
|
+
ppu_util = f"{dev.ppu_utilization}%"
|
|
187
|
+
|
|
188
|
+
line = f"| {dev.index:>3} {dev.name:<16} "
|
|
189
|
+
_safe_addstr(win, y, 0, line, curses.color_pair(PAIR_PROCESS))
|
|
190
|
+
|
|
191
|
+
# Temperature with color
|
|
192
|
+
temp_str = f"{dev.temperature:>3}C"
|
|
193
|
+
tx = len(line)
|
|
194
|
+
_safe_addstr(win, y, tx, temp_str, _temp_color(dev.temperature))
|
|
195
|
+
tx += len(temp_str)
|
|
196
|
+
|
|
197
|
+
# Power
|
|
198
|
+
pwr_str = f" {dev.power_draw:>6.0f}W / {dev.power_limit:.0f}W"
|
|
199
|
+
_safe_addstr(win, y, tx, pwr_str, curses.color_pair(PAIR_PROCESS))
|
|
200
|
+
tx += len(pwr_str)
|
|
201
|
+
|
|
202
|
+
_safe_addstr(win, y, tx, " | ", curses.color_pair(PAIR_BORDER))
|
|
203
|
+
tx += 3
|
|
204
|
+
|
|
205
|
+
# Memory usage with bar
|
|
206
|
+
mem_pct = dev.memory_percent
|
|
207
|
+
bar_width = 12
|
|
208
|
+
_draw_bar(win, y, tx, bar_width, mem_pct)
|
|
209
|
+
tx += bar_width + 1
|
|
210
|
+
mem_str = f"{dev.memory_used:>5}/{dev.memory_total}M"
|
|
211
|
+
_safe_addstr(win, y, tx, mem_str, _util_color(mem_pct))
|
|
212
|
+
tx += len(mem_str)
|
|
213
|
+
|
|
214
|
+
_safe_addstr(win, y, tx, " | ", curses.color_pair(PAIR_BORDER))
|
|
215
|
+
tx += 3
|
|
216
|
+
|
|
217
|
+
# PPU utilization with bar
|
|
218
|
+
_draw_bar(win, y, tx, bar_width, dev.ppu_utilization)
|
|
219
|
+
tx += bar_width + 1
|
|
220
|
+
util_str = f"{dev.ppu_utilization:>3}%"
|
|
221
|
+
_safe_addstr(win, y, tx, util_str, _util_color(dev.ppu_utilization))
|
|
222
|
+
tx += len(util_str)
|
|
223
|
+
|
|
224
|
+
_safe_addstr(win, y, tx, " |", curses.color_pair(PAIR_BORDER))
|
|
225
|
+
y += 1
|
|
226
|
+
|
|
227
|
+
_safe_addstr(win, y, 0, border, curses.color_pair(PAIR_BORDER))
|
|
228
|
+
return y + 1
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _draw_host_panel(win, y: int, width: int, host: HostInfo) -> int:
|
|
232
|
+
"""Draw CPU and memory usage panel. Returns next y."""
|
|
233
|
+
border = "+" + "-" * (width - 2) + "+"
|
|
234
|
+
_safe_addstr(win, y, 0, border, curses.color_pair(PAIR_BORDER))
|
|
235
|
+
y += 1
|
|
236
|
+
|
|
237
|
+
_safe_addstr(
|
|
238
|
+
win, y, 0,
|
|
239
|
+
"| Host:" + " " * (width - 8) + "|",
|
|
240
|
+
curses.color_pair(PAIR_TITLE) | curses.A_BOLD,
|
|
241
|
+
)
|
|
242
|
+
y += 1
|
|
243
|
+
|
|
244
|
+
# CPU row: overall bar + per-core mini bars
|
|
245
|
+
bar_width = 20
|
|
246
|
+
cpu_label = f"CPU {host.cpu_percent:>5.1f}% ({host.cpu_count} cores)"
|
|
247
|
+
load_str = f"Load: {host.load_avg[0]:.2f} {host.load_avg[1]:.2f} {host.load_avg[2]:.2f}"
|
|
248
|
+
|
|
249
|
+
_safe_addstr(win, y, 1, "| ", curses.color_pair(PAIR_BORDER))
|
|
250
|
+
_safe_addstr(win, y, 3, "CPU ", curses.color_pair(PAIR_TITLE) | curses.A_BOLD)
|
|
251
|
+
_draw_bar(win, y, 7, bar_width, host.cpu_percent)
|
|
252
|
+
pct_str = f" {host.cpu_percent:>5.1f}% ({host.cpu_count} cores)"
|
|
253
|
+
_safe_addstr(win, y, 7 + bar_width + 1, pct_str, _util_color(host.cpu_percent))
|
|
254
|
+
|
|
255
|
+
# Memory on the same row, right-aligned area
|
|
256
|
+
mem_x = max(7 + bar_width + 1 + len(pct_str) + 3, width // 2)
|
|
257
|
+
_safe_addstr(win, y, mem_x, "MEM ", curses.color_pair(PAIR_TITLE) | curses.A_BOLD)
|
|
258
|
+
_draw_bar(win, y, mem_x + 4, bar_width, host.mem_percent)
|
|
259
|
+
mem_str = f" {_fmt_bytes(host.mem_used)}/{_fmt_bytes(host.mem_total)} ({host.mem_percent:.1f}%)"
|
|
260
|
+
_safe_addstr(win, y, mem_x + 4 + bar_width + 1, mem_str, _util_color(host.mem_percent))
|
|
261
|
+
_safe_addstr(win, y, width - 1, "|", curses.color_pair(PAIR_BORDER))
|
|
262
|
+
y += 1
|
|
263
|
+
|
|
264
|
+
# Second row: per-core CPU mini view + swap
|
|
265
|
+
_safe_addstr(win, y, 1, "| ", curses.color_pair(PAIR_BORDER))
|
|
266
|
+
_safe_addstr(win, y, 3, "Per-core: ", curses.color_pair(PAIR_PROCESS))
|
|
267
|
+
cx = 13
|
|
268
|
+
cores_per_row = (width - 16) // 5 # each core ~5 chars "XX% "
|
|
269
|
+
for i, cpct in enumerate(host.cpu_percents):
|
|
270
|
+
if cx + 5 >= mem_x - 1:
|
|
271
|
+
break
|
|
272
|
+
core_str = f"{cpct:>3.0f}%"
|
|
273
|
+
_safe_addstr(win, y, cx, core_str, _util_color(cpct))
|
|
274
|
+
cx += 5
|
|
275
|
+
|
|
276
|
+
# Swap on the right
|
|
277
|
+
_safe_addstr(win, y, mem_x, "SWP ", curses.color_pair(PAIR_TITLE) | curses.A_BOLD)
|
|
278
|
+
_draw_bar(win, y, mem_x + 4, bar_width, host.swap_percent)
|
|
279
|
+
swap_str = f" {_fmt_bytes(host.swap_used)}/{_fmt_bytes(host.swap_total)} ({host.swap_percent:.1f}%)"
|
|
280
|
+
_safe_addstr(win, y, mem_x + 4 + bar_width + 1, swap_str, _util_color(host.swap_percent))
|
|
281
|
+
|
|
282
|
+
_safe_addstr(win, y, width - 1, "|", curses.color_pair(PAIR_BORDER))
|
|
283
|
+
y += 1
|
|
284
|
+
|
|
285
|
+
# Third row: load average
|
|
286
|
+
_safe_addstr(win, y, 1, "| ", curses.color_pair(PAIR_BORDER))
|
|
287
|
+
_safe_addstr(win, y, 3, f"Load Avg: {host.load_avg[0]:.2f} {host.load_avg[1]:.2f} {host.load_avg[2]:.2f} (1/5/15 min)", curses.color_pair(PAIR_PROCESS))
|
|
288
|
+
_safe_addstr(win, y, width - 1, "|", curses.color_pair(PAIR_BORDER))
|
|
289
|
+
y += 1
|
|
290
|
+
|
|
291
|
+
_safe_addstr(win, y, 0, border, curses.color_pair(PAIR_BORDER))
|
|
292
|
+
return y + 1
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _draw_process_panel(
|
|
296
|
+
win, y: int, width: int, devices: List[PPUDevice], scroll_offset: int
|
|
297
|
+
) -> int:
|
|
298
|
+
"""Draw processes table. Returns next y."""
|
|
299
|
+
border = "+" + "-" * (width - 2) + "+"
|
|
300
|
+
_safe_addstr(win, y, 0, border, curses.color_pair(PAIR_BORDER))
|
|
301
|
+
y += 1
|
|
302
|
+
|
|
303
|
+
_safe_addstr(
|
|
304
|
+
win, y, 0,
|
|
305
|
+
"| Processes:" + " " * (width - 13) + "|",
|
|
306
|
+
curses.color_pair(PAIR_TITLE) | curses.A_BOLD,
|
|
307
|
+
)
|
|
308
|
+
y += 1
|
|
309
|
+
|
|
310
|
+
# Column header
|
|
311
|
+
hdr = (
|
|
312
|
+
f"| {'PPU':>3} {'PID':>8} {'USER':<10} "
|
|
313
|
+
f"{'PPU-MEM':>8} {'PPU%':>5} {'MEM%':>5} "
|
|
314
|
+
f"{'CPU%':>5} {'TIME':>9} {'Command':<20} |"
|
|
315
|
+
)
|
|
316
|
+
_safe_addstr(win, y, 0, hdr[:width], curses.color_pair(PAIR_TITLE) | curses.A_BOLD)
|
|
317
|
+
y += 1
|
|
318
|
+
|
|
319
|
+
sep = "|" + "-" * (width - 2) + "|"
|
|
320
|
+
_safe_addstr(win, y, 0, sep, curses.color_pair(PAIR_BORDER))
|
|
321
|
+
y += 1
|
|
322
|
+
|
|
323
|
+
# Collect all processes
|
|
324
|
+
all_procs: list[PPUProcess] = []
|
|
325
|
+
for dev in devices:
|
|
326
|
+
all_procs.extend(dev.processes)
|
|
327
|
+
|
|
328
|
+
max_y, _ = win.getmaxyx()
|
|
329
|
+
available_rows = max_y - y - 2 # leave room for bottom border + status
|
|
330
|
+
|
|
331
|
+
if not all_procs:
|
|
332
|
+
_safe_addstr(win, y, 0, f"| {'No running processes':^{width-4}} |", curses.color_pair(PAIR_PROCESS))
|
|
333
|
+
y += 1
|
|
334
|
+
else:
|
|
335
|
+
visible = all_procs[scroll_offset : scroll_offset + max(available_rows, 1)]
|
|
336
|
+
for proc in visible:
|
|
337
|
+
if y >= max_y - 2:
|
|
338
|
+
break
|
|
339
|
+
user = (proc.username[:10] if proc.username else "N/A")
|
|
340
|
+
cmd = proc.command if proc.command else proc.name
|
|
341
|
+
# Truncate command to fit
|
|
342
|
+
cmd_width = max(width - 68, 10)
|
|
343
|
+
if len(cmd) > cmd_width:
|
|
344
|
+
cmd = cmd[:cmd_width - 3] + "..."
|
|
345
|
+
|
|
346
|
+
line = (
|
|
347
|
+
f"| {proc.device_index:>3} {proc.pid:>8} {user:<10} "
|
|
348
|
+
f"{proc.ppu_memory:>6}MiB {'-':>5} {proc.host_memory:>4.1f}% "
|
|
349
|
+
f"{proc.cpu_percent:>4.1f}% {proc.running_time:>9} {cmd:<{cmd_width}} |"
|
|
350
|
+
)
|
|
351
|
+
_safe_addstr(win, y, 0, line[:width], curses.color_pair(PAIR_PROCESS))
|
|
352
|
+
y += 1
|
|
353
|
+
|
|
354
|
+
_safe_addstr(win, y, 0, border, curses.color_pair(PAIR_BORDER))
|
|
355
|
+
y += 1
|
|
356
|
+
|
|
357
|
+
# Show scroll info
|
|
358
|
+
total = len(all_procs)
|
|
359
|
+
if total > available_rows:
|
|
360
|
+
info = f" [{scroll_offset+1}-{min(scroll_offset+available_rows, total)}/{total}] "
|
|
361
|
+
_safe_addstr(win, y, 1, info, curses.color_pair(PAIR_TITLE))
|
|
362
|
+
|
|
363
|
+
return y
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
class TUI:
|
|
367
|
+
def __init__(self, interval: float = 2.0):
|
|
368
|
+
self.interval = interval
|
|
369
|
+
self.devices: List[PPUDevice] = []
|
|
370
|
+
self.host: HostInfo = HostInfo()
|
|
371
|
+
self.driver_version = "N/A"
|
|
372
|
+
self.scroll_offset = 0
|
|
373
|
+
self.running = True
|
|
374
|
+
|
|
375
|
+
def _refresh_data(self) -> None:
|
|
376
|
+
self.devices = query_devices()
|
|
377
|
+
self.host = query_host()
|
|
378
|
+
if self.driver_version == "N/A":
|
|
379
|
+
self.driver_version = get_driver_version()
|
|
380
|
+
|
|
381
|
+
def _total_processes(self) -> int:
|
|
382
|
+
return sum(len(d.processes) for d in self.devices)
|
|
383
|
+
|
|
384
|
+
def run(self, stdscr) -> None:
|
|
385
|
+
_init_colors()
|
|
386
|
+
curses.curs_set(0)
|
|
387
|
+
stdscr.timeout(200) # 200ms for responsive input
|
|
388
|
+
curses.halfdelay(1)
|
|
389
|
+
|
|
390
|
+
self._refresh_data()
|
|
391
|
+
last_refresh = time.time()
|
|
392
|
+
|
|
393
|
+
while self.running:
|
|
394
|
+
now = time.time()
|
|
395
|
+
if now - last_refresh >= self.interval:
|
|
396
|
+
self._refresh_data()
|
|
397
|
+
last_refresh = now
|
|
398
|
+
|
|
399
|
+
stdscr.erase()
|
|
400
|
+
height, width = stdscr.getmaxyx()
|
|
401
|
+
if width < 40 or height < 10:
|
|
402
|
+
_safe_addstr(stdscr, 0, 0, "Terminal too small!", curses.A_BOLD)
|
|
403
|
+
stdscr.refresh()
|
|
404
|
+
key = stdscr.getch()
|
|
405
|
+
if key == ord("q") or key == ord("Q"):
|
|
406
|
+
break
|
|
407
|
+
continue
|
|
408
|
+
|
|
409
|
+
y = 0
|
|
410
|
+
y = _draw_header(stdscr, y, width, self.driver_version)
|
|
411
|
+
y += 1
|
|
412
|
+
y = _draw_device_panel(stdscr, y, width, self.devices)
|
|
413
|
+
y += 1
|
|
414
|
+
y = _draw_host_panel(stdscr, y, width, self.host)
|
|
415
|
+
y += 1
|
|
416
|
+
y = _draw_process_panel(stdscr, y, width, self.devices, self.scroll_offset)
|
|
417
|
+
|
|
418
|
+
stdscr.refresh()
|
|
419
|
+
|
|
420
|
+
# Handle input
|
|
421
|
+
key = stdscr.getch()
|
|
422
|
+
if key == ord("q") or key == ord("Q"):
|
|
423
|
+
break
|
|
424
|
+
elif key == curses.KEY_DOWN or key == ord("j"):
|
|
425
|
+
total = self._total_processes()
|
|
426
|
+
if self.scroll_offset < total - 1:
|
|
427
|
+
self.scroll_offset += 1
|
|
428
|
+
elif key == curses.KEY_UP or key == ord("k"):
|
|
429
|
+
if self.scroll_offset > 0:
|
|
430
|
+
self.scroll_offset -= 1
|
|
431
|
+
elif key == curses.KEY_PPAGE:
|
|
432
|
+
self.scroll_offset = max(0, self.scroll_offset - 10)
|
|
433
|
+
elif key == curses.KEY_NPAGE:
|
|
434
|
+
total = self._total_processes()
|
|
435
|
+
self.scroll_offset = min(total - 1, self.scroll_offset + 10)
|
|
436
|
+
elif key == ord("r") or key == ord("R"):
|
|
437
|
+
self._refresh_data()
|
|
438
|
+
last_refresh = time.time()
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def print_once() -> None:
|
|
442
|
+
"""Print a one-shot snapshot to stdout (non-interactive mode)."""
|
|
443
|
+
devices = query_devices()
|
|
444
|
+
driver_version = get_driver_version()
|
|
445
|
+
|
|
446
|
+
timestamp = time.strftime("%a %b %d %H:%M:%S %Y")
|
|
447
|
+
print(timestamp)
|
|
448
|
+
print("+" + "-" * 79 + "+")
|
|
449
|
+
print(f"| {'ppop - PPU Monitor':<49} Driver Version: {driver_version:<12} |")
|
|
450
|
+
print("+" + "-" * 35 + "+" + "-" * 22 + "+" + "-" * 19 + "+")
|
|
451
|
+
print(
|
|
452
|
+
f"| {'PPU Name':^35}| {'Memory-Usage':^22}| {'PPU-Util':^19}|"
|
|
453
|
+
)
|
|
454
|
+
print("+" + "=" * 35 + "+" + "=" * 22 + "+" + "=" * 19 + "+")
|
|
455
|
+
|
|
456
|
+
for dev in devices:
|
|
457
|
+
name_part = f"{dev.index} {dev.name} {dev.temperature}C {dev.power_status}"
|
|
458
|
+
mem_part = f"{dev.memory_used}MiB / {dev.memory_total}MiB"
|
|
459
|
+
util_part = f"{dev.ppu_utilization}% {dev.compute_mode}"
|
|
460
|
+
print(f"| {name_part:<35}| {mem_part:>20} | {util_part:>17} |")
|
|
461
|
+
|
|
462
|
+
print("+" + "-" * 35 + "+" + "-" * 22 + "+" + "-" * 19 + "+")
|
|
463
|
+
print()
|
|
464
|
+
|
|
465
|
+
# Host info
|
|
466
|
+
host = query_host()
|
|
467
|
+
print("+" + "-" * 79 + "+")
|
|
468
|
+
print(f"| {'Host:':<79}|")
|
|
469
|
+
cpu_bar_filled = int(20 * host.cpu_percent / 100)
|
|
470
|
+
cpu_bar = "|" * cpu_bar_filled + " " * (20 - cpu_bar_filled)
|
|
471
|
+
mem_bar_filled = int(20 * host.mem_percent / 100)
|
|
472
|
+
mem_bar = "|" * mem_bar_filled + " " * (20 - mem_bar_filled)
|
|
473
|
+
print(
|
|
474
|
+
f"| CPU [{cpu_bar}] {host.cpu_percent:>5.1f}% ({host.cpu_count} cores)"
|
|
475
|
+
f" MEM [{mem_bar}] {_fmt_bytes(host.mem_used)}/{_fmt_bytes(host.mem_total)} ({host.mem_percent:.1f}%)"
|
|
476
|
+
)
|
|
477
|
+
swap_bar_filled = int(20 * host.swap_percent / 100)
|
|
478
|
+
swap_bar = "|" * swap_bar_filled + " " * (20 - swap_bar_filled)
|
|
479
|
+
print(
|
|
480
|
+
f"| Load: {host.load_avg[0]:.2f} {host.load_avg[1]:.2f} {host.load_avg[2]:.2f} (1/5/15 min)"
|
|
481
|
+
f" SWP [{swap_bar}] {_fmt_bytes(host.swap_used)}/{_fmt_bytes(host.swap_total)} ({host.swap_percent:.1f}%)"
|
|
482
|
+
)
|
|
483
|
+
print("+" + "-" * 79 + "+")
|
|
484
|
+
print()
|
|
485
|
+
|
|
486
|
+
# Processes
|
|
487
|
+
print("+" + "-" * 79 + "+")
|
|
488
|
+
print(f"| {'Processes:':<79}|")
|
|
489
|
+
print(
|
|
490
|
+
f"| {'PPU':>3} {'PID':>8} {'USER':<10} "
|
|
491
|
+
f"{'PPU-MEM':>8} {'CPU%':>5} {'MEM%':>5} "
|
|
492
|
+
f"{'TIME':>9} {'Command':<15} |"
|
|
493
|
+
)
|
|
494
|
+
print("+" + "=" * 79 + "+")
|
|
495
|
+
|
|
496
|
+
has_proc = False
|
|
497
|
+
for dev in devices:
|
|
498
|
+
for proc in dev.processes:
|
|
499
|
+
has_proc = True
|
|
500
|
+
user = (proc.username[:10] if proc.username else "N/A")
|
|
501
|
+
cmd = proc.command if proc.command else proc.name
|
|
502
|
+
if len(cmd) > 15:
|
|
503
|
+
cmd = cmd[:12] + "..."
|
|
504
|
+
print(
|
|
505
|
+
f"| {proc.device_index:>3} {proc.pid:>8} {user:<10} "
|
|
506
|
+
f"{proc.ppu_memory:>6}MiB {proc.cpu_percent:>4.1f}% "
|
|
507
|
+
f"{proc.host_memory:>4.1f}% {proc.running_time:>9} {cmd:<15} |"
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
if not has_proc:
|
|
511
|
+
print(f"| {'No running processes':^79}|")
|
|
512
|
+
|
|
513
|
+
print("+" + "-" * 79 + "+")
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ppop
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: An interactive PPU process viewer and resource monitor, inspired by nvitop.
|
|
5
|
+
Author-email: Zhongwang Lun <lunandcnn@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: ppu,monitor,process-viewer,tui
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Environment :: Console :: Curses
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Intended Audience :: System Administrators
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: System :: Monitoring
|
|
14
|
+
Requires-Python: >=3.8
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: psutil>=5.6.6
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# ppop
|
|
21
|
+
|
|
22
|
+
An interactive PPU process viewer and resource monitor for the terminal, inspired by [nvitop](https://github.com/XuehaiPan/nvitop).
|
|
23
|
+
|
|
24
|
+
## Features
|
|
25
|
+
|
|
26
|
+
- Real-time PPU device monitoring (utilization, memory, temperature, power)
|
|
27
|
+
- Host CPU and memory usage display
|
|
28
|
+
- Per-process PPU memory tracking
|
|
29
|
+
- Interactive curses-based TUI with color-coded utilization bars
|
|
30
|
+
- Non-interactive one-shot mode for scripts and logging
|
|
31
|
+
|
|
32
|
+
## Demo
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
+-------------------------------------------------------------------------------+
|
|
36
|
+
| ppop - PPU Monitor Driver Version: 1.5.5-7a3ae6 |
|
|
37
|
+
+-----------------------------------+----------------------+-------------------+
|
|
38
|
+
| PPU Name | Memory-Usage | PPU-Util |
|
|
39
|
+
+===================================+======================+===================+
|
|
40
|
+
| 0 PPU-ZW810E 32C 71W / 400W | 2MiB / 98304MiB | 0% Default |
|
|
41
|
+
| 1 PPU-ZW810E 32C 75W / 400W | 2MiB / 98304MiB | 0% Default |
|
|
42
|
+
+-----------------------------------+----------------------+-------------------+
|
|
43
|
+
|
|
44
|
+
+-------------------------------------------------------------------------------+
|
|
45
|
+
| Host: |
|
|
46
|
+
| CPU [|||| ] 21.3% (32 cores) MEM [|||||| ] 32.1GiB/64.0GiB (50.2%)
|
|
47
|
+
+-------------------------------------------------------------------------------+
|
|
48
|
+
|
|
49
|
+
+-------------------------------------------------------------------------------+
|
|
50
|
+
| Processes: |
|
|
51
|
+
| PPU PID USER PPU-MEM CPU% MEM% TIME Command |
|
|
52
|
+
+===============================================================================+
|
|
53
|
+
| 0 12345 user 8192MiB 3.2% 12.6% 1:23 python train.py |
|
|
54
|
+
+-------------------------------------------------------------------------------+
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Requirements
|
|
58
|
+
|
|
59
|
+
- Python 3.8+
|
|
60
|
+
- PPU device with `ppu-smi` installed (default path: `/usr/local/PPU_SDK/ppu-smi/bin/ppu-smi`)
|
|
61
|
+
|
|
62
|
+
## Installation
|
|
63
|
+
|
|
64
|
+
### From source
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
git clone https://github.com/your-username/ppop.git
|
|
68
|
+
cd ppop
|
|
69
|
+
pip install .
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### From PyPI (coming soon)
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pip install ppop
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Usage
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# Interactive mode (real-time monitoring)
|
|
82
|
+
ppop
|
|
83
|
+
|
|
84
|
+
# One-shot mode (print once and exit)
|
|
85
|
+
ppop --once
|
|
86
|
+
|
|
87
|
+
# Custom refresh interval (default: 2s)
|
|
88
|
+
ppop -i 5
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Keyboard Shortcuts (interactive mode)
|
|
92
|
+
|
|
93
|
+
| Key | Action |
|
|
94
|
+
|-----|--------|
|
|
95
|
+
| `q` | Quit |
|
|
96
|
+
| `↑` / `k` | Scroll up |
|
|
97
|
+
| `↓` / `j` | Scroll down |
|
|
98
|
+
| `PgUp` / `PgDn` | Scroll by page |
|
|
99
|
+
| `r` | Force refresh |
|
|
100
|
+
|
|
101
|
+
## Configuration
|
|
102
|
+
|
|
103
|
+
If `ppu-smi` is not at the default path, set the environment variable:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
export PPU_SMI_PATH=/path/to/ppu-smi
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
popup/__init__.py
|
|
5
|
+
popup/__main__.py
|
|
6
|
+
popup/cli.py
|
|
7
|
+
popup/device.py
|
|
8
|
+
popup/tui.py
|
|
9
|
+
ppop.egg-info/PKG-INFO
|
|
10
|
+
ppop.egg-info/SOURCES.txt
|
|
11
|
+
ppop.egg-info/dependency_links.txt
|
|
12
|
+
ppop.egg-info/entry_points.txt
|
|
13
|
+
ppop.egg-info/requires.txt
|
|
14
|
+
ppop.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
psutil>=5.6.6
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
popup
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ppop"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "An interactive PPU process viewer and resource monitor, inspired by nvitop."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Zhongwang Lun", email = "lunandcnn@gmail.com" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["ppu", "monitor", "process-viewer", "tui"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Environment :: Console :: Curses",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Intended Audience :: System Administrators",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Topic :: System :: Monitoring",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"psutil>=5.6.6",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.scripts]
|
|
29
|
+
ppop = "popup.cli:main"
|
|
30
|
+
|
|
31
|
+
[tool.setuptools.packages.find]
|
|
32
|
+
include = ["popup*"]
|
ppop-0.1.0/setup.cfg
ADDED