Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
File size: 4,483 Bytes
13170f7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | """Periodic memory reporting for the worker pool.
Logs system-wide and per-process RSS once per hour so that operators can
spot slow memory growth without attaching a profiler.
"""
import multiprocessing as mp
import threading
import time
from typing import Protocol
from log_utils import setup_logger
logger = setup_logger("MemoryMonitor")
_DAILY = 60 * 60 * 24
_HOURLY = 60 * 60
class _HasWorkerInfo(Protocol):
"""Minimal interface a worker object must expose for memory reporting."""
worker_id: int
status: str
process: mp.Process
def start_memory_reporter(
get_workers: callable,
lock: threading.Condition,
shutdown_flag: callable,
interval_s: float = _DAILY,
max_ram_gb: float = 0,
) -> threading.Thread:
"""Launch a daemon thread that logs memory usage periodically.
Args:
get_workers: Callable returning a list of worker-like objects
(each must have ``worker_id``, ``status``, and ``process``).
lock: Lock to hold while reading the worker list.
shutdown_flag: Callable returning ``True`` when the pool is
shutting down (stops the reporter loop).
interval_s: Seconds between reports (default: daily).
max_ram_gb: Cap on available RAM (GB) shown in reports.
0 means no cap.
Returns:
The started daemon thread.
"""
thread = threading.Thread(
target=_reporter_loop,
args=(get_workers, lock, shutdown_flag, interval_s, max_ram_gb),
daemon=True,
)
thread.start()
return thread
def _reporter_loop(
get_workers: callable,
lock: threading.Condition,
shutdown_flag: callable,
interval_s: float,
max_ram_gb: float,
) -> None:
_POLL_S = 5.0
while not shutdown_flag():
elapsed = 0.0
while elapsed < interval_s and not shutdown_flag():
time.sleep(min(_POLL_S, interval_s - elapsed))
elapsed += _POLL_S
if shutdown_flag():
break
try:
_log_report(get_workers, lock, interval_s, max_ram_gb)
except Exception as exc:
logger.debug(f"Memory report failed: {exc}")
def _log_report(
get_workers: callable,
lock: threading.Condition,
interval_s: float,
max_ram_gb: float,
) -> None:
"""Collect and log memory stats for the main process and each worker."""
try:
import psutil
except ImportError:
return
vm = psutil.virtual_memory()
main_proc = psutil.Process()
main_rss = main_proc.memory_info().rss / (1024 * 1024)
available_gb = vm.available / (1024**3)
if max_ram_gb > 0:
available_gb = min(available_gb, max_ram_gb)
if interval_s == _DAILY:
label = "Daily report"
elif interval_s == _HOURLY:
label = "Hourly report"
elif interval_s >= _DAILY:
label = f"Periodic report (every {interval_s / _DAILY:.0f}d)"
elif interval_s >= _HOURLY:
label = f"Periodic report (every {interval_s / _HOURLY:.0f}h)"
elif interval_s >= 60:
label = f"Periodic report (every {interval_s / 60:.0f}m)"
else:
label = f"Periodic report (every {interval_s:.0f}s)"
lines = [
f"{label} — "
f"system: {vm.used / (1024**3):.1f}/{vm.total / (1024**3):.1f} GB "
f"({vm.percent}% used), available={available_gb:.1f} GB"
f"{f' (capped from {vm.available / (1024**3):.1f} GB)' if max_ram_gb > 0 and vm.available / (1024**3) > max_ram_gb else ''}"
]
lines.append(f" main process (pid={main_proc.pid}): RSS={main_rss:.0f} MB")
worker_total = 0.0
with lock:
workers = list(get_workers())
for w in workers:
if w.status == "dead" or not w.process.is_alive():
lines.append(f" worker {w.worker_id} (pid={w.process.pid}): dead")
continue
try:
w_rss = psutil.Process(w.process.pid).memory_info().rss / (1024 * 1024)
worker_total += w_rss
lines.append(
f" worker {w.worker_id} (pid={w.process.pid}): "
f"RSS={w_rss:.0f} MB, status={w.status}"
)
except (psutil.NoSuchProcess, psutil.AccessDenied):
lines.append(f" worker {w.worker_id} (pid={w.process.pid}): " f"not accessible")
lines.append(
f" total: main={main_rss:.0f} MB + "
f"workers={worker_total:.0f} MB = {main_rss + worker_total:.0f} MB"
)
logger.info("\n".join(lines))
|