11# ------------------------------------------------------------------------------
2- # NodeSniff Agent - Lightweight Linux metrics collector
2+ # NodeSniff Agent - top 10 CPU-consuming processes (non-blocking, cached)
33#
44# Author: Sebastian Zieba <sebastian@zieba.art>
55# License: GNU GPL v3 (non-commercial use only)
6- #
7- # This software is licensed under the terms of the GNU General Public License
8- # version 3 (GPLv3) as published by the Free Software Foundation, **for
9- # non-commercial use only**.
10- #
11- # For commercial licensing, please contact the author directly.
6+ # Version: 1.1.1
7+ # Date: 2025-06-27
128# ------------------------------------------------------------------------------
139
1410import psutil
11+ import threading
1512import time
1613
17- def get ():
14+ _cached_processes = []
15+ _last_update = 0
16+ _update_interval = 30 # seconds
17+ _first_run = True # one-time blocking priming
18+
19+ def _update_cache ():
20+ global _cached_processes , _last_update
21+
22+ processes = []
1823 try :
19- # Priming: For accurate CPU usage, call cpu_percent(interval=None) on all processes first.
20- for proc in psutil .process_iter ():
24+ primed = []
25+ for proc in psutil .process_iter ([ 'pid' , 'name' ] ):
2126 try :
2227 proc .cpu_percent (interval = None )
23- except Exception :
24- continue # Ignore processes that no longer exist
28+ primed .append (proc )
29+ except (psutil .NoSuchProcess , psutil .AccessDenied ):
30+ continue
2531
26- # Short delay to allow psutil to calculate CPU percent over an interval
27- time .sleep (0.1 )
32+ time .sleep (1.0 ) # wait to measure actual CPU usage
2833
29- processes = []
30- # Gather process info including CPU percent and memory usage
31- for proc in psutil .process_iter (['pid' , 'name' , 'cpu_percent' , 'memory_info' ]):
34+ for proc in primed :
3235 try :
33- info = proc .info
34- memory_rss = info . get ( 'memory_info' ) .rss if info . get ( 'memory_info' ) else 0
36+ info = proc .as_dict ( attrs = [ 'pid' , 'name' , 'cpu_percent' , 'memory_info' ])
37+ memory_rss = info [ 'memory_info' ] .rss if info [ 'memory_info' ] else 0
3538 processes .append ({
36- "pid" : info ['pid' ], # Process ID
37- "name" : info ['name' ] or "unknown" , # Process name (fallback if None)
38- "cpu" : round (info ['cpu_percent' ], 1 ), # CPU usage in percent
39- "ram" : round (memory_rss / 1024 / 1024 , 1 ) # RAM usage in MB (RSS)
39+ "pid" : info ['pid' ],
40+ "name" : info ['name' ] or "unknown" ,
41+ "cpu" : round (info ['cpu_percent' ], 1 ),
42+ "ram" : round (memory_rss / 1024 / 1024 , 1 ) # MB
4043 })
4144 except (psutil .NoSuchProcess , psutil .AccessDenied ):
42- continue # Skip processes that are gone or inaccessible
45+ continue
46+
47+ _cached_processes = sorted (processes , key = lambda p : (p ['cpu' ], p ['ram' ]), reverse = True )[:10 ]
48+ _last_update = time .time ()
49+
50+ except Exception :
51+ _cached_processes = []
52+ _last_update = time .time ()
53+
54+ def get ():
55+ global _last_update , _first_run
56+
57+ now = time .time ()
4358
44- # Sort processes by CPU usage descending and return top 10
45- top = sorted (processes , key = lambda p : p ['cpu' ], reverse = True )[:10 ]
46- return {"top_cpu_processes" : top }
59+ if _first_run :
60+ _update_cache () # blocking on first call to fill cache
61+ _first_run = False
62+ elif now - _last_update > _update_interval :
63+ threading .Thread (target = _update_cache , daemon = True ).start ()
4764
48- except Exception as e :
49- # If anything goes wrong, return error string in result
50- return {"top_cpu_processes" : f"Error: { str (e )} " }
65+ return {"top_cpu_processes" : _cached_processes }
5166
67+ # Optional debug entry point
68+ if __name__ == "__main__" :
69+ import json
70+ print (json .dumps (get (), indent = 2 ))
0 commit comments