pmemstat 2.3.22__tar.gz → 2.3.23__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.
- {pmemstat-2.3.22 → pmemstat-2.3.23}/PKG-INFO +20 -33
- {pmemstat-2.3.22 → pmemstat-2.3.23}/README.md +19 -32
- pmemstat-2.3.23/images/pmemstat.png +0 -0
- pmemstat-2.3.23/pmemstat/CpuSmooth.py +337 -0
- {pmemstat-2.3.22 → pmemstat-2.3.23}/pmemstat/pmemstat.py +217 -94
- {pmemstat-2.3.22 → pmemstat-2.3.23}/pyproject.toml +3 -2
- pmemstat-2.3.22/images/pmemstat_2023-05-15.png +0 -0
- pmemstat-2.3.22/images/pmemstat_2023-09-05.png +0 -0
- pmemstat-2.3.22/images/scroll-pos_2023-05-15.png +0 -0
- {pmemstat-2.3.22 → pmemstat-2.3.23}/.gitignore +0 -0
- {pmemstat-2.3.22 → pmemstat-2.3.23}/LICENSE.txt +0 -0
- {pmemstat-2.3.22 → pmemstat-2.3.23}/deploy +0 -0
- /pmemstat-2.3.22/images/help-screen_2023-05-15.png → /pmemstat-2.3.23/images/help-screen.png +0 -0
- /pmemstat-2.3.22/images/scroll-pos_2023-09-05.png → /pmemstat-2.3.23/images/scroll-pos.png +0 -0
- {pmemstat-2.3.22 → pmemstat-2.3.23}/memtest.c +0 -0
- {pmemstat-2.3.22 → pmemstat-2.3.23}/pmemstat/KillThem.py +0 -0
- {pmemstat-2.3.22 → pmemstat-2.3.23}/pmemstat/Window.py +0 -0
- {pmemstat-2.3.22 → pmemstat-2.3.23}/pmemstat/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pmemstat
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.23
|
|
4
4
|
Summary: Report Proportional Memory on Linux
|
|
5
5
|
Project-URL: Homepage, https://github.com/joedefen/pmemstat
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/joedefen/pmemstat/issues
|
|
@@ -17,15 +17,15 @@ Requires-Dist: requests
|
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
18
|
|
|
19
19
|
> **Quick Start**: from the CLI
|
|
20
|
-
> * **If `python3 -V` shows v3.11 or later, install
|
|
21
|
-
> *
|
|
22
|
-
> * `
|
|
23
|
-
>
|
|
24
|
-
>
|
|
25
|
-
> * `python3 -m pip install --user --upgrade pmemstat`
|
|
20
|
+
> * **If `python3 -V` shows v3.11 or later, install with `pipx`**:
|
|
21
|
+
> * install `pipx` per your distro's guidance
|
|
22
|
+
> * then: `pipx upgrade pmemstat || pipx install pmemstat`
|
|
23
|
+
> * **Else for python3.10 and lesser versions, install with `pip`**:
|
|
24
|
+
> * install `pip` per your distro's guidance
|
|
25
|
+
> * then: `python3 -m pip install --user --upgrade pmemstat`
|
|
26
26
|
> * **To run**:
|
|
27
|
-
> *
|
|
28
|
-
> *
|
|
27
|
+
> * simply run: `pmemstat`
|
|
28
|
+
> * type "?" within `pmemstat` to show help screen.
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
# pmemstat - Proportional Memory Status
|
|
@@ -106,26 +106,7 @@ Explanation of some options and arguments:
|
|
|
106
106
|
|
|
107
107
|
|
|
108
108
|
## Example Usage with Explanation of Output
|
|
109
|
-
|
|
110
|
-
10:38:17 Tot=31.3G Avail=21.8G Oth=2.4G Tmp=326.7M Dirty=1.2M PIDs: 202/202
|
|
111
|
-
cpu_pct pswap other data ptotal key/info (exe by mem)
|
|
112
|
-
37.9 0 1,670 5,193 6,862 T 202x --TOTALS in MB --
|
|
113
|
-
──────────────────────────────────────────────────────────────────────────────
|
|
114
|
-
2.1 0 452 1,830 2,282 41x chrome
|
|
115
|
-
18.5 0 2 827 830 2x python->main.py
|
|
116
|
-
0.1 0 87 470 557 8x code
|
|
117
|
-
0.7 0 63 393 456 1x plasmashell
|
|
118
|
-
0.7 0 106 281 389 7x Code
|
|
119
|
-
0.2 0 52 176 227 2x app
|
|
120
|
-
0.0 0 17 200 217 1x DiscoverNotifier
|
|
121
|
-
0.0 0 80 114 194 1x app.asar
|
|
122
|
-
0.0 0 94 76 170 3x signal-desktop
|
|
123
|
-
0.0 0 4 160 165 2x python->lsp_server.py
|
|
124
|
-
2.9 0 29 101 129 1x kwin_x11
|
|
125
|
-
0.0 0 94 9 102 2x Signal
|
|
126
|
-
12.8 0 590 555 1,145 O 131x ---- OTHERS ----
|
|
127
|
-
|
|
128
|
-
```
|
|
109
|
+

|
|
129
110
|
|
|
130
111
|
In the default refreshed window loop, we see
|
|
131
112
|
* a **leader line** with:
|
|
@@ -136,7 +117,14 @@ In the default refreshed window loop, we see
|
|
|
136
117
|
* Features such as BTRFS, ZFS, zRAM, unattached SysV Shared Memory, etc., can cause 'Oth' to be significant.
|
|
137
118
|
* Determining the contributors can be difficult, but start with `sudo slabtop -sc` and feature specific tools (e.g., `zpool list`).
|
|
138
119
|
* how many PIDs are contributing to the report vs the total number of PIDs excluding kernel threads
|
|
120
|
+
* a **second leader line for zRAM** only if zRAM is active with:
|
|
121
|
+
* percent cpu consumed by kernel processes (not normalized ... if there 4 CPUs, then the total CPU can 400%). This is important because, with zRAM, this often is mostly the swap process.
|
|
122
|
+
* **zRAM** - the amount of "Used" RAM that is storing zRAM data
|
|
123
|
+
* **eTot** - the effective Total RAM and the percent of "Tot" RAM (which would typically be above 100%). The bigger **zRAM**, the more accurate this number.
|
|
124
|
+
* **eUsed** - the effective Used RAM and percent of "Tot" RAM.
|
|
125
|
+
* **eAvail** - the effective Available RAM and percent of "Tot" RAM.
|
|
139
126
|
* a **header line with the reported fields** including:
|
|
127
|
+
* **cpu** - percent CPU (again not normalized to 100%)
|
|
140
128
|
* **pswap** - proportional use of swap (per smaps_rollup)
|
|
141
129
|
* **other** - partly sums these categories (shown by the key, `o`):
|
|
142
130
|
* **shSYSV** - proportional use of System V shared memory (per smaps)
|
|
@@ -144,8 +132,7 @@ In the default refreshed window loop, we see
|
|
|
144
132
|
* **stack** - exclusive use of stack memory per smaps
|
|
145
133
|
* **text** - proportional use of memory for text (i.e., the read-only binary code, per smaps)
|
|
146
134
|
* **data** - exclusive use of memory for data (i.e., exclusively used of "heap" memory, per smaps)
|
|
147
|
-
* **ptotal** - proportional use of memory of all categories (i.e, sum of
|
|
148
|
-
* (sometimes) **pss** - proportional use of memory (per smaps_rollup)
|
|
135
|
+
* **ptotal** - proportional use of memory of all categories (i.e, sum of columns to the left); **note**, if zRAM, this includes **pswap**, else not.
|
|
149
136
|
* *empty* - type of the entry which may be:
|
|
150
137
|
* **T** - the grand total
|
|
151
138
|
* **A** - a newly added grouping
|
|
@@ -158,7 +145,7 @@ In the default refreshed window loop, we see
|
|
|
158
145
|
## Help Screen (in Window Mode, Press '?')
|
|
159
146
|
In window mode, press '?' to enter the help screen which looks like:
|
|
160
147
|
|
|
161
|
-

|
|
162
149
|
|
|
163
150
|
**Notes:**
|
|
164
151
|
* There are a number of navigation keys (mostly following vim conventions); in the help screen, they apply to help screen; otherwise, they apply to main screen.
|
|
@@ -171,7 +158,7 @@ Pressing "K" enter "Kill Mode" where you use the navigation keys to highlight a
|
|
|
171
158
|
## Scroll Position (Window Mode)
|
|
172
159
|
|
|
173
160
|
Sometimes, the horizontal line between the header and scrollable region has a reverse video block (under the "351" in this case):
|
|
174
|
-

|
|
175
162
|
|
|
176
163
|
**Notes:**
|
|
177
164
|
* When there is no block, the scrolled document does not overflow the scrollable region.
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
> **Quick Start**: from the CLI
|
|
2
|
-
> * **If `python3 -V` shows v3.11 or later, install
|
|
3
|
-
> *
|
|
4
|
-
> * `
|
|
5
|
-
>
|
|
6
|
-
>
|
|
7
|
-
> * `python3 -m pip install --user --upgrade pmemstat`
|
|
2
|
+
> * **If `python3 -V` shows v3.11 or later, install with `pipx`**:
|
|
3
|
+
> * install `pipx` per your distro's guidance
|
|
4
|
+
> * then: `pipx upgrade pmemstat || pipx install pmemstat`
|
|
5
|
+
> * **Else for python3.10 and lesser versions, install with `pip`**:
|
|
6
|
+
> * install `pip` per your distro's guidance
|
|
7
|
+
> * then: `python3 -m pip install --user --upgrade pmemstat`
|
|
8
8
|
> * **To run**:
|
|
9
|
-
> *
|
|
10
|
-
> *
|
|
9
|
+
> * simply run: `pmemstat`
|
|
10
|
+
> * type "?" within `pmemstat` to show help screen.
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
# pmemstat - Proportional Memory Status
|
|
@@ -88,26 +88,7 @@ Explanation of some options and arguments:
|
|
|
88
88
|
|
|
89
89
|
|
|
90
90
|
## Example Usage with Explanation of Output
|
|
91
|
-
|
|
92
|
-
10:38:17 Tot=31.3G Avail=21.8G Oth=2.4G Tmp=326.7M Dirty=1.2M PIDs: 202/202
|
|
93
|
-
cpu_pct pswap other data ptotal key/info (exe by mem)
|
|
94
|
-
37.9 0 1,670 5,193 6,862 T 202x --TOTALS in MB --
|
|
95
|
-
──────────────────────────────────────────────────────────────────────────────
|
|
96
|
-
2.1 0 452 1,830 2,282 41x chrome
|
|
97
|
-
18.5 0 2 827 830 2x python->main.py
|
|
98
|
-
0.1 0 87 470 557 8x code
|
|
99
|
-
0.7 0 63 393 456 1x plasmashell
|
|
100
|
-
0.7 0 106 281 389 7x Code
|
|
101
|
-
0.2 0 52 176 227 2x app
|
|
102
|
-
0.0 0 17 200 217 1x DiscoverNotifier
|
|
103
|
-
0.0 0 80 114 194 1x app.asar
|
|
104
|
-
0.0 0 94 76 170 3x signal-desktop
|
|
105
|
-
0.0 0 4 160 165 2x python->lsp_server.py
|
|
106
|
-
2.9 0 29 101 129 1x kwin_x11
|
|
107
|
-
0.0 0 94 9 102 2x Signal
|
|
108
|
-
12.8 0 590 555 1,145 O 131x ---- OTHERS ----
|
|
109
|
-
|
|
110
|
-
```
|
|
91
|
+

|
|
111
92
|
|
|
112
93
|
In the default refreshed window loop, we see
|
|
113
94
|
* a **leader line** with:
|
|
@@ -118,7 +99,14 @@ In the default refreshed window loop, we see
|
|
|
118
99
|
* Features such as BTRFS, ZFS, zRAM, unattached SysV Shared Memory, etc., can cause 'Oth' to be significant.
|
|
119
100
|
* Determining the contributors can be difficult, but start with `sudo slabtop -sc` and feature specific tools (e.g., `zpool list`).
|
|
120
101
|
* how many PIDs are contributing to the report vs the total number of PIDs excluding kernel threads
|
|
102
|
+
* a **second leader line for zRAM** only if zRAM is active with:
|
|
103
|
+
* percent cpu consumed by kernel processes (not normalized ... if there 4 CPUs, then the total CPU can 400%). This is important because, with zRAM, this often is mostly the swap process.
|
|
104
|
+
* **zRAM** - the amount of "Used" RAM that is storing zRAM data
|
|
105
|
+
* **eTot** - the effective Total RAM and the percent of "Tot" RAM (which would typically be above 100%). The bigger **zRAM**, the more accurate this number.
|
|
106
|
+
* **eUsed** - the effective Used RAM and percent of "Tot" RAM.
|
|
107
|
+
* **eAvail** - the effective Available RAM and percent of "Tot" RAM.
|
|
121
108
|
* a **header line with the reported fields** including:
|
|
109
|
+
* **cpu** - percent CPU (again not normalized to 100%)
|
|
122
110
|
* **pswap** - proportional use of swap (per smaps_rollup)
|
|
123
111
|
* **other** - partly sums these categories (shown by the key, `o`):
|
|
124
112
|
* **shSYSV** - proportional use of System V shared memory (per smaps)
|
|
@@ -126,8 +114,7 @@ In the default refreshed window loop, we see
|
|
|
126
114
|
* **stack** - exclusive use of stack memory per smaps
|
|
127
115
|
* **text** - proportional use of memory for text (i.e., the read-only binary code, per smaps)
|
|
128
116
|
* **data** - exclusive use of memory for data (i.e., exclusively used of "heap" memory, per smaps)
|
|
129
|
-
* **ptotal** - proportional use of memory of all categories (i.e, sum of
|
|
130
|
-
* (sometimes) **pss** - proportional use of memory (per smaps_rollup)
|
|
117
|
+
* **ptotal** - proportional use of memory of all categories (i.e, sum of columns to the left); **note**, if zRAM, this includes **pswap**, else not.
|
|
131
118
|
* *empty* - type of the entry which may be:
|
|
132
119
|
* **T** - the grand total
|
|
133
120
|
* **A** - a newly added grouping
|
|
@@ -140,7 +127,7 @@ In the default refreshed window loop, we see
|
|
|
140
127
|
## Help Screen (in Window Mode, Press '?')
|
|
141
128
|
In window mode, press '?' to enter the help screen which looks like:
|
|
142
129
|
|
|
143
|
-

|
|
144
131
|
|
|
145
132
|
**Notes:**
|
|
146
133
|
* There are a number of navigation keys (mostly following vim conventions); in the help screen, they apply to help screen; otherwise, they apply to main screen.
|
|
@@ -153,7 +140,7 @@ Pressing "K" enter "Kill Mode" where you use the navigation keys to highlight a
|
|
|
153
140
|
## Scroll Position (Window Mode)
|
|
154
141
|
|
|
155
142
|
Sometimes, the horizontal line between the header and scrollable region has a reverse video block (under the "351" in this case):
|
|
156
|
-

|
|
157
144
|
|
|
158
145
|
**Notes:**
|
|
159
146
|
* When there is no block, the scrolled document does not overflow the scrollable region.
|
|
Binary file
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Class for fetching CPU usage from processes.
|
|
5
|
+
"""
|
|
6
|
+
# pylint: disable=invalid-name,broad-exception-caught,too-many-instance-attributes
|
|
7
|
+
# pylint: disable=import-outside-toplevel,too-many-branches,too-many-locals
|
|
8
|
+
# pylint: disable=consider-using-with,too-many-statements
|
|
9
|
+
import os
|
|
10
|
+
import time
|
|
11
|
+
import re
|
|
12
|
+
import traceback
|
|
13
|
+
import math
|
|
14
|
+
from types import SimpleNamespace
|
|
15
|
+
|
|
16
|
+
class Term:
|
|
17
|
+
""" Escape sequences; e.g., see:
|
|
18
|
+
- https://en.wikipedia.org/wiki/ANSI_escape_code
|
|
19
|
+
- https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797#file-ansi-md
|
|
20
|
+
"""
|
|
21
|
+
esc = '\x1B'
|
|
22
|
+
# pylint: disable=missing-function-docstring,multiple-statements
|
|
23
|
+
@staticmethod
|
|
24
|
+
def erase_line(): return f'{Term.esc}[2K'
|
|
25
|
+
@staticmethod
|
|
26
|
+
def erase_to_eol(): return f'{Term.esc}[0K'
|
|
27
|
+
@staticmethod
|
|
28
|
+
def bold(): return f'{Term.esc}[1m'
|
|
29
|
+
@staticmethod
|
|
30
|
+
def reverse_video(): return f'{Term.esc}[7m'
|
|
31
|
+
@staticmethod
|
|
32
|
+
def normal_video(): return f'{Term.esc}[m'
|
|
33
|
+
@staticmethod
|
|
34
|
+
def pos_up(cnt): return f'{Term.esc}[{cnt}F' if cnt > 0 else ''
|
|
35
|
+
@staticmethod
|
|
36
|
+
def pos_down(cnt): return f'{Term.esc}[{cnt}E' if cnt > 0 else ''
|
|
37
|
+
@staticmethod
|
|
38
|
+
def col(pos): return f'{Term.esc}[{pos}G'
|
|
39
|
+
@staticmethod
|
|
40
|
+
def clear_screen(): return f'{Term.esc}[H{Term.esc}[2J{Term.esc}[3J'
|
|
41
|
+
|
|
42
|
+
class SysStat:
|
|
43
|
+
""" TBD """
|
|
44
|
+
singleton = None
|
|
45
|
+
def __init__(self):
|
|
46
|
+
self.fh = None
|
|
47
|
+
self.prev = None
|
|
48
|
+
self.delta = None
|
|
49
|
+
self.clock_tick = 100
|
|
50
|
+
assert not self.singleton, 'cannot instantiate two SysStat'
|
|
51
|
+
SysStat.singleton = self
|
|
52
|
+
self._set_clock_tick()
|
|
53
|
+
self._refresh()
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def get_singleton():
|
|
57
|
+
""" Return THE SysStat object"""
|
|
58
|
+
return SysStat.singleton if SysStat.singleton else SysStat()
|
|
59
|
+
|
|
60
|
+
def _set_clock_tick(self):
|
|
61
|
+
if self.clock_tick is None:
|
|
62
|
+
try:
|
|
63
|
+
self.clock_tick = os.sysconf(os.sysconf_names['SC_CLK_TCK'])
|
|
64
|
+
except Exception:
|
|
65
|
+
pass
|
|
66
|
+
if self.clock_tick <= 0: # fake it
|
|
67
|
+
self.clock_tick = 100
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def refresh():
|
|
71
|
+
""" TBD """
|
|
72
|
+
return SysStat.get_singleton()._refresh()
|
|
73
|
+
|
|
74
|
+
def _refresh(self):
|
|
75
|
+
""" TBD """
|
|
76
|
+
if not self.fh:
|
|
77
|
+
pathname = '/proc/stat'
|
|
78
|
+
self.fh = open(pathname, "r", encoding='utf-8')
|
|
79
|
+
self.fh.seek(0)
|
|
80
|
+
|
|
81
|
+
ns = SimpleNamespace(mono=time.monotonic(),
|
|
82
|
+
cpu_cnt=0, percent=0, ticks=0)
|
|
83
|
+
delta = SimpleNamespace(**vars(ns))
|
|
84
|
+
|
|
85
|
+
for line in self.fh:
|
|
86
|
+
wds = line.split()
|
|
87
|
+
keyword = wds[0]
|
|
88
|
+
if keyword == 'cpu':
|
|
89
|
+
ns.ticks = int(wds[1])
|
|
90
|
+
ns.ticks += int(wds[3])
|
|
91
|
+
# needed for VM to normalize the %cpu per process
|
|
92
|
+
ns.gross_ticks = sum(int(val) for val in wds[1:])
|
|
93
|
+
elif keyword.startswith('cpu'):
|
|
94
|
+
ns.cpu_cnt += 1
|
|
95
|
+
|
|
96
|
+
if self.prev:
|
|
97
|
+
prev = self.prev
|
|
98
|
+
delta.mono = round(ns.mono - prev.mono, 4)
|
|
99
|
+
delta.ticks = ns.ticks - prev.ticks
|
|
100
|
+
delta.cpu_cnt = ns.cpu_cnt
|
|
101
|
+
delta.max_ticks = math.ceil(self.clock_tick
|
|
102
|
+
* delta.mono * ns.cpu_cnt)
|
|
103
|
+
delta.gross_ticks = ns.gross_ticks - prev.gross_ticks
|
|
104
|
+
if delta.mono > 0:
|
|
105
|
+
delta.percent = round(100
|
|
106
|
+
* delta.ticks / self.clock_tick / delta.mono, 4)
|
|
107
|
+
self.prev = ns
|
|
108
|
+
self.delta = delta
|
|
109
|
+
return delta
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class CpuSmooth:
|
|
113
|
+
"""Class that get smoothed CPU percent of given process"""
|
|
114
|
+
clock_tick = None # number of clock ticks/sec
|
|
115
|
+
prev_system_stats = None
|
|
116
|
+
prev_system_delta = None
|
|
117
|
+
|
|
118
|
+
def __init__(self, pid, avg_secs=0, error=False, DB=False):
|
|
119
|
+
self.pid = pid
|
|
120
|
+
self.error = error # once an error stop beating head against wall
|
|
121
|
+
self.DB = DB
|
|
122
|
+
self.db_info = '' # describe last update
|
|
123
|
+
self.avg_secs = avg_secs # smoothing interval
|
|
124
|
+
self.fh = None
|
|
125
|
+
self.stat_ns = None # last read status
|
|
126
|
+
self.percent = 0 # smoothed percent
|
|
127
|
+
self.hists = []
|
|
128
|
+
self.nickname = '' # crudely fetched on demand (for test)
|
|
129
|
+
self.sys_stat = SysStat.get_singleton()
|
|
130
|
+
|
|
131
|
+
def __del__(self):
|
|
132
|
+
if self.fh:
|
|
133
|
+
try:
|
|
134
|
+
self.fh.close()
|
|
135
|
+
except Exception:
|
|
136
|
+
pass
|
|
137
|
+
|
|
138
|
+
def _set_error(self):
|
|
139
|
+
if self.fh:
|
|
140
|
+
try:
|
|
141
|
+
self.fh.close()
|
|
142
|
+
except Exception:
|
|
143
|
+
pass
|
|
144
|
+
self.fh = None
|
|
145
|
+
self.percent, self.error = 0, True
|
|
146
|
+
return self.percent
|
|
147
|
+
|
|
148
|
+
def get_nickname(self):
|
|
149
|
+
""" Get the nickname of the process (crude)."""
|
|
150
|
+
if self.nickname:
|
|
151
|
+
return self.nickname
|
|
152
|
+
cmdline_file = f'/proc/{self.pid}/cmdline'
|
|
153
|
+
arguments, nickname = [], ''
|
|
154
|
+
try:
|
|
155
|
+
# pylint: disable=consider-using-with
|
|
156
|
+
with open(cmdline_file, encoding='utf-8') as fh:
|
|
157
|
+
for line in fh:
|
|
158
|
+
arguments = line.split('\0')
|
|
159
|
+
break
|
|
160
|
+
except Exception:
|
|
161
|
+
pass
|
|
162
|
+
if arguments:
|
|
163
|
+
wds = os.path.basename(arguments[0]).split() + arguments[1:]
|
|
164
|
+
nickname = re.sub(r'^\W+', '', wds.pop(0))
|
|
165
|
+
nickname = re.sub(r'\W+$', '', nickname)
|
|
166
|
+
# DB(0, f'basename={basename} wds={wds}')
|
|
167
|
+
if nickname in ('python', 'python2', 'python3', 'perl', 'bash', 'ruby',
|
|
168
|
+
'sh', 'ksh', 'zsh') and wds:
|
|
169
|
+
script = os.path.basename(wds[0])
|
|
170
|
+
# DB(0, f'script={script} wds[0]={wds[0]}')
|
|
171
|
+
if script != wds[0]:
|
|
172
|
+
nickname = f'{nickname}->{script}'
|
|
173
|
+
if not nickname:
|
|
174
|
+
ns = self.stat_ns if self.stat_ns else self._get_stat()
|
|
175
|
+
if ns and ns.exec:
|
|
176
|
+
nickname = ns.exec
|
|
177
|
+
self.nickname = nickname
|
|
178
|
+
return nickname
|
|
179
|
+
|
|
180
|
+
def _get_stat(self):
|
|
181
|
+
if not self.fh:
|
|
182
|
+
try:
|
|
183
|
+
# pylint: disable=consider-using-with
|
|
184
|
+
self.fh = open(f'/proc/{self.pid}/stat', encoding='utf-8')
|
|
185
|
+
except (PermissionError, FileNotFoundError):
|
|
186
|
+
return self._set_error()
|
|
187
|
+
try:
|
|
188
|
+
self.fh.seek(0)
|
|
189
|
+
data = self.fh.read().split()
|
|
190
|
+
self.stat_ns = SimpleNamespace(exec=data[1],
|
|
191
|
+
user=int(data[13]), system=int(data[14]),
|
|
192
|
+
nthr=int(data[19]))
|
|
193
|
+
except Exception:
|
|
194
|
+
self._set_error()
|
|
195
|
+
return self.stat_ns
|
|
196
|
+
|
|
197
|
+
def refresh_cpu(self):
|
|
198
|
+
"""Get the Cpu Number for the PID (if possible)"""
|
|
199
|
+
def pct(hist0, hist1):
|
|
200
|
+
delta_ticks = abs(hist0[0] - hist1[0])
|
|
201
|
+
delta_mono = abs(hist0[1] - hist1[1])
|
|
202
|
+
delta_gross_ticks = abs(hist0[2] - hist1[2])
|
|
203
|
+
|
|
204
|
+
percent = 0
|
|
205
|
+
cpu_cnt = self.sys_stat.prev.cpu_cnt
|
|
206
|
+
if delta_gross_ticks > 0:
|
|
207
|
+
percent = cpu_cnt * 100 * delta_ticks / delta_gross_ticks
|
|
208
|
+
|
|
209
|
+
return percent, delta_ticks, delta_mono
|
|
210
|
+
def pct_str(triple):
|
|
211
|
+
return f'{triple[0]:7.3f}%,{triple[1]:5d},{triple[2]:7.4f}s'
|
|
212
|
+
# def adjust_down(delta_ticks):
|
|
213
|
+
# """ Lower current ticks by amount ... meaning add to all
|
|
214
|
+
# previous ticks"""
|
|
215
|
+
# for hist in self.hists[:-1]:
|
|
216
|
+
# hist[0] += delta_ticks
|
|
217
|
+
|
|
218
|
+
if self.error or not self._get_stat():
|
|
219
|
+
return self.percent
|
|
220
|
+
ticks = self.stat_ns.user + self.stat_ns.system
|
|
221
|
+
mono = time.monotonic()
|
|
222
|
+
gross_ticks = self.sys_stat.prev.gross_ticks
|
|
223
|
+
self.hists.append([ticks, mono, gross_ticks])
|
|
224
|
+
|
|
225
|
+
if len(self.hists) < 2: # takes two to tango
|
|
226
|
+
return 0
|
|
227
|
+
floor_mono = mono - self.avg_secs
|
|
228
|
+
while len(self.hists) > 2 and self.hists[0][1] < floor_mono:
|
|
229
|
+
self.hists.pop(0)
|
|
230
|
+
try:
|
|
231
|
+
_, _, delta_mono = pct(self.hists[-1], self.hists[-2])
|
|
232
|
+
except:
|
|
233
|
+
pass
|
|
234
|
+
if delta_mono <= 0.0:
|
|
235
|
+
self.hists.pop()
|
|
236
|
+
return 0
|
|
237
|
+
|
|
238
|
+
self.percent, _, _ = pct(self.hists[-1], self.hists[0])
|
|
239
|
+
|
|
240
|
+
# print(f'{self.percent}%')
|
|
241
|
+
if self.DB:
|
|
242
|
+
deltas = []
|
|
243
|
+
hists = self.hists
|
|
244
|
+
for idx in range(len(hists)-1, 0, -1):
|
|
245
|
+
hist, prev = hists[idx], hists[idx-1]
|
|
246
|
+
deltas.append(f'{hist[0]-prev[0]}'
|
|
247
|
+
+ f'/{hist[1]-prev[1]:.2f}'
|
|
248
|
+
# + ('' if hist[2] <= 1 else f'#{hist[2]}')
|
|
249
|
+
)
|
|
250
|
+
self.db_info = ' '
|
|
251
|
+
if len(hists) >= 2:
|
|
252
|
+
self.db_info = ' '.join([
|
|
253
|
+
f'{self.get_nickname()[:16]:>16} {self.pid:>6d}',
|
|
254
|
+
pct_str(pct(self.hists[-1], self.hists[-2])),
|
|
255
|
+
'//', pct_str(pct(self.hists[-1], self.hists[0])),
|
|
256
|
+
' '.join(deltas)])
|
|
257
|
+
# if self.nickname == 'firefox':
|
|
258
|
+
# print(self.nickname, self.hists)
|
|
259
|
+
return self.percent
|
|
260
|
+
|
|
261
|
+
if __name__ == '__main__':
|
|
262
|
+
def main():
|
|
263
|
+
"""Main loop"""
|
|
264
|
+
import argparse
|
|
265
|
+
parser = argparse.ArgumentParser()
|
|
266
|
+
parser.add_argument('-t', '--top', type=int, default=10,
|
|
267
|
+
help='number of top CPU entries to show')
|
|
268
|
+
parser.add_argument('-o', '--only', type=str, default='',
|
|
269
|
+
help='name must have only substring')
|
|
270
|
+
parser.add_argument('-l', '--loop', type=float, default=1.0, dest='loop_secs',
|
|
271
|
+
help='loop interval in secs [dflt=1.0]')
|
|
272
|
+
opts = parser.parse_args()
|
|
273
|
+
|
|
274
|
+
loop_secs = opts.loop_secs if opts.loop_secs >= 0.25 else 0.25
|
|
275
|
+
loop_secs = opts.loop_secs if opts.loop_secs <= 120.0 else 120.0
|
|
276
|
+
|
|
277
|
+
spots = [] # where to put the top items
|
|
278
|
+
cpus = {}
|
|
279
|
+
losers = set()
|
|
280
|
+
pids = set()
|
|
281
|
+
start_mono = time.monotonic()
|
|
282
|
+
sys_stat = SysStat.get_singleton()
|
|
283
|
+
while True:
|
|
284
|
+
with os.scandir('/proc') as it:
|
|
285
|
+
for entry in it:
|
|
286
|
+
# if re.match(r'^\d+$', entry.name):
|
|
287
|
+
if entry.name.isdigit():
|
|
288
|
+
pids.add(int(entry.name))
|
|
289
|
+
old_losers, losers = losers, set()
|
|
290
|
+
sys_stat.refresh()
|
|
291
|
+
for pid in pids:
|
|
292
|
+
if pid in old_losers:
|
|
293
|
+
losers.add(pid)
|
|
294
|
+
continue
|
|
295
|
+
cpu = cpus.get(pid, None)
|
|
296
|
+
if not cpu:
|
|
297
|
+
cpu = CpuSmooth(pid=pid, avg_secs=10, DB=True)
|
|
298
|
+
if not cpu.error:
|
|
299
|
+
# print(f'adding: {pid} {cpu.get_nickname()}')
|
|
300
|
+
cpus[pid] = cpu
|
|
301
|
+
if cpu.error:
|
|
302
|
+
losers.add(pid)
|
|
303
|
+
continue
|
|
304
|
+
cpu.refresh_cpu()
|
|
305
|
+
top_cpus = sorted(cpus.values(), key=lambda x: x.percent, reverse=True)
|
|
306
|
+
top_cpus = top_cpus[:opts.top]
|
|
307
|
+
top_cpus = sorted(top_cpus, key=lambda x: x.pid)
|
|
308
|
+
run_time = time.monotonic()-start_mono
|
|
309
|
+
total_pct = 0
|
|
310
|
+
for cpu in cpus.values():
|
|
311
|
+
total_pct += cpu.percent
|
|
312
|
+
print(f'--------- {run_time:6.1f}s {total_pct:7.2f}% ----------- ',
|
|
313
|
+
f'{sys_stat.delta}' + Term.erase_to_eol())
|
|
314
|
+
old_spots, spots, todo = spots, [None]*opts.top, set()
|
|
315
|
+
for cpu in top_cpus:
|
|
316
|
+
try:
|
|
317
|
+
idx = old_spots.index(cpu)
|
|
318
|
+
spots[idx] = cpu
|
|
319
|
+
except Exception:
|
|
320
|
+
todo.add(cpu)
|
|
321
|
+
for idx, cpu in enumerate(spots):
|
|
322
|
+
if not cpu:
|
|
323
|
+
cpu = todo.pop()
|
|
324
|
+
spots[idx] = cpu
|
|
325
|
+
print(cpu.db_info + Term.erase_to_eol())
|
|
326
|
+
|
|
327
|
+
time.sleep(loop_secs)
|
|
328
|
+
up_one = Term.pos_up(1) + '\r'
|
|
329
|
+
print(up_one * (2+len(top_cpus)))
|
|
330
|
+
|
|
331
|
+
try:
|
|
332
|
+
main()
|
|
333
|
+
except KeyboardInterrupt:
|
|
334
|
+
pass
|
|
335
|
+
except Exception as exce:
|
|
336
|
+
print("exception:", str(exce))
|
|
337
|
+
print(traceback.format_exc())
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
3
|
"""
|
|
4
4
|
Pending Features:
|
|
@@ -35,7 +35,9 @@ NOTE: kB is a misnomer ... should be "KB". Morons.
|
|
|
35
35
|
# pylint: disable=broad-except,import-outside-toplevel,global-statement
|
|
36
36
|
# pylint: disable=too-many-boolean-expressions,invalid-name
|
|
37
37
|
# pylint: disable=too-many-instance-attributes,too-many-lines
|
|
38
|
-
# pylint: disable=too-many-arguments
|
|
38
|
+
# pylint: disable=too-many-arguments,too-many-branches
|
|
39
|
+
# pylint: disable=too-many-statements,too-many-locals
|
|
40
|
+
# pylint: disable=multiple-statements,too-few-public-methods
|
|
39
41
|
|
|
40
42
|
|
|
41
43
|
import os
|
|
@@ -43,7 +45,6 @@ import re
|
|
|
43
45
|
import sys
|
|
44
46
|
import traceback
|
|
45
47
|
import time
|
|
46
|
-
import subprocess
|
|
47
48
|
import curses
|
|
48
49
|
# from curses.textpad import rectangle
|
|
49
50
|
from types import SimpleNamespace
|
|
@@ -52,9 +53,11 @@ from datetime import datetime, timedelta
|
|
|
52
53
|
try:
|
|
53
54
|
from Window import Window, OptionSpinner
|
|
54
55
|
from KillThem import KillThem
|
|
55
|
-
|
|
56
|
+
from CpuSmooth import CpuSmooth, SysStat
|
|
57
|
+
except Exception:
|
|
56
58
|
from pmemstat.Window import Window, OptionSpinner
|
|
57
59
|
from pmemstat.KillThem import KillThem
|
|
60
|
+
from pmemstat.CpuSmooth import CpuSmooth, SysStat
|
|
58
61
|
|
|
59
62
|
# Trace Levels:
|
|
60
63
|
# 0 - forced, temporary debugging (comment it out)
|
|
@@ -94,14 +97,128 @@ def human(number):
|
|
|
94
97
|
number /= 1024
|
|
95
98
|
if number < 999.95 or not suffixes:
|
|
96
99
|
return f'{number:.1f}{suffix}'
|
|
100
|
+
return '' # impossible, but make pylint happy
|
|
97
101
|
|
|
98
|
-
######
|
|
99
102
|
####################################################################################
|
|
100
|
-
######
|
|
101
|
-
######
|
|
103
|
+
###### ZramProjector class
|
|
102
104
|
####################################################################################
|
|
103
|
-
|
|
105
|
+
class ZramProjector:
|
|
106
|
+
""" Guess the zram numbers """
|
|
107
|
+
def __init__(self):
|
|
108
|
+
self.meminfo = None
|
|
109
|
+
self.e_total = 0
|
|
110
|
+
self.e_avail = 0
|
|
111
|
+
self.e_used = 0
|
|
112
|
+
self.e_max_used = 0
|
|
113
|
+
self.limit_pct = 80
|
|
114
|
+
self.devs = {}
|
|
115
|
+
self.DB = False
|
|
116
|
+
|
|
117
|
+
def human_pct(self, number):
|
|
118
|
+
""" Return the number in human form and as pct total memory. """
|
|
119
|
+
rv = human(number)
|
|
120
|
+
if number > 0 and self.meminfo.MemTotal > 0:
|
|
121
|
+
fraction = number/self.meminfo.MemTotal
|
|
122
|
+
pct = int(round(100*fraction))
|
|
123
|
+
rv += f'/{pct}%'
|
|
124
|
+
return rv
|
|
125
|
+
|
|
126
|
+
def _get_zram_stats(self):
|
|
127
|
+
""" Get only what we want
|
|
128
|
+
orig_data_size uncompressed size of data stored in this disk.
|
|
129
|
+
compr_data_size compressed size of data stored in this disk
|
|
130
|
+
mem_used_total the amount of memory allocated for this disk.
|
|
131
|
+
mem_limit max RAM ZRAM can use to store (0 means unlimited)
|
|
132
|
+
mem_used_max max RAM zram has consumed to store the data
|
|
133
|
+
"""
|
|
134
|
+
fields = ('orig_data_size compr_data_size mem_used_total'
|
|
135
|
+
+ ' mem_limit mem_used_max').split()
|
|
136
|
+
infos = {}
|
|
137
|
+
zram_devices = sorted([device for device in os.listdir('/sys/class/block/')
|
|
138
|
+
if device.startswith('zram')])
|
|
139
|
+
for device in zram_devices:
|
|
140
|
+
pathname = f'/sys/class/block/{device}/mm_stat'
|
|
141
|
+
if not os.path.exists(pathname):
|
|
142
|
+
continue # not active
|
|
143
|
+
with open(pathname, encoding='utf-8') as fh:
|
|
144
|
+
ns = SimpleNamespace()
|
|
145
|
+
for line in fh: # all the goodies are on 1st line
|
|
146
|
+
nums = line.split()
|
|
147
|
+
for idx, field in enumerate(fields):
|
|
148
|
+
setattr(ns, field, int(nums[idx]))
|
|
149
|
+
break
|
|
150
|
+
infos[device] = ns
|
|
151
|
+
for param in ('disksize', ):
|
|
152
|
+
pathname = f'/sys/class/block/{device}/{param}'
|
|
153
|
+
with open(pathname, encoding='utf-8') as fh:
|
|
154
|
+
for line in fh: # one value, one line
|
|
155
|
+
setattr(ns, param, int(line.strip()))
|
|
156
|
+
break
|
|
157
|
+
if self.DB: print(f'DB: {device}: {ns}')
|
|
158
|
+
|
|
159
|
+
self.devs = infos
|
|
160
|
+
return infos
|
|
161
|
+
|
|
162
|
+
def compute_effective(self, meminfoKB):
|
|
163
|
+
""" Compute the effective values """
|
|
164
|
+
def seed(meminfoKB):
|
|
165
|
+
nonlocal self
|
|
166
|
+
meminfo = self.meminfo = SimpleNamespace()
|
|
167
|
+
for key, value in meminfoKB.items():
|
|
168
|
+
setattr(meminfo, key, value*1024)
|
|
169
|
+
# artificial stats
|
|
170
|
+
meminfo.MemUsed = meminfo.MemTotal - meminfo.MemAvailable
|
|
171
|
+
meminfo.MemZram=0
|
|
172
|
+
self.e_total = meminfo.MemTotal
|
|
173
|
+
self.e_avail = meminfo.MemAvailable
|
|
174
|
+
self.e_used = self.e_total - self.e_avail
|
|
175
|
+
self.e_max_used = self.e_used
|
|
176
|
+
return meminfo
|
|
177
|
+
|
|
178
|
+
meminfo = seed(meminfoKB)
|
|
179
|
+
devs = self._get_zram_stats()
|
|
180
|
+
if not devs:
|
|
181
|
+
return
|
|
182
|
+
stats = None
|
|
183
|
+
for stat in devs.values():
|
|
184
|
+
# NOTE: generalization for more than one weak unless all
|
|
185
|
+
# are identically configured
|
|
186
|
+
if not stats:
|
|
187
|
+
stats = vars(stat).copy()
|
|
188
|
+
continue
|
|
189
|
+
for key, value in vars(stat).items():
|
|
190
|
+
stats[key] += value
|
|
191
|
+
stats = SimpleNamespace(**stats)
|
|
192
|
+
self.meminfo.MemZram = stats.mem_used_total
|
|
193
|
+
|
|
194
|
+
e_used = meminfo.MemUsed - stats.mem_used_total + stats.orig_data_size
|
|
195
|
+
ratio = None
|
|
196
|
+
if e_used <= self.e_used: # zram hurts. assume not enuf data to project
|
|
197
|
+
e_used = self.e_used
|
|
198
|
+
ratio = 3.5 # wild guess for projection
|
|
199
|
+
# if self.DB: dump_effective(ns, '(unchg)')
|
|
200
|
+
# return ns
|
|
201
|
+
self.e_used = e_used
|
|
202
|
+
limit_pct = self.limit_pct
|
|
203
|
+
if stats.mem_limit > 0:
|
|
204
|
+
stats_limit_pct = (stats.limit / meminfo.MemTotal)*100
|
|
205
|
+
limit_pct = min(1.0, limit_pct, stats_limit_pct)
|
|
206
|
+
# projected amount of orig used memory that fits in limit
|
|
207
|
+
if ratio is None: # hopefully about 4 ;-)
|
|
208
|
+
ratio = stats.orig_data_size / stats.mem_used_total
|
|
209
|
+
e_max_used = ratio * meminfo.MemTotal * limit_pct/100
|
|
210
|
+
# now limit by disksize
|
|
211
|
+
e_max_used = min(e_max_used, stats.disksize)
|
|
212
|
+
# now add uncompressed memory
|
|
213
|
+
e_max_used += meminfo.MemTotal - e_max_used/ratio
|
|
214
|
+
self.e_max_used = e_max_used
|
|
215
|
+
# self.e_max_used = max(e_max_total, e_used) # TODO: uncomment
|
|
216
|
+
self.e_avail = e_max_used - e_used
|
|
104
217
|
|
|
218
|
+
|
|
219
|
+
####################################################################################
|
|
220
|
+
###### ProcMem class
|
|
221
|
+
####################################################################################
|
|
105
222
|
class ProcMem:
|
|
106
223
|
"""Represents the memory map summation for processes and groups.
|
|
107
224
|
- the ProcMem object represents one process (or pid)
|
|
@@ -158,8 +275,9 @@ class ProcMem:
|
|
|
158
275
|
self.alive = True
|
|
159
276
|
self.is_new = True
|
|
160
277
|
self.wanted = True # until proven otherwise
|
|
278
|
+
self.kernel = False # until proven otherwise
|
|
161
279
|
self.is_changed = False
|
|
162
|
-
self.
|
|
280
|
+
self.why_not = None # populate me with why unwanted
|
|
163
281
|
self.smaps_file = f'/proc/{self.pid}/smaps'
|
|
164
282
|
self.rollup_file = f'/proc/{self.pid}/smaps_rollup'
|
|
165
283
|
self.cpu = None
|
|
@@ -168,56 +286,9 @@ class ProcMem:
|
|
|
168
286
|
|
|
169
287
|
def refresh_cpu(self):
|
|
170
288
|
"""Get the Cpu Number for the PID (if possible)"""
|
|
171
|
-
def init_cpu(error=False):
|
|
172
|
-
return SimpleNamespace(error=error, fh=None,
|
|
173
|
-
percent=0, hists=[])
|
|
174
|
-
|
|
175
|
-
if ProcMem.clock_tick is None:
|
|
176
|
-
try:
|
|
177
|
-
ProcMem.clock_tick = 0
|
|
178
|
-
rv = subprocess.run(['getconf', 'CLK_TCK'],
|
|
179
|
-
capture_output=True, text=True, check=True)
|
|
180
|
-
ProcMem.clock_tick = int(rv.stdout.strip())
|
|
181
|
-
except Exception:
|
|
182
|
-
pass
|
|
183
|
-
if self.clock_tick <= 0:
|
|
184
|
-
ProcMem.clock_tick = 100
|
|
185
|
-
|
|
186
289
|
if not self.cpu:
|
|
187
|
-
self.cpu =
|
|
188
|
-
|
|
189
|
-
# pylint: disable=consider-using-with
|
|
190
|
-
self.cpu.fh = open(f'/proc/{self.pid}/stat', encoding='utf-8')
|
|
191
|
-
except (PermissionError, FileNotFoundError):
|
|
192
|
-
self.cpu = init_cpu(error=True)
|
|
193
|
-
return
|
|
194
|
-
if self.cpu.error:
|
|
195
|
-
return
|
|
196
|
-
try:
|
|
197
|
-
self.cpu.fh.seek(0)
|
|
198
|
-
data = self.cpu.fh.read().split()
|
|
199
|
-
user, system = int(data[13]), int(data[14])
|
|
200
|
-
except Exception:
|
|
201
|
-
self.cpu = init_cpu(error=True)
|
|
202
|
-
return
|
|
203
|
-
ticks = user + system
|
|
204
|
-
mono = time.monotonic()
|
|
205
|
-
|
|
206
|
-
if self.cpu.hists: # initialized
|
|
207
|
-
self.cpu.hists.append((ticks, mono))
|
|
208
|
-
floor_mono = mono - ProcMem.opts.cpu_avg_secs
|
|
209
|
-
while len(self.cpu.hists) > 1 and self.cpu.hists[0][1] < floor_mono:
|
|
210
|
-
self.cpu.hists.pop(0)
|
|
211
|
-
delta_ticks = ticks - self.cpu.hists[0][0]
|
|
212
|
-
delta_time = mono - self.cpu.hists[0][1]
|
|
213
|
-
self.cpu.percent = 0
|
|
214
|
-
if delta_time > 0 and delta_ticks >= 0:
|
|
215
|
-
self.cpu.percent = round(100
|
|
216
|
-
* delta_ticks / ProcMem.clock_tick / delta_time, 8)
|
|
217
|
-
# print(f'{self.cpu.percent}%')
|
|
218
|
-
|
|
219
|
-
else:
|
|
220
|
-
self.cpu.hists.append((ticks, mono))
|
|
290
|
+
self.cpu = CpuSmooth(self.pid, avg_secs= ProcMem.opts.cpu_avg_secs)
|
|
291
|
+
return self.cpu.refresh_cpu() # sets self.cpu.percent
|
|
221
292
|
|
|
222
293
|
def get_cmdline(self):
|
|
223
294
|
"""Get the command line of the PID."""
|
|
@@ -235,8 +306,7 @@ class ProcMem:
|
|
|
235
306
|
return
|
|
236
307
|
arguments = line.split('\0')
|
|
237
308
|
if not arguments or not arguments[0]: # kernel process
|
|
238
|
-
self.wanted = False
|
|
239
|
-
self.whynot = 'KernelProcess'
|
|
309
|
+
self.wanted, self.kernel = False, True
|
|
240
310
|
# print(f'{self.pid}: kernel thread')
|
|
241
311
|
return
|
|
242
312
|
# DB(0, f'{self.pid}: {arguments}')
|
|
@@ -260,8 +330,8 @@ class ProcMem:
|
|
|
260
330
|
# not really expecting this ... probably a bug
|
|
261
331
|
print(f' WARNING: skip pid={self.pid} no-basename exc={exc}')
|
|
262
332
|
print(traceback.format_exc())
|
|
263
|
-
self.wanted = False
|
|
264
|
-
self.
|
|
333
|
+
self.wanted = self.kernel = False
|
|
334
|
+
self.why_not = 'CannotGetCmdline'
|
|
265
335
|
return
|
|
266
336
|
|
|
267
337
|
## print(f'DBDB: {self.pid} {ProcMem.opts.pids}')
|
|
@@ -269,7 +339,7 @@ class ProcMem:
|
|
|
269
339
|
if (ProcMem.opts.pids and str(self.pid) not in ProcMem.opts.pids
|
|
270
340
|
and self.exebasename not in ProcMem.opts.pids):
|
|
271
341
|
self.wanted = False
|
|
272
|
-
self.
|
|
342
|
+
self.why_not = 'FilteredByArgs'
|
|
273
343
|
# print(f' >>>> unwanted {pid}')
|
|
274
344
|
## print(f'DBDB: {self.pid} wanted={self.wanted} whynot={self.whynot}')
|
|
275
345
|
self.set_key()
|
|
@@ -287,13 +357,13 @@ class ProcMem:
|
|
|
287
357
|
lines = fhandle.read().splitlines()
|
|
288
358
|
except (PermissionError, FileNotFoundError) as exc:
|
|
289
359
|
# normal cases: not permitted or this is a race where the pid is terminating
|
|
290
|
-
self.
|
|
360
|
+
self.why_not = f'CannotReadLines({type(exc).__name__})'
|
|
291
361
|
except Exception as exc:
|
|
292
362
|
# unexpected cases (probably a bug)
|
|
293
363
|
if not self.opts.window:
|
|
294
364
|
print(f'ERROR: skip pid={self.pid}',
|
|
295
365
|
f'no-smaps-or-rollup-lines exc={type(exc).__name__}')
|
|
296
|
-
self.
|
|
366
|
+
self.why_not = f'CannotReadLines({type(exc).__name__})'
|
|
297
367
|
return lines
|
|
298
368
|
|
|
299
369
|
def get_rollup_lines(self):
|
|
@@ -308,7 +378,7 @@ class ProcMem:
|
|
|
308
378
|
|
|
309
379
|
if not rollup_lines:
|
|
310
380
|
self.wanted = False
|
|
311
|
-
self.
|
|
381
|
+
self.why_not = 'CannotReadRollups'
|
|
312
382
|
elif DebugLevel:
|
|
313
383
|
DB(3, f'pid={self.pid} {self.exebasename} #rollup_lines={len(rollup_lines)}')
|
|
314
384
|
|
|
@@ -326,7 +396,7 @@ class ProcMem:
|
|
|
326
396
|
|
|
327
397
|
if not smaps_lines:
|
|
328
398
|
self.wanted = False
|
|
329
|
-
self.
|
|
399
|
+
self.why_not = 'CannotReadSmaps'
|
|
330
400
|
else:
|
|
331
401
|
if DebugLevel:
|
|
332
402
|
DB(1, f'pid={self.pid} {self.exebasename} #smaps_lines={len(smaps_lines)}')
|
|
@@ -414,6 +484,8 @@ class ProcMem:
|
|
|
414
484
|
summary['ptotal'] += val
|
|
415
485
|
elif tag == 'SwapPss':
|
|
416
486
|
summary['pswap'] += val
|
|
487
|
+
if self.pmemstat.has_zram():
|
|
488
|
+
summary['ptotal'] += val
|
|
417
489
|
continue
|
|
418
490
|
print(f'ERROR: cannot parse "{line}" [{self.rollup_file}:{idx+1}]')
|
|
419
491
|
summary['pss'] = summary['ptotal'] # for consistency
|
|
@@ -487,20 +559,19 @@ class ProcMem:
|
|
|
487
559
|
"""Process one PID"""
|
|
488
560
|
self.alive = True
|
|
489
561
|
self.is_changed = False
|
|
490
|
-
if not self.
|
|
562
|
+
if not self.why_not and not self.cmdline:
|
|
491
563
|
self.get_cmdline()
|
|
492
564
|
if not self.cmdline:
|
|
493
565
|
return
|
|
494
566
|
rollup_lines = []
|
|
495
|
-
if not self.
|
|
567
|
+
if not self.why_not:
|
|
496
568
|
rollup_lines = self.get_rollup_lines()
|
|
497
|
-
if self.
|
|
498
|
-
DB(4, f'pid={self.pid} {self.exebasename} whynot={self.
|
|
569
|
+
if self.why_not:
|
|
570
|
+
DB(4, f'pid={self.pid} {self.exebasename} whynot={self.why_not}')
|
|
499
571
|
return
|
|
500
572
|
self.is_changed = False
|
|
501
573
|
rollup_summary = self.parse_rollups(rollup_lines)
|
|
502
574
|
if self.opts.cpu:
|
|
503
|
-
self.refresh_cpu()
|
|
504
575
|
rollup_summary['cpu_pct'] = self.cpu.percent
|
|
505
576
|
group = self.pmemstat.get_group(self.key)
|
|
506
577
|
if not group.alive:
|
|
@@ -535,6 +606,11 @@ class PmemStat:
|
|
|
535
606
|
setattr(opts, 'cpu_avg_secs', 20) # pseudo option
|
|
536
607
|
self.groups_by_line = {}
|
|
537
608
|
self._set_units()
|
|
609
|
+
self.zram_projector = ZramProjector()
|
|
610
|
+
|
|
611
|
+
def has_zram(self):
|
|
612
|
+
"""Have zRAM actual? """
|
|
613
|
+
return bool(self.zram_projector and self.zram_projector.devs)
|
|
538
614
|
|
|
539
615
|
def get_sortby(self):
|
|
540
616
|
"""Make sort_by sensible."""
|
|
@@ -777,37 +853,66 @@ class PmemStat:
|
|
|
777
853
|
# pylint: disable=too-many-branches
|
|
778
854
|
|
|
779
855
|
def pr_top_of_report(appKB):
|
|
780
|
-
nonlocal self, meminfoKB, wanted_prcs,
|
|
856
|
+
nonlocal self, meminfoKB, wanted_prcs, total_user_pids, kernel_cpu
|
|
857
|
+
windowed = bool(self.window)
|
|
858
|
+
resume = False
|
|
781
859
|
# print timestamp of report
|
|
782
|
-
if
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
860
|
+
leader = '' if windowed else '--- '
|
|
861
|
+
leader += f'{now.strftime("%H:%M:%S")}'
|
|
862
|
+
self.emit(leader, to_head=True, resume=resume,
|
|
863
|
+
attr=curses.A_BOLD if self.loop_num % 2 else None)
|
|
864
|
+
leader = ''
|
|
865
|
+
resume = True
|
|
866
|
+
|
|
867
|
+
used = meminfoKB["MemTotal"] - meminfoKB["MemAvailable"]
|
|
790
868
|
leader += f' Tot={human(meminfoKB["MemTotal"]*1024)}'
|
|
869
|
+
leader += f' Used={human(used*1024)}'
|
|
791
870
|
leader += f' Avail={human(meminfoKB["MemAvailable"]*1024)}'
|
|
792
871
|
if appKB:
|
|
793
872
|
other = (meminfoKB["MemTotal"] - appKB
|
|
794
873
|
- meminfoKB["MemAvailable"] - meminfoKB["Shmem"])
|
|
795
874
|
leader += f' Oth={human(other*1024)}'
|
|
796
|
-
leader += f' Tmp={human(meminfoKB["Shmem"]*1024)}'
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
875
|
+
leader += f' Sh+Tmp={human(meminfoKB["Shmem"]*1024)}'
|
|
876
|
+
if len(wanted_prcs) < total_user_pids:
|
|
877
|
+
leader += f' PIDs={len(wanted_prcs)}/{total_user_pids}'
|
|
878
|
+
else:
|
|
879
|
+
leader += f' PIDs={total_user_pids}'
|
|
800
880
|
if self.opts.search:
|
|
801
|
-
self.emit(' /', to_head=True, resume=
|
|
881
|
+
self.emit(leader +' /', to_head=True, resume=resume)
|
|
882
|
+
resume = True
|
|
802
883
|
self.emit(self.opts.search, to_head=True,
|
|
803
|
-
resume=
|
|
804
|
-
self.emit('/', to_head=True, resume=
|
|
884
|
+
resume=resume, attr=curses.A_UNDERLINE)
|
|
885
|
+
self.emit('/', to_head=True, resume=resume)
|
|
886
|
+
else:
|
|
887
|
+
self.emit(leader, to_head=True, resume=resume)
|
|
888
|
+
|
|
889
|
+
# second line only if zRAM
|
|
890
|
+
if not self.has_zram():
|
|
891
|
+
return
|
|
892
|
+
resume = False
|
|
893
|
+
proj = self.zram_projector
|
|
894
|
+
if self.opts.cpu:
|
|
895
|
+
leader = f'{kernel_cpu:8.1f}/ker '
|
|
896
|
+
self.emit(leader, to_head=True, resume=resume)
|
|
897
|
+
resume = True
|
|
898
|
+
self.emit(f' zRAM={human(self.zram_projector.meminfo.MemZram)}',
|
|
899
|
+
to_head=True, attr=curses.A_BOLD, resume=resume)
|
|
900
|
+
resume = True
|
|
901
|
+
leader = ''
|
|
902
|
+
leader += f' eTot:{proj.human_pct(proj.e_max_used)}'
|
|
903
|
+
leader += f' eUsed:{proj.human_pct(proj.e_used)}'
|
|
904
|
+
leader += f' eAvail:{proj.human_pct(proj.e_avail)}'
|
|
905
|
+
# leader += f' Dirty={human(meminfoKB["Dirty"]*1024)}'
|
|
906
|
+
self.emit(leader, to_head=True, resume=resume)
|
|
805
907
|
|
|
806
908
|
# pylint: disable=too-many-branches
|
|
807
909
|
self.loop_num += 1
|
|
808
910
|
meminfoKB = self.get_meminfo()
|
|
809
|
-
|
|
810
|
-
|
|
911
|
+
self.zram_projector.compute_effective(meminfoKB)
|
|
912
|
+
total_user_pids = 0
|
|
913
|
+
total_kernel_pids = 0
|
|
914
|
+
kernel_cpu = total_user_pids
|
|
915
|
+
all_pids = []
|
|
811
916
|
wanted_prcs = {}
|
|
812
917
|
|
|
813
918
|
self.prep_new_loop(regroup)
|
|
@@ -829,9 +934,10 @@ class PmemStat:
|
|
|
829
934
|
for entry in it:
|
|
830
935
|
# if re.match(r'^\d+$', entry.name):
|
|
831
936
|
if entry.name.isdigit():
|
|
832
|
-
|
|
937
|
+
all_pids.append(entry.name)
|
|
833
938
|
|
|
834
|
-
|
|
939
|
+
prcs = []
|
|
940
|
+
for pid in all_pids:
|
|
835
941
|
## print(f'DBDB pid={pid} self.opts.pids={opts.pids}')
|
|
836
942
|
prc = self.prcs.get(pid, None)
|
|
837
943
|
if not prc:
|
|
@@ -839,10 +945,25 @@ class PmemStat:
|
|
|
839
945
|
self.prcs[pid] = prc
|
|
840
946
|
else:
|
|
841
947
|
prc.is_new = False
|
|
948
|
+
prcs.append(prc)
|
|
949
|
+
|
|
950
|
+
# do cpu together that stats are consistent
|
|
951
|
+
if self.opts.cpu:
|
|
952
|
+
SysStat.refresh()
|
|
953
|
+
for prc in prcs:
|
|
954
|
+
if prc.wanted or prc.kernel:
|
|
955
|
+
percent = prc.refresh_cpu()
|
|
956
|
+
if prc.kernel:
|
|
957
|
+
kernel_cpu += percent
|
|
958
|
+
total_kernel_pids += 1
|
|
959
|
+
else:
|
|
960
|
+
total_user_pids += 1
|
|
961
|
+
|
|
962
|
+
for prc in prcs:
|
|
842
963
|
prc.prc_pid()
|
|
964
|
+
pid = prc.pid
|
|
843
965
|
## if str(pid) in opts.pids:
|
|
844
966
|
## print(f'DBDB pid={pid} dir={vars(prc)}')
|
|
845
|
-
total_pids += 0 if prc.whynot == 'KernelProcess' else 1
|
|
846
967
|
|
|
847
968
|
if prc.wanted:
|
|
848
969
|
wanted_prcs[pid] = prc
|
|
@@ -939,6 +1060,8 @@ class PmemStat:
|
|
|
939
1060
|
if not group.alive and group.o_summary and remainder > 0:
|
|
940
1061
|
remainder -= 1
|
|
941
1062
|
self.pr_summary('x', group.o_summary)
|
|
1063
|
+
if not self.window:
|
|
1064
|
+
self.emit('')
|
|
942
1065
|
|
|
943
1066
|
def emit(self, line, to_head=False, attr=None, resume=False):
|
|
944
1067
|
""" Emit a line of the report"""
|
|
@@ -949,7 +1072,7 @@ class PmemStat:
|
|
|
949
1072
|
else:
|
|
950
1073
|
self.window.add_body(line, attr=attr, resume=resume)
|
|
951
1074
|
else:
|
|
952
|
-
print(line)
|
|
1075
|
+
print(('' if resume else '\n') + line, end='')
|
|
953
1076
|
|
|
954
1077
|
def help_screen(self):
|
|
955
1078
|
"""Populate help screen"""
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
# rm -r dist; python3 -m build; ls dist/.
|
|
15
15
|
# python3 -m twine upload dist/*
|
|
16
16
|
# ## Enter __token__ and the saved TOKEN (in bitwarden)
|
|
17
|
-
#
|
|
17
|
+
# pipx upgrade pmemstat || pipx install pmemstat # >= python3.11
|
|
18
|
+
# --OR-- sudo python3 -m pip install pmemstat # <= python3.10
|
|
18
19
|
# ## VISIT https://pypi.org/project/pmemstat and delete old versions
|
|
19
20
|
#
|
|
20
21
|
# TEST Build and test (from project directory):
|
|
@@ -30,7 +31,7 @@ requires = ["hatchling"]
|
|
|
30
31
|
build-backend = "hatchling.build"
|
|
31
32
|
|
|
32
33
|
[project]
|
|
33
|
-
version = "2.3.
|
|
34
|
+
version = "2.3.23"
|
|
34
35
|
name = "pmemstat"
|
|
35
36
|
authors = [
|
|
36
37
|
{name = "Joe Defen", email = "joedef@gmail.com"},
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/pmemstat-2.3.22/images/help-screen_2023-05-15.png → /pmemstat-2.3.23/images/help-screen.png
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|