From cbca4c9c77ec59dcc328834215c075f99f4c7312 Mon Sep 17 00:00:00 2001 From: cyan Date: Thu, 3 Apr 2025 03:31:25 +0200 Subject: [PATCH] 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 --- .github/workflows/pr_test_build_android.yml | 2 +- .github/workflows/pr_test_build_linux.yml | 2 +- cw_monero/lib/api/coins_info.dart | 49 +++- cw_monero/lib/api/transaction_history.dart | 44 ++- cw_monero/lib/api/wallet.dart | 18 +- cw_monero/lib/api/wallet_manager.dart | 1 + cw_monero/lib/monero_account_list.dart | 17 +- cw_monero/lib/monero_unspent.dart | 33 ++- cw_monero/lib/monero_wallet.dart | 28 +- cw_monero/lib/monero_wallet_service.dart | 7 - cw_wownero/lib/api/wallet.dart | 2 + cw_zano/lib/zano_wallet_api.dart | 4 +- devtools_options.yaml | 1 - lib/di.dart | 2 + lib/monero/cw_monero.dart | 6 + lib/router.dart | 6 + lib/routes.dart | 2 +- .../screens/dev/moneroc_call_profiler.dart | 253 ++++++++++++++++++ .../screens/settings/other_settings_page.dart | 9 +- lib/utils/feature_flag.dart | 1 + .../dashboard/dashboard_view_model.dart | 99 +++---- lib/wownero/cw_wownero.dart | 5 + lib/zano/cw_zano.dart | 5 + pubspec_base.yaml | 3 +- tool/configure.dart | 4 + 25 files changed, 498 insertions(+), 105 deletions(-) delete mode 100644 devtools_options.yaml create mode 100644 lib/src/screens/dev/moneroc_call_profiler.dart diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml index 08a283467..8f6139747 100644 --- a/.github/workflows/pr_test_build_android.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -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: | diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml index bf55134ad..a341aed0d 100644 --- a/.github/workflows/pr_test_build_linux.yml +++ b/.github/workflows/pr_test_build_linux.yml @@ -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: | diff --git a/cw_monero/lib/api/coins_info.dart b/cw_monero/lib/api/coins_info.dart index ef7d3cfd6..83382f001 100644 --- a/cw_monero/lib/api/coins_info.dart +++ b/cw_monero/lib/api/coins_info.dart @@ -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 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 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 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 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 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 thawCoin(int index) async { + await coinsMutex.acquire(); + final coinsPtr = coins!.address; + await Isolate.run(() => monero.Coins_thaw(Pointer.fromAddress(coinsPtr), index: index)); + coinsMutex.release(); +} diff --git a/cw_monero/lib/api/transaction_history.dart b/cw_monero/lib/api/transaction_history.dart index 162b9ac1a..a51e415a9 100644 --- a/cw_monero/lib/api/transaction_history.dart +++ b/cw_monero/lib/api/transaction_history.dart @@ -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> 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 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> 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 = []; + 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> getAllTransactions() async { return list; } -Transaction getTransaction(String txId) { - return Transaction(txInfo: monero.TransactionHistory_transactionById(txhistory!, txid: txId)); +Map> txCache = {}; +Future 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 createTransactionSync( diff --git a/cw_monero/lib/api/wallet.dart b/cw_monero/lib/api/wallet.dart index 033df53cf..9abd67c60 100644 --- a/cw_monero/lib/api/wallet.dart +++ b/cw_monero/lib/api/wallet.dart @@ -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); -} \ No newline at end of file +} + +Map> debugCallLength() => monero.debugCallLength; diff --git a/cw_monero/lib/api/wallet_manager.dart b/cw_monero/lib/api/wallet_manager.dart index 0dcb3c851..b43773447 100644 --- a/cw_monero/lib/api/wallet_manager.dart +++ b/cw_monero/lib/api/wallet_manager.dart @@ -137,6 +137,7 @@ void restoreWalletFromSeedSync( wptr = newWptr; setRefreshFromBlockHeight(height: restoreHeight); + setupBackgroundSync(password, newWptr); monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.passphrase", value: passphrase); diff --git a/cw_monero/lib/monero_account_list.dart b/cw_monero/lib/monero_account_list.dart index aa23e276f..82a0efd32 100644 --- a/cw_monero/lib/monero_account_list.dart +++ b/cw_monero/lib/monero_account_list.dart @@ -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 getAll() => account_list.getAllAccount().map((accountRow) { + Map> _cachedAccounts = {}; + + List 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 addAccount({required String label}) async { await account_list.addAccount(label: label); diff --git a/cw_monero/lib/monero_unspent.dart b/cw_monero/lib/monero_unspent.dart index 8a104edf4..292c76dbe 100644 --- a/cw_monero/lib/monero_unspent.dart +++ b/cw_monero/lib/monero_unspent.dart @@ -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; } diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index 00e50f37f..de9fe8569 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -169,6 +169,7 @@ abstract class MoneroWalletBase extends WalletBase> fetchTransactions() async { - transaction_history.refreshTransactions(); - return (await _getAllTransactionsOfAccount(walletAddresses.account?.id)) + await transaction_history.refreshTransactions(); + final resp = (await _getAllTransactionsOfAccount(walletAddresses.account?.id)) .fold>( {}, (Map acc, MoneroTransactionInfo tx) { acc[tx.id] = tx; return acc; }); + return resp; } Future updateTransactions() async { @@ -710,8 +714,17 @@ abstract class MoneroWalletBase extends WalletBase + 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> debugCallLength() => wownero.debugCallLength; \ No newline at end of file diff --git a/cw_zano/lib/zano_wallet_api.dart b/cw_zano/lib/zano_wallet_api.dart index f2c5469c4..f10e0b2d6 100644 --- a/cw_zano/lib/zano_wallet_api.dart +++ b/cw_zano/lib/zano_wallet_api.dart @@ -508,4 +508,6 @@ Future _closeWallet(int hWallet) async { }); printV("Closing wallet: $str"); return str; -} \ No newline at end of file +} + +Map> debugCallLength() => zano.debugCallLength; \ No newline at end of file diff --git a/devtools_options.yaml b/devtools_options.yaml deleted file mode 100644 index 7e7e7f67d..000000000 --- a/devtools_options.yaml +++ /dev/null @@ -1 +0,0 @@ -extensions: diff --git a/lib/di.dart b/lib/di.dart index 03de7ce47..dcd0165ec 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -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 setup({ getIt.registerFactory(() => SeedVerificationPage(getIt.get())); getIt.registerFactory(() => DevMoneroBackgroundSyncPage(getIt.get())); + getIt.registerFactory(() => DevMoneroCallProfilerPage()); _isSetupFinished = true; } diff --git a/lib/monero/cw_monero.dart b/lib/monero/cw_monero.dart index a955e1d98..6c72cbe67 100644 --- a/lib/monero/cw_monero.dart +++ b/lib/monero/cw_monero.dart @@ -424,4 +424,10 @@ class CWMonero extends Monero { bool isViewOnly() { return isViewOnlyBySpendKey(null); } + + @override + Map> debugCallLength() { + return monero_wallet_api.debugCallLength(); + } + } diff --git a/lib/router.dart b/lib/router.dart index b0501c550..e69f75f21 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -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 createRoute(RouteSettings settings) { builder: (_) => getIt.get(), ); + case Routes.devMoneroCallProfiler: + return MaterialPageRoute( + builder: (_) => getIt.get(), + ); + default: return MaterialPageRoute( builder: (_) => Scaffold( diff --git a/lib/routes.dart b/lib/routes.dart index be5e9f05d..f7f3e2f6f 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -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'; diff --git a/lib/src/screens/dev/moneroc_call_profiler.dart b/lib/src/screens/dev/moneroc_call_profiler.dart new file mode 100644 index 000000000..e7043fea9 --- /dev/null +++ b/lib/src/screens/dev/moneroc_call_profiler.dart @@ -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 createState() => _PerformanceDebugState(); +} + +enum ProfilableWallet { + monero, + wownero, + zano, +} + +class _PerformanceDebugState extends State { + List widgets = []; + + final dashboardViewModel = getIt.get(); + + 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> 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 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 l) { + return l.reduce(min); + } + + int _max(List l) { + return l.reduce(max); + } + + int _n95th(List l) { + final l0 = l.toList(); + l0.sort(); + int i = (0.95 * l.length).ceil() - 1; + return l0[i]; + } + + double _avg(List l) { + int c = 0; + for (var i = 0; i < l.length; i++) { + c += l[i]; + } + return c / l.length; + } + + int _total(List 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, + ), + ), + ); + } +} diff --git a/lib/src/screens/settings/other_settings_page.dart b/lib/src/screens/settings/other_settings_page.dart index ca1c1b2cb..f841e2998 100644 --- a/lib/src/screens/settings/other_settings_page.dart +++ b/lib/src/screens/settings/other_settings_page.dart @@ -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)), diff --git a/lib/utils/feature_flag.dart b/lib/utils/feature_flag.dart index 6e829d474..c968c5d75 100644 --- a/lib/utils/feature_flag.dart +++ b/lib/utils/feature_flag.dart @@ -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); } \ No newline at end of file diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index a07ba26ef..a2a5bdc0d 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -271,32 +271,10 @@ abstract class DashboardViewModelBase with Store { }); _transactionDisposer?.reaction.dispose(); - _transactionDisposer = reaction( - (_) => appStore.wallet!.transactionHistory.transactions.values.toList(), - (List 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 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 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 ); } diff --git a/lib/wownero/cw_wownero.dart b/lib/wownero/cw_wownero.dart index e20b6fbbf..26092486b 100644 --- a/lib/wownero/cw_wownero.dart +++ b/lib/wownero/cw_wownero.dart @@ -361,4 +361,9 @@ class CWWownero extends Wownero { void wownerocCheck() { checkIfMoneroCIsFine(); } + + @override + Map> debugCallLength() { + return wownero_wallet_api.debugCallLength(); + } } diff --git a/lib/zano/cw_zano.dart b/lib/zano/cw_zano.dart index 19fec04e4..7bd515e32 100644 --- a/lib/zano/cw_zano.dart +++ b/lib/zano/cw_zano.dart @@ -131,4 +131,9 @@ class CWZano extends Zano { @override bool validateAddress(String address) => ZanoUtils.validateAddress(address); + + @override + Map> debugCallLength() { + return api.debugCallLength(); + } } diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 2af036db1..2275e3164 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -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: diff --git a/tool/configure.dart b/tool/configure.dart index 63ed2de84..0c514de17 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -425,6 +425,7 @@ abstract class Monero { void setLedgerConnection(Object wallet, ledger.LedgerConnection connection); void resetLedgerConnection(); void setGlobalLedgerConnection(ledger.LedgerConnection connection); + Map> debugCallLength(); } abstract class MoneroSubaddressList { @@ -610,6 +611,7 @@ abstract class Wownero { WalletService createWowneroWalletService(Box walletInfoSource, Box unspentCoinSource); Map pendingTransactionInfo(Object transaction); String getLegacySeed(Object wallet, String langName); + Map> 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 getZanoAsset(WalletBase wallet, String contractAddress); String getAddress(WalletBase wallet); bool validateAddress(String address); + Map> debugCallLength(); } """; const zanoEmptyDefinition = 'Zano? zano;\n';