Leak report: stay quiet when a patcher dies whose entry holds no model

ComfyUI builds a lookup entry per patcher on every model load and drops it when the model is already loaded. Its hand-over still fires when the patcher is freed, and was logged as "a dead patcher died; its parent is gone" although nothing can be lost there.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-11-10 01:29:46 +02:00
co-authored by Claude Fable 5.2
parent 9c415f81ee
commit db1602c6e6
2 changed files with 28 additions and 3 deletions
+3 -1
View File
@@ -130,12 +130,14 @@ def watch_switch(mm) -> bool:
try:
parent = self._parent_model()
real = self.real_model() if callable(self.real_model) else None
if real is None: # a lookup entry ComfyUI built and dropped, or one whose model is freed: nothing to lose
return original(self)
_seen[0] += 1
if parent is None or _seen[0] <= 40: # a hand-over with no parent always shows; the others for the first 40 of the session
depth, p = 0, parent
while p is not None and getattr(p, "parent", None) is not None and depth < 50:
p, depth = p.parent, depth + 1
log.info("FunUI Leak", f"hand-over {_seen[0]}: a {type(real).__name__ if real is not None else 'dead'} patcher died; its parent is "
log.info("FunUI Leak", f"hand-over {_seen[0]}: a {type(real).__name__} patcher died; its parent is "
f"{'gone' if parent is None else type(parent).__name__ + f' with {depth} more above it'}; Python's gc running: {_in_gc[0]}")
except Exception as exc: # noqa: BLE001 -- a diagnostic must never stop a run
log.failed("leak report", exc)
+25 -2
View File
@@ -148,7 +148,7 @@ def test_a_hand_over_with_no_parent_is_logged_after_the_first_forty(monkeypatch)
mm = type("MM", (), {})()
class LM:
def __init__(self): self._parent_model = lambda: None; self.real_model = lambda: None
def __init__(self): self._parent_model = lambda: None; self.real_model = lambda: object()
def _switch_parent(self): pass
mm.LoadedModel = LM
leak_report.watch_switch(mm)
@@ -175,10 +175,33 @@ def test_the_first_forty_hand_overs_with_a_parent_are_logged_and_the_forty_first
mm = type("MM", (), {})()
class LM:
def __init__(self): self._parent_model = lambda: object(); self.real_model = lambda: None
def __init__(self): self._parent_model = lambda: object(); self.real_model = lambda: object()
def _switch_parent(self): pass
mm.LoadedModel = LM
leak_report.watch_switch(mm)
for _ in range(45):
LM()._switch_parent()
assert len(lines) == 40
def test_a_throwaway_entry_that_holds_no_model_is_not_reported(comfyui, capsys):
"""Each time models are loaded ComfyUI builds a fresh entry per patcher to look it up, and drops it when the model is already
loaded. Such an entry holds no model, yet its hand-over still fires when the patcher dies: nothing can be lost there."""
import comfy.model_management as mm
import comfy.model_patcher as mp
original = mm.LoadedModel._switch_parent
try:
leak_report.watch_switch(mm)
base = torch.nn.Linear(2, 2)
base.model_config = type("C", (), {})()
parent = mp.ModelPatcher(base, load_device=torch.device("cpu"), offload_device=torch.device("cpu"))
child = parent.clone()
child.myself = child # a cycle, so only Python's gc can free the pair: it clears the link to the parent first
mm.LoadedModel(child) # the lookup entry, never loaded and never tracked
del parent, child
gc.collect()
assert "hand-over" not in capsys.readouterr().err
finally:
mm.LoadedModel._switch_parent = original
if leak_report._gc_flag in gc.callbacks:
gc.callbacks.remove(leak_report._gc_flag)