CW-1024 Improve performance of xmr wallet ()

* - 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 4c4c33ac6a.

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

This reverts commit d7603445ad.

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

This reverts commit 4c4c33ac6a.

* 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

@ -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: |

View file

@ -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: |

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;

View file

@ -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;

View file

@ -508,4 +508,6 @@ Future<String> _closeWallet(int hWallet) async {
});
printV("Closing wallet: $str");
return str;
}
}
Map<String, List<int>> debugCallLength() => zano.debugCallLength;

View file

@ -1 +0,0 @@
extensions:

View file

@ -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;
}

View file

@ -424,4 +424,10 @@ class CWMonero extends Monero {
bool isViewOnly() {
return isViewOnlyBySpendKey(null);
}
@override
Map<String, List<int>> debugCallLength() {
return monero_wallet_api.debugCallLength();
}
}

View file

@ -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(

View file

@ -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';

View 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,
),
),
);
}
}

View file

@ -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)),

View file

@ -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);
}

View file

@ -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
);
}

View file

@ -361,4 +361,9 @@ class CWWownero extends Wownero {
void wownerocCheck() {
checkIfMoneroCIsFine();
}
@override
Map<String, List<int>> debugCallLength() {
return wownero_wallet_api.debugCallLength();
}
}

View file

@ -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();
}
}

View file

@ -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:

View file

@ -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';