pmemstat 3.0.2__tar.gz → 3.0.4__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
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.1
2
2
  Name: pmemstat
3
- Version: 3.0.2
3
+ Version: 3.0.4
4
4
  Summary: Report Proportional Memory on Linux
5
5
  Keywords: proportional memory,RAM,htop,top,monitor
6
6
  Author-email: Joe Defen <joedef@duck.com>
@@ -9,9 +9,8 @@ Description-Content-Type: text/markdown
9
9
  Classifier: Programming Language :: Python :: 3
10
10
  Classifier: License :: OSI Approved :: MIT License
11
11
  Classifier: Operating System :: POSIX :: Linux
12
- License-File: LICENSE.txt
13
12
  Requires-Dist: psutil > 5.9
14
- Requires-Dist: console-window >= 1.0.0
13
+ Requires-Dist: console-window >= 1.0.5
15
14
  Project-URL: Bug Tracker, https://github.com/joedefen/pmemstat/issues
16
15
  Project-URL: Homepage, https://github.com/joedefen/pmemstat
17
16
 
@@ -94,6 +94,92 @@ def human(number):
94
94
  return f'{number:.1f}{suffix}'
95
95
  return '' # impossible, but make pylint happy
96
96
 
97
+ ####################################################################################
98
+ # PORTABLE FUNCTION - Copy-pasted from zram-advisor
99
+ # When updating: copy entire function from zram_advisor/main.py (lines 86-166)
100
+ ####################################################################################
101
+ def compute_zram_effective(meminfo_total, meminfo_used, meminfo_available,
102
+ zram_orig_data_size, zram_mem_used_total,
103
+ zram_disksize, zram_mem_limit=0, limit_pct=80):
104
+ """
105
+ Compute effective memory values accounting for zRAM compression.
106
+
107
+ This is a PORTABLE function that can be copy-pasted between projects.
108
+ NO class dependencies - all inputs passed as parameters.
109
+
110
+ Args:
111
+ meminfo_total: Total physical RAM in bytes
112
+ meminfo_used: Currently used RAM in bytes
113
+ meminfo_available: Available RAM in bytes
114
+ zram_orig_data_size: Uncompressed data size stored in zRAM (bytes)
115
+ zram_mem_used_total: Physical RAM consumed by zRAM including overhead (bytes)
116
+ zram_disksize: Max uncompressed data zRAM will accept (bytes)
117
+ zram_mem_limit: Max RAM zRAM can use (0=unlimited) (bytes)
118
+ limit_pct: Arbitrary limit on % of RAM zRAM should use (default 80%)
119
+
120
+ Returns:
121
+ SimpleNamespace with:
122
+ - e_used: Effective memory used (uncompressed equivalent)
123
+ - e_avail: Effective memory available
124
+ - e_max_used: Maximum effective memory at full zRAM capacity
125
+ - ratio_current: Current effective compression ratio
126
+ - ratio_projected: Projected ratio accounting for degradation
127
+ - projection_confidence: 'low', 'medium', or 'high'
128
+ - usage_fraction: Fraction of disksize currently used
129
+ """
130
+ # Calculate effective used memory (what it would be if uncompressed)
131
+ e_used = meminfo_used - zram_mem_used_total + zram_orig_data_size
132
+
133
+ # Determine compression ratio
134
+ ratio = None
135
+ if e_used <= meminfo_used: # zRAM not helping yet
136
+ e_used = meminfo_used
137
+ ratio = 2.75 # Conservative guess for projection when no data yet
138
+
139
+ if ratio is None: # Calculate actual ratio
140
+ ratio = zram_orig_data_size / zram_mem_used_total if zram_mem_used_total > 0 else 2.75
141
+
142
+ # Apply memory limit if configured
143
+ if zram_mem_limit > 0:
144
+ stats_limit_pct = (zram_mem_limit / meminfo_total) * 100
145
+ limit_pct = min(100.0, limit_pct, stats_limit_pct)
146
+
147
+ # Calculate usage fraction and apply degradation
148
+ usage_fraction = zram_orig_data_size / zram_disksize if zram_disksize > 0 else 0
149
+
150
+ # Degradation logic based on current usage
151
+ if usage_fraction < 0.10:
152
+ degradation_factor = 0.75 # Aggressive: little real data
153
+ confidence = "low"
154
+ elif usage_fraction < 0.50:
155
+ degradation_factor = 0.85 # Moderate: some real data
156
+ confidence = "medium"
157
+ else:
158
+ degradation_factor = 0.95 # Conservative: lots of real data
159
+ confidence = "high"
160
+
161
+ projected_ratio = ratio * degradation_factor
162
+
163
+ # Project maximum effective memory when zRAM fills to limit
164
+ e_max_used = projected_ratio * meminfo_total * limit_pct / 100
165
+ # Cap by disksize limit
166
+ e_max_used = min(e_max_used, zram_disksize)
167
+ # Add uncompressed memory not in zRAM
168
+ e_max_used += meminfo_total - e_max_used / projected_ratio
169
+
170
+ # Calculate effective available
171
+ e_avail = e_max_used - e_used
172
+
173
+ return SimpleNamespace(
174
+ e_used=e_used,
175
+ e_avail=e_avail,
176
+ e_max_used=e_max_used,
177
+ ratio_current=ratio,
178
+ ratio_projected=projected_ratio,
179
+ projection_confidence=confidence,
180
+ usage_fraction=usage_fraction
181
+ )
182
+
97
183
  ####################################################################################
98
184
  ###### ZramProjector class
99
185
  ####################################################################################
@@ -156,7 +242,7 @@ class ZramProjector:
156
242
  return infos
157
243
 
158
244
  def compute_effective(self, meminfoKB):
159
- """ Compute the effective values """
245
+ """ Compute the effective values - now uses portable function """
160
246
  def seed(meminfoKB):
161
247
  nonlocal self
162
248
  meminfo = self.meminfo = SimpleNamespace()
@@ -175,6 +261,8 @@ class ZramProjector:
175
261
  devs = self._get_zram_stats()
176
262
  if not devs:
177
263
  return
264
+
265
+ # Aggregate stats from all zRAM devices
178
266
  stats = None
179
267
  for stat in devs.values():
180
268
  # NOTE: generalization for more than one weak unless all
@@ -187,29 +275,25 @@ class ZramProjector:
187
275
  stats = SimpleNamespace(**stats)
188
276
  self.meminfo.MemZram = stats.mem_used_total
189
277
 
190
- e_used = meminfo.MemUsed - stats.mem_used_total + stats.orig_data_size
191
- ratio, self.ratio = None, 0.0
192
- if e_used <= self.e_used: # zram hurts. assume not enuf data to project
193
- e_used = self.e_used
194
- self.ratio = ratio = 3.5 # wild guess for projection
195
- # if self.DB: dump_effective(ns, '(unchg)')
196
- # return ns
197
- self.e_used = e_used
198
- limit_pct = self.limit_pct
199
- if stats.mem_limit > 0:
200
- stats_limit_pct = (stats.limit / meminfo.MemTotal)*100
201
- limit_pct = min(1.0, limit_pct, stats_limit_pct)
202
- # projected amount of orig used memory that fits in limit
203
- if ratio is None: # hopefully about 4 ;-)
204
- self.ratio = ratio = stats.orig_data_size / stats.mem_used_total
205
- e_max_used = ratio * meminfo.MemTotal * limit_pct/100
206
- # now limit by disksize
207
- e_max_used = min(e_max_used, stats.disksize)
208
- # now add uncompressed memory
209
- e_max_used += meminfo.MemTotal - e_max_used/ratio
210
- self.e_max_used = e_max_used
211
- # self.e_max_used = max(e_max_total, e_max_used)
212
- self.e_avail = e_max_used - e_used
278
+ # Call the portable computation function
279
+ result = compute_zram_effective(
280
+ meminfo_total=meminfo.MemTotal,
281
+ meminfo_used=meminfo.MemUsed,
282
+ meminfo_available=meminfo.MemAvailable,
283
+ zram_orig_data_size=stats.orig_data_size,
284
+ zram_mem_used_total=stats.mem_used_total,
285
+ zram_disksize=stats.disksize,
286
+ zram_mem_limit=stats.mem_limit,
287
+ limit_pct=self.limit_pct
288
+ )
289
+
290
+ # Update instance variables from result
291
+ self.e_used = result.e_used
292
+ self.e_avail = result.e_avail
293
+ self.e_max_used = result.e_max_used
294
+ self.ratio = result.ratio_current
295
+ self.ratio_projected = result.ratio_projected
296
+ self.projection_confidence = result.projection_confidence
213
297
 
214
298
 
215
299
  ####################################################################################
@@ -246,7 +330,7 @@ class ProcMem:
246
330
  # units = '' # determined by arguments
247
331
  # fwidth = 11
248
332
  pmemstat = None # the main program object
249
- max_cmd_len = 32 # command line maximum length
333
+ max_cmd_len = 96 # command line maximum length
250
334
  chunk_dict = {
251
335
  'cat': None,
252
336
  'beg': 0,
@@ -286,58 +370,137 @@ class ProcMem:
286
370
  self.cpu = CpuSmooth(self.pid, avg_secs= ProcMem.opts.cpu_avg_secs)
287
371
  return self.cpu.refresh_cpu() # sets self.cpu.percent
288
372
 
373
+ def _get_exebasename(self, exepath, wds):
374
+ """
375
+ Final, definitive, robust executable base name resolver.
376
+ """
377
+
378
+ # --- Phase 1: Determine the Initial Best Name Candidate ---
379
+ # ... (same initial logic as before, using os.readlink)
380
+ basename = os.path.basename(exepath)
381
+ is_bad_name = not basename or basename == 'exe' or len(basename) > 16 or not re.search(r'[a-zA-Z]', basename)
382
+
383
+ if is_bad_name:
384
+ try:
385
+ link_target = os.readlink(f'/proc/{self.pid}/exe')
386
+ if ' (deleted)' not in link_target and 'a.out' not in link_target:
387
+ basename = os.path.basename(link_target)
388
+ except (FileNotFoundError, OSError):
389
+ pass
390
+
391
+ # --- Phase 2: Multi-Step Aggressive Cleanup (The FINAL Final Fix) ---
392
+
393
+ # 1. Remove VQ... style hashes and generic suffixes
394
+ hash_and_bin_pattern = r'(VQ[A-Z0-9]{10,}|-bin|\.bin)'
395
+ basename = re.sub(hash_and_bin_pattern, '', basename).strip()
396
+
397
+ # 2. **CRITICAL FIX:** Remove all tab/PID/browser-related arguments from the tail.
398
+ # This addresses both 'browser 14 tab' AND any simple trailing numbers (like 'browser 4')
399
+ tab_and_num_pattern = r'(?:\s+\d+)?\s+(tab.*|socket.*|rdd.*|process.*|\d+)$'
400
+ basename = re.sub(tab_and_num_pattern, '', basename, flags=re.IGNORECASE).strip()
401
+
402
+ # 3. Remove common internal browser-role suffixes (renderer, rdd, content, etc.)
403
+ roles_pattern = r'-(renderer|gpu|utility|plugin|content|chrm|bsp|extension)$'
404
+ basename = re.sub(roles_pattern, '', basename, flags=re.IGNORECASE).strip()
405
+
406
+ # Final safety cleanup for any leading/trailing punctuation/spaces leftover
407
+ basename = re.sub(r'^\W+|\W+$', '', basename).strip()
408
+
409
+ # --- Phase 3: Last Resort Fallback ---
410
+
411
+ if not basename or basename in ('exe', 'a.out'):
412
+ try:
413
+ basename = os.path.basename(wds[0])
414
+ except IndexError:
415
+ basename = 'exe'
416
+
417
+ # Special Case Protection: Restore I3 if it was reduced
418
+ if basename == 'i':
419
+ basename = 'i3'
420
+
421
+ return basename
422
+
289
423
  def get_cmdline(self):
290
424
  """Get the command line of the PID."""
291
- try:
292
425
 
426
+ try:
293
427
  cmdline_file = f'/proc/{self.pid}/cmdline'
294
428
  try:
295
429
  # pylint: disable=consider-using-with
296
430
  line = open(cmdline_file, encoding='utf-8').read()[:-1]
297
431
  except FileNotFoundError as exc:
298
- # this seems to be a race which ignore; either the process is just
299
- # started or just quickly ended before even identified
300
432
  if DebugLevel:
301
433
  DB(1, f'skip pid={self.pid} no-rollup-lines exc={type(exc).__name__}')
302
434
  return
435
+
303
436
  arguments = line.split('\0')
304
437
  if not arguments or not arguments[0]: # kernel process
305
438
  self.wanted, self.kernel = False, True
306
- # print(f'{self.pid}: kernel thread')
307
439
  return
308
- # DB(0, f'{self.pid}: {arguments}')
309
- # sometimes the first word
310
- wds = os.path.basename(arguments[0]).split() + arguments[1:]
311
- basename = re.sub(r'^\W+', '', wds.pop(0))
312
- basename = re.sub(r'\W+$', '', basename)
313
- # DB(0, f'basename={basename} wds={wds}')
314
- if basename in ('python', 'python2', 'python3', 'perl', 'bash', 'ruby',
440
+
441
+ exepath = arguments[0]
442
+
443
+ # Prepare wds (words/arguments) for use in the helper and cmdline rebuild
444
+ base_name_list = os.path.basename(exepath).split()
445
+ if base_name_list:
446
+ initial_basename = base_name_list.pop(0)
447
+ wds = base_name_list + arguments[1:]
448
+ else:
449
+ initial_basename = ''
450
+ wds = arguments[1:]
451
+
452
+ # Use the robust helper to determine the final, clean basename
453
+ self.exebasename = self._get_exebasename(exepath, wds)
454
+
455
+ # --- START ELABORATION LOGIC ---
456
+
457
+ # 1. Elaboration: The Sudo Wrapper
458
+ if self.exebasename == 'sudo' and wds:
459
+ actual_cmd_name = os.path.basename(wds[0])
460
+ if actual_cmd_name:
461
+ self.exebasename = actual_cmd_name
462
+ del wds[0]
463
+
464
+ # 2. Elaboration: The Interpreter (Python, Perl, etc.)
465
+ if self.exebasename in ('python', 'python2', 'python3', 'perl', 'bash', 'ruby',
315
466
  'sh', 'ksh', 'zsh') and wds:
316
- script = os.path.basename(wds[0])
317
- # DB(0, f'script={script} wds[0]={wds[0]}')
318
- if script != wds[0]:
319
- basename = f'{basename}->{script}'
467
+
468
+ # 2.a. Catch the 'python -m <module>' pattern (The New Fix)
469
+ if len(wds) >= 2 and wds[0] == '-m':
470
+ module_name = wds[1]
471
+ self.exebasename = f'{self.exebasename}->{module_name}'
472
+ # Remove '-m' and the module name from arguments
473
+ del wds[0]
320
474
  del wds[0]
321
- self.exebasename = basename
322
- self.cmdline = ' '.join([basename] + wds)
475
+
476
+ # 2.b. Catch the standard '<script.py>' pattern (Original Logic)
477
+ elif wds[0] and os.path.exists(wds[0]) and os.path.isfile(wds[0]):
478
+ script = os.path.basename(wds[0])
479
+ if script != wds[0]:
480
+ # Only elaborate if it's a clear script name, not a generic main
481
+ if not re.search(r'^__main__\.', script):
482
+ self.exebasename = f'{self.exebasename}->{script}'
483
+ del wds[0] # Remove the script path from arguments
484
+
485
+ # --- END ELABORATION LOGIC ---
486
+
487
+ self.cmdline = ' '.join([self.exebasename] + wds)
323
488
  self.cmdline_trunc = self.cmdline[0:ProcMem.max_cmd_len]
324
- # DB(0, f'basename={basename} cmdline_trunc={self.cmdline}')
489
+
325
490
  except Exception as exc:
326
- # not really expecting this ... probably a bug
491
+ # ... (rest of exception handling is the same)
327
492
  print(f' WARNING: skip pid={self.pid} no-basename exc={exc}')
328
493
  print(traceback.format_exc())
329
494
  self.wanted = self.kernel = False
330
495
  self.why_not = 'CannotGetCmdline'
331
496
  return
332
497
 
333
- ## print(f'DBDB: {self.pid} {ProcMem.opts.pids}')
334
498
  # filter unwanted before too much work
335
499
  if (ProcMem.opts.pids and str(self.pid) not in ProcMem.opts.pids
336
500
  and self.exebasename not in ProcMem.opts.pids):
337
501
  self.wanted = False
338
502
  self.why_not = 'FilteredByArgs'
339
- # print(f' >>>> unwanted {pid}')
340
- ## print(f'DBDB: {self.pid} wanted={self.wanted} why_not={self.why_not}')
503
+
341
504
  self.set_key()
342
505
 
343
506
  def set_key(self):
@@ -1235,7 +1398,12 @@ def rerun_module_as_root(module_name):
1235
1398
  """ rerun using the module name """
1236
1399
  if os.geteuid() != 0: # Re-run the script with sudo
1237
1400
  os.chdir(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
1238
- vp = ['sudo', sys.executable, '-m', module_name] + sys.argv[1:]
1401
+ # Preserve PYTHONPATH to allow sudo to find user-installed packages
1402
+ pythonpath = os.pathsep.join(sys.path)
1403
+ env_vars = os.environ.copy()
1404
+ env_vars['PYTHONPATH'] = pythonpath
1405
+ # Use env to pass PYTHONPATH through sudo
1406
+ vp = ['sudo', 'env', f'PYTHONPATH={pythonpath}', sys.executable, '-m', module_name] + sys.argv[1:]
1239
1407
  os.execvp('sudo', vp)
1240
1408
 
1241
1409
  def main():
@@ -4,7 +4,7 @@ build-backend = "flit_core.buildapi"
4
4
 
5
5
  [project]
6
6
  name = "pmemstat"
7
- version = "3.0.2"
7
+ version = "3.0.4"
8
8
  description = "Report Proportional Memory on Linux"
9
9
  authors = [
10
10
  {name = "Joe Defen", email = "joedef@duck.com"},
@@ -20,7 +20,7 @@ classifiers = [
20
20
  ]
21
21
  dependencies = [
22
22
  'psutil > 5.9',
23
- 'console-window >= 1.0.0',
23
+ 'console-window >= 1.0.5',
24
24
  ]
25
25
 
26
26
  [project.urls]
pmemstat-3.0.4/runner ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env python3
2
+ # runner.py
3
+ # Located in the top-level <svr pmemstat> directory
4
+
5
+ import os
6
+ import sys
7
+
8
+ # Add the project directory to PYTHONPATH for editable install support
9
+ project_dir = os.path.dirname(os.path.abspath(__file__))
10
+ if project_dir not in sys.path:
11
+ sys.path.insert(0, project_dir)
12
+
13
+ # Import and run the main module directly
14
+ if __name__ == '__main__':
15
+ sys.argv[0] = 'pmemstat'
16
+ from pmemstat import main
17
+ main.run()
pmemstat-3.0.2/run.sh DELETED
@@ -1,9 +0,0 @@
1
- #!/bin/bash
2
-
3
- SRC="$(dirname "$0")/src"
4
-
5
- set -x
6
- ##### choose one:
7
-
8
- PYTHONPATH="$SRC" python3 -m pmemstat.main "$@"
9
- # PYTHONPATH="$SRC" python3 src/pmemstat/main.py "$@"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes