From c777c0164e622651cdd2d9aaf92cdd59232e0433 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 23 Aug 2025 15:04:05 +0000 Subject: [PATCH 1/2] util: move DebugMem from util to utils/memory_leak.py --- electrum/util.py | 29 ------------------------- electrum/utils/memory_leak.py | 41 +++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 29 deletions(-) create mode 100644 electrum/utils/memory_leak.py diff --git a/electrum/util.py b/electrum/util.py index 0bad5f510..93f75466f 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -350,35 +350,6 @@ class ThreadJob(Logger): """Called periodically from the thread""" pass -class DebugMem(ThreadJob): - '''A handy class for debugging GC memory leaks''' - def __init__(self, classes, interval=30): - ThreadJob.__init__(self) - self.next_time = 0 - self.classes = classes - self.interval = interval - - def mem_stats(self): - import gc - self.logger.info("Start memscan") - gc.collect() - objmap = defaultdict(list) - for obj in gc.get_objects(): - for class_ in self.classes: - try: - _isinstance = isinstance(obj, class_) - except AttributeError: - _isinstance = False - if _isinstance: - objmap[class_].append(obj) - for class_, objs in objmap.items(): - self.logger.info(f"{class_.__name__}: {len(objs)}") - self.logger.info("Finish memscan") - - def run(self): - if time.time() > self.next_time: - self.mem_stats() - self.next_time = time.time() + self.interval class DaemonThread(threading.Thread, Logger): """ daemon thread that terminates cleanly """ diff --git a/electrum/utils/memory_leak.py b/electrum/utils/memory_leak.py new file mode 100644 index 000000000..8b276d52c --- /dev/null +++ b/electrum/utils/memory_leak.py @@ -0,0 +1,41 @@ +from collections import defaultdict +import time + +from electrum.util import ThreadJob + + +class DebugMem(ThreadJob): + '''A handy class for debugging GC memory leaks + + In console: + >>> from electrum.utils.memory_leak import DebugMem + >>> from electrum.wallet import Abstract_Wallet + >>> plugins.add_jobs([DebugMem([Abstract_Wallet,], interval=5)]) + ''' + def __init__(self, classes, interval=30): + ThreadJob.__init__(self) + self.next_time = 0 + self.classes = classes + self.interval = interval + + def mem_stats(self): + import gc + self.logger.info("Start memscan") + gc.collect() + objmap = defaultdict(list) + for obj in gc.get_objects(): + for class_ in self.classes: + try: + _isinstance = isinstance(obj, class_) + except AttributeError: + _isinstance = False + if _isinstance: + objmap[class_].append(obj) + for class_, objs in objmap.items(): + self.logger.info(f"{class_.__name__}: {len(objs)}") + self.logger.info("Finish memscan") + + def run(self): + if time.time() > self.next_time: + self.mem_stats() + self.next_time = time.time() + self.interval From 41269b9c88dd67a876d635525ebb8a59a52489de Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 23 Aug 2025 15:49:46 +0000 Subject: [PATCH 2/2] utils/memory_leak: add helpers using 3rd-party package "objgraph" we will not start depending on "objgraph", to be clear --- electrum/utils/memory_leak.py | 38 ++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/electrum/utils/memory_leak.py b/electrum/utils/memory_leak.py index 8b276d52c..0d3b463bd 100644 --- a/electrum/utils/memory_leak.py +++ b/electrum/utils/memory_leak.py @@ -1,4 +1,6 @@ from collections import defaultdict +import datetime +import os import time from electrum.util import ThreadJob @@ -7,7 +9,7 @@ from electrum.util import ThreadJob class DebugMem(ThreadJob): '''A handy class for debugging GC memory leaks - In console: + In Qt console: >>> from electrum.utils.memory_leak import DebugMem >>> from electrum.wallet import Abstract_Wallet >>> plugins.add_jobs([DebugMem([Abstract_Wallet,], interval=5)]) @@ -39,3 +41,37 @@ class DebugMem(ThreadJob): if time.time() > self.next_time: self.mem_stats() self.next_time = time.time() + self.interval + + +def debug_memusage_list_all_objects(limit: int = 50) -> list[tuple[str, int]]: + """Return a string listing the most common types in memory.""" + import objgraph # 3rd-party dependency + return objgraph.most_common_types( + limit=limit, + shortnames=False, + ) + + +def debug_memusage_dump_random_backref_chain(objtype: str) -> str: + """Writes a dotfile to cwd, containing the backref chain + for a randomly selected object of type objtype. + + Warning: very slow! + + In Qt console: + >>> debug_memusage_dump_random_backref_chain("Standard_Wallet") + + To convert to image: + $ dot -Tps filename.dot -o outfile.ps + """ + import objgraph # 3rd-party dependency + import random + timestamp = datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%dT%H%M%SZ") + fpath = os.path.abspath(f"electrum_backref_chain_{timestamp}.dot") + with open(fpath, "w") as f: + objgraph.show_chain( + objgraph.find_backref_chain( + random.choice(objgraph.by_type(objtype)), + objgraph.is_proper_module), + output=f) + return fpath