Merge bitcoin-core/gui#598: Avoid recalculating the wallet balance - use model cache

4584d300a4 GUI: remove now unneeded 'm_balances' field from overviewpage (furszy)
050e8b1391 GUI: 'getAvailableBalance', use cached balance if the user did not select UTXO manually (furszy)
96e3264a82 GUI: use cached balance in overviewpage and sendcoinsdialog (furszy)
321335bf02 GUI: add getter for WalletModel::m_cached_balances field (furszy)
e62958dc81 GUI: sendCoinsDialog, remove duplicate wallet().getBalances() call (furszy)

Pull request description:

  As per the title says, we are recalculating the entire wallet balance on different situations calling to `wallet().getBalances()`, when should instead make use of the wallet model cached balance.

  This has the benefits of (1) not spending resources calculating a balance that we already have cached, and (2) avoid blocking the main thread for a long time, in case of big wallets, walking through the entire wallet's tx map more than what it's really needed.

  Changes:

  1) Fix: `SendCoinsDialog` was calling `wallet().getBalances()` twice during `setModel`.
  2) Use the cached balance if the user did not select any UTXO manually inside the wallet model `getAvailableBalance` call.

  -----------------------
  As an extra note, this work born in [#25005](https://github.com/bitcoin/bitcoin/pull/25005) but grew out of scope of it.

ACKs for top commit:
  jarolrod:
    ACK 4584d300a4
  hebasto:
    re-ACK 4584d300a4, only suggested changes and commit message formatting since my [recent](https://github.com/bitcoin-core/gui/pull/598#pullrequestreview-1071268192) review.

Tree-SHA512: 6633ce7f9a82a3e46e75aa7295df46c80a4cd4a9f3305427af203c9bc8670573fa8a1927f14a279260c488cc975a08d238faba2e9751588086fea1dcf8ea2b28
This commit is contained in:
Hennadii Stepanov
2022-08-15 19:35:52 +01:00
7 changed files with 54 additions and 39 deletions

View File

@@ -147,8 +147,6 @@ OverviewPage::OverviewPage(const PlatformStyle *platformStyle, QWidget *parent)
{ {
ui->setupUi(this); ui->setupUi(this);
m_balances.balance = -1;
// use a SingleColorIcon for the "out of sync warning" icon // use a SingleColorIcon for the "out of sync warning" icon
QIcon icon = m_platform_style->SingleColorIcon(QStringLiteral(":/icons/warning")); QIcon icon = m_platform_style->SingleColorIcon(QStringLiteral(":/icons/warning"));
ui->labelTransactionsStatus->setIcon(icon); ui->labelTransactionsStatus->setIcon(icon);
@@ -177,8 +175,9 @@ void OverviewPage::handleTransactionClicked(const QModelIndex &index)
void OverviewPage::setPrivacy(bool privacy) void OverviewPage::setPrivacy(bool privacy)
{ {
m_privacy = privacy; m_privacy = privacy;
if (m_balances.balance != -1) { const auto& balances = walletModel->getCachedBalance();
setBalance(m_balances); if (balances.balance != -1) {
setBalance(balances);
} }
ui->listTransactions->setVisible(!m_privacy); ui->listTransactions->setVisible(!m_privacy);
@@ -197,7 +196,6 @@ OverviewPage::~OverviewPage()
void OverviewPage::setBalance(const interfaces::WalletBalances& balances) void OverviewPage::setBalance(const interfaces::WalletBalances& balances)
{ {
BitcoinUnit unit = walletModel->getOptionsModel()->getDisplayUnit(); BitcoinUnit unit = walletModel->getOptionsModel()->getDisplayUnit();
m_balances = balances;
if (walletModel->wallet().isLegacy()) { if (walletModel->wallet().isLegacy()) {
if (walletModel->wallet().privateKeysDisabled()) { if (walletModel->wallet().privateKeysDisabled()) {
ui->labelBalance->setText(BitcoinUnits::formatWithPrivacy(unit, balances.watch_only_balance, BitcoinUnits::SeparatorStyle::ALWAYS, m_privacy)); ui->labelBalance->setText(BitcoinUnits::formatWithPrivacy(unit, balances.watch_only_balance, BitcoinUnits::SeparatorStyle::ALWAYS, m_privacy));
@@ -276,14 +274,13 @@ void OverviewPage::setWalletModel(WalletModel *model)
ui->listTransactions->setModelColumn(TransactionTableModel::ToAddress); ui->listTransactions->setModelColumn(TransactionTableModel::ToAddress);
// Keep up to date with wallet // Keep up to date with wallet
interfaces::Wallet& wallet = model->wallet(); setBalance(model->getCachedBalance());
interfaces::WalletBalances balances = wallet.getBalances();
setBalance(balances);
connect(model, &WalletModel::balanceChanged, this, &OverviewPage::setBalance); connect(model, &WalletModel::balanceChanged, this, &OverviewPage::setBalance);
connect(model->getOptionsModel(), &OptionsModel::displayUnitChanged, this, &OverviewPage::updateDisplayUnit); connect(model->getOptionsModel(), &OptionsModel::displayUnitChanged, this, &OverviewPage::updateDisplayUnit);
updateWatchOnlyLabels(wallet.haveWatchOnly() && !model->wallet().privateKeysDisabled()); interfaces::Wallet& wallet = model->wallet();
updateWatchOnlyLabels(wallet.haveWatchOnly() && !wallet.privateKeysDisabled());
connect(model, &WalletModel::notifyWatchonlyChanged, [this](bool showWatchOnly) { connect(model, &WalletModel::notifyWatchonlyChanged, [this](bool showWatchOnly) {
updateWatchOnlyLabels(showWatchOnly && !walletModel->wallet().privateKeysDisabled()); updateWatchOnlyLabels(showWatchOnly && !walletModel->wallet().privateKeysDisabled());
}); });
@@ -306,10 +303,10 @@ void OverviewPage::changeEvent(QEvent* e)
void OverviewPage::updateDisplayUnit() void OverviewPage::updateDisplayUnit()
{ {
if(walletModel && walletModel->getOptionsModel()) if (walletModel && walletModel->getOptionsModel()) {
{ const auto& balances = walletModel->getCachedBalance();
if (m_balances.balance != -1) { if (balances.balance != -1) {
setBalance(m_balances); setBalance(balances);
} }
// Update txdelegate->unit with the current unit // Update txdelegate->unit with the current unit

View File

@@ -52,7 +52,6 @@ private:
Ui::OverviewPage *ui; Ui::OverviewPage *ui;
ClientModel *clientModel; ClientModel *clientModel;
WalletModel *walletModel; WalletModel *walletModel;
interfaces::WalletBalances m_balances;
bool m_privacy{false}; bool m_privacy{false};
const PlatformStyle* m_platform_style; const PlatformStyle* m_platform_style;

View File

@@ -164,11 +164,9 @@ void SendCoinsDialog::setModel(WalletModel *_model)
} }
} }
interfaces::WalletBalances balances = _model->wallet().getBalances();
setBalance(balances);
connect(_model, &WalletModel::balanceChanged, this, &SendCoinsDialog::setBalance); connect(_model, &WalletModel::balanceChanged, this, &SendCoinsDialog::setBalance);
connect(_model->getOptionsModel(), &OptionsModel::displayUnitChanged, this, &SendCoinsDialog::updateDisplayUnit); connect(_model->getOptionsModel(), &OptionsModel::displayUnitChanged, this, &SendCoinsDialog::refreshBalance);
updateDisplayUnit(); refreshBalance();
// Coin Control // Coin Control
connect(_model->getOptionsModel(), &OptionsModel::displayUnitChanged, this, &SendCoinsDialog::coinControlUpdateLabels); connect(_model->getOptionsModel(), &OptionsModel::displayUnitChanged, this, &SendCoinsDialog::coinControlUpdateLabels);
@@ -711,9 +709,9 @@ void SendCoinsDialog::setBalance(const interfaces::WalletBalances& balances)
} }
} }
void SendCoinsDialog::updateDisplayUnit() void SendCoinsDialog::refreshBalance()
{ {
setBalance(model->wallet().getBalances()); setBalance(model->getCachedBalance());
ui->customFee->setDisplayUnit(model->getOptionsModel()->getDisplayUnit()); ui->customFee->setDisplayUnit(model->getOptionsModel()->getDisplayUnit());
updateSmartFeeLabel(); updateSmartFeeLabel();
} }
@@ -786,7 +784,7 @@ void SendCoinsDialog::useAvailableBalance(SendCoinsEntry* entry)
m_coin_control->fAllowWatchOnly = model->wallet().privateKeysDisabled() && !model->wallet().hasExternalSigner(); m_coin_control->fAllowWatchOnly = model->wallet().privateKeysDisabled() && !model->wallet().hasExternalSigner();
// Calculate available amount to send. // Calculate available amount to send.
CAmount amount = model->wallet().getAvailableBalance(*m_coin_control); CAmount amount = model->getAvailableBalance(m_coin_control.get());
for (int i = 0; i < ui->entries->count(); ++i) { for (int i = 0; i < ui->entries->count(); ++i) {
SendCoinsEntry* e = qobject_cast<SendCoinsEntry*>(ui->entries->itemAt(i)->widget()); SendCoinsEntry* e = qobject_cast<SendCoinsEntry*>(ui->entries->itemAt(i)->widget());
if (e && !e->isHidden() && e != entry) { if (e && !e->isHidden() && e != entry) {

View File

@@ -97,7 +97,7 @@ private Q_SLOTS:
void on_buttonMinimizeFee_clicked(); void on_buttonMinimizeFee_clicked();
void removeEntry(SendCoinsEntry* entry); void removeEntry(SendCoinsEntry* entry);
void useAvailableBalance(SendCoinsEntry* entry); void useAvailableBalance(SendCoinsEntry* entry);
void updateDisplayUnit(); void refreshBalance();
void coinControlFeatureChanged(bool); void coinControlFeatureChanged(bool);
void coinControlButtonClicked(); void coinControlButtonClicked();
void coinControlChangeChecked(int); void coinControlChangeChecked(int);

View File

@@ -129,6 +129,13 @@ void BumpFee(TransactionView& view, const uint256& txid, bool expectDisabled, st
QVERIFY(text.indexOf(QString::fromStdString(expectError)) != -1); QVERIFY(text.indexOf(QString::fromStdString(expectError)) != -1);
} }
void CompareBalance(WalletModel& walletModel, CAmount expected_balance, QLabel* balance_label_to_check)
{
BitcoinUnit unit = walletModel.getOptionsModel()->getDisplayUnit();
QString balanceComparison = BitcoinUnits::formatWithUnit(unit, expected_balance, false, BitcoinUnits::SeparatorStyle::ALWAYS);
QCOMPARE(balance_label_to_check->text().trimmed(), balanceComparison);
}
//! Simple qt wallet tests. //! Simple qt wallet tests.
// //
// Test widgets can be debugged interactively calling show() on them and // Test widgets can be debugged interactively calling show() on them and
@@ -195,15 +202,10 @@ void TestGUI(interfaces::Node& node)
sendCoinsDialog.setModel(&walletModel); sendCoinsDialog.setModel(&walletModel);
transactionView.setModel(&walletModel); transactionView.setModel(&walletModel);
{ // Update walletModel cached balance which will trigger an update for the 'labelBalance' QLabel.
// Check balance in send dialog walletModel.pollBalanceChanged();
QLabel* balanceLabel = sendCoinsDialog.findChild<QLabel*>("labelBalance"); // Check balance in send dialog
QString balanceText = balanceLabel->text(); CompareBalance(walletModel, walletModel.wallet().getBalance(), sendCoinsDialog.findChild<QLabel*>("labelBalance"));
BitcoinUnit unit = walletModel.getOptionsModel()->getDisplayUnit();
CAmount balance = walletModel.wallet().getBalance();
QString balanceComparison = BitcoinUnits::formatWithUnit(unit, balance, false, BitcoinUnits::SeparatorStyle::ALWAYS);
QCOMPARE(balanceText, balanceComparison);
}
// Send two transactions, and verify they are added to transaction list. // Send two transactions, and verify they are added to transaction list.
TransactionTableModel* transactionTableModel = walletModel.getTransactionTableModel(); TransactionTableModel* transactionTableModel = walletModel.getTransactionTableModel();
@@ -223,12 +225,8 @@ void TestGUI(interfaces::Node& node)
// Check current balance on OverviewPage // Check current balance on OverviewPage
OverviewPage overviewPage(platformStyle.get()); OverviewPage overviewPage(platformStyle.get());
overviewPage.setWalletModel(&walletModel); overviewPage.setWalletModel(&walletModel);
QLabel* balanceLabel = overviewPage.findChild<QLabel*>("labelBalance"); walletModel.pollBalanceChanged(); // Manual balance polling update
QString balanceText = balanceLabel->text().trimmed(); CompareBalance(walletModel, walletModel.wallet().getBalance(), overviewPage.findChild<QLabel*>("labelBalance"));
BitcoinUnit unit = walletModel.getOptionsModel()->getDisplayUnit();
CAmount balance = walletModel.wallet().getBalance();
QString balanceComparison = BitcoinUnits::formatWithUnit(unit, balance, false, BitcoinUnits::SeparatorStyle::ALWAYS);
QCOMPARE(balanceText, balanceComparison);
// Check Request Payment button // Check Request Payment button
ReceiveCoinsDialog receiveCoinsDialog(platformStyle.get()); ReceiveCoinsDialog receiveCoinsDialog(platformStyle.get());

View File

@@ -67,6 +67,10 @@ WalletModel::~WalletModel()
void WalletModel::startPollBalance() void WalletModel::startPollBalance()
{ {
// Update the cached balance right away, so every view can make use of it,
// so them don't need to waste resources recalculating it.
pollBalanceChanged();
// This timer will be fired repeatedly to update the balance // This timer will be fired repeatedly to update the balance
// Since the QTimer::timeout is a private signal, it cannot be used // Since the QTimer::timeout is a private signal, it cannot be used
// in the GUIUtil::ExceptionSafeConnect directly. // in the GUIUtil::ExceptionSafeConnect directly.
@@ -120,12 +124,17 @@ void WalletModel::pollBalanceChanged()
void WalletModel::checkBalanceChanged(const interfaces::WalletBalances& new_balances) void WalletModel::checkBalanceChanged(const interfaces::WalletBalances& new_balances)
{ {
if(new_balances.balanceChanged(m_cached_balances)) { if (new_balances.balanceChanged(m_cached_balances)) {
m_cached_balances = new_balances; m_cached_balances = new_balances;
Q_EMIT balanceChanged(new_balances); Q_EMIT balanceChanged(new_balances);
} }
} }
interfaces::WalletBalances WalletModel::getCachedBalance() const
{
return m_cached_balances;
}
void WalletModel::updateTransaction() void WalletModel::updateTransaction()
{ {
// Balance and number of transactions might have changed // Balance and number of transactions might have changed
@@ -194,7 +203,9 @@ WalletModel::SendCoinsReturn WalletModel::prepareTransaction(WalletModelTransact
return DuplicateAddress; return DuplicateAddress;
} }
CAmount nBalance = m_wallet->getAvailableBalance(coinControl); // If no coin was manually selected, use the cached balance
// Future: can merge this call with 'createTransaction'.
CAmount nBalance = getAvailableBalance(&coinControl);
if(total > nBalance) if(total > nBalance)
{ {
@@ -602,3 +613,8 @@ uint256 WalletModel::getLastBlockProcessed() const
{ {
return m_client_model ? m_client_model->getBestBlockHash() : uint256{}; return m_client_model ? m_client_model->getBestBlockHash() : uint256{};
} }
CAmount WalletModel::getAvailableBalance(const CCoinControl* control)
{
return control && control->HasSelected() ? wallet().getAvailableBalance(*control) : getCachedBalance().balance;
}

View File

@@ -155,6 +155,13 @@ public:
uint256 getLastBlockProcessed() const; uint256 getLastBlockProcessed() const;
// Retrieve the cached wallet balance
interfaces::WalletBalances getCachedBalance() const;
// If coin control has selected outputs, searches the total amount inside the wallet.
// Otherwise, uses the wallet's cached available balance.
CAmount getAvailableBalance(const wallet::CCoinControl* control);
private: private:
std::unique_ptr<interfaces::Wallet> m_wallet; std::unique_ptr<interfaces::Wallet> m_wallet;
std::unique_ptr<interfaces::Handler> m_handler_unload; std::unique_ptr<interfaces::Handler> m_handler_unload;