perf: periodic full GC to bound RSS (plexapi cyclic Element trees) (#802)
measured the 'resource hungry' / lockup issue: browsing every page grows RSS ~300MB -> 1.8GB and it stays. it's not a leak — it's deferred cyclic collection. plexapi parses Plex responses into XML Element trees whose nodes reference each other in cycles; Python's generational GC leaves them in gen2 and sweeps it rarely, so ~227k Element objects pile up. forcing gc.collect() reclaimed ~700MB instantly (1.8GB -> 1.1GB live), confirming. add a daemon that runs a full gc.collect() every 60s so the cyclic garbage is reclaimed on a cadence instead of climbing into lock-up. full collect is ~tens of ms; once a minute is negligible. this is the root of the reporter's 2GB + ramonskie's spike too.
This commit is contained in:
parent
de9e78cf95
commit
f383a62c4f
1 changed files with 20 additions and 0 deletions
|
|
@ -38549,6 +38549,26 @@ 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
|
||||||
|
# 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
|
||||||
|
# (measured: ~300MB fresh -> 1.8GB after browsing every page, ~700MB of it reclaimable
|
||||||
|
# cyclic garbage that a full gc.collect() frees instantly). A scheduled full collect
|
||||||
|
# reclaims them on a cadence so RSS stays bounded instead of climbing into lock-up.
|
||||||
|
# A full collect on this heap is ~tens of ms; once a minute is negligible CPU.
|
||||||
|
def _periodic_gc(interval=60):
|
||||||
|
import gc
|
||||||
|
while True:
|
||||||
|
time.sleep(interval)
|
||||||
|
try:
|
||||||
|
n = gc.collect()
|
||||||
|
if n:
|
||||||
|
logger.debug("periodic gc.collect() reclaimed %d cyclic objects", n)
|
||||||
|
except Exception: # noqa: S110 — best-effort housekeeping, never crash the app
|
||||||
|
pass
|
||||||
|
threading.Thread(target=_periodic_gc, daemon=True, name='periodic-gc').start()
|
||||||
|
logger.info("Periodic GC sweeper started (full collect every 60s)")
|
||||||
|
|
||||||
# Register action handlers and start automation engine
|
# Register action handlers and start automation engine
|
||||||
_register_automation_handlers()
|
_register_automation_handlers()
|
||||||
if automation_engine:
|
if automation_engine:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue