perf: growth-triggered GC instead of fixed timer — caps the peak (#802)
the 60s timer overshot: browsing piled plexapi cyclic garbage faster than once-a-minute caught it, so RSS hit ~2.2GB before a sweep (then dropped to 1.2GB). switch to polling RSS cheaply (every 8s) and collecting as soon as it grows +250MB since the last sweep — so it fires DURING a heavy browse and caps the peak near floor+250MB instead of running to 2GB+. keeps a 120s backstop for slow idle accumulation.
This commit is contained in:
parent
f383a62c4f
commit
b4583f23be
1 changed files with 27 additions and 13 deletions
|
|
@ -38549,25 +38549,39 @@ def start_runtime_services():
|
||||||
# Initialize app start time for uptime tracking
|
# Initialize app start time for uptime tracking
|
||||||
app.start_time = time.time()
|
app.start_time = time.time()
|
||||||
|
|
||||||
# Periodic full garbage collection (#802 / resource usage). plexapi builds large XML
|
# Growth-triggered garbage collection (#802 / resource usage). plexapi builds large XML
|
||||||
# Element trees whose nodes reference each other in cycles; Python's generational GC
|
# Element trees whose nodes reference each other in cycles; Python's generational GC
|
||||||
# parks those in gen2 and only sweeps it rarely, so they accumulate and RSS climbs
|
# parks those in gen2 and sweeps it rarely, so they accumulate and RSS climbs (measured:
|
||||||
# (measured: ~300MB fresh -> 1.8GB after browsing every page, ~700MB of it reclaimable
|
# ~300MB fresh -> 1.8-2.2GB after browsing every page, most of it reclaimable cyclic
|
||||||
# cyclic garbage that a full gc.collect() frees instantly). A scheduled full collect
|
# garbage a full gc.collect() frees instantly). A FIXED timer overshoots — browsing piles
|
||||||
# reclaims them on a cadence so RSS stays bounded instead of climbing into lock-up.
|
# garbage faster than once-a-minute catches it. Instead, poll RSS cheaply and collect as
|
||||||
# A full collect on this heap is ~tens of ms; once a minute is negligible CPU.
|
# soon as it has grown GROW_MB since the last sweep, so it kicks in DURING a heavy browse
|
||||||
def _periodic_gc(interval=60):
|
# and caps the peak near (floor + GROW_MB) instead of letting it run to 2GB+. A backstop
|
||||||
|
# interval still collects slow accumulation when idle.
|
||||||
|
def _growth_triggered_gc():
|
||||||
import gc
|
import gc
|
||||||
|
CHECK_SECONDS = 8 # cheap RSS poll cadence
|
||||||
|
GROW_MB = 250 # collect once RSS climbs this much since the last sweep
|
||||||
|
BACKSTOP_SECONDS = 120 # ...or at least this often regardless
|
||||||
|
try:
|
||||||
|
import psutil
|
||||||
|
proc = psutil.Process(os.getpid())
|
||||||
|
rss_mb = lambda: proc.memory_info().rss / (1024 * 1024)
|
||||||
|
except Exception:
|
||||||
|
rss_mb = lambda: 0.0
|
||||||
|
floor = rss_mb()
|
||||||
|
last = time.time()
|
||||||
while True:
|
while True:
|
||||||
time.sleep(interval)
|
time.sleep(CHECK_SECONDS)
|
||||||
try:
|
try:
|
||||||
n = gc.collect()
|
if (rss_mb() - floor) >= GROW_MB or (time.time() - last) >= BACKSTOP_SECONDS:
|
||||||
if n:
|
gc.collect()
|
||||||
logger.debug("periodic gc.collect() reclaimed %d cyclic objects", n)
|
floor = rss_mb()
|
||||||
|
last = time.time()
|
||||||
except Exception: # noqa: S110 — best-effort housekeeping, never crash the app
|
except Exception: # noqa: S110 — best-effort housekeeping, never crash the app
|
||||||
pass
|
pass
|
||||||
threading.Thread(target=_periodic_gc, daemon=True, name='periodic-gc').start()
|
threading.Thread(target=_growth_triggered_gc, daemon=True, name='gc-sweeper').start()
|
||||||
logger.info("Periodic GC sweeper started (full collect every 60s)")
|
logger.info("GC sweeper started (collect on +250MB growth, backstop 120s)")
|
||||||
|
|
||||||
# Register action handlers and start automation engine
|
# Register action handlers and start automation engine
|
||||||
_register_automation_handlers()
|
_register_automation_handlers()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue