CW-1024 Improve performance of xmr wallet (#2128)

* - enabled development options in CI builds.
- Implemented caching for account retrieval.
- refactor transaction handling in `dashboard_view_model.dart` to improve efficiency and reduce unnecessary updates in xmr.
- `DevMoneroCallProfilerPage`, for profiling performance of xmr,wow,zano wallet calls.

* use FeatureFlag.hasDevOptions

* prevent crashes in monero_c by using mutexes properly
improve performance of _transactionDisposer
remove unnecessary checks

* remove logging, bring back simplified logic

* update _transactionDisposer on length and confirmation of first and last transaction

* address comments from review

* don't throw unhandled exceptions in unawaited async code

* use cached transaction list in getAllSubaddresses, fix usage of txHistoryMutex

* [DNM] fix: crashes when opening wallet, performance issue when syncing and update dependencies

* Revert "use cached transaction list in getAllSubaddresses, fix usage of txHistoryMutex"

This reverts commit 4c4c33ac6a47603e970a6c8d940e90204525b241.

* Revert "[DNM] fix: crashes when opening wallet, performance issue when syncing and update dependencies"

This reverts commit d7603445ad6ae76d76bf179c34728ce242c8c610.

* Revert "use cached transaction list in getAllSubaddresses, fix usage of txHistoryMutex"

This reverts commit 4c4c33ac6a47603e970a6c8d940e90204525b241.

* update shared_preferences

* improve state management performance by not rendering multiple changes in transaction screen on a single frame

* fix wallet switching
This commit is contained in:
cyan 2025-04-03 03:31:25 +02:00 committed by GitHub
parent 27eaa1b1cc
commit cbca4c9c77
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 498 additions and 105 deletions

View file

@ -1,21 +1,42 @@
import 'dart:ffi';
import 'dart:isolate';
import 'package:cw_monero/api/account_list.dart';
import 'package:monero/monero.dart' as monero;
import 'package:mutex/mutex.dart';
monero.Coins? coins = null;
final coinsMutex = Mutex();
void refreshCoins(int accountIndex) {
Future<void> refreshCoins(int accountIndex) async {
if (coinsMutex.isLocked) {
return;
}
coins = monero.Wallet_coins(wptr!);
monero.Coins_refresh(coins!);
final coinsPtr = coins!.address;
await coinsMutex.acquire();
await Isolate.run(() => monero.Coins_refresh(Pointer.fromAddress(coinsPtr)));
coinsMutex.release();
}
int countOfCoins() => monero.Coins_count(coins!);
Future<int> countOfCoins() async {
await coinsMutex.acquire();
final count = monero.Coins_count(coins!);
coinsMutex.release();
return count;
}
monero.CoinsInfo getCoin(int index) => monero.Coins_coin(coins!, index);
Future<monero.CoinsInfo> getCoin(int index) async {
await coinsMutex.acquire();
final coin = monero.Coins_coin(coins!, index);
coinsMutex.release();
return coin;
}
int? getCoinByKeyImage(String keyImage) {
final count = countOfCoins();
Future<int?> getCoinByKeyImage(String keyImage) async {
final count = await countOfCoins();
for (int i = 0; i < count; i++) {
final coin = getCoin(i);
final coin = await getCoin(i);
final coinAddress = monero.CoinsInfo_keyImage(coin);
if (keyImage == coinAddress) {
return i;
@ -24,6 +45,16 @@ int? getCoinByKeyImage(String keyImage) {
return null;
}
void freezeCoin(int index) => monero.Coins_setFrozen(coins!, index: index);
Future<void> freezeCoin(int index) async {
await coinsMutex.acquire();
final coinsPtr = coins!.address;
await Isolate.run(() => monero.Coins_setFrozen(Pointer.fromAddress(coinsPtr), index: index));
coinsMutex.release();
}
void thawCoin(int index) => monero.Coins_thaw(coins!, index: index);
Future<void> thawCoin(int index) async {
await coinsMutex.acquire();
final coinsPtr = coins!.address;
await Isolate.run(() => monero.Coins_thaw(Pointer.fromAddress(coinsPtr), index: index));
coinsMutex.release();
}

View file

@ -1,6 +1,7 @@
import 'dart:ffi';
import 'dart:isolate';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_monero/api/account_list.dart';
import 'package:cw_monero/api/exceptions/creation_transaction_exception.dart';
import 'package:cw_monero/api/monero_output.dart';
@ -13,15 +14,23 @@ import 'package:monero/src/generated_bindings_monero.g.dart' as monero_gen;
import 'package:mutex/mutex.dart';
Map<int, Map<String, String>> txKeys = {};
String getTxKey(String txId) {
txKeys[wptr!.address] ??= {};
if (txKeys[wptr!.address]![txId] != null) {
return txKeys[wptr!.address]![txId]!;
}
final txKey = monero.Wallet_getTxKey(wptr!, txid: txId);
final status = monero.Wallet_status(wptr!);
if (status != 0) {
final error = monero.Wallet_errorString(wptr!);
monero.Wallet_errorString(wptr!);
txKeys[wptr!.address]![txId] = "";
return "";
}
txKeys[wptr!.address]![txId] = txKey;
return txKey;
}
final txHistoryMutex = Mutex();
monero.TransactionHistory? txhistory;
bool isRefreshingTx = false;
@ -34,6 +43,7 @@ Future<void> refreshTransactions() async {
await Isolate.run(() {
monero.TransactionHistory_refresh(Pointer.fromAddress(ptr));
});
await Future.delayed(Duration.zero);
txHistoryMutex.release();
isRefreshingTx = false;
}
@ -45,8 +55,24 @@ Future<List<Transaction>> getAllTransactions() async {
await txHistoryMutex.acquire();
txhistory ??= monero.Wallet_history(wptr!);
final startAddress = txhistory!.address * wptr!.address;
int size = countOfTransactions();
final list = List.generate(size, (index) => Transaction(txInfo: monero.TransactionHistory_transaction(txhistory!, index: index)));
final list = <Transaction>[];
for (int index = 0; index < size; index++) {
if (index % 25 == 0) {
// Give main thread a chance to do other things.
await Future.delayed(Duration.zero);
}
if (txhistory!.address * wptr!.address != startAddress) {
printV("Loop broken because txhistory!.address * wptr!.address != startAddress");
break;
}
final txInfo = monero.TransactionHistory_transaction(txhistory!, index: index);
final txHash = monero.TransactionInfo_hash(txInfo);
txCache[wptr!.address] ??= {};
txCache[wptr!.address]![txHash] = Transaction(txInfo: txInfo);
list.add(txCache[wptr!.address]![txHash]!);
}
txHistoryMutex.release();
final accts = monero.Wallet_numSubaddressAccounts(wptr!);
for (var i = 0; i < accts; i++) {
@ -79,8 +105,18 @@ Future<List<Transaction>> getAllTransactions() async {
return list;
}
Transaction getTransaction(String txId) {
return Transaction(txInfo: monero.TransactionHistory_transactionById(txhistory!, txid: txId));
Map<int, Map<String, Transaction>> txCache = {};
Future<Transaction> getTransaction(String txId) async {
if (txCache[wptr!.address] != null && txCache[wptr!.address]![txId] != null) {
return txCache[wptr!.address]![txId]!;
}
await txHistoryMutex.acquire();
final tx = monero.TransactionHistory_transactionById(txhistory!, txid: txId);
final txDart = Transaction(txInfo: tx);
txCache[wptr!.address] ??= {};
txCache[wptr!.address]![txId] = txDart;
txHistoryMutex.release();
return txDart;
}
Future<PendingTransactionDescription> createTransactionSync(

View file

@ -6,6 +6,7 @@ import 'package:cw_core/root_dir.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_monero/api/account_list.dart';
import 'package:cw_monero/api/exceptions/setup_wallet_exception.dart';
import 'package:cw_monero/api/wallet_manager.dart';
import 'package:flutter/foundation.dart';
import 'package:monero/monero.dart' as monero;
import 'package:mutex/mutex.dart';
@ -199,12 +200,15 @@ void startRefreshSync() {
}
void setRefreshFromBlockHeight({required int height}) =>
monero.Wallet_setRefreshFromBlockHeight(wptr!,
refresh_from_block_height: height);
void setRefreshFromBlockHeight({required int height}) {
monero.Wallet_setRefreshFromBlockHeight(wptr!,
refresh_from_block_height: height);
}
void setRecoveringFromSeed({required bool isRecovery}) =>
monero.Wallet_setRecoveringFromSeed(wptr!, recoveringFromSeed: isRecovery);
void setRecoveringFromSeed({required bool isRecovery}) {
monero.Wallet_setRecoveringFromSeed(wptr!, recoveringFromSeed: isRecovery);
monero.Wallet_store(wptr!);
}
final storeMutex = Mutex();
@ -394,4 +398,6 @@ String signMessage(String message, {String address = ""}) {
bool verifyMessage(String message, String address, String signature) {
return monero.Wallet_verifySignedMessage(wptr!, message: message, address: address, signature: signature);
}
}
Map<String, List<int>> debugCallLength() => monero.debugCallLength;

View file

@ -137,6 +137,7 @@ void restoreWalletFromSeedSync(
wptr = newWptr;
setRefreshFromBlockHeight(height: restoreHeight);
setupBackgroundSync(password, newWptr);
monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.passphrase", value: passphrase);

View file

@ -1,5 +1,6 @@
import 'package:cw_core/monero_amount_format.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_monero/api/wallet_manager.dart';
import 'package:mobx/mobx.dart';
import 'package:cw_core/account.dart';
import 'package:cw_monero/api/account_list.dart' as account_list;
@ -44,7 +45,18 @@ abstract class MoneroAccountListBase with Store {
}
}
List<Account> getAll() => account_list.getAllAccount().map((accountRow) {
Map<int, List<Account>> _cachedAccounts = {};
List<Account> getAll() {
final allAccounts = account_list.getAllAccount();
final currentCount = allAccounts.length;
_cachedAccounts[account_list.wptr!.address] ??= [];
if (_cachedAccounts[account_list.wptr!.address]!.length == currentCount) {
return _cachedAccounts[account_list.wptr!.address]!;
}
_cachedAccounts[account_list.wptr!.address] = allAccounts.map((accountRow) {
final balance = monero.SubaddressAccountRow_getUnlockedBalance(accountRow);
return Account(
@ -53,6 +65,9 @@ abstract class MoneroAccountListBase with Store {
balance: moneroAmountToString(amount: monero.Wallet_amountFromString(balance)),
);
}).toList();
return _cachedAccounts[account_list.wptr!.address]!;
}
Future<void> addAccount({required String label}) async {
await account_list.addAccount(label: label);

View file

@ -7,28 +7,33 @@ class MoneroUnspent extends Unspent {
MoneroUnspent(
String address, String hash, String keyImage, int value, bool isFrozen, this.isUnlocked)
: super(address, hash, value, 0, keyImage) {
getCoinByKeyImage(keyImage).then((coinId) {
if (coinId == null) return;
getCoin(coinId).then((coin) {
_frozen = monero.CoinsInfo_frozen(coin);
});
});
}
bool _frozen = false;
@override
set isFrozen(bool freeze) {
printV("set isFrozen: $freeze ($keyImage): $freeze");
final coinId = getCoinByKeyImage(keyImage!);
if (coinId == null) throw Exception("Unable to find a coin for address $address");
if (freeze) {
freezeCoin(coinId);
} else {
thawCoin(coinId);
}
getCoinByKeyImage(keyImage!).then((coinId) async {
if (coinId == null) return;
if (freeze) {
await freezeCoin(coinId);
_frozen = true;
} else {
await thawCoin(coinId);
_frozen = false;
}
});
}
@override
bool get isFrozen {
printV("get isFrozen");
final coinId = getCoinByKeyImage(keyImage!);
if (coinId == null) throw Exception("Unable to find a coin for address $address");
final coin = getCoin(coinId);
return monero.CoinsInfo_frozen(coin);
}
bool get isFrozen => _frozen;
final bool isUnlocked;
}

View file

@ -169,6 +169,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
if (monero_wallet.getCurrentHeight() <= 1) {
monero_wallet.setRefreshFromBlockHeight(
height: walletInfo.restoreHeight);
setupBackgroundSync(password, wptr!);
}
}
@ -570,6 +571,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
walletInfo.restoreHeight = height;
walletInfo.isRecovery = true;
monero_wallet.setRefreshFromBlockHeight(height: height);
setupBackgroundSync(password, wptr!);
monero_wallet.rescanBlockchainAsync();
await startSync();
_askForUpdateBalance();
@ -585,9 +587,9 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
unspentCoins.clear();
final coinCount = countOfCoins();
final coinCount = await countOfCoins();
for (var i = 0; i < coinCount; i++) {
final coin = getCoin(i);
final coin = await getCoin(i);
final coinSpent = monero.CoinsInfo_spent(coin);
if (coinSpent == false && monero.CoinsInfo_subaddrAccount(coin) == walletAddresses.account!.id) {
final unspent = MoneroUnspent(
@ -600,7 +602,8 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
);
// TODO: double-check the logic here
if (unspent.hash.isNotEmpty) {
unspent.isChange = transaction_history.getTransaction(unspent.hash).isSpend == true;
final tx = await transaction_history.getTransaction(unspent.hash);
unspent.isChange = tx.isSpend == true;
}
unspentCoins.add(unspent);
}
@ -692,14 +695,15 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
@override
Future<Map<String, MoneroTransactionInfo>> fetchTransactions() async {
transaction_history.refreshTransactions();
return (await _getAllTransactionsOfAccount(walletAddresses.account?.id))
await transaction_history.refreshTransactions();
final resp = (await _getAllTransactionsOfAccount(walletAddresses.account?.id))
.fold<Map<String, MoneroTransactionInfo>>(
<String, MoneroTransactionInfo>{},
(Map<String, MoneroTransactionInfo> acc, MoneroTransactionInfo tx) {
acc[tx.id] = tx;
return acc;
});
return resp;
}
Future<void> updateTransactions() async {
@ -710,8 +714,17 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
_isTransactionUpdating = true;
final transactions = await fetchTransactions();
transactionHistory.clear();
transactionHistory.addMany(transactions);
final currentIds = transactionHistory.transactions.keys.toSet();
final newIds = transactions.keys.toSet();
// Remove transactions that no longer exist
currentIds.difference(newIds).forEach((id) =>
transactionHistory.transactions.remove(id));
// Add or update transactions
transactions.forEach((key, tx) =>
transactionHistory.transactions[key] = tx);
await transactionHistory.save();
_isTransactionUpdating = false;
} catch (e) {
@ -778,6 +791,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
monero_wallet.setRecoveringFromSeed(isRecovery: true);
monero_wallet.setRefreshFromBlockHeight(height: height);
setupBackgroundSync(password, wptr!);
}
int _getHeightDistance(DateTime date) {

View file

@ -159,19 +159,12 @@ class MoneroWalletService extends WalletService<
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfoSource,
password: password);
final isValid = wallet.walletAddresses.validate();
if (wallet.isHardwareWallet) {
wallet.setLedgerConnection(gLedger!);
gLedger = null;
}
if (!isValid) {
await restoreOrResetWalletFiles(name);
wallet.close(shouldCleanup: false);
return openWallet(name, password);
}
await wallet.init();
return wallet;