Skip to content

Commit e28836e

Browse files
address_synchronizer: add a cache in front of get_utxos()
get_utxos() is called pretty often, both spuriously, and on focus change, on tab switch, &c. It blocks as it iterates, functionally, /every/ address the wallet knows of. On large wallets (like testnet vpub5VfkVzoT7qgd5gUKjxgGE2oMJU4zKSktusfLx2NaQCTfSeeSY3S723qXKUZZaJzaF6YaF8nwQgbMTWx54Ugkf4NZvSxdzicENHoLJh96EKg from #6625 with 11k TXes and 10.5k addresses), this takes 1.3s of 100%ed CPU usage, basically in a loop from the UI thread. get_utxos() is 50-70% of the flame-graph when sampling a synced wallet process. This data is a function of the block-chain state, and we have hooks that notify us of when the block-chain state changes: we can just cache the result and only re-compute it then. For example, here's a trace log where get_utxos() has print(end - start, len(domain), block_height) and a transaction is clearing: 1.3775344607420266 10540 4507192 0.0010390589013695717 10540 4507192 cached! 0.001393263228237629 10540 4507192 cached! 0.0009001069702208042 10540 4507192 cached! 0.0010241391137242317 10540 4507192 cached! ... 0.00207632128149271 10540 4507192 cached! 0.001397700048983097 10540 4507192 cached! invalidate_cache 1.4686454269103706 10540 4507192 0.0012429207563400269 10540 4507192 cached! 0.0015075239352881908 10540 4507192 cached! 0.0010459059849381447 10540 4507192 cached! 0.0009669591672718525 10540 4507192 cached! ... on_event_blockchain_updated invalidate_cache 1.3897203942760825 10540 4507193 0.0010689008049666882 10540 4507193 cached! 0.0010420521721243858 10540 4507193 cached! ... invalidate_cache 1.408584670163691 10540 4507193 0.001336586195975542 10540 4507193 cached! 0.0009196233004331589 10540 4507193 cached! 0.0009176661260426044 10540 4507193 cached! ... about 30s of low activity. Without this patch, the UI is prone to freezing, running behind, and I wouldn't be surprised if UI thread blocking on Windows ends up crashing to some extent as the issue notes. In the log, this manifests as a much slower but consistent stream of full 1.3-1.4s updates during use, and every time the window is focused.
1 parent fdaafd5 commit e28836e

File tree

1 file changed

+20
-5
lines changed

1 file changed

+20
-5
lines changed

electrum/address_synchronizer.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
# SOFTWARE.
2323

2424
import asyncio
25+
import copy
2526
import threading
2627
import itertools
2728
from collections import defaultdict
@@ -99,9 +100,15 @@ def __init__(self, db: 'WalletDB', config: 'SimpleConfig', *, name: str = None):
99100
self.threadlocal_cache = threading.local()
100101

101102
self._get_balance_cache = {}
103+
self._get_utxos_cache = {}
102104

103105
self.load_and_cleanup()
104106

107+
@with_lock
108+
def invalidate_cache(self):
109+
self._get_balance_cache.clear()
110+
self._get_utxos_cache.clear()
111+
105112
def diagnostic_name(self):
106113
return self.name or ""
107114

@@ -203,7 +210,7 @@ def start_network(self, network: Optional['Network']) -> None:
203210
@event_listener
204211
@with_lock
205212
def on_event_blockchain_updated(self, *args):
206-
self._get_balance_cache = {} # invalidate cache
213+
self.invalidate_cache()
207214
self.db.put('stored_height', self.get_local_height())
208215

209216
async def stop(self):
@@ -335,7 +342,7 @@ def add_value_from_prev_output():
335342
pass
336343
else:
337344
self.db.add_txi_addr(tx_hash, addr, ser, v)
338-
self._get_balance_cache.clear() # invalidate cache
345+
self.invalidate_cache()
339346
for txi in tx.inputs():
340347
if txi.is_coinbase_input():
341348
continue
@@ -353,7 +360,7 @@ def add_value_from_prev_output():
353360
addr = txo.address
354361
if addr and self.is_mine(addr):
355362
self.db.add_txo_addr(tx_hash, addr, n, v, is_coinbase)
356-
self._get_balance_cache.clear() # invalidate cache
363+
self.invalidate_cache()
357364
# give v to txi that spends me
358365
next_tx = self.db.get_spent_outpoint(tx_hash, n)
359366
if next_tx is not None:
@@ -405,7 +412,7 @@ def remove_from_spent_outpoints():
405412
remove_from_spent_outpoints()
406413
self._remove_tx_from_local_history(tx_hash)
407414
for addr in itertools.chain(self.db.get_txi_addresses(tx_hash), self.db.get_txo_addresses(tx_hash)):
408-
self._get_balance_cache.clear() # invalidate cache
415+
self.invalidate_cache()
409416
self.db.remove_txi(tx_hash)
410417
self.db.remove_txo(tx_hash)
411418
self.db.remove_tx_fee(tx_hash)
@@ -503,7 +510,7 @@ def remove_local_transactions_we_dont_have(self):
503510
def clear_history(self):
504511
self.db.clear_history()
505512
self._history_local.clear()
506-
self._get_balance_cache.clear() # invalidate cache
513+
self.invalidate_cache()
507514

508515
@with_lock
509516
def _get_tx_sort_key(self, tx_hash: str) -> Tuple[int, int]:
@@ -970,6 +977,13 @@ def get_utxos(
970977
if excluded_addresses:
971978
domain = set(domain) - set(excluded_addresses)
972979
mempool_height = block_height + 1 # height of next block
980+
cache_key = sha256(
981+
','.join(sorted(domain))
982+
+ f";{mature_only};{confirmed_funding_only};{confirmed_spending_only};{nonlocal_only};{block_height}"
983+
)
984+
cached = self._get_utxos_cache.get(cache_key)
985+
if cached is not None:
986+
return copy.deepcopy(cached)
973987
for addr in domain:
974988
txos = self.get_addr_outputs(addr)
975989
for txo in txos.values():
@@ -987,6 +1001,7 @@ def get_utxos(
9871001
continue
9881002
coins.append(txo)
9891003
continue
1004+
self._get_utxos_cache[cache_key] = copy.deepcopy(coins)
9901005
return coins
9911006

9921007
def is_used(self, address: str) -> bool:

0 commit comments

Comments
 (0)