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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pmemstat
3
- Version: 2.3.22
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 using `pipx`**:
21
- > * `python3 -m pip install --user pipx # if pipx not installed`
22
- > * `python3 -m pipx ensurepath # if needed (restart terminal)`
23
- > * `pipx upgrade pmemstat || pipx install pmemstat # to install/upgrade`
24
- > * **Else for python3.10 and lesser versions, install using `pip`**:
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
- > * `pmemstat # to run`
28
- > * Type "?" within pmemstat, show help screen.
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
+ ![pmemstat](https://github.com/joedefen/pmemstat/blob/main/images/pmemstat.png?raw=true)
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 all columns to the left except **pswap**, per smaps)
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
- ![helpscreen example](https://github.com/joedefen/pmemstat/blob/main/images/help-screen_2023-05-15.png?raw=true)
148
+ ![helpscreen example](https://github.com/joedefen/pmemstat/blob/main/images/help-screen.png?raw=true)
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
- ![scroll-pos example](https://github.com/joedefen/pmemstat/blob/main/images/scroll-pos_2023-09-05.png?raw=true)
161
+ ![scroll-pos example](https://github.com/joedefen/pmemstat/blob/main/images/scroll-pos.png?raw=true)
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 using `pipx`**:
3
- > * `python3 -m pip install --user pipx # if pipx not installed`
4
- > * `python3 -m pipx ensurepath # if needed (restart terminal)`
5
- > * `pipx upgrade pmemstat || pipx install pmemstat # to install/upgrade`
6
- > * **Else for python3.10 and lesser versions, install using `pip`**:
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
- > * `pmemstat # to run`
10
- > * Type "?" within pmemstat, show help screen.
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
+ ![pmemstat](https://github.com/joedefen/pmemstat/blob/main/images/pmemstat.png?raw=true)
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 all columns to the left except **pswap**, per smaps)
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
- ![helpscreen example](https://github.com/joedefen/pmemstat/blob/main/images/help-screen_2023-05-15.png?raw=true)
130
+ ![helpscreen example](https://github.com/joedefen/pmemstat/blob/main/images/help-screen.png?raw=true)
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
- ![scroll-pos example](https://github.com/joedefen/pmemstat/blob/main/images/scroll-pos_2023-09-05.png?raw=true)
143
+ ![scroll-pos example](https://github.com/joedefen/pmemstat/blob/main/images/scroll-pos.png?raw=true)
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 python
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
- except:
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.whynot = None # populate me with why unwanted
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 = init_cpu()
188
- try:
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.whynot = 'CannotGetCmdline'
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.whynot = 'FilteredByArgs'
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.whynot = f'CannotReadLines({type(exc).__name__})'
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.whynot = f'CannotReadLines({type(exc).__name__})'
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.whynot = 'CannotReadRollups'
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.whynot = 'CannotReadSmaps'
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.whynot and not self.cmdline:
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.whynot:
567
+ if not self.why_not:
496
568
  rollup_lines = self.get_rollup_lines()
497
- if self.whynot:
498
- DB(4, f'pid={self.pid} {self.exebasename} whynot={self.whynot}')
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, total_pids
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 not self.window:
783
- leader = f'\n---- {now.strftime("%H:%M:%S")}'
784
- else:
785
- leader = f'{now.strftime("%H:%M:%S")}'
786
- self.emit(leader, to_head=True,
787
- attr=curses.A_BOLD if self.loop_num % 2 else None)
788
- leader = ''
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
- leader += f' Dirty={human(meminfoKB["Dirty"]*1024)}'
798
- leader += f' PIDs: {len(wanted_prcs)}/{total_pids} {read_smaps}'
799
- self.emit(leader, to_head=True, resume=bool(self.window))
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=True)
881
+ self.emit(leader +' /', to_head=True, resume=resume)
882
+ resume = True
802
883
  self.emit(self.opts.search, to_head=True,
803
- resume=True, attr=curses.A_UNDERLINE)
804
- self.emit('/', to_head=True, resume=True)
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
- total_pids = 0
810
- allpids = []
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
- allpids.append(entry.name)
937
+ all_pids.append(entry.name)
833
938
 
834
- for pid in allpids:
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
- # sudo python3 -m pip install pmemstat
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.22"
34
+ version = "2.3.23"
34
35
  name = "pmemstat"
35
36
  authors = [
36
37
  {name = "Joe Defen", email = "joedef@gmail.com"},
File without changes
File without changes
File without changes
File without changes
File without changes