Created
July 8, 2026 00:27
-
-
Save microlinux/db22cf903771da79540cc958efafed89 to your computer and use it in GitHub Desktop.
Program to snapshot system resource usage and process information. Originally written for analysis of ODX (https://github.com/WebODM/ODX) performance.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/env python3 | |
| """ | |
| peeper.py | |
| Program to snapshot system resource usage and process information. Originally | |
| written for analysis of ODX (https://github.com/WebODM/ODX) performance. | |
| Output format (JSON): | |
| { | |
| "time": 1783055343.2371664, | |
| "cpu_total_used": 64.7, | |
| "cpu_cores": { | |
| "0": { | |
| "used": 30.6, | |
| "freq": 4547.17 | |
| }, | |
| "1": { | |
| "used": 11.1, | |
| "freq": 4547.17 | |
| }, | |
| "2": { | |
| "used": 58.0, | |
| "freq": 4547.18 | |
| }, | |
| "3": { | |
| "used": 26.0, | |
| "freq": 4547.16 | |
| }, | |
| "4": { | |
| "used": 46.5, | |
| "freq": 550.0 | |
| }, | |
| "5": { | |
| "used": 76.0, | |
| "freq": 550.0 | |
| }, | |
| "6": { | |
| "used": 89.9, | |
| "freq": 550.0 | |
| }, | |
| "7": { | |
| "used": 100.0, | |
| "freq": 4547.18 | |
| }, | |
| "8": { | |
| "used": 67.7, | |
| "freq": 550.0 | |
| }, | |
| "9": { | |
| "used": 96.0, | |
| "freq": 4547.18 | |
| }, | |
| "10": { | |
| "used": 21.0, | |
| "freq": 4547.17 | |
| }, | |
| "11": { | |
| "used": 2.0, | |
| "freq": 550.0 | |
| }, | |
| "12": { | |
| "used": 100.0, | |
| "freq": 4547.17 | |
| }, | |
| "13": { | |
| "used": 73.0, | |
| "freq": 550.0 | |
| }, | |
| "14": { | |
| "used": 46.5, | |
| "freq": 4547.17 | |
| }, | |
| "15": { | |
| "used": 81.2, | |
| "freq": 550.0 | |
| }, | |
| "16": { | |
| "used": 69.3, | |
| "freq": 550.0 | |
| }, | |
| "17": { | |
| "used": 88.0, | |
| "freq": 550.0 | |
| }, | |
| "18": { | |
| "used": 39.0, | |
| "freq": 550.0 | |
| }, | |
| "19": { | |
| "used": 72.3, | |
| "freq": 550.0 | |
| }, | |
| "20": { | |
| "used": 98.0, | |
| "freq": 4547.16 | |
| }, | |
| "21": { | |
| "used": 100.0, | |
| "freq": 4547.17 | |
| }, | |
| "22": { | |
| "used": 100.0, | |
| "freq": 4547.17 | |
| }, | |
| "23": { | |
| "used": 100.0, | |
| "freq": 4547.16 | |
| }, | |
| "24": { | |
| "used": 100.0, | |
| "freq": 4547.17 | |
| }, | |
| "25": { | |
| "used": 21.0, | |
| "freq": 4547.16 | |
| }, | |
| "26": { | |
| "used": 78.0, | |
| "freq": 550.0 | |
| }, | |
| "27": { | |
| "used": 100.0, | |
| "freq": 4547.18 | |
| }, | |
| "28": { | |
| "used": 0.0, | |
| "freq": 550.0 | |
| }, | |
| "29": { | |
| "used": 26.0, | |
| "freq": 4547.16 | |
| }, | |
| "30": { | |
| "used": 54.0, | |
| "freq": 550.0 | |
| }, | |
| "31": { | |
| "used": 100.0, | |
| "freq": 4547.17 | |
| } | |
| }, | |
| "ram_total": 134987984896, | |
| "ram_used": 23532359680, | |
| "ram_avail": 111455625216, | |
| "swap_total": 255997239296, | |
| "swap_used": 0, | |
| "swap_avail": 255997239296, | |
| "disk_total": 3934595964928, | |
| "disk_used": 97899986944, | |
| "disk_avail": 3636752879616, | |
| "gpu_cpu_used": 0.0, | |
| "gpu_ram_total": 6291456000, | |
| "gpu_ram_used": 1024000, | |
| "gpu_ram_avail": 5881856000, | |
| "procs": { | |
| "184507": { | |
| "cmd": "renderdem /var/www/data/10e824de-3d92-4cb2-95f9-a6af17a1b6e5/odm_georeferencing/odm_georeferenced_model.laz --outdir /var/www/data/10e824de- 3d92-4cb2-95f9-a6af17a1b6e5/odm_dem --output-type max --radiuses 0.02,0.02828427 1247461905,0.04000000000000001 --resolution 0.02 --max-tiles 0 --decimation 1 -- classification -1 --tile-size 4096 --force", | |
| "ram_used": 15243636736, | |
| "cpu_used": 47.47 | |
| }, | |
| "184592": { | |
| "cmd": "/home/trm/peeper/bin/python3 /home/trm/peeper/peeper.py -d /home/trm/peeper/data.log -e /home/trm/peeper/error.log", | |
| "ram_used": 16252928, | |
| "cpu_used": 0.03 | |
| } | |
| } | |
| } | |
| """ | |
| from argparse import ArgumentParser | |
| from datetime import datetime | |
| from json import dumps, loads | |
| from pathlib import Path | |
| from psutil import AccessDenied, NoSuchProcess, ZombieProcess, cpu_count, cpu_freq, cpu_percent, process_iter, swap_memory, virtual_memory | |
| from shutil import disk_usage | |
| from subprocess import run | |
| from sys import stderr | |
| from time import sleep, time | |
| from traceback import format_exc | |
| def parse_args(): | |
| """Parse command line arguments. | |
| Returns: | |
| argparse.Namespace | |
| """ | |
| parser = ArgumentParser(description='Prints system resource usage and process info as a JSON encoded dictionary to stdout. Processes with zero CPU or RAM usage are not captured.') | |
| parser.add_argument('-f', action='store_true', help='Format stdout for a human') | |
| parser.add_argument('-d', metavar='FILEPATH', type=str, help='Also write data to a file') | |
| parser.add_argument('-e', metavar='FILEPATH', type=str, help='Also write errors to a file') | |
| parser.add_argument('-m', metavar='MOUNTPOINT', default='/', help='Mount point of file system to check (default: /)') | |
| parser.add_argument('-s', metavar='SECONDS', type=int, default=1, help='Time period for CPU measurement in seconds (default: 1)') | |
| return parser.parse_args() | |
| def get_cpu_info(sleep_time=1): | |
| """Get usage (%) and frequency (MHz) of each CPU core. | |
| Args: | |
| sleep_time (float|int): Time period for CPU measurement in seconds. | |
| Returns: | |
| Dictionary containing total CPU usage and another dictionary where CPU core numbers are indexes. Each index is a dictionary containing usage and frequency. | |
| """ | |
| used = cpu_percent(interval=sleep_time, percpu=True) | |
| freq = cpu_freq(percpu=True) | |
| cores = {} | |
| total = 0 | |
| for i in range(len(used)): | |
| cores[i] = {'used': round(used[i], 2), 'freq': round(freq[i].current, 2)} | |
| total = total + cores[i]['used'] | |
| total = round(total / len(cores), 1) | |
| return {'cpu_total_used': total, 'cpu_cores': cores} | |
| def get_disk_info(mount_point='/'): | |
| """Get total, used and available disk space (bytes) for a file system. | |
| Args: | |
| mount_point (string): Mount point of the file system to check. | |
| Returns: | |
| Dictionary | |
| """ | |
| total, used, avail = disk_usage(mount_point) | |
| return {'disk_total': total, 'disk_used': used, 'disk_avail': avail} | |
| def get_gpu_info(): | |
| """Get total, used and available GPU RAM (bytes) and CPU usage (%). Values will be None if the underlying shell command fails. | |
| Returns: | |
| Dictionary | |
| """ | |
| try: | |
| result = run(['nvidia-smi', '--query-gpu', 'memory.used,memory.free,memory.total,clocks.gr,clocks.sm,utilization.gpu'], capture_output=True, text=True) | |
| gpu_ram_used, _, gpu_ram_avail, _, gpu_ram_total, _, _, _, _, _, gpu_cpu_used, _ = result.stdout.strip().splitlines()[1].split() | |
| return {'gpu_cpu_used': float(gpu_cpu_used), 'gpu_ram_total': int(gpu_ram_total) * 1024000, 'gpu_ram_used': int(gpu_ram_used) * 1024000, 'gpu_ram_avail': int(gpu_ram_avail) * 1024000} | |
| except: | |
| return {'gpu_cpu_used': None, 'gpu_ram_total': None, 'gpu_ram_used': None, 'gpu_ram_avail': None} | |
| def get_mem_info(): | |
| """Get total, used and available system RAM and swap (bytes). | |
| Returns: | |
| Dictionary | |
| """ | |
| ram = virtual_memory() | |
| swap = swap_memory() | |
| return {'ram_total': ram.total, 'ram_used': ram.used, 'ram_avail': ram.available, 'swap_total': swap.total, 'swap_used': swap.used, 'swap_avail': swap.free} | |
| def get_procs(sleep_time=1): | |
| """Get the PID, command, RAM (bytes) and CPU used (%) for processes. Processes using zero RAM, zero CPU or those that die during data collection are not captured. | |
| Args: | |
| sleep_time (float|int): Time period for CPU measurement in seconds. | |
| Returns: | |
| Dictionary where PIDs are indexes. | |
| """ | |
| procs = {} | |
| dead_procs = [] | |
| for proc in process_iter(attrs=['pid', 'name', 'memory_info']): | |
| try: | |
| cmd = ' '.join(proc.cmdline()).strip() | |
| ram = proc.info['memory_info'].rss | |
| proc.cpu_percent(interval=None) | |
| except (AccessDenied, NoSuchProcess, ZombieProcess): | |
| continue | |
| procs[proc.info['pid']] = {'cmd': cmd, 'ram_used': ram, 'obj': proc} | |
| sleep(sleep_time) | |
| for pid, info in procs.items(): | |
| try: | |
| procs[pid]['cpu_used'] = round(procs[pid]['obj'].cpu_percent(interval=None) / cpu_count(), 2) | |
| except (AccessDenied, NoSuchProcess, ZombieProcess): | |
| dead_procs.append(pid) | |
| for pid in dead_procs: | |
| del procs[pid] | |
| for pid in procs: | |
| del procs[pid]['obj'] | |
| return procs | |
| """Begin main program""" | |
| try: | |
| args = parse_args() | |
| cpu_info = get_cpu_info(args.s) | |
| mem_info = get_mem_info() | |
| disk_info = get_disk_info(args.m) | |
| gpu_info = get_gpu_info() | |
| procs = get_procs() | |
| discard_procs = [] | |
| for pid in procs: | |
| if procs[pid]['cpu_used'] == 0.0 or procs[pid]['ram_used'] == 0: | |
| discard_procs.append(pid) | |
| for pid in discard_procs: | |
| del procs[pid] | |
| json_string = dumps({'time': time()} | cpu_info | mem_info | disk_info | gpu_info | {'procs': procs}) | |
| if args.f: | |
| print(dumps(loads(json_string), indent=4)) | |
| else: | |
| print(json_string) | |
| if args.d: | |
| with open(args.d, 'a+') as fh: | |
| fh.write(f'{json_string}\n') | |
| except Exception as e: | |
| stderr.write('The execution of this program resulted in abject failure, for the vague reason stated below.\n\n') | |
| stderr.write(format_exc()) | |
| if args.e: | |
| with open(args.e, 'a+') as fh: | |
| fh.write(f'{datetime.fromtimestamp(time()).strftime("%Y-%m-%d %H:%M:%S")} {format_exc()}\n') | |
| exit(1) | |
| exit(0) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment