2026-01-20 22:27:14 +00:00
|
|
|
import asyncio
|
2025-08-23 15:04:05 +00:00
|
|
|
from collections import defaultdict
|
2025-08-23 15:49:46 +00:00
|
|
|
import datetime
|
|
|
|
|
import os
|
2025-08-23 15:04:05 +00:00
|
|
|
import time
|
2026-01-20 15:37:15 +00:00
|
|
|
from typing import Sequence, Mapping, TypeVar, Optional
|
|
|
|
|
import weakref
|
2025-08-23 15:04:05 +00:00
|
|
|
|
2026-01-20 22:27:14 +00:00
|
|
|
from electrum import util
|
2025-08-23 15:04:05 +00:00
|
|
|
from electrum.util import ThreadJob
|
|
|
|
|
|
|
|
|
|
|
2026-01-20 15:37:15 +00:00
|
|
|
_U = TypeVar('_U')
|
|
|
|
|
|
|
|
|
|
def count_objects_in_memory(mclasses: Sequence[type[_U]]) -> Mapping[type[_U], Sequence[weakref.ref[_U]]]:
|
|
|
|
|
import gc
|
|
|
|
|
gc.collect()
|
|
|
|
|
objmap = defaultdict(list)
|
|
|
|
|
for obj in gc.get_objects():
|
|
|
|
|
for class_ in mclasses:
|
|
|
|
|
try:
|
|
|
|
|
_isinstance = isinstance(obj, class_)
|
|
|
|
|
except AttributeError:
|
|
|
|
|
_isinstance = False
|
|
|
|
|
if _isinstance:
|
|
|
|
|
objmap[class_].append(weakref.ref(obj))
|
|
|
|
|
return objmap
|
|
|
|
|
|
|
|
|
|
|
2025-08-23 15:04:05 +00:00
|
|
|
class DebugMem(ThreadJob):
|
|
|
|
|
'''A handy class for debugging GC memory leaks
|
|
|
|
|
|
2025-08-23 15:49:46 +00:00
|
|
|
In Qt console:
|
2025-08-23 15:04:05 +00:00
|
|
|
>>> 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):
|
|
|
|
|
self.logger.info("Start memscan")
|
2026-01-20 15:37:15 +00:00
|
|
|
objmap = count_objects_in_memory(self.classes)
|
2025-08-23 15:04:05 +00:00
|
|
|
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
|
2025-08-23 15:49:46 +00:00
|
|
|
|
|
|
|
|
|
2026-01-20 22:27:14 +00:00
|
|
|
async def wait_until_obj_is_garbage_collected(wr: weakref.ref) -> None:
|
|
|
|
|
"""Async wait until the object referenced by `wr` is GC-ed."""
|
|
|
|
|
obj = wr()
|
|
|
|
|
if obj is None:
|
|
|
|
|
return
|
|
|
|
|
evt_gc = asyncio.Event() # set when obj is finally GC-ed.
|
|
|
|
|
wr2 = weakref.ref(obj, lambda _x: util.run_sync_function_on_asyncio_thread(evt_gc.set, block=False))
|
|
|
|
|
del obj
|
|
|
|
|
while True:
|
|
|
|
|
try:
|
|
|
|
|
async with util.async_timeout(0.01):
|
|
|
|
|
await evt_gc.wait()
|
|
|
|
|
except asyncio.TimeoutError:
|
|
|
|
|
import gc
|
|
|
|
|
gc.collect()
|
|
|
|
|
else:
|
|
|
|
|
break
|
|
|
|
|
assert evt_gc.is_set()
|
|
|
|
|
|
|
|
|
|
|
2025-08-23 15:49:46 +00:00
|
|
|
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,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-01-20 15:55:11 +00:00
|
|
|
def debug_memusage_dump_random_backref_chain(objtype: str) -> Optional[str]:
|
2025-08-23 15:49:46 +00:00
|
|
|
"""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")
|
2026-01-20 15:55:11 +00:00
|
|
|
objects = objgraph.by_type(objtype)
|
|
|
|
|
if not objects:
|
|
|
|
|
return None
|
|
|
|
|
random_obj = random.choice(objects)
|
2025-08-23 15:49:46 +00:00
|
|
|
with open(fpath, "w") as f:
|
|
|
|
|
objgraph.show_chain(
|
|
|
|
|
objgraph.find_backref_chain(
|
2026-01-20 15:55:11 +00:00
|
|
|
random_obj,
|
2025-08-23 15:49:46 +00:00
|
|
|
objgraph.is_proper_module),
|
|
|
|
|
output=f)
|
|
|
|
|
return fpath
|