mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2025-04-11 15:01:54 +00:00
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 commit4c4c33ac6a
. * Revert "[DNM] fix: crashes when opening wallet, performance issue when syncing and update dependencies" This reverts commitd7603445ad
. * Revert "use cached transaction list in getAllSubaddresses, fix usage of txHistoryMutex" This reverts commit4c4c33ac6a
. * 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:
parent
27eaa1b1cc
commit
cbca4c9c77
25 changed files with 498 additions and 105 deletions
.github/workflows
cw_monero/lib
cw_wownero/lib/api
cw_zano/lib
devtools_options.yamllib
pubspec_base.yamltool
2
.github/workflows/pr_test_build_android.yml
vendored
2
.github/workflows/pr_test_build_android.yml
vendored
|
@ -274,7 +274,7 @@ jobs:
|
|||
|
||||
- name: Build
|
||||
run: |
|
||||
flutter build apk --release --split-per-abi
|
||||
flutter build apk --dart-define=hasDevOptions=true --release --split-per-abi
|
||||
|
||||
- name: Rename apk file
|
||||
run: |
|
||||
|
|
2
.github/workflows/pr_test_build_linux.yml
vendored
2
.github/workflows/pr_test_build_linux.yml
vendored
|
@ -225,7 +225,7 @@ jobs:
|
|||
|
||||
- name: Build linux
|
||||
run: |
|
||||
flutter build linux --release
|
||||
flutter build linux --dart-define=hasDevOptions=true --release
|
||||
|
||||
- name: Compress release
|
||||
run: |
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -137,6 +137,7 @@ void restoreWalletFromSeedSync(
|
|||
wptr = newWptr;
|
||||
|
||||
setRefreshFromBlockHeight(height: restoreHeight);
|
||||
setupBackgroundSync(password, newWptr);
|
||||
|
||||
monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.passphrase", value: passphrase);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -354,3 +354,5 @@ String signMessage(String message, {String address = ""}) {
|
|||
bool verifyMessage(String message, String address, String signature) {
|
||||
return wownero.Wallet_verifySignedMessage(wptr!, message: message, address: address, signature: signature);
|
||||
}
|
||||
|
||||
Map<String, List<int>> debugCallLength() => wownero.debugCallLength;
|
|
@ -508,4 +508,6 @@ Future<String> _closeWallet(int hWallet) async {
|
|||
});
|
||||
printV("Closing wallet: $str");
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, List<int>> debugCallLength() => zano.debugCallLength;
|
|
@ -1 +0,0 @@
|
|||
extensions:
|
|
@ -35,6 +35,7 @@ import 'package:cake_wallet/entities/hardware_wallet/require_hardware_wallet_con
|
|||
import 'package:cake_wallet/entities/parse_address_from_domain.dart';
|
||||
import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart';
|
||||
import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart';
|
||||
import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart';
|
||||
import 'package:cake_wallet/src/screens/settings/background_sync_page.dart';
|
||||
import 'package:cake_wallet/view_model/dev/monero_background_sync.dart';
|
||||
import 'package:cake_wallet/view_model/link_view_model.dart';
|
||||
|
@ -1450,5 +1451,6 @@ Future<void> setup({
|
|||
getIt.registerFactory(() => SeedVerificationPage(getIt.get<WalletSeedViewModel>()));
|
||||
|
||||
getIt.registerFactory(() => DevMoneroBackgroundSyncPage(getIt.get<DevMoneroBackgroundSync>()));
|
||||
getIt.registerFactory(() => DevMoneroCallProfilerPage());
|
||||
_isSetupFinished = true;
|
||||
}
|
||||
|
|
|
@ -424,4 +424,10 @@ class CWMonero extends Monero {
|
|||
bool isViewOnly() {
|
||||
return isViewOnlyBySpendKey(null);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, List<int>> debugCallLength() {
|
||||
return monero_wallet_api.debugCallLength();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@ import 'package:cake_wallet/src/screens/dashboard/pages/nft_details_page.dart';
|
|||
import 'package:cake_wallet/src/screens/dashboard/pages/transactions_page.dart';
|
||||
import 'package:cake_wallet/src/screens/dashboard/sign_page.dart';
|
||||
import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart';
|
||||
import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart';
|
||||
import 'package:cake_wallet/src/screens/disclaimer/disclaimer_page.dart';
|
||||
import 'package:cake_wallet/src/screens/exchange/exchange_page.dart';
|
||||
import 'package:cake_wallet/src/screens/exchange/exchange_template_page.dart';
|
||||
|
@ -841,6 +842,11 @@ Route<dynamic> createRoute(RouteSettings settings) {
|
|||
builder: (_) => getIt.get<DevMoneroBackgroundSyncPage>(),
|
||||
);
|
||||
|
||||
case Routes.devMoneroCallProfiler:
|
||||
return MaterialPageRoute<void>(
|
||||
builder: (_) => getIt.get<DevMoneroCallProfilerPage>(),
|
||||
);
|
||||
|
||||
default:
|
||||
return MaterialPageRoute<void>(
|
||||
builder: (_) => Scaffold(
|
||||
|
|
|
@ -112,7 +112,7 @@ class Routes {
|
|||
static const torPage = '/tor_page';
|
||||
static const backgroundSync = '/background_sync';
|
||||
static const devMoneroBackgroundSync = '/dev/monero_background_sync';
|
||||
|
||||
static const devMoneroCallProfiler = '/dev/monero_call_profiler';
|
||||
static const signPage = '/sign_page';
|
||||
static const connectDevices = '/device/connect';
|
||||
static const urqrAnimatedPage = '/urqr/animated_page';
|
||||
|
|
253
lib/src/screens/dev/moneroc_call_profiler.dart
Normal file
253
lib/src/screens/dev/moneroc_call_profiler.dart
Normal file
|
@ -0,0 +1,253 @@
|
|||
// code shamelessly stolen from xmruw
|
||||
// https://raw.githubusercontent.com/MrCyjaneK/unnamed_monero_wallet/refs/heads/master-rewrite/lib/pages/debug/performance.dart
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:cake_wallet/di.dart';
|
||||
import 'package:cake_wallet/monero/monero.dart';
|
||||
import 'package:cake_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:cake_wallet/wownero/wownero.dart';
|
||||
import 'package:cake_wallet/zano/zano.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:cake_wallet/src/screens/base_page.dart';
|
||||
|
||||
class DevMoneroCallProfilerPage extends BasePage {
|
||||
DevMoneroCallProfilerPage();
|
||||
|
||||
@override
|
||||
String? get title => "[dev] xmr call profiler";
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) {
|
||||
return PerformanceDebug();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
class PerformanceDebug extends StatefulWidget {
|
||||
const PerformanceDebug({super.key});
|
||||
|
||||
@override
|
||||
State<PerformanceDebug> createState() => _PerformanceDebugState();
|
||||
}
|
||||
|
||||
enum ProfilableWallet {
|
||||
monero,
|
||||
wownero,
|
||||
zano,
|
||||
}
|
||||
|
||||
class _PerformanceDebugState extends State<PerformanceDebug> {
|
||||
List<Widget> widgets = [];
|
||||
|
||||
final dashboardViewModel = getIt.get<DashboardViewModel>();
|
||||
|
||||
late ProfilableWallet wallet = switch (dashboardViewModel.wallet.type) {
|
||||
WalletType.monero => ProfilableWallet.monero,
|
||||
WalletType.wownero => ProfilableWallet.wownero,
|
||||
WalletType.zano => ProfilableWallet.zano,
|
||||
_ => throw Exception("Unknown wallet type"),
|
||||
};
|
||||
final precalc = 1700298;
|
||||
|
||||
late Map<String, List<int>> debugCallLength = switch (wallet) {
|
||||
ProfilableWallet.monero => monero!.debugCallLength(),
|
||||
ProfilableWallet.wownero => wownero!.debugCallLength(),
|
||||
ProfilableWallet.zano => zano!.debugCallLength(),
|
||||
};
|
||||
|
||||
int getOpenWalletTime() {
|
||||
if (debugCallLength["MONERO_Wallet_init"] == null) {
|
||||
return precalc;
|
||||
}
|
||||
if (debugCallLength["MONERO_Wallet_init"]!.isEmpty) {
|
||||
return precalc;
|
||||
}
|
||||
return debugCallLength["MONERO_Wallet_init"]!.last;
|
||||
}
|
||||
|
||||
late final String perfInfo = """
|
||||
---- Performance tuning
|
||||
This page lists all calls that take place during the app runtime.-
|
||||
As per Flutter docs we can read:
|
||||
> Flutter aims to provide 60 frames per second (fps) performance, or 120 fps-
|
||||
performance on devices capable of 120Hz updates.
|
||||
|
||||
With that in mind we will aim to render frames every 8.3ms (~8333 µs). It is-
|
||||
however acceptable to reach 16.6 ms (~16666 µs) but we should also keep in mind-
|
||||
that there are also UI costs that aren't part of this benchmark.
|
||||
|
||||
For some calls it is also acceptable to exceed this amount of time, for example-
|
||||
MONERO_Wallet_init takes ~${getOpenWalletTime()}µs-
|
||||
(${(getOpenWalletTime() / frameTime).toStringAsFixed(2)} frames). That time would-
|
||||
be unnaceptable in most situations but since we call this function only when-
|
||||
opening the wallet it is completely fine to freeze the UI for the time being --
|
||||
as the user won't even notice that something happened.
|
||||
|
||||
---- Details
|
||||
count: how many times did we call this function [total time (% of frame)]
|
||||
average: average execution time (% of frame)
|
||||
min: fastest execution (% of frame)
|
||||
max: slowest execution (% of frame)
|
||||
95th: 95% of the time, the function is faster than this amount of time (% of frame)
|
||||
"""
|
||||
.split("-\n")
|
||||
.join(" ");
|
||||
|
||||
late final frameTime = 8333;
|
||||
late final frameGreenTier = frameTime ~/ 100;
|
||||
late final frameBlueTier = frameTime ~/ 10;
|
||||
late final frameBlueGreyTier = frameTime ~/ 2;
|
||||
late final frameYellowTier = frameTime;
|
||||
late final frameOrangeTier = frameTime * 2;
|
||||
|
||||
Color? perfc(num frame) {
|
||||
if (frame < frameGreenTier) return Colors.green;
|
||||
if (frame < frameBlueTier) return Colors.blue;
|
||||
if (frame < frameBlueGreyTier) return Colors.blueGrey;
|
||||
if (frame < frameGreenTier) return Colors.green;
|
||||
if (frame < frameYellowTier) return Colors.yellow;
|
||||
if (frame < frameOrangeTier) return Colors.orange;
|
||||
return Colors.red;
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_buildWidgets();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
SelectableText cw(String text, Color? color) {
|
||||
return SelectableText(
|
||||
text,
|
||||
style: TextStyle(color: color),
|
||||
);
|
||||
}
|
||||
|
||||
void _buildWidgets() {
|
||||
List<Widget> ws = [];
|
||||
ws.add(Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SelectableText(perfInfo),
|
||||
cw("< 1% of a frame (max: $frameGreenTierµs)", Colors.green),
|
||||
cw("< 10% of a frame (max: $frameBlueTierµs)", Colors.blue),
|
||||
cw("< 50% of a frame (max: $frameBlueGreyTierµs)", Colors.blueGrey),
|
||||
cw("< 100% of a frame (max: $frameYellowTierµs)", Colors.yellow),
|
||||
cw("< 200% of a frame (max: $frameOrangeTierµs)", Colors.orange),
|
||||
cw("> 200% of a frame (UI junk visible)", Colors.red),
|
||||
],
|
||||
));
|
||||
final keys = debugCallLength.keys.toList();
|
||||
keys.sort((s1, s2) =>
|
||||
_n95th(debugCallLength[s2]!) -
|
||||
_n95th(debugCallLength[s1]!));
|
||||
for (var key in keys) {
|
||||
final value = debugCallLength[key];
|
||||
if (value == null) continue;
|
||||
final avg = _avg(value);
|
||||
final min = _min(value);
|
||||
final max = _max(value);
|
||||
final np = _n95th(value);
|
||||
final total = _total(value);
|
||||
ws.add(
|
||||
Card(
|
||||
child: ListTile(
|
||||
title: Text(
|
||||
key,
|
||||
style: TextStyle(color: perfc(np)),
|
||||
),
|
||||
subtitle: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(children: [
|
||||
cw("count: ${value.length}", null),
|
||||
const Spacer(),
|
||||
cw("${_str(total / 1000)}ms", perfc(total)),
|
||||
]),
|
||||
cw("average: ${_str(avg)}µs (~${_str(avg / (frameTime))}f)",
|
||||
perfc(avg)),
|
||||
cw("min: $minµs (~${_str(min / (frameTime) * 100)})",
|
||||
perfc(min)),
|
||||
cw("max: $maxµs (~${_str(max / (frameTime) * 100)}%)",
|
||||
perfc(max)),
|
||||
cw("95th: $npµs (~${_str(np / (frameTime) * 100)}%)",
|
||||
perfc(np)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (debugCallLength.isNotEmpty) {
|
||||
ws.add(
|
||||
PrimaryButton(
|
||||
text: "Purge statistics",
|
||||
onPressed: _purgeStats,
|
||||
color: Colors.red,
|
||||
textColor: Colors.white,
|
||||
),
|
||||
);
|
||||
}
|
||||
setState(() {
|
||||
widgets = ws;
|
||||
});
|
||||
}
|
||||
|
||||
void _purgeStats() {
|
||||
debugCallLength.clear();
|
||||
_buildWidgets();
|
||||
}
|
||||
|
||||
int _min(List<int> l) {
|
||||
return l.reduce(min);
|
||||
}
|
||||
|
||||
int _max(List<int> l) {
|
||||
return l.reduce(max);
|
||||
}
|
||||
|
||||
int _n95th(List<int> l) {
|
||||
final l0 = l.toList();
|
||||
l0.sort();
|
||||
int i = (0.95 * l.length).ceil() - 1;
|
||||
return l0[i];
|
||||
}
|
||||
|
||||
double _avg(List<int> l) {
|
||||
int c = 0;
|
||||
for (var i = 0; i < l.length; i++) {
|
||||
c += l[i];
|
||||
}
|
||||
return c / l.length;
|
||||
}
|
||||
|
||||
int _total(List<int> l) {
|
||||
int c = 0;
|
||||
for (var i = 0; i < l.length; i++) {
|
||||
c += l[i];
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
String _str(num d) => d.toStringAsFixed(2);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Column(
|
||||
children: widgets,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ import 'package:cake_wallet/src/screens/settings/widgets/settings_cell_with_arro
|
|||
import 'package:cake_wallet/src/screens/settings/widgets/settings_picker_cell.dart';
|
||||
import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart';
|
||||
import 'package:cake_wallet/src/screens/settings/widgets/settings_version_cell.dart';
|
||||
import 'package:cake_wallet/utils/feature_flag.dart';
|
||||
import 'package:cake_wallet/view_model/settings/other_settings_view_model.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
@ -64,12 +65,18 @@ class OtherSettingsPage extends BasePage {
|
|||
handler: (BuildContext context) =>
|
||||
Navigator.of(context).pushNamed(Routes.readDisclaimer),
|
||||
),
|
||||
if (kDebugMode && _otherSettingsViewModel.walletType == WalletType.monero)
|
||||
if (FeatureFlag.hasDevOptions && _otherSettingsViewModel.walletType == WalletType.monero)
|
||||
SettingsCellWithArrow(
|
||||
title: '[dev] monero background sync',
|
||||
handler: (BuildContext context) =>
|
||||
Navigator.of(context).pushNamed(Routes.devMoneroBackgroundSync),
|
||||
),
|
||||
if (FeatureFlag.hasDevOptions && [WalletType.monero, WalletType.wownero, WalletType.zano].contains(_otherSettingsViewModel.walletType))
|
||||
SettingsCellWithArrow(
|
||||
title: '[dev] xmr call profiler',
|
||||
handler: (BuildContext context) =>
|
||||
Navigator.of(context).pushNamed(Routes.devMoneroCallProfiler),
|
||||
),
|
||||
Spacer(),
|
||||
SettingsVersionCell(
|
||||
title: S.of(context).version(_otherSettingsViewModel.currentVersion)),
|
||||
|
|
|
@ -6,4 +6,5 @@ class FeatureFlag {
|
|||
static const bool isInAppTorEnabled = false;
|
||||
static const bool isBackgroundSyncEnabled = true;
|
||||
static const int verificationWordsCount = kDebugMode ? 0 : 2;
|
||||
static const bool hasDevOptions = bool.fromEnvironment('hasDevOptions', defaultValue: kDebugMode);
|
||||
}
|
|
@ -271,32 +271,10 @@ abstract class DashboardViewModelBase with Store {
|
|||
});
|
||||
|
||||
_transactionDisposer?.reaction.dispose();
|
||||
|
||||
_transactionDisposer = reaction(
|
||||
(_) => appStore.wallet!.transactionHistory.transactions.values.toList(),
|
||||
(List<TransactionInfo> txs) {
|
||||
|
||||
transactions.clear();
|
||||
|
||||
transactions.addAll(
|
||||
txs.where((tx) {
|
||||
if (wallet.type == WalletType.monero) {
|
||||
return monero!.getTransactionInfoAccountId(tx) == monero!.getCurrentAccount(wallet).id;
|
||||
}
|
||||
if (wallet.type == WalletType.wownero) {
|
||||
return wow.wownero!.getTransactionInfoAccountId(tx) == wow.wownero!.getCurrentAccount(wallet).id;
|
||||
}
|
||||
return true;
|
||||
}).map(
|
||||
(tx) => TransactionListItem(
|
||||
transaction: tx,
|
||||
balanceViewModel: balanceViewModel,
|
||||
settingsStore: appStore.settingsStore,
|
||||
key: ValueKey('${wallet.type.name}_transaction_history_item_${tx.id}_key'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
(_) => appStore.wallet!.transactionHistory.transactions.length *
|
||||
appStore.wallet!.transactionHistory.transactions.values.first.confirmations,
|
||||
_transactionDisposerCallback
|
||||
);
|
||||
|
||||
if (hasSilentPayments) {
|
||||
|
@ -311,6 +289,50 @@ abstract class DashboardViewModelBase with Store {
|
|||
reaction((_) => settingsStore.mwebAlwaysScan, (bool value) => _checkMweb());
|
||||
}
|
||||
|
||||
|
||||
bool _isTransactionDisposerCallbackRunning = false;
|
||||
|
||||
void _transactionDisposerCallback(int _) async {
|
||||
// Simple check to prevent the callback from being called multiple times in the same frame
|
||||
if (_isTransactionDisposerCallbackRunning) return;
|
||||
_isTransactionDisposerCallbackRunning = true;
|
||||
await Future.delayed(Duration.zero);
|
||||
|
||||
|
||||
try {
|
||||
final currentAccountId = wallet.type == WalletType.monero
|
||||
? monero!.getCurrentAccount(wallet).id
|
||||
: wallet.type == WalletType.wownero
|
||||
? wow.wownero!.getCurrentAccount(wallet).id
|
||||
: null;
|
||||
final List<TransactionInfo> relevantTxs = [];
|
||||
|
||||
for (final tx in appStore.wallet!.transactionHistory.transactions.values) {
|
||||
bool isRelevant = true;
|
||||
if (wallet.type == WalletType.monero) {
|
||||
isRelevant = monero!.getTransactionInfoAccountId(tx) == currentAccountId;
|
||||
} else if (wallet.type == WalletType.wownero) {
|
||||
isRelevant = wow.wownero!.getTransactionInfoAccountId(tx) == currentAccountId;
|
||||
}
|
||||
|
||||
if (isRelevant) {
|
||||
relevantTxs.add(tx);
|
||||
}
|
||||
}
|
||||
// printV("Transaction disposer callback (relevantTxs: ${relevantTxs.length} current: ${transactions.length})");
|
||||
|
||||
transactions.clear();
|
||||
transactions.addAll(relevantTxs.map((tx) => TransactionListItem(
|
||||
transaction: tx,
|
||||
balanceViewModel: balanceViewModel,
|
||||
settingsStore: appStore.settingsStore,
|
||||
key: ValueKey('${wallet.type.name}_transaction_history_item_${tx.id}_key'),
|
||||
)));
|
||||
} finally {
|
||||
_isTransactionDisposerCallbackRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
void _checkMweb() {
|
||||
if (hasMweb) {
|
||||
mwebEnabled = bitcoin!.getMwebEnabled(wallet);
|
||||
|
@ -789,30 +811,9 @@ abstract class DashboardViewModelBase with Store {
|
|||
_transactionDisposer?.reaction.dispose();
|
||||
|
||||
_transactionDisposer = reaction(
|
||||
(_) => appStore.wallet!.transactionHistory.transactions.values.toList(),
|
||||
(List<TransactionInfo> txs) {
|
||||
|
||||
transactions.clear();
|
||||
|
||||
transactions.addAll(
|
||||
txs.where((tx) {
|
||||
if (wallet.type == WalletType.monero) {
|
||||
return monero!.getTransactionInfoAccountId(tx) == monero!.getCurrentAccount(wallet).id;
|
||||
}
|
||||
if (wallet.type == WalletType.wownero) {
|
||||
return wow.wownero!.getTransactionInfoAccountId(tx) == wow.wownero!.getCurrentAccount(wallet).id;
|
||||
}
|
||||
return true;
|
||||
}).map(
|
||||
(tx) => TransactionListItem(
|
||||
transaction: tx,
|
||||
balanceViewModel: balanceViewModel,
|
||||
settingsStore: appStore.settingsStore,
|
||||
key: ValueKey('${wallet.type.name}_transaction_history_item_${tx.id}_key'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
(_) => appStore.wallet!.transactionHistory.transactions.length *
|
||||
appStore.wallet!.transactionHistory.transactions.values.first.confirmations,
|
||||
_transactionDisposerCallback
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -361,4 +361,9 @@ class CWWownero extends Wownero {
|
|||
void wownerocCheck() {
|
||||
checkIfMoneroCIsFine();
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, List<int>> debugCallLength() {
|
||||
return wownero_wallet_api.debugCallLength();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -131,4 +131,9 @@ class CWZano extends Zano {
|
|||
|
||||
@override
|
||||
bool validateAddress(String address) => ZanoUtils.validateAddress(address);
|
||||
|
||||
@override
|
||||
Map<String, List<int>> debugCallLength() {
|
||||
return api.debugCallLength();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ dependencies:
|
|||
url: https://github.com/cake-tech/qr.flutter.git
|
||||
ref: cake-4.0.2
|
||||
version: 4.0.2
|
||||
shared_preferences: 2.3.2
|
||||
shared_preferences: 2.5.3
|
||||
# provider: ^6.0.3
|
||||
rxdart: ^0.28.0
|
||||
yaml: ^3.1.1
|
||||
|
@ -83,7 +83,6 @@ dependencies:
|
|||
version: 1.0.0
|
||||
flutter_plugin_android_lifecycle: 2.0.23
|
||||
path_provider_android: ^2.2.1
|
||||
shared_preferences_android: 2.3.3
|
||||
url_launcher_android: 6.3.14
|
||||
url_launcher_linux: 3.1.1 # https://github.com/flutter/flutter/issues/153083
|
||||
sensitive_clipboard:
|
||||
|
|
|
@ -425,6 +425,7 @@ abstract class Monero {
|
|||
void setLedgerConnection(Object wallet, ledger.LedgerConnection connection);
|
||||
void resetLedgerConnection();
|
||||
void setGlobalLedgerConnection(ledger.LedgerConnection connection);
|
||||
Map<String, List<int>> debugCallLength();
|
||||
}
|
||||
|
||||
abstract class MoneroSubaddressList {
|
||||
|
@ -610,6 +611,7 @@ abstract class Wownero {
|
|||
WalletService createWowneroWalletService(Box<WalletInfo> walletInfoSource, Box<UnspentCoinsInfo> unspentCoinSource);
|
||||
Map<String, String> pendingTransactionInfo(Object transaction);
|
||||
String getLegacySeed(Object wallet, String langName);
|
||||
Map<String, List<int>> debugCallLength();
|
||||
}
|
||||
|
||||
abstract class WowneroSubaddressList {
|
||||
|
@ -1253,6 +1255,7 @@ import 'package:cw_zano/model/zano_transaction_info.dart';
|
|||
import 'package:cw_zano/zano_formatter.dart';
|
||||
import 'package:cw_zano/zano_wallet.dart';
|
||||
import 'package:cw_zano/zano_wallet_service.dart';
|
||||
import 'package:cw_zano/zano_wallet_api.dart' as api;
|
||||
import 'package:cw_zano/zano_utils.dart';
|
||||
""";
|
||||
const zanoCwPart = "part 'cw_zano.dart';";
|
||||
|
@ -1279,6 +1282,7 @@ abstract class Zano {
|
|||
Future<CryptoCurrency?> getZanoAsset(WalletBase wallet, String contractAddress);
|
||||
String getAddress(WalletBase wallet);
|
||||
bool validateAddress(String address);
|
||||
Map<String, List<int>> debugCallLength();
|
||||
}
|
||||
""";
|
||||
const zanoEmptyDefinition = 'Zano? zano;\n';
|
||||
|
|
Loading…
Reference in a new issue